local curdir = std.getinfo(1).source:gsub("^(.+[\\/])[^\\/]+$", "%1"):gsub("^@", ""); require "fmt" require "snapshots" local tostring = std.tostr --luacheck: no self if std.ref'@theme' then std.ref'@theme'.set ('win.scroll.mode', 3) end local mrd = require "morph/mrd" local inp_split = " :.,!?-" local input = std.ref '@input' local function utf_bb(b, pos) if type(b) ~= 'string' or b:len() == 0 then return 0 end local utf8 = (std.game.codepage == 'UTF-8' or std.game.codepage == 'utf-8') if not utf8 then return 1 end local i = pos or b:len() local l = 0 while b:byte(i) >= 0x80 and b:byte(i) <= 0xbf do i = i - 1 l = l + 1 if i <= 1 then break end end return l + 1 end local function utf_ff(b, pos) if type(b) ~= 'string' or b:len() == 0 then return 0 end local utf8 = (std.game.codepage == 'UTF-8' or std.game.codepage == 'utf-8') if not utf8 then return 1 end local i = pos or 1 local l = 0 if b:byte(i) < 0x80 then return 1 end i = i + 1 l = l + 1 while b:byte(i) and b:byte(i) >= 0x80 and b:byte(i) <= 0xbf do i = i + 1 l = l + 1 if i > b:len() then break end end return l end local function utf_len(b) local i = 1 local n = 0 if not b or b:len() == 0 then return 0 end while i <= b:len() do i = i + utf_ff(b, i) n = n + 1 end return n end local function utf_char(b, c) local i = 1 local n = 0 local s while i <= b:len() do s = i i = i + utf_ff(b, i) n = n + 1 if n == c then return b:sub(s, i - 1) end end return end local function utf_chars(b) local i = 1 local s local res = {} local ff = std.rawget(_G, 'utf8_next') or utf_ff while i <= b:len() do s = i i = i + ff(b, i) table.insert(res, b:sub(s, i - 1)) end return res end --- Returns the Levenshtein distance between the two given strings. -- https://gist.github.com/Badgerati/3261142 -- @param str1 string1 -- @param str2 string2 local function utf_lev(str1, str2) str1 = str1 or '' str2 = str2 or '' local chars1 = utf_chars(str1) local chars2 = utf_chars(str2) local len1 = #chars1 local len2 = #chars2 local matrix = {} local cost -- quick cut-offs to save time if (len1 == 0) then return len2 elseif (len2 == 0) then return len1 elseif (str1 == str2) then return 0 end -- initialise the base matrix values for i = 0, len1, 1 do matrix[i] = {} matrix[i][0] = i end for j = 0, len2, 1 do matrix[0][j] = j end -- actual Levenshtein algorithm for i = 1, len1, 1 do for j = 1, len2, 1 do if (chars1[i] == chars2[j]) then cost = 0 else cost = 1 end matrix[i][j] = math.min(matrix[i-1][j] + 1, matrix[i][j-1] + 1, matrix[i-1][j-1] + cost) end end -- return the last value - this is the Levenshtein distance return matrix[len1][len2] end local okey = input.key local mp local function use_text_event() return instead.text_input and instead.text_input() end if PLATFORM == "ANDROID" or PLATFORM == "IOS" or PLATFORM == "SFOS" then local oclick = input.click function input:click(press, mb, x, y, ...) local theme = std.ref'@theme' if not instead.text_input or not press or mb ~= 1 or not theme or mp.autohelp then if oclick then return oclick(self, press, mb, x, y, ...) end return false end local xx = std.tonum(theme.get 'inv.x') local yy = std.tonum(theme.get 'inv.y') local ww = std.tonum(theme.get 'inv.w') local hh = std.tonum(theme.get 'inv.h') if x >= xx and y >= yy and x < xx + ww and y < yy + hh then instead.text_input(not instead.text_input()) end if oclick then return oclick(self, press, mb, x, y, ...) end return false end end function input:text(sym) if sym == " " then -- in old key logic return false end if iface:raw_mode() then return false end mp:inp_insert(sym) mp:post_inp() return '@mp_key '..tostring(sym) end function input:key(press, key) local mod if key:find("alt") then mp.alt = press mod = true elseif key:find("ctrl") then mp.ctrl = press mod = true elseif key:find("shift") then mp.shift = press mod = true end if key:find("enter") or key:find("return") then key = 'enter' end if press and not mod and not (mp.ctrl or mp.alt) then if mp:key(key) then mp:post_inp() if key == 'f6' and mp.autoplay then key = 'enter' end return '@mp_key '..tostring(key) end end if okey then return okey(self, press, key) end end mp = std.obj { nam = '@metaparser'; score = false; expert_mode = true; autohelp = false; autohelp_limit = 1000; autohelp_noverbs = false; togglehelp = true; errhints = true; autocompl = true; undo = 0; compl_thresh = 0; compare_len = 3; detailed_inv = false; daemons = std.list {}; { version = "1.9"; cache = { tokens = {}, nouns = false }; scope = std.list {}; logfile = false; lognum = 0; clear_on_move = true; auto_animate = true; autoplay = false; pushed = {}; autoplay_command = false; inp = ''; cur = 1; utf = { bb = std.rawget(_G, 'utf8_prev') or utf_bb; ff = std.rawget(_G, 'utf8_next') or utf_ff; len = std.rawget(_G, 'utf8_len') or utf_len; char = std.rawget(_G, 'utf8_char') or utf_char; }; lev_thresh = 3; lev_ratio = 0.20; history = {}; persistent = std.list {}; winsize = 16 * 1024; history_len = 100; history_pos = 0; cursor = fmt.b("|"); prompt = "> "; ctrl = false; shift = false; alt = false; words = {}; parsed = {}; hints = {}; unknown = {}; multi = {}; token = {}; shortcut = {}; snapshot = false; reaction = false; redirect = false; msg = {}; mrd = mrd; args = {}; vargs = {}; debug = { trace_action = false }; completions = {}; event = false; xevent = false; aliases = {}; first = false; first_it = false; second_it = false; second = false; first_hint = ''; second_hint = ''; hint = { live = 'live', neuter = 'neuter', male = 'male', female = 'female', plural = 'plural', }; shorten = {}; shorten_expert = {}; _pager_mode = false; }; text = ''; -- dict = {}; } function mp:post_inp() if mp.autohelp then mp:compl_fill(mp:compl(mp.inp)) elseif mp.autocompl then mp:compl(mp.inp) end end function mp:onedit() end function mp:trim() if self.winsize == 0 then self.text = "" return end local text = self.text while text:len() > self.winsize do local text2 = text:gsub("^%^?[^%^]*%^%^", "") if text2 == text then break end text = text2 end self.text = text end function mp:key(key) if key == 'f1' and (self.togglehelp or DEBUG) then self.autohelp = not self.autohelp return true end if key == 'f6' and DEBUG then self:autoscript() return true end if key == 'left' then return self:inp_left() end if key == 'right' then return self:inp_right() end if key == 'up' then return self:key_history_prev() end if key == 'down' then return self:key_history_next() end if key == 'space' then -- local inp = mp:docompl(self.inp) -- if inp == self.inp then mp:inp_insert(' ') -- else -- self.inp = inp -- self.cur = self.inp:len() + 1 -- end return true end if key == 'tab' then self.inp = mp:docompl(self.inp) self.cur = self.inp:len() + 1 return true end if key == 'backspace' then if self:inp_remove() then return true end return true -- avoid scrolling end if key == 'enter' then return true end if use_text_event() then return false end if key:len() > 1 then return false end key = mp.shift and mrd.lang.kbd.shifted[key] or mrd.lang.kbd[key] or key if key then mp:inp_insert(key) return true end return false end function mp:inp_left() if self.cur > 1 then local i = mp.utf.bb(self.inp, self.cur - 1) self.cur = self.cur - i return true end end function mp:inp_right() if self.cur <= self.inp:len() then local i = mp.utf.ff(self.inp, self.cur) self.cur = self.cur + i return true end end function mp:inp_split() local pre = self.inp:sub(1, self.cur - 1); local post = self.inp:sub(self.cur); return pre, post end function mp:inp_insert(k) local pre, post = self:inp_split() self.cur = self.cur + k:len() self.inp = pre .. k .. post end function mp:inp_remove() local pre, post = self:inp_split() if not pre or pre == '' then return false end local i = mp.utf.bb(pre) self.inp = self.inp:sub(1, pre:len() - i) .. post self.cur = self.cur - i return true end function mp:esc(s) local rep = function(str) return fmt.nb(str) end if not s then return end local r = s:gsub("[<>]+", rep):gsub("[ \t]", rep); return r end local keys_en = { "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" } local function str_strip(str) return std.strip(str) end local function str_split(str, delim) local a = std.split(str, delim) for k, _ in ipairs(a) do a[k] = str_strip(a[k]) end return a end --- Returns true if parser mode is disabled function mp:noparser() return std.here().noparser or game.noparser end instead.get_inv = std.cacheable('inv', function(horiz) if mp:noparser() then return end local delim = instead.hinv_delim if not horiz then delim = instead.inv_delim end local pre, post = mp:inp_split() local ret = iface:bold(mp.prompt) .. mp:esc(pre)..mp.cursor..mp:esc(post) .. '\n' if mp.autohelp then ret = iface:xref(ret, mp, "") end if not mp.autohelp and not std.here().forcehelp then local r = std.call(std.here(), 'help') return ret .. (r or '') end if mp.autohelp_noverbs and mp.inp:find("^[ \t]*$") then return ret end delim = delim or ' | ' for _, v in ipairs(mp.completions) do local t = iface:xref(std.fmt(v.word), mp, v.word) if v.ob and have(v.ob) then t = iface:em(t) end if _ >= mp.autohelp_limit then ret = ret .. t .. ' ...' .. delim break end ret = ret .. t .. delim end if #mp.completions == 0 or mp.completions.eol then ret = ret .. iface:xref(mp.msg.enter or "", mp, "") .. delim if mp.completions.vargs then ret = ret .. iface:xref(mp.keyboard_space or "", mp, "") .. delim ret = ret .. iface:xref(mp.keyboard_backspace or "", mp, "") .. delim for _, v in ipairs(mp.keyboard or keys_en) do ret = ret .. iface:xref(v, mp, v, 'letter') .. delim end end end ret = ret:gsub(delim .."$", "") return ret end) --- Returns (add to) table with scope objects -- @param wh where to start scope -- @param oo table -- @param recurs recursive flag -- @param scope scope list (optional) function mp:objects(wh, oo, recurs, scope) wh:for_each(function(v) if v:disabled() then return nil, false end if v:visible() or (scope and scope:lookup(v)) then table.insert(oo, v) if scope and v.scope then if std.is_obj(v.scope, 'list') then scope:cat(v.scope) elseif type(v.scope) == 'function' then v:scope(scope) end end end if recurs == false or v:closed() then if std.is_obj(wh, 'list') then return -- just skip end return nil, false end if std.is_obj(wh, 'list') then self:objects(v, oo, recurs, scope) end end) end local darkness = std.obj { nam = '@darkness'; } function mp:nouns() local scope = self.scope scope:zap() if type(std.here().nouns) == 'function' then return std.here():nouns() end local oo = {} self:objects(std.me():inroom(), oo, true, scope) self:objects(std.list { std.me() }, oo, true, scope) self:objects(self.persistent, oo, true, scope) local ob = std.list {} if std.here().word then ob:add(std.here()) end if self:thedark() then ob:add(darkness) end self:objects(ob, oo, false, scope) local dups = {} scope:for_each(function(v) if not dups[v] then table.insert(oo, v) dups[v] = true end end) return oo end function mp.token.noun_obj(w) return mp.token.noun(w) end function mp.token.noun(w) local attr = w.morph or '' local oo local ww = {} if w.pat == '{noun_obj}' then local hint = str_split(w.morph, ",") local o = std.ref(hint[1]) oo = {} if o:visible() and not o:disabled() then table.insert(oo, o) mp:objects(o, oo) end else oo = mp.cache.nouns or {} end local sm_dup = { } local syms = {} for _, o in ipairs(oo) do local d = {} local r = o:noun(attr, d) for k, v in ipairs(d) do local hidden = (k ~= 1) or w.hidden if o:has 'multi' then hidden = w.hidden or (v.idx ~= 1) end if o == std.me() and mp.myself then for _, vm in ipairs(mp:myself(o, w.morph)) do table.insert(ww, { optional = w.optional, word = vm, morph = attr, ob = o, alias = o.alias, hidden = hidden }) end break else table.insert(ww, { optional = w.optional, word = r[k], ob = o, morph = attr, alias = v.alias, hidden = hidden }) end end if o == mp.first_it then table.insert(syms, 1, o) elseif o == mp.second_it then table.insert(syms, o) end end -- for k = 1, #syms do -- table.insert(oo, k, syms[k]) -- end oo = syms for _, o in ipairs(oo) do for _, v in ipairs(mp:synonyms(o, w.morph)) do if not sm_dup[v] then table.insert(ww, { optional = w.optional, word = v, ob = o, morph = attr, alias = o.alias, hidden = true, synonym = true }) sm_dup[v] = true end end end return ww end function mp.token.select(w) return mp.token.noun(w) end local norm_cache = { hash = {}, list = {}} function mp:norm(t) local key = t local cc = norm_cache.hash[t] if cc then return cc end t = mrd.lang.lower(mrd.lang.norm(t)):gsub("[ \t]+", " ") table.insert(norm_cache.list, 1, t) norm_cache.hash[key] = t local len = #norm_cache.list if len > 512 then cc = norm_cache.list[len] table.remove(norm_cache.list, len) norm_cache.hash[cc] = nil end return t end --- Check if two strings are equal, using two possible check modes. -- If lev is set, use it as Levenstein equality threshold. -- If not, normalize the strings and check equality. -- -- @see word_search -- @see mp:lookup_noun -- @param t1 first string -- @param t2 second string -- @param lev use levenstein or just normalize and compare function mp:eq(t1, t2, lev) if t1:find("%*$") then local t = t1:gsub("%*$", "") t = self:norm(t) return self:__startswith(t2, t) end if lev then local l = utf_lev(t1, t2) if l < lev and l / (utf_len(t1) + utf_len(t2)) <= self.lev_ratio then return l end return false end return self:norm(t1) == self:norm(t2) end local function starteq(t1, t2) if t2:len() >= t1:len() or mp.utf.len(t2) < mp.compare_len then return mp:eq(t1, t2) end t1 = t1:sub(1, t2:len()) return mp:norm(t1) == mp:norm(t2) end function mp:pattern(t, delim) local words = {} local pat = str_split(self:norm(t), delim or "|") for _, v in ipairs(pat) do local w = { } local ov = v if v:sub(1, 1) == '~' then v = v:sub(2) v = str_strip(v) w.hidden = true end if v:sub(1, 1) == '+' then v = v:sub(2) v = str_strip(v) w.optional = true w.default = true end if v:sub(1, 1) == '?' then v = v:sub(2) v = str_strip(v) w.optional = true end v = v:gsub("%+", " ") -- spaces if v:find("[^/]+/[^/]*$") then local s, e = v:find("/[^/]*$") w.morph = v:sub(s + 1, e) v = v:sub(1, s - 1) v = str_strip(v) end w.pat = v if v:find("^{[^}]+}$") then -- completion function v = v:gsub("^{", ""):gsub("}$", "") if type(self.token[v]) ~= 'function' then std.err("Wrong subst function: ".. v, 2); end local key = ov -- .. '/' .. (w.morph or '') local tok = self.cache.tokens[key] if not tok then tok = self.token[v](w) self.cache.tokens[key] = tok end while type(tok) == 'string' do tok = self:pattern(tok) end if type(tok) == 'table' then for _, xw in ipairs(tok) do table.insert(words, xw) end end else local vv = mp:pref_pattern(v) if #vv == 1 then w.word = v table.insert(words, w) else for _, exv in ipairs(vv) do local ww = std.clone(w) ww.word = exv table.insert(words, ww) end end end end return words end function mp:verb_find(tag, w) w = w or game for k, v in ipairs(w.__Verbs or {}) do if v.tag == tag then return v, k end end end function mp:verb_remove(tag, w) local v, k = self:verb_find(tag, w) if v then table.remove((w or game).__Verbs, k) end return v end function mp:pref_pattern(v) if not v:find("^%[[^%]]+%]") then return { v } end local _, e = v:find("]", 1, true) local pre = v:sub(2, e - 1) local post = v:sub(e + 1) pre = pre:gsub("^|", " |"):gsub("|$", "| "):gsub("||", "| |"); pre = str_split(pre, "|") local ret = {} for _, pref in ipairs(pre) do table.insert(ret, pref .. post) end return ret end function mp:verb(t, w, extend, extend_words) local rem w = w or game if type(t) ~= 'table' then std.err("Wrong 1-arg to mp:verb()", 2) end if type(w) ~= 'table' then std.err("Wrong 2-arg to mp:verb()", 2) end if not w.__Verbs then std.rawset(w, '__Verbs', {}) end local verb = {} local n = 1 if std.is_tag(t[1]) then verb.tag = t[1] rem = self:verb_remove(verb.tag, w) n = 2 end if extend and (not rem or not verb.tag) then std.err("Extending non existing verb "..verb.tag or '#Undefined', 2) end if extend then verb.verb = rem.verb verb.dsc = rem.dsc end if not extend or extend_words then if type(t[n]) ~= 'string' then std.err("Wrong verb pattern in mp:verb()", 2) end local verbs = self:pattern(mp.fmt(t[n]), ",") if extend then for _, v in ipairs(verbs) do table.insert(verb.verb, v) end else verb.verb = verbs end n = n + 1 if not extend then verb.dsc = {} end end if type(t[n]) ~= 'string' then if t[n] or not extend_words then std.err("Wrong verb descriptor mp:verb()", 2) end end while type(t[n]) == 'string' do local dsc = str_split(t[n], ":") local pat if #dsc == 1 then table.insert(verb.dsc, { pat = {}, ev = dsc[1] }) elseif #dsc == 2 then pat = str_split(dsc[1], ' ') if pat[1] == '~' then table.remove(pat, 1) for k, v in ipairs(pat) do pat[k] = v:gsub("[^ |]+", function(s) return "~" .. s end) end end table.insert(verb.dsc, { pat = pat, ev = dsc[2] }) else std.err("Wrong verb descriptor: " .. t[n]) end n = n + 1 end verb.hint = t.hint table.insert(w.__Verbs, 1, verb) return verb end function mp:verbs() local w = std.here().__Verbs or std.me().__Verbs or game.__Verbs or {} local t = {} for _, v in ipairs(w) do table.insert(t, v) end for _, v in ipairs(mp.__Verbs or {}) do table.insert(t, v) end return t end local function word_search(t, w, lev) local rlev w = str_split(w, inp_split) for k = 1, #t - #w + 1 do local found = true for i = 1, #w do local found2 = false for ii = k, k + #w - 1 do if type(lev) == 'function' then rlev = lev(w[i], t[ii]) else rlev = mp:eq(w[i], t[ii], lev) end if rlev then found2 = true break end end if not found2 then found = false break end end if found then return k, #w, rlev end end end function mp:lookup_short(words, w) local ret = { } for _,v in ipairs(words) do if self:__startswith(v, w) then table.insert(ret, { i = _, w = v, pos = #ret }) end end table.sort(ret, function(a, b) if a.w:len() == b.w:len() then return a.pos < b.pos end return a.w:len() < b.w:len() end) if #ret == 0 then return end return ret[1].i, 1 end --- Return false if unparsed word is critical in parse sense -- @param w table with skipped words function mp:skip_filter() return true end function mp:lookup_verb(words, lev) local ret = {} local w = self:verbs() for _, v in ipairs(w) do -- verbs local lev_v = {} for _, vv in ipairs(v.verb) do local verb = vv.word .. (vv.morph or "") local i, len, rlev i, len, rlev = word_search(words, verb, lev and self.lev_thresh) if not i and not lev and verb ~= vv.word then i, len = self:lookup_short(words, vv.word) end if i and i > 1 and not self:skip_filter({words[i - 1]}) then i = nil end if i then if lev then table.insert(lev_v, { lev = rlev, verb = v, verb_nr = i, verb_len = len, word_nr = _ } ) else local vc = std.clone(v) vc.verb_nr = i vc.verb_len = len vc.word_nr = _ table.insert(ret, vc) end end end if lev and #lev_v > 0 then table.sort(lev_v, function(a, b) if a.lev == b.lev then return a.word_nr < b.word_nr end return a.lev < b.lev end) lev_v[1].verb.verb_nr = lev_v[1].verb_nr lev_v[1].verb.verb_len = lev_v[1].verb_len lev_v[1].verb.word_nr = lev_v[1].word_nr lev_v[1].verb.lev = lev_v[1].lev table.insert(ret, lev_v[1].verb) end end if lev then table.sort(ret, function(a, b) if a.lev == b.lev then return a.word_nr < b.word_nr end return a.lev < b.lev end) elseif #ret > 0 then table.sort(ret, function(a, b) if a.verb_nr == b.verb_nr then return a.word_nr < b.word_nr end return a.verb_nr < b.verb_nr end) local vlev = ret[1].verb_nr local ret2 = {} for _, v in ipairs(ret) do if v.verb_nr == vlev then table.insert(ret2, v) else break end end ret = ret2 end return ret end local function tab_sub(t, s, e) local r = {} e = e or #t for i = s, e do table.insert(r, t[i]) end return r end --[[ local function tab_exclude(t, s, e) local r = {} e = e or #t for i = 1, #t do if i < s or i > e then table.insert(r, t[i]) end end return r end ]]-- function mp:docompl(str, maxw) local full local force = maxw local inp, pre = self:compl_ctx() if mp.utf.len(pre) < self.compl_thresh then return str end if not maxw then full = false local compl = self:compl(str) for _, v in ipairs(compl) do if not maxw then full = true maxw = v.word else local maxw2 = '' local utf_word = utf_chars(v.word) local utf_maxw = utf_chars(maxw) for k = 1, #utf_maxw do if mrd.lang.norm(utf_maxw[k]) == mrd.lang.norm(utf_word[k]) then maxw2 = maxw2 .. utf_maxw[k] else full = false break end end maxw = maxw2 end end if full then if #compl > 1 then full = false end end else full = true end if maxw and maxw ~= '' then str = inp .. maxw end if force or full then str = str .. ' ' end return str end function mp:__startswith(w, v) return w:find(v, 1, true) == 1 end function mp:startswith(w, v) return (self:norm(w)):find(self:norm(v), 1, true) == 1 end function mp:hint_verbs(v) if not v.tag then return true end if type(v.hint) == 'function' then return v:hint() end local r = true if std.here().hint_verbs_only then r = false for _, vv in ipairs(std.here().hint_verbs_only) do if v.tag == vv then r = true break end end return r end if game.hint_verbs then r = false for _, vv in ipairs(game.hint_verbs) do if v.tag == vv then r = true break end end end if r then return r end if std.here().hint_verbs then for _, vv in ipairs(std.here().hint_verbs) do if v.tag == vv then r = true break end end end return r end function mp:compl_verb(_) local poss = {} for _, v in ipairs(self:verbs()) do local filter = not self:hint_verbs(v) for _, vv in ipairs(v.verb) do local verb = vv.word .. (vv.morph or "") table.insert(poss, { word = verb, hidden = (_ ~= 1) or vv.hidden or filter}) end end return poss end function mp:animate(w) if w:has'animate' == false then return false end return w:has'animate' or (self.auto_animate and w:hint'live') end local function holded_by(ob, holder) if not holder then return true end local h = holder.ob if not h then return false end if ob:where() == h then return true end for _, hm in ipairs(holder.multi or {}) do if ob:where() == hm then return true end end return false end local function multi_held(ob, attrs) if not attrs.held and not attrs.scene then return true end if ((attrs.held and have(ob)) or (attrs.scene and not have(ob))) then return true end return false end local function multi_select(vv, attrs, holder) local res = {} local ob = vv.ob if holded_by(ob, holder) and multi_held(ob, attrs) then table.insert(res, ob) end for _, h in ipairs(vv.multi or {}) do if holded_by(h, holder) then ob = h if multi_held(ob, attrs) then table.insert(res, ob) end end end local dist = -1; ob = res[1] for _, v in ipairs(res) do local d = mp:distance(v) if d < dist or dist == -1 then ob = v dist = d end end return ob or vv.ob, res end function mp:compl_filter(v) local hidden = v.hidden if not hidden and v.ob and v.ob.hint_noun ~= nil then if type(v.ob.hint_noun) == 'function' then hidden = not v.ob:hint_noun(v) else hidden = not v.ob.hint_noun end end if hidden and self.compl_thresh == 0 then return false end local _, pre = self:compl_ctx() if mp.utf.len(pre) < self.compl_thresh then return false end if not v.ob or not v.morph then return true end local attrs = { held = false; scene = false; container = false; inside = false; enterable = false; edible = false; supporter = false; live = false, compass = false, } for _, h in ipairs(str_split(v.morph, ",")) do if attrs[h] ~= nil then attrs[h] = h end end for _, a in ipairs { 'container', 'enterable', 'supporter', 'edible' } do if attrs[a] and not v.ob:has(a) then return false end end if v.ob and mp:compass_dir(v.ob) and not attrs.compass then return false end if attrs.live and not self:animate(v.ob) then return false end if attrs.inside and not v.ob:has'container' and not v.ob:has'supporter' then return false end if not attrs.held and not attrs.scene then return true end if attrs.held and have(multi_select(v, attrs)) then return true end if attrs.scene and (not have(multi_select(v, attrs)) and v.ob ~= std.me()) then return true end return false end function mp:compl_fill(compl, eol, vargs) local ctx = self.completions.ctx self.completions = {} self.completions.ctx = ctx self.completions.eol = eol self.completions.vargs = vargs for _, v in ipairs(compl) do if self:compl_filter(v) then table.insert(self.completions, v) end end end function mp:compl_reset() self.completions = { ctx = {} } end function mp:compl_ctx_current() local ctx = self.completions.ctx local new = {} for _, v in ipairs(ctx) do if v.inp == '' and self.inp == '' then break end if self:startswith(self.inp, v.inp) then table.insert(new, v) else break end end self.completions.ctx = new end function mp:compl_ctx_push(poss) if #poss == 0 then return end local ctx = self.completions.ctx table.insert(ctx, poss) local top = #ctx ctx[top].inp = self.inp end function mp:compl_ctx() local ctx = self.completions.ctx local top = #ctx if top == 0 then return self.inp, '' end local inp = self.inp local ctx_inp = ctx[top].inp local _, e = inp:find(ctx_inp, 1, true) local pre = '' if e then pre = inp:sub(e + 1) end return ctx[top].inp, pre end function mp:compl_ctx_poss() local ctx = self.completions.ctx local top = #ctx local res = {} if top == 0 then return res end ctx = ctx[top] local _, pre = self:compl_ctx() for _, v in ipairs(ctx) do if self:__startswith(v.word, pre) or self:__startswith(self:norm(v.word), pre) then table.insert(res, v) end end return res end function mp:compl(str) local words = str_split(self:norm(str), inp_split) local poss local ret = {} local dups = {} local eol local e = str:find(" $") local vargs collectgarbage("stop") self:compl_ctx_current(); poss = self:compl_ctx_poss() if (#poss == 0 and e) or #words == 0 then -- no context if #words == 0 or (#words == 1 and not e) then -- verb? poss, eol = self:compl_verb(words) local oo = self:nouns() -- and hidden nouns for _, o in ipairs(oo) do local ww = {} o:noun(ww) for _, n in ipairs(ww) do local hidden = true if o.raw_word then hidden = n.alias ~= 1 and not o:has'multi' end table.insert(poss, { word = n.word, hidden = hidden }) end end else -- matches self.cache.nouns = self:nouns() poss, eol, vargs = self:compl_match(words) end self:compl_ctx_push(poss) end local _, pre = self:compl_ctx() for _, v in ipairs(poss) do if v.word == '*' then vargs = true end if self:startswith(v.word, pre) and not v.word:find("%*$") then if not dups[v.word] then dups[v.word] = v table.insert(ret, v) else local o = dups[v.word] if o.hidden then o.hidden = v.hidden end if v.ob then if o.ob then o.multi = o.multi or {} table.insert(o.multi, v.ob) else o.ob = v.ob end end end end end table.sort(ret, function(a, b) return a.word < b.word end) collectgarbage("restart") return ret, eol, vargs end local function lev_sort(t) for _, v in ipairs(t) do v.i = _ end table.sort(t, function(a, b) if a.lev == b.lev then return a.i < b.i end return a.lev > b.lev end) local lev = t[1] and t[1].lev -- local fuzzy = t[1] and t[1].fuzzy local fuzzy = {} for _, v in ipairs(t) do if v.lev ~= lev then break end if v.fuzzy then table.insert(fuzzy, v) end end if #fuzzy > 0 then t = fuzzy t.fuzzy = true end local res = { lev = 0, match = #t > 0 and t[1].match } local dup = {} for _, v in ipairs(t) do if v.lev ~= lev then break end res.lev = lev if v.word then if not dup[v.word] then table.insert(res, v.word) dup[v.word] = true end else for _, vv in ipairs(v) do if not dup[vv] then table.insert(res, vv) dup[vv] = true end end end end return res end function mp:compl_match(words) -- local verb = { words[1] } local verbs = self:lookup_verb(words) -- table.remove(words, 1) -- remove verb local matches = {} local hints = {} local res = {} local multi collectgarbage("stop") for _, v in ipairs(verbs) do local m, h, _, mu = self:match(v, words, true) if #m > 0 then table.insert(matches, { verb = v, match = m[1] }) end if #h > 0 then table.insert(hints, h) end multi = multi or (#mu > 0) end collectgarbage("restart") hints = lev_sort(hints) if multi then -- #matches > 0 or #hints == 0 or multi then return res end for _, v in ipairs(hints) do if #matches > 0 and #matches[1].match > hints.lev then return res, false, not not matches[1].match.vargs end local pat = self:pattern(v) for _, p in ipairs(pat) do table.insert(res, p) end end if #hints == 0 and #matches > 0 then return res, true, not not matches[1].match.vargs end return res, #matches > 0 end function mp:match(verb, w, compl) local matches = {} local found local hints = {} local unknown = {} local multi = {} local vargs local parsed_verb = {} local fixed_verb = verb.verb[verb.word_nr] fixed_verb = fixed_verb.word .. (fixed_verb.morph or '') table.insert(parsed_verb, fixed_verb) for _, d in ipairs(verb.dsc) do -- verb variants -- local was_noun = false local match = { args = {}, vargs = {}, ev = d.ev, wildcards = 0, verb = parsed_verb, defaults = 0 } local a = {} found = (#d.pat == 0) for k, v in ipairs(w) do if k < verb.verb_nr or k >= verb.verb_nr + verb.verb_len then table.insert(a, v) end end local skip = {} local all_optional = true local rlev = 1 local need_required = false local default = false for lev, v in ipairs(d.pat) do -- pattern arguments if v == '*' or v == '~*' then vargs = true -- found v = '*' end local noun = not not v:find("^~?{noun}") local pat = self:pattern(v) -- pat -- possible words local best = #a + 1 local best_len = 1 local word local required = false found = false local wildcard = false for _, pp in ipairs(pat) do -- single argument if v == '*' then break end required = not pp.optional if not pp.optional then need_required = true all_optional = false end if pp.default then word = pp.word default = true end local new_wildcard local k, len = word_search(a, pp.word) if not k and mp.compare_len > 0 then k, len = word_search(a, pp.word, starteq) new_wildcard = true else new_wildcard = false end if k and ((k < best or len > best_len) or (not new_wildcard and wildcard and k <= best and len >= best_len)) then wildcard = new_wildcard best = k word = pp.word found = pp best_len = len if pp.synonym or word:find("%*$") then -- subst word = found.ob:noun(found.morph, found.alias) wildcard = true end end end if found then need_required = false if found.ob then local exact for _, pp in ipairs(pat) do if pp.ob and pp.ob ~= found.ob and self:eq(found.word, pp.word) then if not found.multi then found.multi = {} end if not exact and pp.ob:noun(found.morph, pp.alias) == pp.word then -- excactly match exact = pp.ob table.insert(found.multi, found.ob) else table.insert(found.multi, pp.ob) end if found.ob:noun(found.alias) ~= pp.ob:noun(pp.alias) then table.insert(multi, { word = pp.ob:noun(pp.alias), lev = rlev }) end end end if exact then found = std.clone(found) found.ob = exact multi = {} end if #multi > 0 and found.multi then table.insert(multi, 1, { word = found.ob:noun(found.alias), lev = rlev }) found = false break end end if vargs then for i = 1, best - 1 do table.insert(match.vargs, a[i]) table.insert(match, a[i]) end if #match.vargs == 0 then -- * in the pattern center found = false break end rlev = rlev + 1 vargs = false end -- if false then -- a = tab_exclude(a, best, best + best_len - 1) -- else -- if not was_noun then for i = 1, best - 1 do table.insert(skip, a[i]) end -- end a = tab_sub(a, best + best_len) -- table.remove(a, 1) -- end table.insert(match, word) table.insert(match.args, found) if wildcard then match.wildcards = match.wildcards + 1 end rlev = rlev + 1 -- was_noun = not not found.ob elseif vargs then if lev == #d.pat then -- last? if #a == 0 then need_required = true end while #a > 0 do table.insert(match.vargs, a[1]) table.insert(match, a[1]) table.remove(a, 1) end else need_required = need_required or required end if not need_required then found = true else found = false if #a > 0 or #match.vargs > 0 then table.insert(hints, { word = v, lev = rlev }) else table.insert(hints, { word = '*', lev = rlev }) end end if not found then break end elseif required then for i = 1, best - 1 do table.insert(unknown, { word = a[i], lev = rlev, noun = noun }) end if best <= 1 and #skip > 0 then for i = 1, #skip do table.insert(unknown, { word = skip[i], lev = rlev, skip = true }) end end if not compl and mp.errhints then for _, pp in ipairs(pat) do -- single argument if mp.utf.len(pp.word) >= 3 then local k, _ = word_search(a, pp.word, self.lev_thresh) if k then table.insert(hints, { word = pp.word, lev = rlev, fuzzy = true, match = match }) end end end end table.insert(hints, { word = v, lev = rlev, match = match }) break else if word then table.insert(match, word) if default then match.defaults = match.defaults + 1 end end table.insert(match.args, { word = false, optional = true } ) -- table.insert(hints, { word = v, lev = rlev }) found = true end end -- if #multi > 0 then -- matches = {} -- break -- end if found or all_optional then match.extra = (#a ~= 0) table.insert(match, 1, fixed_verb) -- w[verb.verb_nr]) if self:skip_filter(skip) then table.insert(matches, match) end if #match.vargs == 0 and not vargs then match.vargs = false end end end for k, v in ipairs(matches) do v.nr = k --[[ print("-----------", k) for kk, vv in ipairs(v) do print(vv) end ]]-- end table.sort(matches, function(a, b) local na, nb = #a - a.defaults, #b - b.defaults if na == nb and a.wildcards == b.wildcards then return a.nr < b.nr end if na == nb then return a.wildcards < b.wildcards end return na > nb end) if #matches > 0 and matches[1].extra then local lev = #matches[1] if #unknown > 0 then for _, v in ipairs(unknown) do if v.lev >= lev and not v.skip then -- and v.noun then matches = {} break end end end if #multi > 0 and #matches > 0 then for _, v in ipairs(multi) do if v.lev >= lev and not v.skip then matches = {} break end end end end --[[ if #unknown > 0 then local nmatches = {} for _, v in ipairs(matches) do if not v.extra then table.insert(nmatches, v) end end matches = nmatches end end if false then print "MATCHES: " for _, v in ipairs(matches) do for _, vv in pairs(v) do print(_, vv) end end -- for _, v in ipairs(hints) do -- for _, vv in ipairs(hints) do -- print(vv.word, vv.fuzzy, vv.lev) -- end -- end end ]]-- hints = lev_sort(hints) unknown = lev_sort(unknown) multi = lev_sort(multi) if #hints > 0 and #unknown > 0 then if hints.lev > unknown.lev then unknown = {} elseif hints.lev < unknown.lev then hints = {} end end return matches, hints, unknown, multi end local function get_events(self, ev) local events = {} self.aliases = {} -- self.first_it = false -- self.second_it = false self.multi = {} for _, v in ipairs(ev) do local ea = str_split(v) local e = ea[1] local args = {} table.remove(ea, 1) local reverse = false for _, vv in ipairs(ea) do if vv == 'reverse' then reverse = true end end local attrs = {} local holder for k, vv in ipairs(self.args) do if vv and std.is_obj(vv.ob) then attrs[k] = {} for _, h in ipairs(str_split(vv.morph, ",")) do attrs[k][h] = true if h == 'holder' then holder = vv end end end end for k, vv in ipairs(self.args) do if vv and std.is_obj(vv.ob) then local ob, m = multi_select(vv, attrs[k], holder) self.multi[ob] = m if reverse then table.insert(args, 1, ob) else table.insert(args, ob) end self.aliases[ob] = vv.alias end end if self.vargs and #self.vargs > 0 then local varg = '' for _, vv in ipairs(self.vargs) do if varg ~= '' then varg = varg .. ' ' end varg = varg .. vv end table.insert(args, varg) end if std.is_obj(args[1]) then self.first_it = args[1] end if std.is_obj(args[2]) then self.second_it = args[2] end table.insert(events, { ev = e, args = args }) end return events end --- Take a value or run the function --- returns two valuses. retval and true if method was called -- @param wh what -- @param fn function function mp:runorval(wh, fn, ...) if wh[fn] == nil then return nil, false end if type(wh[fn]) == 'function' then local v local r = wh[fn](wh, ...) if r == false then v = false else v = true end return r, v end return wh[fn], true end function mp:call(ob, ev, ...) -- self.event = ev for _, v in ipairs({ob, ...}) do if self.aliases[v] then std.rawset(v, '__word_alias', self.aliases[v]) end end -- self.reaction = std.pget() or false local r, v = std.call(ob, ev, ...) -- std.cctx().txt = self.reaction self.reaction = self.reaction or v or false if self.debug.trace_action and v then dprint("mp:call ", ob, ev, ...) p("mp:call ", ob, " ", ev, " ") for _, t in ipairs {...} do pr (tostring(t), " ") end pn() end for _, a in ipairs(self.aliases) do std.rawset(a, '__word_alias', nil) end return r, v end function mp:events_call(events, oo, t) if not t then t = '' else t = t .. '_' end for _, o in ipairs(oo) do for _, e in ipairs(events) do self.event = e.ev local meta = self.event and self.event:find("Meta", 1, true) local ename = t .. e.ev local eany = t .. 'Any' local edef = t .. 'Default' local ob = o self.first = std.is_obj(e.args[1]) and e.args[1] self.second = std.is_obj(e.args[2]) and e.args[2] self.first_hint = self.first and self.first:gram().hint self.second_hint = self.second and self.second:gram().hint if o == 'obj' then ob = e.args[1] table.remove(e.args, 1) end local r, v if (meta and o == mp) or (not meta and std.is_obj(ob) and (o ~= 'obj' or ob ~= std.here())) then r, v = self:call(ob, eany, e.ev, std.unpack(e.args)) if r then std.pn(r) end if not v then if ob[ename] then r, v = self:call(ob, ename, std.unpack(e.args)) if r then std.pn(r) end else r, v = self:call(ob, edef, e.ev, std.unpack(e.args)) if r then std.pn(r) end end end end if r then pn() end if o == 'obj' then table.insert(e.args, 1, ob) end if v and t ~= 'post_' then return v end end end return false end function mp:__action(events) local r self.reaction = false self.redirect = false r = self:events_call(events, { mp, game, std.here(), 'obj' }, 'before') if not r then r = self:events_call(events, { 'obj', std.here(), game, mp }) if not r then self:events_call(events, { 'obj', std.here(), game, mp }, 'after') end end if not self.redirect then self:events_call(events, { 'obj', std.here(), game, mp }, 'post') end end function mp:save_ctx() return { first = self.first, second = self.second, first_hint = self.first_hint, second_hint = self.second_hint, event = self.event; xevent = self.xevent } end function mp:restore_ctx(ctx) self.first, self.second = ctx.first, ctx.second self.first_hint, self.second_hint = ctx.first_hint, ctx.second_hint self.event = ctx.event self.xevent = ctx.xevent end --- Execute a method -- @usage mp:runmethods('before', 'LetGo', wh, w) function mp:runmethods(t, verb, ...) local events = { {ev = verb, args = { ... }}} local ctx = self:save_ctx() self.xevent = self.event local r, v = self:events_call(events, { 'obj' }, t) self:restore_ctx(ctx) self.reaction = false return r, v end --- Execute a new sequence without terminating the current one function mp:subaction(verb, ...) local events = { {ev = verb, args = { ... }}} local ctx = self:save_ctx() self.xevent = self.event local r, v = self:__action(events) self:restore_ctx(ctx) self.reaction = false return r, v end --- Switch the sequence to a new event function mp:xaction(verb, ...) local events = { {ev = verb, args = { ... }}} local r, v = self:__action(events) self.redirect = true return r, v end function mp:action() local parsed = self.parsed local ev = str_split(parsed.ev, "|") local events = get_events(self, ev) self:__action(events) end function mp:correct(inp) if self:comment() then return end local rinp = '' for _, v in ipairs(self.parsed) do if rinp ~= '' then rinp = rinp .. ' ' end rinp = rinp .. v end local cmprinp = rinp:gsub("["..inp_split.."]+", " ") if not self:eq(cmprinp, inp) then pn(fmt.em("("..rinp..")")) end end function mp:log(t) if mp.logfile then t = std.fmt(t) local f = io.open(mp.logfile, "a+b") if not f then return end f:write((t or '').."\n\n") f:close() end end function mp:show_prompt(inp) if std.cmd[1] == 'look' then return false end if std.here():has 'cutscene' or std.here():has 'noprompt' or player_moved() or std.abort_cmd then return false end if self.prompt then pn(fmt.b(self.prompt .. inp)) end return true end function mp:comment() if self.inp:find("^[ \t]*%*") then return true end end --- Main parse function. Input goes here function mp:parse(inp) inp = std.strip(inp) if self.expert_mode and not self:comment() and not std.here():type'dlg' then local multi_inp = str_split(inp, ".\r\n") or {} inp = multi_inp[1] or inp for i = 2, #multi_inp do self:push(multi_inp[i]) end end mp:log("> "..inp) local noprompt = not mp:show_prompt(inp) inp = inp:gsub("[ ]+", " "):gsub("["..inp_split.."]+", " "):gsub("[ \t]+$", "") local r, v if self:comment() then r = false v = nil else self.cache.nouns = self:nouns() r, v = self:input(self:norm(inp)) end self.cache = { tokens = {} }; -- to completion if not r then if v then -- pn() self:err(v) local s = std.game s:reaction(std.pget()) r = s:display(false) s:lastdisp(r) std.abort_cmd = true return r end else if std.cmd[1] ~= 'look' and not noprompt then self:correct(inp) end -- here we do action mp:action() end mp:post_action() end std.world.display = function(s, state) local l, av, pv if mp.text == '' and game:time() == 1 and state ~= false then local r = std.call(game, 'dsc') if type(r) == 'string' then mp.text = r .. '^^' end end if mp.clear_on_move and game:time() ~= 1 then if player_moved() then mp:clear() end end -- mp:trim() local reaction = s:reaction() or nil if reaction == "" then reaction = nil end if state then -- reaction = iface:em(reaction) av, pv = s:events() av = iface:em(av) pv = iface:em(pv) if s.player:need_scene() then -- t = iface:bold(std.titleof(stead.here())) l = s.player:look() -- objects [and scene] end end l = std.par(std.scene_delim, reaction or false, av or false, l or false, pv or false) or '' mp:log(l) if mp._pager_mode then mp.text = mp.text .. fmt.anchor() .. l .. '^^' -- .. fmt.anchor() else mp.text = mp.text .. l .. '^^' -- .. fmt.anchor() end return mp.text end function mp:pager_mode(mode) self._pager_mode = not not mode if std.ref'@theme' then std.ref'@theme'.set ('win.scroll.mode', mode and 1 or 3) end end function mp:completion(word) self.inp = self:docompl(self.inp, word) self.cur = self.inp:len() + 1 self:compl_fill(self:compl(self.inp)) end function mp:key_history_prev() if #self.history == 0 then return end self.history_pos = self.history_pos + 1 if self.history_pos > #self.history then self.history_pos = #self.history end self.inp = self.history[self.history_pos] self.cur = self.inp:len() + 1 return true end function mp:key_history_next() if #self.history == 0 then return end self.history_pos = self.history_pos - 1 if self.history_pos < 1 then self.history_pos = 0 self.inp = '' self.cur = 1 return true end self.inp = self.history[self.history_pos] self.cur = self.inp:len() + 1 return true end --- Add input string to auto execution mode -- @param cmd input string function mp:push(cmd) if not cmd then return false end table.insert(self.pushed, cmd) end --- Returns true if some auto command is pending function mp:autoplay_pending() return self.autoplay or #self.pushed > 0 or self.autoplay_command end --- Internal function that gets commands from script or pushed commands -- @see mp:push function mp:autoplay_inp() local auto_inp = false self.autoplay_command = false if #self.pushed > 0 then auto_inp = table.remove(self.pushed, 1) elseif not self.autoplay then return false end self.inp = auto_inp or self.autoplay:read("*line") or false if not self.inp then self.inp = '' self.autoplay:close() self.autoplay = false else if not auto_inp then dprint("> ", self.inp) end self.autoplay_command = true end return true end function mp:key_enter() if self:noparser() then return end if not mp:autoplay_pending() and (#self.history == 0 or self.history[1] ~= self.inp) and std.strip(self.inp) ~= '' then table.insert(self.history, 1, self.inp) end self.history_pos = 0 if #self.history > self.history_len then table.remove(self.history, #self.history) end self:compl_reset(); local r, v = std.call(mp, 'parse', self.inp) self.inp = ''; --[[ if std.here():has'cutscene' then self.inp = mp.cutscene.default_Verb or '' if self.inp ~= '' then self.inp = self.inp .. ' ' end end ]]-- self:autoplay_inp() self.cur = self.inp:len() + 1; mp:post_inp() -- self:completion() return r, v end function mp:lookup_noun(w, lev) local k, len local res = {} local oo = self.cache.nouns for _, o in ipairs(oo) do local ww = {} o:noun(ww) for _, d in ipairs(ww) do k, len = word_search(w, d.word, lev) if k and len == #w then d.ob = o table.insert(res, d) break end end end if #res == 0 then return res end local uniq = {} local same for _, v in ipairs(res) do local t = v.ob:noun(v.alias) if not uniq[t] then uniq[t] = v else same = true end end res = {} for _, v in pairs(uniq) do table.insert(res, v) end table.sort(res, function(a, b) return a.word:len() > b.word:len() end) if same then res = { res[1] } end self.aliases = {} for _, o in ipairs(res) do self.aliases[o.ob] = o.alias end return res end function mp:shorten_input(w) if #w < 1 or std.here():type 'dlg' then return end local str if #w == 1 and self.shorten[w[1]] then str = self.shorten[w[1]] end if self.expert_mode and not str and self.shorten_expert[w[1]] then str = self.shorten_expert[w[1]] end if not str then return end local t = str_split(str, inp_split) table.remove(w, 1) for i, v in ipairs(t) do table.insert(w, i, v) end end function mp:input(str) -- self.cache = { tokens = {} }; local hints = {} local unknown = {} local multi = {} self.hints = hints self.unknown = unknown self.multi = multi if (self.default_Verb or std.here().default_Verb) and str == "" then str = std.here().default_Verb or self.default_Verb end if type(mp.pre_input) == 'function' then str = mp:pre_input(str) if not str then return false end end local w = str_split(str, inp_split) mp:shorten_input(w) self.words = w if #w == 0 then return false, "EMPTY_INPUT" end local ob = self:lookup_noun(w) local verbs = {} if #ob == 0 then verbs = self:lookup_verb(w) end if #verbs == 0 then -- match object? if #ob > 1 then self.multi = {} for _, v in ipairs(ob) do table.insert(self.multi, v.ob:noun(v.alias)) end return false, "MULTIPLE" end if #ob == 0 then -- try fuzzy if not mp.errhints then return false, "UNKNOWN_VERB" end ob = self:lookup_noun(w, self.lev_thresh) if #ob >= 1 then for _, v in ipairs(ob) do table.insert(self.hints, v.word) end return false, "UNKNOWN_WORD" end return false, "UNKNOWN_VERB" end -- it is the object! if ob[1].ob.default_Event then w = std.call(ob[1].ob, 'default_Event') else w = self.default_Event or "Exam" end -- pn() mp.first_it = ob[1].ob self:xaction(w, ob[1].ob) -- verbs = self:lookup_verb(w) -- if #verbs == 0 then -- return false, "UNKNOWN_VERB" -- end return end local matches = {} for _, v in ipairs(verbs) do local m, h, u, mu = self:match(v, w) if #m > 0 then table.insert(matches, { verb = v, match = m[1] }) end if #h > 0 then table.insert(hints, h) end if #u > 0 then table.insert(unknown, u) end if #mu > 0 then table.insert(multi, mu) end end table.sort(matches, function(a, b) return #a.match > #b.match end) hints = lev_sort(hints) unknown = lev_sort(unknown) multi = lev_sort(multi) local mlev = #matches > 0 and #matches[1].match or 0 if (hints.lev or 0)> mlev or (unknown.lev or 0) > mlev then matches = {} end if #matches == 0 then self.hints = hints self.unknown = unknown self.multi = multi if #multi > 0 then self.multi = multi return false, "MULTIPLE" end return false, "INCOMPLETE" end self.parsed = matches[1].match self.args = self.parsed.args self.vargs = self.parsed.vargs or {} return true end function MetaVerb(t) return mp:verb(t, mp) end function Verb(t, w) return mp:verb(t, w) end function VerbExtend(t, w) return mp:verb(t, w or false, true) end function VerbExtendWord(t, w) return mp:verb(t, w or false, true, true) end function VerbRemove(t, w) return mp:verb_remove(t, w) end function VerbHint(t, fn, w) local v = mp:verb_find(t, w) if not v then dprint("Unknown verb: " .. tostring(t)) return end if v then v.hint = fn end end std.rawset(_G, 'mp', mp) std.mod_cmd( function(cmd) if mp.snapshot then snapshots:write(mp.snapshot) mp.snapshot = false end if cmd[1] == '@metaparser' then mp.inp = cmd[2] or '' cmd[1] = '@mp_key' cmd[2] = 'enter' end if cmd[2] == '@metaparser' then if cmd[3] == '' then mp.inp = ''; mp.cur = 1; mp:onedit() mp:compl_fill(mp:compl(mp.inp)) return true, false elseif cmd[3] == '' then return mp:key_enter() end if cmd[3] == '' then mp:inp_insert(' ') mp:onedit() mp:compl_fill(mp:compl(mp.inp)) return true, false end if cmd[3] == '' then mp:inp_remove() mp:onedit() mp:compl_fill(mp:compl(mp.inp)) return true, false end if cmd[4] == 'letter' then mp.inp = mp.inp .. cmd[3] mp.cur = mp.inp:len() + 1 else mp:completion(cmd[3]) end mp:onedit() return true, false end if (cmd[1] == '@mp_key' and cmd[2] == 'enter') or cmd[1] == 'look' then mp:trim() if not std.game.__started and cmd[1] == 'look' then std.game:__start() end if mp:noparser() then return true, false end -- mp.inp = mp:docompl(mp.inp) local r, v, n repeat if n then std.abort_cmd = false std.me():moved(false) std.me():need_scene(false) end r, v = mp:key_enter(cmd[1] == 'look') n = true until not mp:autoplay_pending() or mp:noparser() mp:onedit() return r, v end if cmd[1] ~= '@mp_key' then return end mp:onedit() return true, false end) function mp:autoscript(w) if self.autoplay then self.autoplay:close() end self.autoplay = io.open(w or 'autoscript') or false if self.autoplay then self:MetaTranscriptOn(); std.cmd = { 'autoscript' } return true end return false end std.mod_init( function() if DEBUG and mp.undo == 0 then mp.undo = 5 end _'game'.__daemons = std.list {} end) function mp:init(lang) if type(std.SOURCES_DIRS) == 'table' then mrd.dirs = std.SOURCES_DIRS end if type(std.DICT_FILE) == 'string' then mrd.dict_file = std.DICT_FILE end table.insert(mrd.dirs, curdir) mrd:init(lang) cutscene = mp.cutscene gameover = mp.gameover door = mp.door end std.mod_start(function() mp:compl_reset() mp:compl_fill(mp:compl("")) -- if instead.text_input then instead.text_input(true) end end, 2) instead.mouse_filter(0) -- speedup undo local obusy = std.busy local busy_count = 0 function std.busy() busy_count = busy_count + 1 if (busy_count % 100) == 0 then obusy() end end function instead.fading() return instead.need_fading() or player_moved() end instead.notitle = true instead.noways = true local opr = std.pr local function shortcut(ob, hint) return ob:noun(hint) end function mp.shortcut.where(hint) return shortcut(std.me():where(), hint) end function mp.shortcut.firstwhere(hint) return shortcut(mp.first:where(), hint) end function mp.shortcut.secondwhere(hint) return shortcut(mp.second:where(), hint) end function mp.shortcut.here(hint) return shortcut(std.here(), hint) end function mp.shortcut.first(hint) return shortcut(mp.first, hint) end function mp.shortcut.firstit(hint) return mp.first:it(hint) end function mp.shortcut.second(hint) return shortcut(mp.second, hint) end function mp.shortcut.me(hint) return shortcut(std.me(), hint) end mp.msg.verbs = {} function mp.shortcut.verb(hint) local verb = mp.msg.verbs[hint] if not verb then return hint end return mp.shortcut.word(verb) end function mp:shortcut_obj(ob) if ob == '#first' then ob = mp.first elseif ob == '#second' then ob = mp.second elseif ob == '#firstwhere' then ob = mp.first:where() elseif ob == '#secondwhere' then ob = mp.second:where() elseif ob == '#me' then ob = std.me() elseif ob == '#where' then ob = std.me():where() elseif ob == '#here' then ob = std.here() else ob = false end return ob end local function hint_append(hint, h) if h == "" or not h then return hint end if hint == "" or not hint then return h end return hint .. ',' .. h end function mp.shortcut.word(hint) local w = str_split(hint, ",") if #w == 0 then return hint end local verb = w[1] table.remove(w, 1) hint = '' for _, k in ipairs(w) do if k == '#first' then hint = hint_append(hint, mp.first_hint) elseif k == '#second' then hint = hint_append(hint, mp.second_hint) elseif k:find("#", 1, true) == 1 then local ob = mp:shortcut_obj(k) if not ob then std.err("Wrong shortcut word: "..k, 2) end hint = hint_append(hint, ob:gram().hint) else hint = hint_append(hint, k) end end local t = mp.mrd:noun(verb .. '/' .. hint) return t end function mp.shortcut.if_hint(hint) local w = str_split(hint, ",") if #w < 3 then return hint end local attr = w[2] local ob = w[1] ob = mp:shortcut_obj(ob) if not ob then std.err("Wrong object in if_has shortcut: "..hint, 2) end if not ob:hint(attr) then return w[4] or '' end return w[3] or '' end function mp.shortcut.if_has(hint) local w = str_split(hint, ",") if #w < 3 then return hint end local attr = w[2] local ob = w[1] ob = mp:shortcut_obj(ob) if not ob then std.err("Wrong object in if_has shortcut: "..hint, 2) end if not ob:has(attr) then return w[4] or '' end return w[3] or '' end function mp.fmt(...) local args = {} for _, v in ipairs({...}) do local finish if type(v) == 'string' then repeat finish = true v = v:gsub("{#[^{}]*}", function(w) local ww = w w = w:gsub("^{#", ""):gsub("}$", "") local hint = w:gsub("^[^/]*/?", "") w = w:gsub("/[^/]*$", "") local cap = mp.mrd.lang.is_cap(w) w = w:lower() if mp.shortcut[w] then w = mp.shortcut[w](hint) if cap then w = mp.mrd.lang.cap(w) end else std.err("Wrong shortcut: ".. ww, 2) end finish = false return w end) until finish end table.insert(args, v) end local ret for i = 1, #args do ret = std.par('', ret or false, std.tostr(args[i])); end return ret end function std.pr(...) local ctx = std.cctx() if not ctx or not ctx.self then return opr(...) end return opr(mp.fmt(...)) end function std.obj:persist() self:attr 'persist' mp.persistent:add(self) return self end function std.obj:hint(hint) return self:gram()[mrd.lang.gram_t[hint] or hint] end --- Return a pronoun for the object (capitalized). -- @see mp:it function mp:It(w, hint) local t = self:it(w, hint) return mp.mrd.lang.cap(t) end --- Return a pronoun for the object. -- This is language-dependent. -- @param hint pronoun case -- @see mp:it function std.obj:it(hint) if mp.it then return mp:it(self, hint) else if self:hint'plural' then return 'they' elseif self:hint'female' then return 'she' elseif self:hint 'male' then return 'he' else return "it" end end end function std.obj:It(hint) local t = self:it(hint) return mp.mrd.lang.cap(t) end function mp:traceinside(w, fn) local ww = w and w.obj or std.here().obj while #ww > 0 do local nww = {} for _, o in ipairs(ww) do local r, v = fn(o) if r ~= nil then return r end if v ~= false then for _, vv in ipairs(o.obj) do table.insert(nww, vv) end end end ww = nww end end --- Trace an object upwards (check every parent) -- @param w where -- @param fn function function mp:trace(w, fn) local ww = {} w:where(ww) while #ww > 0 do local nww = {} for _, o in ipairs(ww) do local r, v = fn(o) if r ~= nil then return r end if v ~= false then o:where(nww) end end ww = nww end end function std.obj:attr(str) local a = str_split(str, ", ") for _, v in ipairs(a) do local val = (v:find("~", 1, true) ~= 1) v = v:gsub("^~", "") self['__attr__' .. v] = val end return self end function std.obj:hasnt(attr) return not self:has(attr) end function std.obj:has(attr) attr = std.strip(attr) local val = (attr:find("~", 1, true) ~= 1) attr = attr:gsub("^~", "") if val then return self['__attr__' .. attr] else return not self['__attr__' .. attr] end end function iface:title(t) return(iface:bold( mrd.lang.cap(t))) end