-- What the tutorial scenes share: the keys a reader presses, pressed the same way, and the two -- checks every tutorial ends on. A tutorial in docs/Forge.adoc names keys; its scene presses -- them through here, so the text and the test cannot drift apart without the sweep saying so. -- -- A scene sets TUTORIAL (its number and name) before loading this, and calls tutorialBegin to -- open a description, tutorialShot to take a named picture, and tutorialEnd to check the result -- against the shipped description and run the built game. FORGE_LIBRARY = true dofile("Forge/Forge.singe") local font = fontLoad("Singe/FreeSansBold.ttf", 15) fontSelect(font) fontQuality(FONT_QUALITY_BLENDED) overlaySetResolution(720, 480) TUTORIAL_OUT = singeGetDataPath() -- The engine hands onKeyPressed two integers; so does this. function key(code, character) forgeKey(character or 0, code.value) end -- Types text as a person would, one character at a time. function typeText(text) for at = 1, #text do forgeKey(string.byte(text, at), 0) end end -- ENTER until the field is the one being edited. Answers whether it was reached. local function reach(field) local guard = 0 while (FORGE.lastField ~= field) and (guard < 40) do key(SCANCODE.RETURN) guard = guard + 1 end if FORGE.lastField ~= field then debugPrint(string.format("%s FAIL the form never reached %s", TUTORIAL.tag, field)) return false end return true end -- ESC out of the form: off a list first, then off the field it left being typed. local function leave() if FORGE.picker ~= nil then key(SCANCODE.ESCAPE) end if FORGE.editing ~= nil then key(SCANCODE.ESCAPE) end end -- Fills in one field of the form ENTER walks: ENTER until the field is the one being edited, -- type the value, ENTER to keep it, ESC to leave the form. Exactly the keys the text names. -- A field with a list narrows it to the value and takes it, which moves on to the next field. function form(field, value) if not reach(field) then return end if FORGE.picker ~= nil then pick(value) else typeText(tostring(value)) key(SCANCODE.RETURN) end leave() end -- Empties one field of the form: ENTER until it is the one being edited, DELETE, ESC. function clear(field) if not reach(field) then return end if FORGE.picker ~= nil then pick("(none)") else key(SCANCODE.DELETE) end leave() end -- Narrows the picker by typing and takes the first match. function pick(name) typeText(name) key(SCANCODE.RETURN) end -- Selects the entity with an id, in the entities panel. function selectEntity(id) for index, entry in ipairs(forgeRoom().entities) do if entry.id == id then forgeSelect(index) return end end debugPrint(string.format("%s FAIL no entity called %s", TUTORIAL.tag, id)) end -- Puts the panel on a mode, by TAB as a reader would. function panel(mode) local guard = 0 while (FORGE.mode ~= mode) and (guard < 6) do key(SCANCODE.TAB) guard = guard + 1 end end -- A picture for the book, drawn now and named. function tutorialShot(name) singeScreenshot(string.format("%02d-%s", TUTORIAL.number, name)) end -- Opens the tutorial's working copy: a fresh New game, or the previous tutorial's shipped -- description, so each starts where the last left off. function tutorialBegin(fromShipped) local work = TUTORIAL_OUT .. TUTORIAL.name .. ".game" if fromShipped then if not authorCopy("Forge/tutorials/" .. fromShipped .. ".game", work) then debugPrint(TUTORIAL.tag .. " FAIL could not copy " .. fromShipped) singeQuit() end else authorSave(forgeTemplate(), work) end if not forgeBegin(work) then debugPrint(TUTORIAL.tag .. " FAIL could not open " .. work) singeQuit() end return work end -- The two checks: what the keys made equals the shipped description, compiled; and the game it -- makes runs. A shipped description that is not there yet is written, so a new tutorial's scene -- produces the file the book ships, and the next run checks against it. function tutorialEnd() local shipped = "Forge/tutorials/" .. TUTORIAL.name .. ".game" local built forgeSave() -- The compiled header names the description's file, which a release has set and a shipped -- copy has not; the comparison is of the game, not of where it came from. FORGE.game.source = nil if lfs.attributes(shipped, "mode") ~= "file" then authorCopy(FORGE.path, TUTORIAL_OUT .. TUTORIAL.name .. ".shipped.game") debugPrint(TUTORIAL.tag .. " WROTE " .. TUTORIAL_OUT .. TUTORIAL.name .. ".shipped.game -- copy it to " .. shipped) else local mine = authorCompile(FORGE.game) local theirs = authorCompile(authorLoad(shipped)) if mine == theirs then debugPrint(TUTORIAL.tag .. " the keys made the shipped description") else local out = assert(io.open(TUTORIAL_OUT .. TUTORIAL.name .. ".mine.singe", "w")) out:write(mine) out:close() out = assert(io.open(TUTORIAL_OUT .. TUTORIAL.name .. ".theirs.singe", "w")) out:write(theirs) out:close() debugPrint(TUTORIAL.tag .. " FAIL the keys made something else; see " .. TUTORIAL_OUT .. TUTORIAL.name .. ".mine.singe") end end built = forgeBuild(TUTORIAL_OUT .. TUTORIAL.name .. ".singe") forgeClose() return built end