Skip to content

Commit

Permalink
feat(vm): export read stats
Browse files Browse the repository at this point in the history
  • Loading branch information
gudzpoz committed Jun 5, 2024
1 parent b4da10b commit 7998854
Show file tree
Hide file tree
Showing 7 changed files with 185 additions and 59 deletions.
7 changes: 3 additions & 4 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,9 @@ Markdown compiler implementation:
- [ ] Save & load.
- [X] API.
- [ ] Examples? Or detailed Lua documentation.
- [ ] Fast-forward (skipping only texts that users have read).
- [ ] Instead, we should probably export read counts along with the lines
to let the user / the outer game engine decide whether to skip lines
(or even display some relevant text / highlighting).
- [X] Export read counts along with the lines to let the user
/ the outer game engine decide whether to skip lines
(or even display some relevant text / highlighting).
- [ ] Complete porting The Intercept, which is a little bit lengthy.

### `v0.4.0`
Expand Down
78 changes: 65 additions & 13 deletions vm/spec/vm_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ local utils = require("spec.test_utils")
local StackedEnv = require("mdvm.stacked_env")
local TablePath = require("mdvm.table_path")

assert:set_parameter("TableFormatLevel", -1)

--- @param root table
local function wrap(root)
local vm = brocatel.VM._new({
Expand Down Expand Up @@ -127,8 +129,8 @@ describe("VM", function()
local get = assert(vm.env:get("GET"))
assert.is_nil(get(args, "k"))
assert.equal("v", get(args:resolve(nil, nil, 2, "args"), "k"))
local visited = assert(vm.env:get("VISITS"))
assert.equal(1, visited(vm.env:get("a")))
local visits = assert(vm.env:get("VISITS"))
assert.equal(1, visits(vm.env:get("a")))
assert.error(function() vm.env:set("GET", 0) end)
assert.no_error(function() vm.env:set("_GET", 0) end)
end
Expand All @@ -138,8 +140,8 @@ describe("VM", function()
assert.same({ "Hello", "end" }, utils.gather_til_end(vm))
assert.same({
main = {
{ I = 1, R = { 0xe } },
{ args = { { I = 1, k = "v", R = { 2 } }, { { I = 1, R = { 2 } } } } },
{ I = 1, R = { 0xe } },
{ args = { { I = 1, k = "v" }, { { I = 1, R = { 2 } } } } },
}
}, vm.savedata.stats)
end)
Expand Down Expand Up @@ -253,38 +255,42 @@ describe("VM", function()
local output = assert(vm:next())
assert.same({
tags = true,
visited = false,
select = {
{ key = 2, option = { tags = true, text = "Selection #1" } },
{ key = 5, option = { tags = true, text = "Selection #3" } },
{ key = 6, option = { tags = true, text = "Selection #4" } },
{ key = 2, option = { tags = true, text = "Selection #1", visited = false } },
{ key = 5, option = { tags = true, text = "Selection #3", visited = false } },
{ key = 6, option = { tags = true, text = "Selection #4", visited = false } },
},
}, output)
assert.equal("Result #3", vm:next(5).text)
assert.equal("Hello", vm:next().text)

assert.same({
tags = true,
visited = true,
select = {
{ key = 2, option = { tags = true, text = "Selection #1" } },
{ key = 6, option = { tags = true, text = "Selection #4" } },
{ key = 2, option = { tags = true, text = "Selection #1", visited = false } },
{ key = 6, option = { tags = true, text = "Selection #4", visited = false } },
},
}, vm:next())
assert.equal("Result #4", vm:next(6).text)
assert.equal("Hello", vm:next().text)
assert.same({
tags = true,
visited = true,
select = {
{ key = 2, option = { tags = true, text = "Selection #1" } },
{ key = 6, option = { tags = true, text = "Selection #4" } },
{ key = 2, option = { tags = true, text = "Selection #1", visited = false } },
{ key = 6, option = { tags = true, text = "Selection #4", visited = true } },
},
}, vm:next())
assert.equal("Result #4", vm:next(6).text)
assert.equal("Hello", vm:next().text)

assert.same({
tags = true,
visited = true,
select = {
{ key = 2, option = { tags = true, text = "Selection #1" } },
{ key = 2, option = { tags = true, text = "Selection #1", visited = false } },
},
}, vm:next())
assert.equal("Result #1", vm:next(2).text)
Expand Down Expand Up @@ -312,7 +318,53 @@ describe("VM", function()
assert.is_nil(vm:next())
end)

it("too many jumps", function ()
it("text read count", function()
local vm
vm = wrap({
{
labels = {
first = { 2 },
}
},
"Text1",
"Text2",
{
{},
"Text3",
"Text4",
{
---@param args TablePath
func = function(args) vm.env:get("FUNC").S_ONCE(args) end,
args = {
{},
{ {}, "Selection #1", "Result #1" },
{ {}, { function() return vm.env:get("RECUR")(1) end,
{ {}, "Selection #2" } }, "Result #2" },
},
},
},
{
link = { "first" }, root_node = "main",
},
})
for i = 1, 4 do
assert.same({ tags = true, text = "Text" .. tostring(i), visited = false }, vm:next())
end
assert.same({ tags = true, visited = false, select = {
{ key = 2, option = { tags = true, text = "Selection #1", visited = false } },
{ key = 3, option = { tags = true, text = "Selection #2", visited = false } },
}}, vm:next())
assert.same({ tags = true, text = "Result #2", visited = false }, vm:next(3))
for i = 1, 4 do
assert.same({ tags = true, text = "Text" .. tostring(i), visited = true }, vm:next())
end
assert.same({ tags = true, visited = true, select = {
{ key = 2, option = { tags = true, text = "Selection #1", visited = false } },
{ key = 3, option = { tags = true, text = "Selection #2", visited = true } },
}}, vm:next())
end)

it("too many jumps", function()
local vm = wrap({
{
labels = {
Expand Down
39 changes: 26 additions & 13 deletions vm/src/mdvm/env_api.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ local utils = require("mdvm.utils")
---@param self brocatel.VM
return function (self)
local env = self.env
local ip = assert(self:_get_coroutine()).ip
local lua = env:get_lua_env()
lua.IP = ip
lua.VM = self

local function get_ip()
return assert(self:_get_coroutine()).ip
end
env:set_api("IP", get_ip, true)
env:set_api("GET", function(path, key)
path = self.env.is_label(path) and assert(self:lookup_label(path)) or path
return history.get(self.savedata.stats, path, key)
Expand All @@ -27,24 +28,24 @@ return function (self)
end)

env:set_api("END", function(path)
local IP = assert(env:get("IP"))
local ip = get_ip()
-- For calls like `END()` or `END(true)`
if not path or type(path) == "boolean" then
-- Tries to return to the calling subroutine.
if not path and self:pop_stack_frame() then
return
end
-- Otherwise (or when `END(true)` is called), terminates the story execution.
IP:set(TablePath.from({ IP[1] }))
ip:set(TablePath.from({ ip[1] }))
return
end
-- For calls like `END({ "label", "path_name" })`, it breaks that array (which is probably a loop or something).
path = self.env.is_label(path) and assert(self:lookup_label(path)) or path
assert(path:is_parent_to(IP))
assert(path:is_parent_to(ip))
local root = assert(self:_ensure_root(path))
path = path:copy()
path:step(root)
IP:set(path)
ip:set(path)
end)

env:set_api("EVAL", function(path, extra_env)
Expand All @@ -56,7 +57,7 @@ return function (self)
--- @param path table|TablePath
local function visits(path)
path = self.env.is_label(path) and assert(self:lookup_label(path)) or path
return history.get(self.savedata.stats, path, "I") or 0
return history.get_recorded_count(self.savedata.stats, path)
end
env:set_api("VISITS", visits)
env:set_api("VISITED", function(path) return visits(path) > 0 end)
Expand All @@ -67,8 +68,9 @@ return function (self)
local current = self.savedata.current
local counts = history.get(self.savedata.stats, args, "S") or {}
local root = assert(self:_ensure_root(args))
local ip = get_ip()
if current.input then
env:get("IP"):set(args:copy():resolve(current.input, 3))
ip:set(args:copy():resolve(current.input, 3))
local count = counts[current.input] or 0
count = count + 1
counts[current.input] = count
Expand All @@ -77,7 +79,7 @@ return function (self)
return
end

env:get("IP"):set(args:copy():resolve(nil))
ip:set(args:copy():resolve(nil))
recur = recur or 0
assert(recur == true or recur >= 0)
local selectables = {} --- @type Selectable[]
Expand Down Expand Up @@ -106,10 +108,16 @@ return function (self)
end
})
end
local line, tags = self:eval_with_env(local_env, args:copy():resolve(i), utils.get_keys(inner))
local path = args:copy():resolve(i)
local line, tags = self:eval_with_env(
local_env,
path,
utils.get_keys(inner)
)
if line and should_recur and tags then
local visited = counts[i] and counts[i] > 0 or false
selectables[#selectables + 1] = {
option = { text = line, tags = tags },
option = { text = line, tags = tags, visited = visited },
key = i,
}
end
Expand All @@ -118,7 +126,12 @@ return function (self)
ip:step(root)
return nil, true
end
current.output = { select = selectables, tags = true }
self.flags.redirected = true -- Stop _fetch_and_next from incrementing ip
current.output = {
select = selectables,
tags = true,
visited = history.get_recorded_count(self.savedata.stats, args:copy():resolve(nil)) > 0,
}
end
env:set_api("FUNC", {
SELECT = user_select,
Expand Down
47 changes: 33 additions & 14 deletions vm/src/mdvm/history.lua
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ end
--- Fetches a bit in a bit set.
--- @param bitset table<number, number>
--- @param index number
--- @return boolean bit the bit
function history.get_bit(bitset, index)
local offset = (index - 1) % bits_per_number
local i = (index - 1 - offset) / bits_per_number + 1
Expand All @@ -62,7 +63,7 @@ end
--- @param save table
--- @param path TablePath
--- @param key string
--- @returns string|number|boolean|nil
--- @return string|number|boolean|table|nil
function history.get(save, path, key)
for _, segment in ipairs(path) do
save = save[segment]
Expand All @@ -73,7 +74,7 @@ function history.get(save, path, key)
local meta = save[1]
if meta then
assert(type(meta) == "table")
return meta[key]
return meta[assert(key)]
end
return nil
end
Expand All @@ -91,7 +92,8 @@ function history.set(save, root, path, key, value)
local node = path:get(root, 1)
assert(node and node.func)
else
assert(path:is_array(root))
local is_array = path:is_array(root)
assert(is_array, tostring(path))
end
for _, segment in ipairs(path) do
save = makedir(save, segment)
Expand All @@ -107,12 +109,37 @@ end
--- Records the path change.
---
--- - For text nodes, it simply marks the line as read.
--- @param save table
--- @param root table
--- @param path TablePath
function history.record_simple(save, root, path)
local old = path:copy()
local i = old[#old]
if type(i) ~= "number" then
return
end
if not old:get(root) or not old:is_array(root, 1) then
return
end

old[#old] = nil
local reads = history.get(save, old, "R")
if type(reads) ~= "table" then
reads = {}
history.set(save, root, old, "R", reads)
end
old[#old + 1] = i
history.set_bit(reads, i)
end

--- Records the path change.
---
--- - For arrays (probably with labeled ones), it increments their visited counter.
--- @param save table
--- @param root table
--- @param old TablePath|nil
--- @param new TablePath
function history.record_simple(save, root, old, new)
function history.record_move(save, root, old, new)
if old and old:equals(new) then
return
end
Expand All @@ -137,15 +164,6 @@ function history.record_simple(save, root, old, new)
if not old or meta.I == 0 then
meta.I = meta.I + 1
end

if i < #new then
local read = meta.R
if not read then
read = {}
meta.R = read
end
history.set_bit(read, new[i + 1])
end
end
i = i + 1
end
Expand All @@ -154,6 +172,7 @@ end
--- Returns the visited count.
--- @param save table
--- @param path TablePath
--- @return number count the visited count
function history.get_recorded_count(save, path)
local is_array, node = path:is_array(save)
if is_array then
Expand All @@ -163,7 +182,7 @@ function history.get_recorded_count(save, path)
is_array, parent = path:is_array(save, 1)
if is_array then
local bitset = assert(parent)[1].R
return bitset and history.get_bit(bitset, path[#path]) or 0
return bitset and (history.get_bit(bitset, path[#path]) and 1) or 0
end
return 0
end
Expand Down
2 changes: 2 additions & 0 deletions vm/src/mdvm/savedata.lua
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ local TablePath = require("mdvm.table_path")
---
--- @field tags table<string, string>|boolean|nil tags
--- @field text string|nil the translated and interpolated text
--- @field visited boolean whether the text has been visited

--- @class Selectable
---
Expand All @@ -38,6 +39,7 @@ local TablePath = require("mdvm.table_path")
---
--- @field tags table<string, string>|boolean|nil tags
--- @field text string|nil the translated and interpolated text
--- @field visited boolean whether the text or the select has been visited
--- @field select Selectable[]|nil the selectable options

--- @class IOCache
Expand Down
Loading

0 comments on commit 7998854

Please sign in to comment.