From da3e724a990e1d7482c1654623e01723ba890534 Mon Sep 17 00:00:00 2001 From: Eugene Sandulenko Date: Mon, 6 Nov 2006 13:19:12 +0000 Subject: Predictive input for AGI engine. To Do: - Multitap - scummvm.ini-based dictionary path - speedup dictionary loading svn-id: r24635 --- engines/agi/graphics.cpp | 4 +- engines/agi/graphics.h | 2 +- engines/agi/keyboard.cpp | 22 +++ engines/agi/module.mk | 1 + engines/agi/predictive.cpp | 350 +++++++++++++++++++++++++++++++++++++++++++ engines/agi/text.h | 30 ++++ tools/construct-pred-dict.pl | 63 ++++++++ tools/extract-words-tok.pl | 73 +++++++++ 8 files changed, 542 insertions(+), 3 deletions(-) create mode 100644 engines/agi/predictive.cpp create mode 100755 tools/construct-pred-dict.pl create mode 100755 tools/extract-words-tok.pl diff --git a/engines/agi/graphics.cpp b/engines/agi/graphics.cpp index 3b9c754d97..8499e29ea1 100644 --- a/engines/agi/graphics.cpp +++ b/engines/agi/graphics.cpp @@ -282,7 +282,7 @@ void print_character(int x, int y, char c, int fg, int bg) { * @param a set if the button has focus * @param p set if the button is pressed */ -void draw_button(int x, int y, const char *s, int a, int p) { +void draw_button(int x, int y, const char *s, int a, int p, int fgcolor, int bgcolor) { int len = strlen(s); int x1, y1, x2, y2; @@ -292,7 +292,7 @@ void draw_button(int x, int y, const char *s, int a, int p) { y2 = y + CHAR_LINES + 2; while (*s) { - put_text_character(0, x + (!!p), y + (!!p), *s++, a ? 15 : 0, a ? 0 : 15); + put_text_character(0, x + (!!p), y + (!!p), *s++, a ? fgcolor : bgcolor, a ? bgcolor : fgcolor); x += CHAR_COLS; } diff --git a/engines/agi/graphics.h b/engines/agi/graphics.h index 85be6eb3c3..2f45dcb289 100644 --- a/engines/agi/graphics.h +++ b/engines/agi/graphics.h @@ -63,7 +63,7 @@ void flush_screen(void); void clear_screen(int); void clear_console_screen(int); void draw_box(int, int, int, int, int, int, int); -void draw_button(int, int, const char *, int, int); +void draw_button(int, int, const char *, int, int, int fgcolor = 0, int bgcolor = 0); int test_button(int, int, const char *); void draw_rectangle(int, int, int, int, int); void save_block(int, int, int, int, uint8 *); diff --git a/engines/agi/keyboard.cpp b/engines/agi/keyboard.cpp index a35fe8a547..9c401e067c 100644 --- a/engines/agi/keyboard.cpp +++ b/engines/agi/keyboard.cpp @@ -170,6 +170,16 @@ int handle_controller(int key) { break; } } + + if (mouse.y >= game.line_user_input * CHAR_LINES && + mouse.y <= (game.line_user_input + 1) * CHAR_LINES) { + if (_text->predictiveDialog()) { + strcpy((char *)game.input_buffer, _text->_predictiveResult); + handle_keys(KEY_ENTER); + } + return true; + } + if (!opt.agimouse) { /* Handle mouse button events */ if (key == BUTTON_LEFT) { @@ -201,6 +211,18 @@ void handle_getstring(int key) { debugC(3, kDebugLevelInput, "handling key: %02x", key); switch (key) { + case BUTTON_LEFT: + if (mouse.y >= stringdata.y * CHAR_LINES && + mouse.y <= (stringdata.y + 1) * CHAR_LINES) { + if (_text->predictiveDialog()) { + strcpy(game.strings[stringdata.str], _text->_predictiveResult); + new_input_mode(INPUT_NORMAL); + print_character(stringdata.x + strlen(game.strings[stringdata.str]) + 1, + stringdata.y, ' ', game.color_fg, game.color_bg); + return; + } + } + break; case KEY_ENTER: debugC(3, kDebugLevelInput, "KEY_ENTER"); game.has_prompt = 0; diff --git a/engines/agi/module.mk b/engines/agi/module.mk index b155a12c9b..f56b910fc4 100644 --- a/engines/agi/module.mk +++ b/engines/agi/module.mk @@ -23,6 +23,7 @@ MODULE_OBJS = \ op_test.o \ patches.o \ picture.o \ + predictive.o \ savegame.o \ sound.o \ sprite.o \ diff --git a/engines/agi/predictive.cpp b/engines/agi/predictive.cpp new file mode 100644 index 0000000000..2de626d1f4 --- /dev/null +++ b/engines/agi/predictive.cpp @@ -0,0 +1,350 @@ +/* ScummVM - Scumm Interpreter + * Copyright (C) 2006 The ScummVM project + * + * This program 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 2 + * of the License, or (at your option) any later version. + + * This program 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 this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * $URL$ + * $Id$ + * + */ + +#include "agi/agi.h" +#include "agi/graphics.h" +#include "agi/keyboard.h" +#include "agi/text.h" + +#include "common/func.h" + +namespace Agi { + +#define kModePre 0 +#define kModeNum 1 +#define kModeAbc 2 + +bool TextMan::predictiveDialog(void) { + int key, active = 0; + bool rc = false; + int x; + int y; + int bx[17], by[17]; + String prefix = ""; + char temp[MAXWORDLEN + 1]; + + const char *buttonStr[] = { "1", "2", "3", "4", "5", "6", "7", "8", "9", "0" }; + const char *buttons[] = { + "(1)'-.&", "(2)abc", "(3)def", + "(4)ghi", "(5)jkl", "(6)mno", + "(7)pqrs", "(8)tuv", "(9)wxyz", + "next", "add", + "<", + "Cancel", "OK", + "Pre", "(0) ", NULL + }; + const int colors[] = { + 15, 0, 15, 0, 15, 0, + 15, 0, 15, 0, 15, 0, + 15, 0, 15, 0, 15, 0, + 15, 12, 15, 12, + 15, 0, + 15, 0, 15, 0, + 14, 0, 15, 0, 0, 0 + }; + const char *modes[] = { "Pre", "123", "Abc" }; + + if (!_dict.size()) + loadDict(); + + draw_window(50, 40, 269, 159); + draw_rectangle(62, 54, 249, 66, MSG_BOX_TEXT); + flush_block(62, 54, 249, 66); + + print_character(3, 11, game.cursor_char, MSG_BOX_COLOUR, MSG_BOX_TEXT); + + bx[15] = 73; // Zero/space + by[15] = 120; + bx[9] = 120; // next + by[9] = 120; + bx[10] = 160; // add + by[10] = 120; + bx[14] = 200; // Mode + by[14] = 120; + bx[11] = 252; // Backspace + by[11] = 57; + bx[12] = 180; // Cancel + by[12] = 140; + bx[13] = 240; // OK + by[13] = 140; + + x = 73; + y = 75; + for (int i = 0; i < 9; i++) { + bx[i] = x; + by[i] = y; + x += 60; + if (i % 3 == 2) { + y += 15; + x = 73; + } + } + + /* clear key queue */ + while (keypress()) { + get_key(); + } + + _wordPosition = 0; + _currentCode = ""; + _currentWord = ""; + _matchedWord = ""; + _wordNumber = 0; + _nextIsActive = _addIsActive = false; + + int mode = kModePre; + + bool needRefresh = true; + + while (42) { + if (needRefresh) { + for (int i = 0; buttons[i]; i++) { + int color1 = colors[i * 2]; + int color2 = colors[i * 2 + 1]; + + if (i == 9 && !_nextIsActive) { // Next + color2 = 7; + } + if (i == 10 && !_addIsActive) { // Add + color2 = 7; + } + if (i == 14) { + draw_button(bx[i], by[i], modes[mode], i == active, 0, color1, color2); + } else { + draw_button(bx[i], by[i], buttons[i], i == active, 0, color1, color2); + } + } + + if (_currentWord != "") { + temp[MAXWORDLEN] = 0; + + strncpy(temp, prefix.c_str(), MAXWORDLEN); + strncat(temp, _currentWord.c_str(), MAXWORDLEN); + + for (int i = prefix.size() + _currentCode.size(); i < MAXWORDLEN; i++) + temp[i] = ' '; + + print_text(temp, 0, 8, 7, MAXWORDLEN, 15, 0); + flush_block(62, 54, 249, 66); + } + } + + poll_timer(); /* msdos driver -> does nothing */ + key = do_poll_keyboard(); + switch (key) { + case KEY_ENTER: + rc = true; + goto press; + case KEY_ESCAPE: + rc = false; + goto getout; + case BUTTON_LEFT: + for (int i = 0; buttons[i]; i++) { + if (test_button(bx[i], by[i], buttons[i])) { + needRefresh = true; + active = i; + + if (active == 15 && mode != kModeNum) { // Space + strncpy(temp, _currentWord.c_str(), _currentCode.size()); + + temp[_currentCode.size()] = 0; + + prefix += temp; + prefix += " "; + _wordPosition = 0; + _currentCode = ""; + } if (active < 9 || active == 11 || active == 15) { // number or backspace + if (active == 11) { // backspace + if (_currentCode.size()) { + _currentCode.deleteLastChar(); + _wordPosition--; + } else { + if (prefix.size()) + prefix.deleteLastChar(); + } + } else if (active == 15) { // zero + _currentCode += buttonStr[9]; + _wordPosition++; + } else { + _currentCode += buttonStr[active]; + _wordPosition++; + } + + if (mode == kModeNum) { + _currentWord = _currentCode; + } else if (mode == kModePre) { + if (!matchWord() && _currentCode.size()) { + _currentCode.deleteLastChar(); + _wordPosition--; + matchWord(); + } + } + } else if (active == 9) { // next + if (_nextIsActive) { + int wordsNumber = (_matchedWord.size() + 1) / _currentCode.size(); + int start; + + _wordNumber = (_wordNumber + 1) % wordsNumber; + + start = _wordNumber * (_currentCode.size() + 1); + + strncpy(temp, _matchedWord.c_str() + start, _currentCode.size()); + temp[_matchedWord.size() + 1] = 0; + + _currentWord = temp; + } + } else if (active == 10) { // add + debug(0, "add"); + } else if (active == 13) { // Ok + rc = true; + goto press; + } else if (active == 14) { // Mode + mode++; + if (mode > kModeAbc) + mode = kModePre; + } else { + goto press; + } + } + } + break; + case 0x09: /* Tab */ + debugC(3, kDebugLevelText, "Focus change"); + active++; + active %= ARRAYSIZE(buttons) - 1; + needRefresh = true; + break; + } + do_update(); + } + + press: + strncpy(_predictiveResult, prefix.c_str(), 40); + strncat(_predictiveResult, _currentWord.c_str(), 40); + _predictiveResult[prefix.size() + _currentCode.size() + 1] = 0; + + getout: + close_window(); + + return rc; +} + +static char *ltrim(char *t) { + while (isspace(*t)) + t++; + return t; +} + +static char *rtrim(char *t) { + int l = strlen(t) - 1; + while (l >= 0 && isspace(t[l])) + t[l--] = 0; + return t; +} + +#define MAXLINELEN 80 + +void TextMan::loadDict(void) { + Common::File in; + char buf[MAXLINELEN]; + + in.open("pred.txt"); + + while (!in.eos()) { + if (!in.readLine(buf, MAXLINELEN)) + break; + + // Skip leading & trailing whitespaces + char *t = rtrim(ltrim(buf)); + char *k = t; + int len = 0; + char key[30]; + + // Skip empty lines + if (*t == 0) + continue; + + while (!isspace(*t)) { + len++; + t++; + } + + while (isspace(*t)) + t++; + + strncpy(key, k, len); + key[len] = 0; + + _dict[String(key)] = String(t); + _dictKeys.push_back(String(key)); + } + + Common::sort(_dictKeys.begin(), _dictKeys.end()); + + debug(0, "Loaded %d keys", _dict.size()); +} + +bool TextMan::matchWord(void) { + _addIsActive = false; + + if (!_currentCode.size()) { + return false; + } + + if (_dict.contains(_currentCode)) { + _currentWord = _matchedWord = _dict[_currentCode]; + + _nextIsActive = ((_matchedWord.size() + 1) / _currentCode.size() > 1); + return true; + } + + // Else search first partial match + for (uint i = 0; i < _dictKeys.size(); i++) { + bool matched = true; + + if (_dictKeys[i].size() < _wordPosition) + continue; + + for (uint j = 0; j < _dictKeys[i].size() && j < _wordPosition; j++) { + if (_currentCode[j] != _dictKeys[i][j]) { + matched = false; + break; + } + } + if (matched && _dictKeys[i].size() >= _wordPosition) { + _currentWord = _matchedWord = _dict[_dictKeys[i]]; + + _nextIsActive = ((_matchedWord.size() + 1) / _currentCode.size() > 1); + return true; + } + } + + _currentWord = _matchedWord = ""; + _nextIsActive = false; + _addIsActive = true; + + return false; +} + + + +} // End of namespace Agi diff --git a/engines/agi/text.h b/engines/agi/text.h index 28324fc005..ede10dbc44 100644 --- a/engines/agi/text.h +++ b/engines/agi/text.h @@ -26,9 +26,14 @@ #define AGI_TEXT_H #include "agi/agi.h" +#include "common/hash-str.h" namespace Agi { +#define MAXWORDLEN 24 + +typedef Common::String String; + class TextMan { public: int message_box(const char *); @@ -44,6 +49,7 @@ public: void write_prompt(void); void clear_lines(int, int, int); void flush_lines(int, int); + bool predictiveDialog(void); private: void print_status(const char *message, ...); @@ -51,6 +57,30 @@ private: void blit_textbox(const char *p, int y, int x, int len); void erase_textbox(); char *safe_strcat(char *s, const char *t); + + void loadDict(void); + + bool matchWord(void); + +private: + typedef Common::HashMap DictMap; + + DictMap _dict; + Common::StringList _dictKeys; + + String _currentCode; + String _currentWord; + String _matchedWord; + + int _wordNumber; + + int _wordPosition; + + bool _nextIsActive; + bool _addIsActive; + +public: + char _predictiveResult[40]; }; extern TextMan *_text; diff --git a/tools/construct-pred-dict.pl b/tools/construct-pred-dict.pl new file mode 100755 index 0000000000..780d8843ab --- /dev/null +++ b/tools/construct-pred-dict.pl @@ -0,0 +1,63 @@ +#!perl +# +# ScummVM - Scumm Interpreter +# Copyright (C) 2006 The ScummVM project +# +# This program 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 2 +# of the License, or (at your option) any later version. +# +# This program 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 this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# $URL$ +# $Id$ +# + +# This script constructs dictionary for use with predictive input +# +# Feed it with list of words. One word per line + +%words = (); + +while(<>) { + chomp; + + s/[\x7f-\xff]//g; # remove unprintable characters + + next if /^.$/; # skip one character words + next if $_ eq ""; # skip empty words + + $words{$_} = 1; +} + +%list = (); + +for $_ (sort keys %words) { + $word = $_; + + s/['-.&_!]/1/g; + s/[abc]/2/g; + s/[def]/3/g; + s/[ghi]/4/g; + s/[jkl]/5/g; + s/[mno]/6/g; + s/[pqrs]/7/g; + s/[tuv]/8/g; + s/[wxyz]/9/g; + + $list{$_} .= "$word "; +} + +for $k (sort keys %list) { + chop $list{$k}; + + print "$k $list{$k}\n"; +} diff --git a/tools/extract-words-tok.pl b/tools/extract-words-tok.pl new file mode 100755 index 0000000000..53c7ae09a9 --- /dev/null +++ b/tools/extract-words-tok.pl @@ -0,0 +1,73 @@ +#!perl +# +# ScummVM - Scumm Interpreter +# Copyright (C) 2006 The ScummVM project +# +# This program 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 2 +# of the License, or (at your option) any later version. +# +# This program 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 this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# $URL$ +# $Id$ +# + +# This script extracts AGI words.tok file +# +# It produces one word per line. Multiword verbs get splitted +# +# Typical usage: +# +# for i in agigames/*/words.tok +# do +# tools/extract-words-tok.pl "$i" +# done | tools/construct-pred-dict.pl +# + +local $/; +local $file = <>; + +#$off = ord(substr($file, $i * 2, 1)) * 256 + ord(substr($file, $i * 2 + 1, 1)); +#$offn = ord(substr($file, ($i + 1) * 2, 1)) * 256 + ord(substr($file, ($i + 1) * 2 + 1, 1)); + +$off = 52; + +$word = ""; +$mode = 0; + +while ($off < length $file) { + $c = (ord(substr($file, $off, 1))); + if ($mode == 0) { + $word = substr $word, 0, $c; + $mode = 1; + $off++; + next; + } + + $r = ($c & 0x7f) ^ 0x7f; + $word .= chr($r); + + if ($c & 0x80) { + for $w (split ' ', $word) { + print "$w\n"; + } + $off += 3; + $mode = 0; + next; + } + + $off++; +} + +for $w (split ' ', $word) { + print "$w\n"; +} -- cgit v1.2.3