scummvm/engines/agi/keyboard.cpp
2016-02-01 01:34:36 +01:00

638 lines
18 KiB
C++

/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* 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.
*
*/
#include "common/events.h"
#include "agi/agi.h"
#include "agi/graphics.h"
#include "agi/keyboard.h"
#include "agi/menu.h"
#include "agi/text.h"
namespace Agi {
//
// IBM-PC keyboard scancodes
//
const uint8 scancodeTable[26] = {
30, // A
48, // B
46, // C
32, // D
18, // E
33, // F
34, // G
35, // H
23, // I
36, // J
37, // K
38, // L
50, // M
49, // N
24, // O
25, // P
16, // Q
19, // R
31, // S
20, // T
22, // U
47, // V
17, // W
45, // X
21, // Y
44 // Z
};
void AgiEngine::processEvents() {
Common::Event event;
int key = 0;
while (_eventMan->pollEvent(event)) {
switch (event.type) {
case Common::EVENT_PREDICTIVE_DIALOG:
showPredictiveDialog();
break;
case Common::EVENT_LBUTTONDOWN:
if (_game.mouseEnabled) {
key = AGI_MOUSE_BUTTON_LEFT;
_mouse.button = kAgiMouseButtonLeft;
keyEnqueue(key);
_mouse.pos.x = event.mouse.x;
_mouse.pos.y = event.mouse.y;
}
break;
case Common::EVENT_RBUTTONDOWN:
if (_game.mouseEnabled) {
key = AGI_MOUSE_BUTTON_RIGHT;
_mouse.button = kAgiMouseButtonRight;
keyEnqueue(key);
_mouse.pos.x = event.mouse.x;
_mouse.pos.y = event.mouse.y;
}
break;
case Common::EVENT_WHEELUP:
if (_game.mouseEnabled) {
key = AGI_MOUSE_WHEEL_UP;
keyEnqueue(key);
}
break;
case Common::EVENT_WHEELDOWN:
if (_game.mouseEnabled) {
key = AGI_MOUSE_WHEEL_DOWN;
keyEnqueue(key);
}
break;
case Common::EVENT_MOUSEMOVE:
if (_game.mouseEnabled) {
_mouse.pos.x = event.mouse.x;
_mouse.pos.y = event.mouse.y;
if (!_game.mouseFence.isEmpty()) {
if (_mouse.pos.x < _game.mouseFence.left)
_mouse.pos.x = _game.mouseFence.left;
if (_mouse.pos.x > _game.mouseFence.right)
_mouse.pos.x = _game.mouseFence.right;
if (_mouse.pos.y < _game.mouseFence.top)
_mouse.pos.y = _game.mouseFence.top;
if (_mouse.pos.y > _game.mouseFence.bottom)
_mouse.pos.y = _game.mouseFence.bottom;
g_system->warpMouse(_mouse.pos.x, _mouse.pos.y);
}
}
break;
case Common::EVENT_LBUTTONUP:
case Common::EVENT_RBUTTONUP:
if (_game.mouseEnabled) {
_mouse.button = kAgiMouseButtonUp;
_mouse.pos.x = event.mouse.x;
_mouse.pos.y = event.mouse.y;
}
break;
case Common::EVENT_KEYDOWN:
if (event.kbd.hasFlags(Common::KBD_CTRL) && event.kbd.keycode == Common::KEYCODE_d) {
_console->attach();
break;
}
key = event.kbd.ascii;
if (event.kbd.keycode >= Common::KEYCODE_KP0 && event.kbd.keycode <= Common::KEYCODE_KP9) {
if (!(event.kbd.flags & Common::KBD_NUM)) {
// HACK: Num-Lock not enabled
// We shouldn't get a valid ascii code in these cases. We fix it here, so that cursor keys
// on the numpad work properly.
key = 0;
}
}
if ((key) && (key <= 0xFF)) {
// No special key, directly accept it
// Is ISO-8859-1, we need lower 128 characters only, which is plain ASCII, so no mapping required
if (Common::isAlpha(key)) {
// Key is A-Z.
// Map Ctrl-A to 1, Ctrl-B to 2, etc.
if (event.kbd.flags & Common::KBD_CTRL) {
key = toupper(key) - 'A' + 1;
} else if (event.kbd.flags & Common::KBD_ALT) {
// Map Alt-A, Alt-B etc. to special scancode values according to an internal scancode table.
key = scancodeTable[toupper(key) - 'A'] << 8;
}
}
} else {
key = 0;
switch (event.kbd.keycode) {
case Common::KEYCODE_LEFT:
case Common::KEYCODE_KP4:
if (_allowSynthetic || !event.synthetic)
key = AGI_KEY_LEFT;
break;
case Common::KEYCODE_RIGHT:
case Common::KEYCODE_KP6:
if (_allowSynthetic || !event.synthetic)
key = AGI_KEY_RIGHT;
break;
case Common::KEYCODE_UP:
case Common::KEYCODE_KP8:
if (_allowSynthetic || !event.synthetic)
key = AGI_KEY_UP;
break;
case Common::KEYCODE_DOWN:
case Common::KEYCODE_KP2:
if (_allowSynthetic || !event.synthetic)
key = AGI_KEY_DOWN;
break;
case Common::KEYCODE_PAGEUP:
case Common::KEYCODE_KP9:
if (_allowSynthetic || !event.synthetic)
key = AGI_KEY_UP_RIGHT;
break;
case Common::KEYCODE_PAGEDOWN:
case Common::KEYCODE_KP3:
if (_allowSynthetic || !event.synthetic)
key = AGI_KEY_DOWN_RIGHT;
break;
case Common::KEYCODE_HOME:
case Common::KEYCODE_KP7:
if (_allowSynthetic || !event.synthetic)
key = AGI_KEY_UP_LEFT;
break;
case Common::KEYCODE_END:
case Common::KEYCODE_KP1:
if (_allowSynthetic || !event.synthetic)
key = AGI_KEY_DOWN_LEFT;
break;
case Common::KEYCODE_KP5:
key = AGI_KEY_STATIONARY;
break;
case Common::KEYCODE_F1:
key = AGI_KEY_F1;
break;
case Common::KEYCODE_F2:
key = AGI_KEY_F2;
break;
case Common::KEYCODE_F3:
key = AGI_KEY_F3;
break;
case Common::KEYCODE_F4:
key = AGI_KEY_F4;
break;
case Common::KEYCODE_F5:
key = AGI_KEY_F5;
break;
case Common::KEYCODE_F6:
key = AGI_KEY_F6;
break;
case Common::KEYCODE_F7:
key = AGI_KEY_F7;
break;
case Common::KEYCODE_F8:
key = AGI_KEY_F8;
break;
case Common::KEYCODE_F9:
key = AGI_KEY_F9;
break;
case Common::KEYCODE_F10:
key = AGI_KEY_F10;
break;
case Common::KEYCODE_F11:
key = AGI_KEY_F11;
break;
case Common::KEYCODE_F12:
key = AGI_KEY_F12;
break;
case Common::KEYCODE_KP_ENTER:
key = AGI_KEY_ENTER;
break;
default:
break;
}
}
if (key)
keyEnqueue(key);
break;
case Common::EVENT_KEYUP:
if (_egoHoldKey)
_game.screenObjTable[SCREENOBJECTS_EGO_ENTRY].direction = 0;
default:
break;
}
}
}
/**
* Raw key grabber.
* poll_keyboard() is the raw key grabber (above the gfx driver, that is).
* It handles console keys and insulates AGI from the console. In the main
* loop, handle_keys() handles keyboard input and ego movement.
*/
int AgiEngine::doPollKeyboard() {
int key = 0;
// If a key is ready, rip it
if (isKeypress()) {
key = getKeypress();
debugC(3, kDebugLevelInput, "key %02x pressed", key);
}
return key;
}
int16 AgiEngine::getSpecialMenuControllerSlot() {
int16 controllerSlotESC = -1;
int16 controllerSlotSpecial = -1;
for (uint16 curMapping = 0; curMapping < MAX_CONTROLLER_KEYMAPPINGS; curMapping++) {
if (_game.controllerKeyMapping[curMapping].keycode == _game.specialMenuTriggerKey) {
if (controllerSlotSpecial < 0) {
controllerSlotSpecial = _game.controllerKeyMapping[curMapping].controllerSlot;
}
}
if (_game.controllerKeyMapping[curMapping].keycode == AGI_MENU_TRIGGER_PC) {
if (controllerSlotESC < 0) {
controllerSlotESC = _game.controllerKeyMapping[curMapping].controllerSlot;
}
}
}
if (controllerSlotSpecial >= 0) {
// special menu controller slot found
if (controllerSlotSpecial != controllerSlotESC) {
// not the same as the ESC slot (is the same in Manhunter AppleIIgs, we need to replace "pause"
if (controllerSlotSpecial >= 10) {
// slot needs to be at least 10
// Atari ST SQ1 maps the special key, but doesn't trigger any menu with it
// the controller slot in this case is 8.
return controllerSlotSpecial;
}
}
}
return -1;
}
bool AgiEngine::handleMouseClicks(uint16 &key) {
// No mouse click? -> exit
if (key != AGI_MOUSE_BUTTON_LEFT)
return false;
Common::Rect displayLineRect(DISPLAY_WIDTH, FONT_DISPLAY_HEIGHT);
int16 statusRow = _text->statusRow_Get();
displayLineRect.moveTo(0, statusRow * FONT_DISPLAY_HEIGHT);
if (displayLineRect.contains(_mouse.pos)) {
if (getFlag(VM_FLAG_MENUS_WORK) && _menu->isAvailable()) {
warning("click on status line -> menu TODO");
// TODO: menu
// This should be done in a better way as in simulate ESC key
// Sierra seems to have hardcoded it in some way, but we would have to verify, what flags
// they checked. The previous way wasn't accurate. Mouse support for menu is missing atm anyway.
//if ((getflag(VM_FLAG_MENUS_WORK) || (getFeatures() & GF_MENUS)) && _mouse.y <= CHAR_LINES) {
// newInputMode(INPUTMODE_MENU);
// return true;
//}
key = 0; // eat event
return true;
}
}
if (_text->promptIsEnabled() && (!cycleInnerLoopIsActive()) ) {
// Prompt is currently enabled, but no inner loop is active
int16 promptRow = _text->promptRow_Get();
displayLineRect.moveTo(0, promptRow * FONT_DISPLAY_HEIGHT);
if (displayLineRect.contains(_mouse.pos)) {
// and user clicked within the line of the prompt
showPredictiveDialog();
key = 0; // eat event
return true;
}
}
if (cycleInnerLoopIsActive()) {
// inner loop active, check what kind of loop it is. Then process / forward it
switch (_game.cycleInnerLoopType) {
case CYCLE_INNERLOOP_GETSTRING:
case CYCLE_INNERLOOP_GETNUMBER: {
// process in here
int16 stringRow, stringColumn, stringMaxLen;
_text->stringPos_Get(stringRow, stringColumn);
stringMaxLen = _text->stringGetMaxLen();
Common::Rect displayRect(stringMaxLen * FONT_DISPLAY_WIDTH, FONT_DISPLAY_HEIGHT);
displayRect.moveTo(stringColumn * FONT_DISPLAY_WIDTH, stringRow * FONT_DISPLAY_HEIGHT);
if (displayRect.contains(_mouse.pos)) {
// user clicked inside the input space
showPredictiveDialog();
key = 0; // eat event
return true;
}
break;
}
case CYCLE_INNERLOOP_INVENTORY:
// TODO: forward
break;
case CYCLE_INNERLOOP_MENU:
// TODO: forward
break;
case CYCLE_INNERLOOP_SYSTEMUI_SELECTSAVEDGAMESLOT:
// TODO: forward
break;
default:
break;
}
}
return false;
}
bool AgiEngine::handleController(uint16 key) {
ScreenObjEntry *screenObjEgo = &_game.screenObjTable[SCREENOBJECTS_EGO_ENTRY];
if (key == 0) // nothing pressed
return false;
// This previously skipped processing, when ESC was pressed and called menu directly.
// This original approach was bad, because games check different flags before actually allowing the
// user to enter the menu. We checked a few common flags, like for example the availability of the prompt.
// But this stopped the user being able to enter the menu, when the original interpreter actually allowed it.
// We now instead implement this feature using another way for those platforms.
if (key == AGI_KEY_ESCAPE) {
// Escape pressed, user probably wants to trigger the menu
// For PC, just passing ASCII code for ESC will normally trigger a controller
// and the scripts will then trigger the menu
// For other platforms, ESC was handled by platforms to trigger "pause game" instead
// We need to change ESC to a platform specific code to make it work properly.
//
// There are exceptions though. Mixed Up Mother Goose on AppleIIgs for example actually sets up
// ESC for pause only. That's why we also check, if the key is actually mapped to a controller.
// For this special case, we actually replace the pause function with a menu trigger.
// Replacing "pause" all the time wouldn't work out as well, becaue games like KQ1 on Apple IIgs
// actually disable "pause" when ego has been killed, which means we wouldn't be able to access
// the menu anymore in that case.
if (_menu->isAvailable()) {
// menu is actually available
if (_game.specialMenuTriggerKey) {
int16 specialMenuControllerSlot = getSpecialMenuControllerSlot();
if (specialMenuControllerSlot >= 0) {
// menu trigger found, trigger it now
_game.controllerOccured[specialMenuControllerSlot] = true;
return true;
}
}
// Otherwise go on and look for the ESC controller
}
}
// AGI 3.149 games, The Black Cauldron and King's Quest 4 need KEY_ESCAPE to use menus
// Games with the GF_ESCPAUSE flag need KEY_ESCAPE to pause the game
// (key == KEY_ESCAPE && getVersion() != 0x3149 && getGameID() != GID_BC && getGameID() != GID_KQ4 && !(getFeatures() & GF_ESCPAUSE)) )
// return false;
if ((getGameID() == GID_MH1 || getGameID() == GID_MH2) && (key == AGI_KEY_ENTER) &&
(_game.inputMode == INPUTMODE_NONE)) {
key = 0x20; // Set Enter key to Space in Manhunter when there's no text input
}
debugC(3, kDebugLevelInput, "key = %04x", key);
for (uint16 curMapping = 0; curMapping < MAX_CONTROLLER_KEYMAPPINGS; curMapping++) {
if (_game.controllerKeyMapping[curMapping].keycode == key) {
debugC(3, kDebugLevelInput, "event %d: key press", _game.controllerKeyMapping[curMapping].controllerSlot);
_game.controllerOccured[_game.controllerKeyMapping[curMapping].controllerSlot] = true;
return true;
}
}
int16 newDirection = 0;
switch (key) {
case AGI_KEY_UP:
newDirection = 1;
break;
case AGI_KEY_DOWN:
newDirection = 5;
break;
case AGI_KEY_LEFT:
newDirection = 7;
break;
case AGI_KEY_RIGHT:
newDirection = 3;
break;
case AGI_KEY_UP_RIGHT:
newDirection = 2;
break;
case AGI_KEY_DOWN_RIGHT:
newDirection = 4;
break;
case AGI_KEY_UP_LEFT:
newDirection = 8;
break;
case AGI_KEY_DOWN_LEFT:
newDirection = 6;
break;
default:
break;
}
if (_game.playerControl) {
if (!(getFeatures() & GF_AGIMOUSE)) {
// Handle mouse button events
if (!_game.mouseHidden) {
if (key == AGI_MOUSE_BUTTON_LEFT) {
if (getGameID() == GID_PQ1 && getVar(VM_VAR_CURRENT_ROOM) == 116) {
// WORKAROUND: Special handling for mouse clicks in the newspaper
// screen of PQ1. Fixes bug #3018770.
newDirection = 3; // fake a right arrow key (next page)
} else {
// Click-to-walk mouse interface
//v->flags |= fAdjEgoXY;
// setting fAdjEgoXY here will at least break "climbing the log" in SQ2
// in case you walked to the log by using the mouse, so don't!!!
int16 egoDestinationX = _mouse.pos.x;
int16 egoDestinationY = _mouse.pos.y;
adjustPosToGameScreen(egoDestinationX, egoDestinationY);
screenObjEgo->motionType = kMotionEgo;
if (egoDestinationX < (screenObjEgo->xSize / 2)) {
screenObjEgo->move_x = -1;
} else {
screenObjEgo->move_x = egoDestinationX - (screenObjEgo->xSize / 2);
}
screenObjEgo->move_y = egoDestinationY;
screenObjEgo->move_stepSize = screenObjEgo->stepSize;
return true;
}
}
}
}
}
if (newDirection || key == AGI_KEY_STATIONARY) {
// TODO: not sure, what original AGI did with AdjEgoXY
screenObjEgo->flags &= ~fAdjEgoXY;
if (screenObjEgo->direction == newDirection) {
setVar(VM_VAR_EGO_DIRECTION, 0);
} else {
setVar(VM_VAR_EGO_DIRECTION, newDirection);
}
if (_game.playerControl) {
screenObjEgo->motionType = kMotionNormal;
}
return true;
}
return false;
}
bool AgiEngine::showPredictiveDialog() {
GUI::PredictiveDialog predictiveDialog;
inGameTimerPause();
predictiveDialog.runModal();
inGameTimerResume();
Common::String predictiveResult(predictiveDialog.getResult());
uint16 predictiveResultLen = predictiveResult.size();
if (predictiveResult.size()) {
// User actually entered something
for (int16 resultPos = 0; resultPos < predictiveResultLen; resultPos++) {
keyEnqueue(predictiveResult[resultPos]);
}
if (!cycleInnerLoopIsActive()) {
if (_text->promptIsEnabled()) {
// add ENTER, when the input is probably meant for the prompt
keyEnqueue(AGI_KEY_ENTER);
}
} else {
switch (_game.cycleInnerLoopType) {
case CYCLE_INNERLOOP_GETSTRING:
case CYCLE_INNERLOOP_GETNUMBER:
// add ENTER, when the input is probably meant for GetString/GetNumber
keyEnqueue(AGI_KEY_ENTER);
break;
default:
break;
}
}
return true;
}
return false;
}
int AgiEngine::waitKey() {
int key = 0;
clearKeyQueue();
debugC(3, kDebugLevelInput, "waiting...");
while (!(shouldQuit() || _restartGame || getFlag(VM_FLAG_RESTORE_JUST_RAN))) {
pollTimer();
key = doPollKeyboard();
if (key == AGI_KEY_ENTER || key == AGI_KEY_ESCAPE || key == AGI_MOUSE_BUTTON_LEFT)
break;
pollTimer();
g_system->updateScreen();
}
// Have to clear it as original did not set this variable, and we do it in doPollKeyboard()
// Fixes bug #2823759
_game.keypress = 0;
return key;
}
int AgiEngine::waitAnyKey() {
int key = 0;
clearKeyQueue();
debugC(3, kDebugLevelInput, "waiting... (any key)");
while (!(shouldQuit() || _restartGame)) {
pollTimer();
key = doPollKeyboard();
if (key)
break;
g_system->updateScreen();
}
// Have to clear it as original did not set this variable, and we do it in doPollKeyboard()
_game.keypress = 0;
return key;
}
bool AgiEngine::isKeypress() {
processEvents();
return _keyQueueStart != _keyQueueEnd;
}
int AgiEngine::getKeypress() {
int k;
while (_keyQueueStart == _keyQueueEnd) // block
pollTimer();
keyDequeue(k);
return k;
}
void AgiEngine::clearKeyQueue() {
while (isKeypress()) {
getKeypress();
}
}
} // End of namespace Agi