/* * $Id: InputFunctions.java 536 2008-02-19 06:03:27Z weiju $ * * Created on 12/22/2005 * Copyright 2005-2008 by Wei-ju Wu * This file is part of The Z-machine Preservation Project (ZMPP). * * ZMPP is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * ZMPP is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with ZMPP. If not, see <http://www.gnu.org/licenses/>. */ package org.zmpp.vm; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import org.zmpp.base.Memory; import org.zmpp.encoding.ZCharDecoder; import org.zmpp.encoding.ZsciiEncoding; import org.zmpp.encoding.ZsciiString; import org.zmpp.encoding.ZsciiStringBuilder; import org.zmpp.encoding.ZsciiStringTokenizer; /** * This class contains functions that deal with user input. * * @author Wei-ju Wu * @version 1.0 */ public class InputFunctions implements InputLine { private CommandHistory history; private Machine machine; private static final ZsciiString WHITESPACE = new ZsciiString(new char[]{' ', '\n', '\t', '\r'}); /** * This class represents the interrupt method in timed input. */ public class InterruptThread extends Thread { /** * The interval in milliseconds. */ private int time; /** * The packed routine address. */ private int routineAddress; /** * The input buffer. */ private List<Character> inputbuffer; /** * Status variable. */ public boolean running; public InterruptThread(int time, int routineAddress, List<Character> inputbuffer) { this.time = time; this.routineAddress = routineAddress; this.inputbuffer = inputbuffer; this.running = true; } public synchronized boolean isRunning() { return running; } public synchronized void terminate() { running = false; // note: interrupt() can only be executed in a signed applet interrupt(); try { join(); } catch (Exception ex) { ex.printStackTrace(System.err); } } public void run() { final Output output = machine.getOutput(); while (isRunning()) { // We sleep for the given time try { Thread.sleep(time); } catch (InterruptedException ex) { } if (isRunning()) { displayCursor(false); final short retval = machine.getCpu().callInterrupt(routineAddress); if (retval == 1) { machine.getInput().getSelectedInputStream().cancelInput(); break; } // REDISPLAY INPUT HERE // We need to find out if the routine has printed anything to // the screen if yes, the input needs to be redisplayed if (inputbuffer != null && machine.getCpu().interruptDidOutput()) { for (char zsciiChar : inputbuffer) { output.printZsciiChar(zsciiChar, false); } } output.flushOutput(); displayCursor(true); } } } } /** * Constructor. * * @param machine the machine object */ public InputFunctions(Machine machine) { this.machine = machine; this.history = new CommandHistory(this); } // ********************************************************************* // ****** SREAD/AREAD - the most complex and flexible function within the // ****** Z-machine. This function takes input from the user and // ****** calls the tokenizer for lexical analysis. It also recognizes // ****** terminator characters and controls the output as well as // ****** calling an optional interrupt routine. // ********************************************************************* /** * {@inheritDoc} */ public char readLine(final int textbuffer, final int time, final int routineAddress) { machine.getOutput().flushOutput(); displayCursor(true); // Using a synchronized list final List<Character> inputbuffer = Collections.synchronizedList(new ArrayList<Character>()); history.reset(); final int pointer = checkForPreviousInput(textbuffer, inputbuffer); // Timed input final InterruptThread thread = startInterruptThread(routineAddress, time, inputbuffer); final char terminateChar = doInputLoop(textbuffer, pointer, inputbuffer); storeInput(inputbuffer, terminateChar); terminateInterruptThread(thread); displayCursor(false); return handleTerminateChar(terminateChar); } /** * {@inheritDoc} */ public int deletePreviousChar(final List<Character> inputbuffer, final int pointer) { // Decrement the buffer pointer int newpointer = pointer; if (inputbuffer.size() > 0) { final char deleteChar = inputbuffer.remove(inputbuffer.size() - 1); newpointer--; machine.getOutput().deletePreviousZsciiChar(deleteChar); } return newpointer; } /** * {@inheritDoc} */ public int addChar(final List<Character> inputbuffer, final int textbuffer, final int pointer, final char zsciiChar) { // Do not include the terminator in the buffer // Note: we convert ASCII characters to lower case to allow the // transcription of umlauts int newpointer = pointer; final Memory memory = machine.getGameData().getMemory(); final ZsciiEncoding encoding = machine.getGameData().getZsciiEncoding(); memory.writeUnsignedByte(textbuffer + newpointer, (short) encoding.toLower(zsciiChar)); inputbuffer.add(zsciiChar); newpointer++; machine.getOutput().printZsciiChar(zsciiChar, true); return newpointer; } /** * This method checks the buffer for previous input. * * @param textbuffer the text buffer * @param inputbuffer the input buffer * @return */ public int checkForPreviousInput(final int textbuffer, final List<Character> inputbuffer) { final int version = machine.getGameData().getStoryFileHeader().getVersion(); // We determine the start of the input here // From V5, the first byte contains the number of characters typed // so we skip that first byte final int textbufferstart = determineTextBufferStart(version); int pointer = textbufferstart; if (version >= 5) { final Memory memory = machine.getGameData().getMemory(); // The clunky feature to include previous input into the current input // Simply adjust the pointer, the differencing at the end of the // function will then calculate the total int numCharactersTyped = memory.readByte(textbuffer + 1); if (numCharactersTyped < 0) { numCharactersTyped = 0; } if (numCharactersTyped > 0) { for (int i = 0; i < numCharactersTyped; i++) { final char zsciichar = (char) memory.readUnsignedByte( textbuffer + textbufferstart + i); inputbuffer.add(zsciichar); } } pointer += numCharactersTyped; } return pointer; } /** * Depending on the terminating character and the story file version, either * write a 0 to the end of the text buffer or write the length of to the * text buffer's first byte. * * @param terminateChar the terminating character * @param textbuffer the text buffer * @param textpointer points at the position behind the last input char */ public void checkTermination(final char terminateChar, final int textbuffer, final int textpointer) { final int version = machine.getGameData().getStoryFileHeader().getVersion(); final Memory memory = machine.getGameData().getMemory(); if (version >= 5) { // Check if was cancelled final byte numCharsTyped = (terminateChar == ZsciiEncoding.NULL) ? 0 : (byte) (textpointer - 2); // Write the number of characters typed in byte 1 memory.writeUnsignedByte(textbuffer + 1, numCharsTyped); } else { // Terminate with 0 byte in versions < 5 // Check if input was cancelled int terminatepos = textpointer; if (terminateChar == ZsciiEncoding.NULL) { terminatepos = 0; } memory.writeByte(textbuffer + terminatepos, (byte) 0); } } public InterruptThread startInterruptThread(final int routineAddress, final int time, final List<Character> inputbuffer) { InterruptThread thread = null; final int version = machine.getGameData().getStoryFileHeader().getVersion(); if (version >= 4 && time > 0 && routineAddress != 0) { final double dtime = ((double) time) / 10.0 * 1000.0; thread = new InterruptThread((int) dtime, routineAddress, inputbuffer); thread.start(); } return thread; } public void terminateInterruptThread(final InterruptThread thread) { // Synchronize with timed input thread if (thread != null) { thread.terminate(); } } /** * This is the main input loop. * * @param textbuffer the text buffer address * @param pointerstart the offset of the text pointer start, either 0 or 1 * @param pointer the starting pointer including previous input * @param inputbuffer the input buffer * @return the terminating character */ public char doInputLoop(final int textbuffer, final int pointer, final List<Character> inputbuffer) { char zsciiChar; final Memory memory = machine.getGameData().getMemory(); final int bufferlen = memory.readUnsignedByte(textbuffer); int newpointer = pointer; boolean flushBeforeGet = true; do { zsciiChar = machine.getInput().getSelectedInputStream() .getZsciiChar(flushBeforeGet); flushBeforeGet = false; // all subsequent input should not flush the buffer displayCursor(false); if (zsciiChar == ZsciiEncoding.DELETE) { newpointer = deletePreviousChar(inputbuffer, newpointer); } else if (!isTerminatingCharacter(zsciiChar)) { if (history.isHistoryChar(zsciiChar)) { newpointer = history.switchHistoryEntry(inputbuffer, textbuffer, newpointer, zsciiChar); } else { newpointer = addChar(inputbuffer, textbuffer, newpointer, zsciiChar); } } displayCursor(true); } while (!isTerminatingCharacter(zsciiChar) && newpointer < bufferlen - 1); checkTermination(zsciiChar, textbuffer, newpointer); return zsciiChar; } private boolean isTerminatingCharacter(final char zsciiChar) { return isFileHeaderTerminator(zsciiChar) || zsciiChar == ZsciiEncoding.NEWLINE || zsciiChar == ZsciiEncoding.NULL; } private boolean isFileHeaderTerminator(final char zsciiChar) { final StoryFileHeader fileheader = machine.getGameData().getStoryFileHeader(); if (fileheader.getVersion() >= 5) { final int terminatorTable = fileheader.getTerminatorsAddress(); if (terminatorTable == 0) { return false; } // Check the terminator table final Memory memory = machine.getGameData().getMemory(); short terminator; for (int i = 0;; i++) { terminator = memory.readUnsignedByte(terminatorTable + i); if (terminator == 0) { break; } if (terminator == 255) { return ZsciiEncoding.isFunctionKey(zsciiChar); } if (terminator == zsciiChar) { return true; } } } return false; } /** * Depending on the terminating character, return the terminator to the * caller. We need this since aread stores the terminating character as a * result. If a newline was typed as the terminator, a newline will be * echoed, in all other cases, the terminator is simply returned. * * @param terminateChar the terminating character * @return a terminating character that can be stored as a result */ public char handleTerminateChar(final char terminateChar) { if (terminateChar == ZsciiEncoding.NEWLINE) { // Echo a newline into the streams // must be called with isInput == false since we are not // in input mode anymore when we receive NEWLINE machine.getOutput().printZsciiChar(ZsciiEncoding.NEWLINE, false); } return terminateChar; } // ********************************************************************** // ****** READ_CHAR // ******************************* /** * {@inheritDoc} */ public char readChar(final int time, final int routineAddress) { machine.getOutput().flushOutput(); displayCursor(true); InterruptThread thread = startInterruptThread(routineAddress, time, null); final char result = machine.getInput().getSelectedInputStream().getZsciiChar(true); //System.out.println("readChar(): " + result); terminateInterruptThread(thread); displayCursor(false); return result; } /** * {@inheritDoc} */ public void tokenize(final int textbuffer, final int parsebuffer, final int dictionaryAddress, final boolean flag) { final Memory memory = machine.getGameData().getMemory(); Dictionary dictionary = machine.getGameData().getDictionary(); if (dictionaryAddress > 0) { dictionary = new UserDictionary(memory, dictionaryAddress, machine.getGameData().getZCharDecoder()); } final int version = machine.getGameData().getStoryFileHeader().getVersion(); final int bufferlen = memory.readUnsignedByte(textbuffer); final int textbufferstart = determineTextBufferStart(version); final int charsTyped = (version >= 5) ? memory.readUnsignedByte(textbuffer + 1) : 0; // from version 5, text starts at position 2 final ZsciiString input = bufferToZscii(textbuffer + textbufferstart, bufferlen, charsTyped); final List<ZsciiString> tokens = tokenize(input); final Map<ZsciiString, Integer> parsedTokens = new HashMap<ZsciiString, Integer>(); // Write the number of tokens in byte 1 of the parse buffer final int maxwords = memory.readUnsignedByte(parsebuffer); // Do not go beyond the limit of maxwords final int numParsedTokens = Math.min(maxwords, tokens.size()); // Write the number of parsed tokens into byte 1 of the parse buffer memory.writeUnsignedByte(parsebuffer + 1, (short) numParsedTokens); int parseaddr = parsebuffer + 2; for (int i = 0; i < numParsedTokens; i++) { final ZsciiString token = tokens.get(i); final int entryAddress = dictionary.lookup(token); //System.out.println("token: '" + token + "' entryAddress: " + entryAddress); int startIndex = 0; if (parsedTokens.containsKey(token)) { final int timesContained = parsedTokens.get(token); parsedTokens.put(token, timesContained + 1); for (int j = 0; j < timesContained; j++) { final int found = input.indexOf(token, startIndex); startIndex = found + token.length(); } } else { parsedTokens.put(token, 1); } int tokenIndex = input.indexOf(token, startIndex); tokenIndex++; // adjust by the buffer length byte if (version >= 5) { // if version >= 5, there is also numbers typed byte tokenIndex++; } // if the tokenize flag is not set, write out the entry to the // parse buffer, if it is set then, only write the token position // if the token was recognized if (!flag || flag && entryAddress > 0) { // This is one slot memory.writeUnsignedShort(parseaddr, entryAddress); memory.writeUnsignedByte(parseaddr + 2, (short) token.length()); memory.writeUnsignedByte(parseaddr + 3, (short) tokenIndex); } parseaddr += 4; } } /** * Turns the buffer into a ZSCII string. This function reads at most * |bufferlen| bytes and treats each byte as an ASCII character. The * characters will be concatenated to the result string. * * @param address the buffer address * @param bufferlen the buffer length * @param charsTyped from version 5, this is the number of characters to * include in the input * @return the string contained in the buffer */ private ZsciiString bufferToZscii(final int address, final int bufferlen, final int charsTyped) { final Memory memory = machine.getGameData().getMemory(); // If charsTyped is set, use that value as the limit final int numChars = (charsTyped > 0) ? charsTyped : bufferlen; // read input from text buffer final ZsciiStringBuilder buffer = new ZsciiStringBuilder(); for (int i = 0; i < numChars; i++) { final char charByte = (char) memory.readUnsignedByte(address + i); if (charByte == 0) { break; } buffer.append(charByte); } return buffer.toZsciiString(); } /** * Turns the specified input string into tokens. It will take whitespace * implicitly and dictionary separators explicitly to tokenize the stream, * dictionary specified separators are included in the result list. * * @param input the input string * @return the tokens */ private List<ZsciiString> tokenize(final ZsciiString input) { final List<ZsciiString> result = new ArrayList<ZsciiString>(); // Retrieve the defined separators final ZsciiStringBuilder separators = new ZsciiStringBuilder(); separators.append(WHITESPACE); final Dictionary dictionary = machine.getGameData().getDictionary(); final ZCharDecoder decoder = machine.getGameData().getZCharDecoder(); for (int i = 0, n = dictionary.getNumberOfSeparators(); i < n; i++) { separators.append(decoder.decodeZChar((char) dictionary.getSeparator(i))); } // The tokenizer will also return the delimiters final ZsciiString delim = separators.toZsciiString(); final ZsciiStringTokenizer tok = new ZsciiStringTokenizer(input, delim); while (tok.hasMoreTokens()) { final ZsciiString token = tok.nextToken(); if (!Character.isWhitespace(token.charAt(0))) { result.add(token); } } return result; } /** * Depending on the version, this returns the offset where text starts in * the text buffer. In versions up to 4 this is 1, since we have the buffer * size in the first byte, from versions 5, we also have the number of typed * characters in the second byte. * * @param version the story file version * @return 1 if version < 4, 2, otherwise */ private int determineTextBufferStart(final int version) { return (version < 5) ? 1 : 2; } /** * Draws the cursor and refreshes the screen. * * @param flag true for display, false for clear */ private synchronized void displayCursor(final boolean flag) { machine.getScreen().displayCursor(flag); machine.getScreen().redraw(); } // ********************************************************************* // ***** History methods // *********************************** private void storeInput(final List<Character> inputbuffer, final char terminateChar) { if (terminateChar != ZsciiEncoding.NULL) { history.addInputLine(inputbuffer); } } }