Readthrough mode is now available for any author to use! Currently the code is adv3 only, but I hope to get a Lite version too (or find an amazing soul that wants to do the conversion).
[Edit: it’s now available]
Readthrough mode is intended for two main purposes.
One: if you are bringing in new testers and you only need them to test later parts of the game, this mode will help them get briefed on the rest of your story in the process of whisking them to the point where you need the work.
Two: bringing players in to your (finished) game who might otherwise never try it because of its length, its puzzle difficulty, their puzzle disinterestedness, or general parserphobia.
The mode seems to me to be a particularly favorable parser introduction to those who are leery of or daunted by parser games, because not only can they see the game play out its commands without effort on their part, they can also dabble freely with entering their own commands with no risk, since the next time they enter a blank command the readthrough takes over again.
I have posted about this previously, but for those who missed it, this is a mode where a player can launch the game, and simply hit the Enter key (with a blank command line) to watch the game play the next step. They can proceed all the way through the game in this manner. However, the game allows the player to enter their own commands as well, with the understanding that the next time a blank command is entered, their autonomous actions will all be undone and the readthrough will proceed from where it left off. They can, however, exit the mode entirely at any point in the readthrough sequence, in which case they can play autonomously from the state that the game was left in. This could be particularly helpful for testers.
‘Undo’ can get a little squirrelly in certain situations, so some issues may turn up there, but I hope someone might give it a whirl!
It should be able to be incorporated into any existing adv3 game. All the author needs to do is supply a list of command strings (which is probably most easily accomplished by using the RECORD feature built into TADS… the code can load the strings from a RECORD file) and perhaps determine how they want to present the option of launching the game in readthrough mode versus normal mode.
If you have randomization in your game, and you need to forcibly move characters or set data, you can include anonymous functions in your step list, which will be executed directly before the following command.
// in a RECORD file: a string that will be compiled by DynamicFunc
>east
function { igor.moveIntoForTravel(igorsLair); }
>ask igor about himself
// OR steps explicitly typed: can use an actual function pointer
['east',
function { igor.moveIntoForTravel(igorsLair); },
'ask igor about himself' ]
The code will be posted below, but I’ll give an overview of what the code is made of in order to make it work.
We create a readthru
object, to store various data pertaining to the mode. Most of the principal mechanics occur in a modification of readMainCommandTokens
. Undo causes lots of weird situations so we need to modify UndoAction
. libGlobal
needs a couple of mods, and we need some verbs for starting the mode, and also for leaving it if the player chooses to continue autonomously from a given point. We also need to add one change to executeAction
to have control over whether the library calls savepoint()
or not.
// Your project needs to #include dynfunc.t
//convenience functions
savepointOn() { libGlobal.suspendUndoSave = nil; }
savepointOff() { libGlobal.suspendUndoSave = true; }
modify libGlobal
readthruMode = nil
suspendUndoSave = nil
;
readthru: PreinitObject
// if this is nil, it is to be assumed that the author has
// manually typed the list of command strings in the [steps] property
loadStepsFromFile = true
stepFileName = 'rthSteps.txt'
verticalSpaceAfterCmd = '<.p>' //users may prefer '\n' or something custom
lookAfterUndo = nil
//* any initial tokens not found in this list will cause the mode to suspend
// savepoint(), and will revert the game to the state at the last readthrough
//step the next time there is an empty command or an explicit 'undo'
excludeTokens = ['save','restore','restart','hint','script']
//* optional text to display describing the mode the player is entering
infoBlurb = ""
//* this property could potentially be filled out as a list of single-quote strings,
// but most likely an author will just play the game in RECORD mode
// and the game will load this property from the resulting file
steps = static new Vector(1200)
//* this method should be overridden to determine how the game starts. You might
// have a sort of welcome screen, or a single interactive prompt upon game start that
// asks which mode to play in... at any rate, this method will be called after the player
// enters 'readthrough', so you need to make sure the playerChar ends up in the
// starting loc and gets a lookAround, situated ready to execute the first command
// in the readthrough steps
transitionToGame { }
// this should be defined as a condition, based on the way you present the opening
// of your game. Perhaps (gPlayerChar.isIn(welcomeScreen)) or !gRevealed('tag').
// Or you could explicitly set this to nil in transitionToGame
gameNotBegun = true
execute() {
if(!loadStepsFromFile) return;
local f = File.openTextFile(stepFileName,FileAccessReadWriteKeep);
local line;
while((line = f.readFile()) != nil) {
if(!line.startsWith('\>') && !line.startsWith('function')) continue;
steps.append(line);
}
f.closeFile();
}
launch() {
if(gameNotBegun) {
libGlobal.readthruMode = true;
infoBlurb;
// you may wish to call a pause here
transitionToGame;
}
else "<<if libGlobal.readthruMode>>Readthrough mode is currently running...
<<else>>Readthrough mode can only be launched at game startup... ";
}
stepCt = 1 //* the next step waiting to be executed
needsUndo = nil
needsDecr = nil
scriptedUndo = nil
lastCmd = ''
shownSaveNotice = nil
// infoBlurb = "\ \b\b\b<.p>Welcome to the readthrough! We wish to brief you on the fundamentals of experiencing the game in this mode. <.p>To advance, all you have to do is leave the command line blank and hit [Enter]. You could proceed through the whole game in this manner. However, if at any time you want to take the liberty of entering your own commands, to see what would happen if such-and-such, you can do so. In fact you can do so for as long as you please, but be aware that the next time you hit [Enter] with a blank command line, the game will undo all of your autonomous decisions, and continue from where it left in the auto-play sequence. As a further \"however\", if you use auto-play to reach a certain point in the game and then you decide you want to continue autonomously, you may enter READTHROUGH OFF, after which point you will not be returned to the auto-play sequence. You cannot reenter readthrough mode from the middle of a game, but you may save readthrough games where they are in order to branch off in normal play mode. <<inputManager.pauseForMore(true)>><.p>One final note is that the readthrough is not meant to be the same as a blazing-fast ultra-minimal walkthrough. It plays more so like a real player, who would have to examine things to get clues, come back to certain locations later after they\'ve figured out what they need to do there, and maybe do a few silly things for fun. <.p>With that, we proceed! <<inputManager.pauseForMore(true)>>"
;
DefineIAction(Readthrough)
actionTime = 0
execAction() { readthru.launch; } ;
VerbRule(Readthrough) ('start'|'begin'|) ('slideshow'|'slide''show'|'slides'|'auto''play'|'autoplay'|'auto-play'|'readthrough'|'readthru') : ReadthroughAction
;
DefineIAction(ReadthroughOff)
actionTime = 0
execAction { libGlobal.readthruMode = nil;
"We have exited auto-play mode. You can continue the game from here with complete autonomy. You will not be able to reenter auto-play mode without restarting it from the beginning. If you wish to remain in auto-play mode, or wish to save the auto-play game before entering autonomous mode, enter UNDO now. "; } ;
VerbRule(ReadthroughOff) (('slideshow'|'slide''show'|'slides'|'auto''play'|'autoplay'|'auto-play'|'readthrough') ('mode'|) 'off') | ('leave'|'end'|'quit'|'stop')('slideshow'|'slide''show'|'slides') ('mode'|) : ReadthroughOffAction ;
replace readMainCommandTokens(which) {
local str; local toks;
for (;;) {
if(readthru.scriptedUndo) {
readthru.scriptedUndo = nil;
str = ''; //* we just came from performUndo and want to bypass getting input to execute the next readthrough step
}
else str = readMainCommand(which);
str = StringPreParser.runAll(str, which);
if (str == nil) return nil;
try toks = cmdTokenizer.tokenize(str);
catch (TokErrorNoMatch tokExc) {
gLibMessages.invalidCommandToken(tokExc.curChar_.htmlify());
continue;
}
if (toks.length() != 0) {
if(libGlobal.readthruMode && !readthru.excludeTokens.indexOf(toks[1][3].toLower()) ) {
if(toks[1][3].toLower()=='undo') {
if(libGlobal.suspendUndoSave) ; //* just return the tokens: we'll jump back to the last game state in the auto-play sequence
else readthru.needsDecr = true; //* They want to back up the actual readthrough step: need extra measures so that the step counter doesn't go out of sync
}
else { //* They entered a regular command during readthrough mode. Save the state now and then turn off savepoint so they can enter commands at will, and still be returned to the auto-play sequence
readthru.needsUndo = true;
if(!libGlobal.suspendUndoSave) savepoint();
savepointOff();
}
}
return [str, toks];
}
//* We have an empty command string. Special handling for readthrough mode; otherwise show the standard emptyCommandResponse
else if(libGlobal.readthruMode) {
if(readthru.needsUndo) undo();
readthru.needsUndo = nil;
savepointOn();
local str = readthru.steps[readthru.stepCt];
//* process any functions in the step list, and then get the next command
while(dataType(str)==TypeFuncPtr || dataType(str)==TypeSString && str.startsWith('function')) {
if(dataType(str)==TypeSString) str = Compiler.compile(str);
str();
str = readthru.steps[++readthru.stepCt];
}
//* if we used RECORD to make our step list, our string will be prefixed with '>' and suffixed with '\n'. Remove these characters before sending the command to the parser.
if(readthru.loadStepsFromFile) str = str.substr(2,-1);
//* print comments without taking an action cycle
if(str.startsWith('*')) { "<b><<str>></b>";
++readthru.stepCt;
continue; }
else {
str = StringPreParser.runAll(str, which);
if(str==nil) return nil;
readthru.lastCmd = str;
"<b>> <<str>></b><<readthru.verticalSpaceAfterCmd>>";
toks = cmdTokenizer.tokenize(str);
if(toks.length) {
++readthru.stepCt;
return [str,toks];
}
}
}
gLibMessages.emptyCommandResponse();
}
}
modify UndoAction
performUndo(asCommand) {
//* we need to store some values before we perform undo()
local s = libGlobal.suspendUndoSave;
local nd = readthru.needsDecr;
//* Users: feel free to sophisticate this. I didn't want to go through all the rex parsing that would be necessary to separate individual commands out of something like '> north. east. south. enter cave' and keep the stepCt in sync
if(readthru.lastCmd.find('.')) { "<.p>Apologies... we cannot perform an undo after a string of multiple commands. ";
return nil; }
if (undo()) {
PostUndoObject.classExec();
local oldActor = gActor; local oldIssuer = gIssuingActor; local oldAction = gAction;
gActor = gPlayerChar; gIssuingActor = gPlayerChar; gAction = self;
try {
if(!s && !libGlobal.readthruMode) gLibMessages.undoOkay(libGlobal.lastActorForUndo, libGlobal.lastCommandForUndo);
else if(libGlobal.readthruMode) {
//* A player entered 'undo' after some autonomous commands
if(s) "<.p>We now return to the last auto-play step. <.p>";
//* A player has been using the readthrough sequence but wants to back up one
else "<.p>Undone. \b";
if(nd) {
--readthru.stepCt;
readthru.needsDecr = nil;
}
if(!asCommand) readthru.scriptedUndo = true;
}
if(!libGlobal.readthruMode || readthru.lookAfterUndo) libGlobal.playerChar.lookAround(true);
}
finally { gActor = oldActor; gIssuingActor = oldIssuer; gAction = oldAction; }
if (asCommand) AgainAction.saveForAgain(gPlayerChar, gPlayerChar, nil, self);
savepointOn();
return true; }
else {
gLibMessages.undoFailed();
return nil; }
}
;
modify executeAction(targetActor, targetActorPhrase,issuingActor, countsAsIssuerTurn, action){
local rm, results;
startOver:
rm = GlobalRemapping.findGlobalRemapping(issuingActor, targetActor, action);
targetActor = rm[1];
action = rm[2];
results = new BasicResolveResults();
results.setActors(targetActor, issuingActor);
try {
action.resolveNouns(issuingActor, targetActor, results);}
catch (RemapActionSignal sig){
sig.action_.setRemapped(action);
action = sig.action_;
goto startOver; }
if (action.includeInUndo
&& action.parentAction == nil
&& (targetActor.isPlayerChar()
|| (issuingActor.isPlayerChar() && countsAsIssuerTurn))
&& !libGlobal.suspendUndoSave) { // ADDED
libGlobal.lastCommandForUndo = action.getOrigText();
libGlobal.lastActorForUndo = (targetActorPhrase == nil ? nil : targetActorPhrase.getOrigText());
savepoint(); }
if (countsAsIssuerTurn && !action.isConversational(issuingActor)) {
issuingActor.lastInterlocutor = targetActor;
issuingActor.addBusyTime(nil,issuingActor.orderingTime(targetActor));
targetActor.nonIdleTurn();}
if (issuingActor != targetActor
&& !action.isConversational(issuingActor)
&& !targetActor.obeyCommand(issuingActor, action)) {
if (issuingActor.orderingTime(targetActor) == 0)
issuingActor.addBusyTime(nil, 1);
action.saveActionForAgain(issuingActor, countsAsIssuerTurn,targetActor, targetActorPhrase);
throw new TerminateCommandException(); }
action.doAction(issuingActor, targetActor, targetActorPhrase,countsAsIssuerTurn); }
PreSaveObject
execute {
if(libGlobal.suspendUndoSave) {
if(libGlobal.readthruMode && !readthru.shownSaveNotice) {
say('<.p>You have entered autonomous commands since the last readthrough step. We regrettably cannot save autonomous changes if the game is to stay in readthrough mode. If you wish to save your autonomous changes, you can first enter READTHROUGH OFF, and then SAVE... just note that you will be unable to reenter readthrough mode from that point. If you wish to save the game in readthrough mode, enter SAVE again, but be aware that we will undo the game to the state it was in at the last readthrough step. <.p>');
readthru.shownSaveNotice = true;
}
else if(libGlobal.readthruMode) "<.p> We return the game to the readthrough sequence before saving... <.p>";
else "<.p>Undoing to the last savepoint before saving the game... <.p>";
undo();
savepointOn();
}
}
;
P.S. If you use a RECORD file, manually remove any meta commands that appear while turning it on or off… and make sure the file ends with a final blank line, because the code as it is trims the last character off of every line, expecting a \n. And of course, put the file in the same folder as your t3 file, so it can find it at compile time/preinit. The file shouldn’t be needed once the game is built.