/* 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/config-manager.h"
#include "common/events.h"
#include "common/system.h"
#include "common/archive.h"
#include "common/debug.h"
#include "common/error.h"
#include "common/keyboard.h"
#include "common/savefile.h"
#include "common/textconsole.h"

#include "engines/util.h"

#include "graphics/cursorman.h"
#include "graphics/palette.h"
#include "gui/debugger.h"

#include "tucker/tucker.h"
#include "tucker/graphics.h"

namespace Tucker {

TuckerEngine::TuckerEngine(OSystem *system, Common::Language language, uint32 flags)
	: Engine(system), _gameLang(language), _gameFlags(flags), _rnd("tucker") {
	_console = new TuckerConsole(this);

	resetVariables();

	_execData3Counter = 0;
	_currentSaveLoadGameState = 1;
	_fileLoadSize = 0;
	_csDataSize = 0;
	_startSlot = ConfMan.hasKey("save_slot") ? ConfMan.getInt("save_slot") : -1;

	_player = nullptr;
	_loadTempBuf = nullptr;
	_cursorGfxBuf = nullptr;
	_charsetGfxBuf = nullptr;
	_panelGfxBuf = nullptr;
	_itemsGfxBuf = nullptr;
	_spritesGfxBuf = nullptr;
	_locationBackgroundGfxBuf = nullptr;
	_data5Buf = nullptr;
	_data3GfxBuf = nullptr;
	_quadBackgroundGfxBuf = nullptr;
	_objTxtBuf = nullptr;
	_panelObjectsGfxBuf = nullptr;
	_ptTextBuf = nullptr;
	_infoBarBuf = nullptr;
	_bgTextBuf = nullptr;
	_charNameBuf = nullptr;
	_locationBackgroundMaskBuf = nullptr;
	_csDataBuf = nullptr;
}

TuckerEngine::~TuckerEngine() {
	delete _console;
}

bool TuckerEngine::hasFeature(EngineFeature f) const {
	switch (f) {
	case kSupportsRTL:
	case kSupportsLoadingDuringRuntime:
	case kSupportsSavingDuringRuntime:
		return true;
	default:
		return false;
	}
}

Common::Error TuckerEngine::run() {
	initGraphics(kScreenWidth, kScreenHeight);
	syncSoundSettings();
	_compressedSound.openFile();
	if (_startSlot == -1)
		handleIntroSequence();
	if ((_gameFlags & kGameFlagIntroOnly) == 0 && !shouldQuit()) {
		mainLoop();
	}
	_compressedSound.closeFile();
	return Common::kNoError;
}

int TuckerEngine::getRandomNumber() {
	return _rnd.getRandomNumber(0x7FFF);
}

void TuckerEngine::allocateBuffers() {
	_locationBackgroundGfxBuf = (uint8 *)calloc(1, 640 * 200);
	_loadTempBuf = (uint8 *)calloc(1, 64010);
	_panelGfxBuf = (uint8 *)calloc(1, 64010);
	_itemsGfxBuf = (uint8 *)calloc(1, 19200);
	_charsetGfxBuf = (uint8 *)calloc(1, 22400);
	_cursorGfxBuf = (uint8 *)calloc(1, 256 * 7);
	_infoBarBuf = (uint8 *)calloc(1, 1000);
	_charNameBuf = nullptr;
	_bgTextBuf = nullptr;
	_objTxtBuf = nullptr;
	_panelObjectsGfxBuf = (uint8 *)calloc(1, 20000);
	_data5Buf = nullptr;
	_data3GfxBuf = (uint8 *)calloc(1, 250000);
	_quadBackgroundGfxBuf = (uint8 *)calloc(1, 320 * 140 * 4);
	_locationBackgroundMaskBuf = (uint8 *)calloc(1, 640 * 140);
	_csDataBuf = nullptr;
	_spritesGfxBuf = (uint8 *)calloc(1, 160000);
	_ptTextBuf = nullptr;
	memset(_charWidthTable, 0, sizeof(_charWidthTable));
}

void TuckerEngine::freeBuffers() {
	free(_locationBackgroundGfxBuf);
	free(_loadTempBuf);
	free(_panelGfxBuf);
	free(_itemsGfxBuf);
	free(_charsetGfxBuf);
	free(_cursorGfxBuf);
	free(_infoBarBuf);
	free(_charNameBuf);
	free(_bgTextBuf);
	free(_objTxtBuf);
	free(_panelObjectsGfxBuf);
	free(_data5Buf);
	free(_data3GfxBuf);
	free(_quadBackgroundGfxBuf);
	free(_locationBackgroundMaskBuf);
	free(_csDataBuf);
	free(_spritesGfxBuf);
	free(_ptTextBuf);
}

void TuckerEngine::resetVariables() {
	_quitGame = false;
	_fastMode = false;
	_syncCounter = 0;
	_lastFrameTime = _system->getMillis();
	_mainLoopCounter1 = _mainLoopCounter2 = 0;
	_timerCounter2 = 0;
	_part = _currentPart = kPartInit;
	_location = kLocationNone;
	_nextLocation = (_gameFlags & kGameFlagDemo) ? kLocationInitDemo : kLocationInit;
	_gamePaused = false;
	_gameDebug = false;
	_displaySpeechText = (_gameFlags & kGameFlagNoSubtitles) ? false : ConfMan.getBool("subtitles");
	memset(_flagsTable, 0, sizeof(_flagsTable));

	_gameHintsIndex = 0;
	_gameHintsCounter = 0;
	_gameHintsStringNum = 0;

	_displayGameHints = false;
	_displayHintsText = false;

	if ((_gameFlags & kGameFlagDemo) == 0) {
		_locationWidthTable = _locationWidthTableGame;
		_locationHeightTable = _locationHeightTableGame;
	} else {
		_locationWidthTable = _locationWidthTableDemo;
		_locationHeightTable = _locationHeightTableDemo;
	}

	memset(_sprA02Table, 0, sizeof(_sprA02Table));
	memset(_sprC02Table, 0, sizeof(_sprC02Table));
	memset(_actionsTable, 0, sizeof(_actionsTable));
	_actionsCount = 0;
	memset(_locationObjectsTable, 0, sizeof(_locationObjectsTable));
	_locationObjectsCount = 0;
	memset(_spritesTable, 0, sizeof(_spritesTable));
	_spritesCount = 0;
	memset(_locationAnimationsTable, 0, sizeof(_locationAnimationsTable));
	_locationAnimationsCount = 0;
	memset(_dataTable, 0, sizeof(_dataTable));
	_dataCount = 0;
	memset(_charPosTable, 0, sizeof(_charPosTable));
	_charPosCount = 0;
	memset(_locationSoundsTable, 0, sizeof(_locationSoundsTable));
	_locationSoundsCount = 0;
	memset(_locationMusicsTable, 0, sizeof(_locationMusicsTable));
	_locationMusicsCount = 0;

	_mousePosX = _mousePosY = 0;
	_prevMousePosX = _prevMousePosY = 0;
	_mouseButtonsMask = 0;
	_mouseClick = 0;
	_saveOrLoadGamePanel = 0;
	_mouseIdleCounter = 0;
	_leftMouseButtonPressed = _rightMouseButtonPressed = false;
	_lastKeyPressed = 0;
	memset(_inputKeys, 0, sizeof(_inputKeys));
	_cursorStyle = kCursorNormal;
	_cursorState = kCursorStateNormal;
	_updateCursorFlag = false;

	_panelStyle = kPanelStyleIcons;
	_panelState = kPanelStateNormal;
	_panelType  = kPanelTypeNormal;
	_forceRedrawPanelItems = true;
	_redrawPanelItemsCounter = 0;
	memset(_panelObjectsOffsetTable, 0, sizeof(_panelObjectsOffsetTable));
	_switchPanelCounter = 0;
	_conversationOptionsCount = 0;
	_fadedPanel = false;
	_panelLockedFlag = false;
	_conversationOptionLinesCount = 0;
	memset(_inventoryItemsState, 0, sizeof(_inventoryItemsState));
	memset(_inventoryObjectsList, 0, sizeof(_inventoryObjectsList));
	_inventoryObjectsOffset = 0;
	_inventoryObjectsCount = 0;
	_lastInventoryObjectIndex = 0;

	_currentFxSet = 0;
	_currentFxDist = 0;
	_currentFxScale = 0;
	_currentFxVolume = 0;
	_currentFxIndex = 0;
	_speechSoundNum = 0;
	_speechVolume = kMaxSoundVolume;
	memset(_miscSoundFxNum, 0, sizeof(_miscSoundFxNum));
	memset(_speechHistoryTable, 0, sizeof(_speechHistoryTable));
	_charSpeechSoundCounter = 0;
	memset(_miscSoundFxDelayCounter, 0, sizeof(_miscSoundFxDelayCounter));
	_characterSoundFxDelayCounter = 0;
	_characterSoundFxNum = 0;
	_speechSoundBaseNum = 0;

	_pendingActionIndex = 0;
	_pendingActionDelay = 0;
	_charPositionFlagNum = 0;
	_charPositionFlagValue = 0;
	_actionVerb = _currentActionVerb = _previousActionVerb = kVerbWalk;
	_actionVerbLocked = false;
	_nextAction = 0;
	_selectedObjectNum = 0;
	_selectedObjectType = 0;
	_selectedCharacterNum = 0;
	_actionObj1Type = _actionObj2Type = 0;
	_actionObj1Num = _actionObj2Num = 0;
	_actionRequiresTwoObjects = false;
	_actionPosX = 0;
	_actionPosY = 0;
	_selectedObjectLocationMask = false;
	memset(&_selectedObject, 0, sizeof(_selectedObject));
	_selectedCharacterDirection = 0;
	_selectedCharacter2Num = 0;
	_currentActionObj1Num = _currentActionObj2Num = 0;
	_currentInfoString1SourceType = _currentInfoString2SourceType = 0;
	memset(_speechActionCounterTable, 0, sizeof(_speechActionCounterTable));
	_actionCharacterNum = 0;

	_csDataLoaded = false;
	_csDataHandled = false;
	_stopActionOnSoundFlag = false;
	_stopActionOnSpeechFlag = false;
	_stopActionOnPanelLock = false;
	_csDataTableCount = 0;
	_stopActionCounter = 0;
	_actionTextColor = 0;
	_nextTableToLoadIndex = 0;
	memset(_nextTableToLoadTable, 0, sizeof(_nextTableToLoadTable));
	_soundInstructionIndex = 0;
	_tableInstructionsPtr = nullptr;
	memset(_tableInstructionObj1Table, 0, sizeof(_tableInstructionObj1Table));
	memset(_tableInstructionObj2Table, 0, sizeof(_tableInstructionObj2Table));
	_tableInstructionFlag = false;
	_tableInstructionItemNum1 = _tableInstructionItemNum2 = 0;
	memset(_instructionsActionsTable, 0, sizeof(_instructionsActionsTable));
	_validInstructionId = false;

	memset(_spriteFramesTable, 0, sizeof(_spriteFramesTable));
	memset(_spriteAnimationsTable, 0, sizeof(_spriteAnimationsTable));
	memset(_spriteAnimationFramesTable, 0, sizeof(_spriteAnimationFramesTable));
	_spriteAnimationFrameIndex = 0;
	_backgroundSpriteCurrentFrame = 0;
	_backgroundSpriteLastFrame = 0;
	_backgroundSpriteCurrentAnimation = -1;
	_disableCharactersPath = false;
	_skipCurrentCharacterDraw = false;
	_yPosCurrent = 131;
	_xPosCurrent = 160;
	_characterSpeechDataPtr = nullptr;
	_ptTextOffset = 0;
	memset(_characterAnimationsTable, 0, sizeof(_characterAnimationsTable));
	memset(_characterStateTable, 0, sizeof(_characterStateTable));
	_backgroundSprOffset = 0;
	_mainSpritesBaseOffset = 0;
	_currentSpriteAnimationLength = 0;
	_currentSpriteAnimationFrame = 0;
	_currentSpriteAnimationFrame2 = 0;
	_characterAnimationIndex = -1;
	_characterFacingDirection = _characterPrevFacingDirection = 0;
	_characterBackFrontFacing = _characterPrevBackFrontFacing = false;
	_characterAnimationNum = 0;
	_noCharacterAnimationChange = 0;
	_characterSpriteAnimationFrameCounter = 0;
	_locationMaskIgnore = false;
	_locationMaskType = 0;
	_locationMaskCounter = 0;
	_handleMapCounter = 0;
	_noPositionChangeAfterMap = false;
	_changeBackgroundSprite = false;
	_updateSpriteFlag1 = false;
	_updateSpriteFlag2 = false;

	_mirroredDrawing = false;
	_loadLocBufPtr = nullptr;
	_backgroundSpriteDataPtr = nullptr;
	_locationHeight = 0;
	_scrollOffset = 0;
	_currentGfxBackgroundCounter = 0;
	_currentGfxBackground = nullptr;
	_fadePaletteCounter = 0;
	memset(_currentPalette, 0, sizeof(_currentPalette));
	_fullRedraw = false;
	_dirtyRectsPrevCount = _dirtyRectsCount = 0;

	_updateLocationFadePaletteCounter = 0;
	_updateLocationCounter = 10;
	_updateLocationPos = 0;
	for (int i = 0; i < 5; ++i) {
		_updateLocationXPosTable[i] = 160;
		_updateLocationYPosTable[i] = 131;
	}
	memset(_updateLocationFlagsTable, 0, sizeof(_updateLocationFlagsTable));
	memset(_updateLocationXPosTable2, 0, sizeof(_updateLocationXPosTable2));
	memset(_updateLocationYPosTable2, 0, sizeof(_updateLocationYPosTable2));
	memset(_updateLocationYMaxTable, 0, sizeof(_updateLocationYMaxTable));
	memset(_updateLocation14Step, 0, sizeof(_updateLocation14Step));
	memset(_updateLocation14ObjNum, 0, sizeof(_updateLocation14ObjNum));
	memset(_updateLocation14Delay, 0, sizeof(_updateLocation14Delay));
	_updateLocationCounter2 = 0;
	_updateLocationFlag = false;
	_updateLocation70StringLen = 0;
	memset(_updateLocation70String, 0, sizeof(_updateLocation70String));

	_lastSaveTime = _system->getMillis();
}

void TuckerEngine::mainLoop() {
	allocateBuffers();
	resetVariables();

	loadCharSizeDta();
	if ((_gameFlags & kGameFlagDemo) != 0) {
		addObjectToInventory(30);
		addObjectToInventory(12);
	}
	loadCharset();
	loadPanel();

	loadFile("infobar.txt", _infoBarBuf);
	// WORKAROUND capitalized "With"/"Con" in the English/Spanish versions
	// Fixes Trac#10445.
	if (_gameLang == Common::EN_ANY) {
		_infoBarBuf[getPositionForLine(kVerbPrepositionWith, _infoBarBuf)] = 'w';
	} else if (_gameLang == Common::ES_ESP) {
		_infoBarBuf[getPositionForLine(kVerbPrepositionWith, _infoBarBuf)] = 'c';
	}

	_data5Buf    = loadFile("data5.c", nullptr);
	_bgTextBuf   = loadFile("bgtext.c", nullptr);
	_charNameBuf = loadFile("charname.c", nullptr);
	_csDataBuf   = loadFile("csdata.c", nullptr);
	_csDataSize = _fileLoadSize;

	_currentSaveLoadGameState = 1;

	loadBudSpr();
	loadCursor();
	setCursorStyle(_cursorStyle);
	setCursorState(_cursorState);

	_flagsTable[219] = 1;
	_flagsTable[105] = 1;

	_spriteAnimationFrameIndex =  _spriteAnimationsTable[14]._firstFrameIndex;

	if (ConfMan.hasKey("save_slot")) {
		const int slot = ConfMan.getInt("save_slot");
		if (slot >= 0 && slot <= kLastSaveSlot) {
			loadGameState(slot);
		}
	} else if (ConfMan.hasKey("boot_param")) {
		_nextLocation = (Location)ConfMan.getInt("boot_param");
	}

	do {
		++_syncCounter;
		if (_flagsTable[137] != _flagsTable[138]) {
			loadBudSpr();
			_flagsTable[138] = _flagsTable[137];
		}
		if (_syncCounter >= 2) {
			_syncCounter = 0;
			waitForTimer(2);
		}
		updateMouseState();
		if (_fadePaletteCounter < 16) {
			if (_fadePaletteCounter > 1) {
				fadeOutPalette();
			}
			++_fadePaletteCounter;
		}
		if (_fadePaletteCounter > 19 && _fadePaletteCounter < 34) {
			fadeInPalette();
			++_fadePaletteCounter;
		}
		if (_nextAction != 0) {
			loadActionsTable();
		}
		if (_nextLocation != kLocationNone) {
			setupNewLocation();
		}
		updateCharPosition();
		if (_cursorState == kCursorStateNormal) {
			updateCursor();
		} else if (_panelType == kPanelTypeLoadSavePlayQuit) {
			handleMouseOnPanel();
		}
		if (_mainLoopCounter2 == 0) {
			updateFlagsForCharPosition();
		}
		if (_syncCounter == 0 || !_disableCharactersPath) {
			updateCharactersPath();
		}
		if (_mainLoopCounter2 == 0) {
			updateCharacterAnimation();
			if (_backgroundSpriteCurrentAnimation == -1) {
				_flagsTable[207] = 0;
				if (_flagsTable[220] > 0) {
					_flagsTable[_flagsTable[220]] = _flagsTable[221];
					_flagsTable[220] = 0;
				}
				if (_flagsTable[158] == 1) {
					_flagsTable[158] = 0;
					_skipCurrentCharacterDraw = true;
				}
				_mainLoopCounter1 = 0;
			}
		}
		if (_mainLoopCounter1 == 0) {
			updateSprites();
			updateData3();
			updateSfxData3_1();
		}
		++_mainLoopCounter2;
		handleMap();
		updateScreenScrolling();
		if (_mainLoopCounter2 > 4) {
			_mainLoopCounter2 = 0;
			updateSfxData3_2();
		}
		++_mainLoopCounter1;
		if (_mainLoopCounter1 > 5) {
			_mainLoopCounter1 = 0;
		}
		if (_locationHeight == 140) {
			togglePanelStyle();
			redrawPanelItems();
			if (_displayGameHints && _gameHintsIndex < 6) {
				updateGameHints();
			}
			if (_panelType == kPanelTypeNormal) {
				if (_panelLockedFlag || _pendingActionDelay > 0) {
					if (!_fadedPanel) {
						updateItemsGfxColors(0x60, 0x80);
						_fadedPanel = true;
					}
				} else {
					_fadedPanel = false;
					clearItemsGfx();
					if (_gamePaused) {
						drawPausedInfoBar();
					} else if (_displayHintsText && _mouseIdleCounter > 1000) {
						drawGameHintString();
					} else {
						drawInfoString();
					}
				}
			}
		}
		_mainSpritesBaseOffset = 0;
		if (_locationWidthTable[_location] > 3) {
			++_currentGfxBackgroundCounter;
			if (_currentGfxBackgroundCounter > 39) {
				_currentGfxBackgroundCounter = 0;
			}
			_currentGfxBackground = _quadBackgroundGfxBuf + (_currentGfxBackgroundCounter / 10) * 44800;
			if (_fadePaletteCounter < 34 && _location == kLocationFishingTrawler) {
				int offset = (_currentGfxBackgroundCounter > 29 ? 1 : (_currentGfxBackgroundCounter / 10));
				_spritesTable[0]._gfxBackgroundOffset = offset * 640;
				_mainSpritesBaseOffset = offset;
			}
			_fullRedraw = true;
		} else {
			_currentGfxBackground = _quadBackgroundGfxBuf;
		}
		if (_syncCounter != 0) {
			continue;
		}
		if (_scrollOffset < 320) {
			Graphics::copyRect(_locationBackgroundGfxBuf + _scrollOffset, 640, _currentGfxBackground + _scrollOffset, 320, 320 - _scrollOffset, _locationHeight);
		}
		if (_scrollOffset > 0) {
			Graphics::copyRect(_locationBackgroundGfxBuf + 320, 640, _currentGfxBackground + 44800, 320, _scrollOffset, _locationHeight);
		}
		drawData3();
		execData3PreUpdate();
		for (int i = 0; i < _spritesCount; ++i) {
			if (!_spritesTable[i]._disabled) {
				drawSprite(i);
			}
		}
		if (!_skipCurrentCharacterDraw) {
			if (_backgroundSpriteCurrentAnimation > -1 && _backgroundSpriteCurrentFrame > 0) {
				drawBackgroundSprites();
			} else {
				drawCurrentSprite();
			}
		}
		if (_locationHeight == 140) {
			redrawPanelOverBackground();
		}
		if (_panelType == kPanelTypeLoadSaveSavegame) {
			saveOrLoad();
		}
		execData3PostUpdate();
		if (_timerCounter2 > 45) {
			_timerCounter2 = 0;
		}
		updateSoundsTypes3_4();
		if (_currentFxSet != 0) {
			setSoundVolumeDistance();
		}
		updateCharSpeechSound(_displaySpeechText);
		redrawScreen(_scrollOffset);
		startCharacterSounds();
		for (int num = 0; num < 2; ++num) {
			if (_miscSoundFxDelayCounter[num] > 0) {
				--_miscSoundFxDelayCounter[num];
				if (_miscSoundFxDelayCounter[num] == 0) {
					const int index = _miscSoundFxNum[num];
					startSound(_locationSoundsTable[index]._offset, index, _locationSoundsTable[index]._volume);
				}
			}
		}
		if (_gamePaused && _charSpeechSoundCounter == 0) {
			stopSounds();
			while (1) {
				waitForTimer(1);
				if (_inputKeys[kInputKeyPause]) {
					_inputKeys[kInputKeyPause] = false;
					if (_charSpeechSoundCounter <= 0) {
						break;
					}
				}
				if (_charSpeechSoundCounter == 0) {
					if (_lastKeyPressed >= Common::KEYCODE_1 && _lastKeyPressed <= Common::KEYCODE_5) {
						if (_speechHistoryTable[_lastKeyPressed - Common::KEYCODE_1] > 0) {
							startSpeechSound(_speechHistoryTable[_lastKeyPressed - Common::KEYCODE_1], 100);
							_charSpeechSoundCounter = kDefaultCharSpeechSoundCounter;
						}
						_lastKeyPressed = 0;
					}
				}
				updateCharSpeechSound(false);
			}
			playSounds();
			_gamePaused = false;
		}
		if (_inputKeys[kInputKeyPause]) {
			_inputKeys[kInputKeyPause] = false;
			if (_location != kLocationComputerScreen) {
				_gamePaused = true;
			}
		}
		if (_inputKeys[kInputKeyToggleTextSpeech]) {
			_inputKeys[kInputKeyToggleTextSpeech] = false;
			if ((_gameFlags & kGameFlagNoSubtitles) == 0) {
				_displaySpeechText = !_displaySpeechText;
				ConfMan.setBool("subtitles", _displaySpeechText);
			}
		}
		if (_inputKeys[kInputKeyHelp]) {
			_inputKeys[kInputKeyHelp] = false;
			if (_displayGameHints && _displayHintsText) {
				_gameHintsStringNum = _gameHintsIndex + 1;
				_mouseIdleCounter = 1100;
			}
		}
		if (_inputKeys[kInputKeyEscape]) {
			_inputKeys[kInputKeyEscape] = false;
			if (_gameDebug) {
				_flagsTable[236] = 74;
			}
		}

		if (_flagsTable[236] > 70) {
			handleCreditsSequence();
			_quitGame = true;
		}

		if (shouldPerformAutoSave(_lastSaveTime)) {
			writeAutosave();
		}
	} while (!_quitGame && _flagsTable[100] == 0);

	// auto save on quit
	writeAutosave();

	if (_flagsTable[100] == 1) {
		handleCongratulationsSequence();
	}
	unloadSprA02_01();
	unloadSprC02_01();
	freeBuffers();
}

void TuckerEngine::waitForTimer(int ticksCount) {
	uint32 end = _lastFrameTime + ticksCount * 1000 / 46;
	do {
		parseEvents();
		_system->delayMillis(10);
		_lastFrameTime = _system->getMillis();
	} while (!_fastMode && _lastFrameTime < end);
	_timerCounter2 += ticksCount;
}

void TuckerEngine::parseEvents() {
	Common::Event ev;
	while (_eventMan->pollEvent(ev)) {
		switch (ev.type) {
		case Common::EVENT_KEYDOWN:
			switch (ev.kbd.ascii) {
			// do not use KEYCODE_PERIOD here so that it works with most keyboard layouts
			case '.':
				_inputKeys[kInputKeySkipSpeech] = true;
				break;
			}
			switch (ev.kbd.keycode) {
			case Common::KEYCODE_f:
				if (ev.kbd.hasFlags(Common::KBD_CTRL)) {
					_fastMode = !_fastMode;
				}
				break;
			case Common::KEYCODE_p:
				_inputKeys[kInputKeyPause] = true;
				break;
			case Common::KEYCODE_F1:
				_inputKeys[kInputKeyTogglePanelStyle] = true;
				break;
			case Common::KEYCODE_F2:
				_inputKeys[kInputKeyToggleTextSpeech] = true;
				break;
			case Common::KEYCODE_F3:
				_inputKeys[kInputKeyHelp] = true;
				break;
			case Common::KEYCODE_ESCAPE:
				_inputKeys[kInputKeyEscape] = true;
				_inputKeys[kInputKeySkipSpeech] = true;
				break;
			case Common::KEYCODE_d:
				if (ev.kbd.hasFlags(Common::KBD_CTRL)) {
					this->getDebugger()->attach();
					this->getDebugger()->onFrame();
				}
				break;
			default:
				break;
			}
			_lastKeyPressed = ev.kbd.keycode;
			break;
		case Common::EVENT_MOUSEMOVE:
			updateCursorPos(ev.mouse.x, ev.mouse.y);
			break;
		case Common::EVENT_LBUTTONDOWN:
			updateCursorPos(ev.mouse.x, ev.mouse.y);
			_mouseButtonsMask |= 1;
			break;
		case Common::EVENT_LBUTTONUP:
			updateCursorPos(ev.mouse.x, ev.mouse.y);
			break;
		case Common::EVENT_RBUTTONDOWN:
			updateCursorPos(ev.mouse.x, ev.mouse.y);
			_mouseButtonsMask |= 2;
			_inputKeys[kInputKeySkipSpeech] = true;
			break;
		case Common::EVENT_RBUTTONUP:
			updateCursorPos(ev.mouse.x, ev.mouse.y);
			break;
		case Common::EVENT_WHEELUP:
			_mouseButtonsMask |= 4;
			break;
		case Common::EVENT_WHEELDOWN:
			_mouseButtonsMask |= 8;
			break;
		default:
			break;
		}
	}

	if (_inputKeys[kInputKeyTogglePanelStyle]) {
		if (_panelType == kPanelTypeNormal && _panelState == kPanelStateNormal) {
			_switchPanelCounter = 1;
			_panelState = kPanelStateShrinking;
		}
		_inputKeys[kInputKeyTogglePanelStyle] = false;
	}

	if (_inputKeys[kInputKeySkipSpeech]) {
		if (isSpeechSoundPlaying()) {
			stopSpeechSound();
		}
		_inputKeys[kInputKeySkipSpeech] = false;
	}

	_quitGame = shouldQuit();
}

void TuckerEngine::updateCursorPos(int x, int y) {
	_prevMousePosX = _mousePosX;
	_prevMousePosY = _mousePosY;
	_mousePosX = x;
	_mousePosY = y;
}

void TuckerEngine::setCursorStyle(CursorStyle style) {
	_cursorStyle = style;
	static const int cursorW = 16;
	static const int cursorH = 16;
	CursorMan.replaceCursor(_cursorGfxBuf + _cursorStyle * 256, cursorW, cursorH, 1, 1, 0);
}

void TuckerEngine::setCursorState(CursorState state) {
	_cursorState = state;
	CursorMan.showMouse(_cursorState != kCursorStateDisabledHidden);
}

void TuckerEngine::showCursor(bool visible) {
	CursorMan.showMouse(visible);
}

void TuckerEngine::setupNewLocation() {
	debug(2, "setupNewLocation() current %d next %d", _location, _nextLocation);
	_location = _nextLocation;
	loadObj();
	_nextLocation = kLocationNone;
	_fadePaletteCounter = 0;
	_mainLoopCounter2 = 0;
	_mainLoopCounter1 = 0;
	_characterFacingDirection = 0;
	_actionVerbLocked = false;
	_locationMaskIgnore = false;
	_backgroundSprOffset = 0;
	if (_backgroundSpriteCurrentAnimation > 0 && _backgroundSpriteCurrentFrame > 0) {
		_backgroundSpriteCurrentAnimation = -1;
		_backgroundSpriteCurrentFrame = 0;
	}
	if (!_panelLockedFlag || (_backgroundSpriteCurrentAnimation > 0 && _location != kLocationVentSystem)) {
		_locationMaskType = 0;
	} else {
		_locationMaskType = 3;
	}
	while (_spriteAnimationFramesTable[_spriteAnimationFrameIndex] != 999) {
		++_spriteAnimationFrameIndex;
	}
	_execData3Counter = 0;
	stopSounds();
	loadLoc();
	loadData4();
	loadData3();
	loadActionFile();
	loadCharPos();
	loadSprA02_01();
	loadSprC02_01();
	loadFx();
	playSounds();
	if (_flagsTable[215] > 0) {
		handleMeanwhileSequence();
		_flagsTable[215] = 0;
	}
	if (_flagsTable[231] > 0) {
		handleMeanwhileSequence();
		_flagsTable[231] = 0;
	}
}

void TuckerEngine::copyLocBitmap(const char *filename, int offset, bool isMask) {
	int type = !isMask ? 1 : 0;
	if (offset > 0 && _location == kLocationPark) {
		type = 0;
	}
	loadImage(filename, _loadTempBuf, type);
	uint8 *dst = isMask ? _locationBackgroundMaskBuf : _locationBackgroundGfxBuf;
	dst += offset;
	const uint8 *src = _loadTempBuf;
	for (int y = 0; y < _locationHeight; ++y) {
		memcpy(dst, src, 320);
		src += 320;
		dst += 640;
	}
}

void TuckerEngine::updateMouseState() {
	if (_cursorState != kCursorStateDisabledHidden) {
		_leftMouseButtonPressed = (_mouseButtonsMask & 1) != 0;
		if (_leftMouseButtonPressed) {
			_mouseIdleCounter = 0;
			_gameHintsStringNum = 0;
		}
		_rightMouseButtonPressed = (_mouseButtonsMask & 2) != 0;
		_mouseWheelUp   = _mouseButtonsMask & 4;
		_mouseWheelDown = _mouseButtonsMask & 8;
		_mouseButtonsMask = 0;
		if (_prevMousePosX == _mousePosX && _prevMousePosY == _mousePosY) {
			++_mouseIdleCounter;
		} else {
			_mouseIdleCounter = 0;
			_gameHintsStringNum = 0;
		}
	}
	if (_cursorState == kCursorStateDialog) {
		if (_panelType == kPanelTypeEmpty) {
			setCursorStyle(kCursorTalk);
		}
#if 0
		// confine cursor to dialog area
		if (_mousePosY < 140) {
			_mousePosY = 140;
			_system->warpMouse(_mousePosX, _mousePosY);
		}
#endif
	}
}

void TuckerEngine::updateCharPositionHelper() {
	setCursorState(kCursorStateDisabledHidden );
	_charSpeechSoundCounter = kDefaultCharSpeechSoundCounter;
	_currentActionVerb = kVerbWalk;
	startSpeechSound(_speechSoundNum, _speechVolume);
	int pos = getPositionForLine(_speechSoundNum, _characterSpeechDataPtr);
	_characterSpeechDataPtr += pos;
	_speechSoundNum = 0;
}

void TuckerEngine::updateCharPosition() {
	if (_currentActionVerb == kVerbWalk || _locationMaskCounter == 0) {
		return;
	}
	if (_currentActionVerb == kVerbLook && _location != kLocationRoystonsHomeBoxroom) {
		int pos;
		_actionPosX = _xPosCurrent;
		_actionPosY = _yPosCurrent - 64;
		_actionTextColor = 1;
		_actionCharacterNum = 99;
		switch (_currentInfoString1SourceType) {
		case 0:
			if (_currentActionObj1Num == 0) {
				return;
			}
			if (_currentActionObj1Num == 259) {
				handleSpecialObjectSelectionSequence();
				_currentActionVerb = kVerbWalk;
				return;
			}
			_speechSoundNum = _currentActionObj1Num;
			_characterSpeechDataPtr = _ptTextBuf;
			pos = getPositionForLine(_speechSoundNum + 1865, _characterSpeechDataPtr);
			if (_characterSpeechDataPtr[pos] == '*') {
				switch (_characterSpeechDataPtr[pos + 1]) {
				case 'E':
					++_flagsTable[200];
					if (_flagsTable[200] > 6) {
						_flagsTable[200] = 1;
					}
					_speechSoundNum = 262 + _flagsTable[200];
					break;
				case 'M':
					++_flagsTable[200];
					if (_flagsTable[200] > 10) {
						_flagsTable[200] = 1;
					}
					_speechSoundNum = 268 + _flagsTable[200];
					break;
				case 'R':
					++_flagsTable[200];
					if (_flagsTable[200] > 10) {
						_flagsTable[200] = 1;
					}
					_speechSoundNum = 281 + _flagsTable[200];
					break;
				}
			}
			_speechSoundNum += 1865;
			updateCharPositionHelper();
			return;
		case 1:
			if (_locationAnimationsTable[_selectedCharacter2Num]._getFlag == 1) {
				_speechSoundNum = _speechSoundBaseNum + _locationAnimationsTable[_selectedCharacter2Num]._inventoryNum;
				_characterSpeechDataPtr = _ptTextBuf;
				updateCharPositionHelper();
				return;
			} else if (_currentActionObj1Num == 91) {
				handleSpecialObjectSelectionSequence();
				_currentActionVerb = kVerbWalk;
				return;
			}
			break;
		case 2:
			_characterSpeechDataPtr = _ptTextBuf;
			_speechSoundNum = 2175 + _charPosTable[_selectedCharacterNum]._description;
			if (_charPosTable[_selectedCharacterNum]._description != 0) {
				updateCharPositionHelper();
				return;
			}
			break;
		}
	}
	int actionKey = _currentActionObj2Num * 1000000 + _currentInfoString2SourceType * 100000 + _currentActionVerb * 10000 + _currentInfoString1SourceType * 1000 + _currentActionObj1Num;
	debug(3, "updateCharPosition() actionKey %d",  actionKey);
	bool skip = false;
	Action *action = nullptr;
	for (int i = 0; i < _actionsCount && !skip; ++i) {
		action = &_actionsTable[i];
		if (action->_key == actionKey) {
			skip = true;
			if (action->_testFlag1Num != 0) {
				if (action->_testFlag1Num < 500) {
					if (action->_testFlag1Num >= 300)
						error("updateCharPosition() - Unexpected value for _testFlag1Num : %d", action->_testFlag1Num);
					if (_flagsTable[action->_testFlag1Num] != action->_testFlag1Value)
						skip = false;
				} else if (_inventoryItemsState[action->_testFlag1Num - 500] != action->_testFlag1Value) {
					skip = false;
				}
				debug(3, "updateCharPosition() flag1 %d value %d", action->_testFlag1Num, action->_testFlag1Value);
			}
			if (action->_testFlag2Num != 0) {
				if (action->_testFlag2Num < 500) {
					if (action->_testFlag2Num >= 300)
						error("updateCharPosition() - Unexpected value for _testFlag1Num : %d", action->_testFlag1Num);
					if (_flagsTable[action->_testFlag2Num] != action->_testFlag2Value)
						skip = false;
				} else if (_inventoryItemsState[action->_testFlag2Num - 500] != action->_testFlag2Value) {
					skip = false;
				}
				debug(3, "updateCharPosition() flag2 %d value %d", action->_testFlag2Num, action->_testFlag2Value);
			}
		}
	}
	if (!skip) {
		playSpeechForAction(_currentActionVerb);
		_currentActionVerb = kVerbWalk;
		return;
	}
	assert(action);
	if (action->_speech != 6) {
		if (action->_speech < 100) {
			_spriteAnimationFrameIndex = _spriteAnimationsTable[action->_speech]._firstFrameIndex;
			_currentSpriteAnimationLength = _spriteAnimationsTable[action->_speech]._numParts;
			_mirroredDrawing = (action->_flipX != 0);
			_characterFacingDirection = 5;
			_mainLoopCounter2 = 0;
		} else {
			_backgroundSpriteCurrentAnimation = action->_speech - 100;
			_backgroundSpriteCurrentFrame = 0;
			_mirroredDrawing = false;
		}
	}
	_pendingActionDelay = action->_delay;
	_charPositionFlagNum = action->_setFlagNum;
	_charPositionFlagValue = action->_setFlagValue;
	_pendingActionIndex = action->_index;
	_characterSoundFxDelayCounter = action->_fxDelay;
	_characterSoundFxNum = action->_fxNum;
	_previousActionVerb = _currentActionVerb;
	_currentActionVerb = kVerbWalk;
}

void TuckerEngine::updateFlagsForCharPosition() {
	if (_pendingActionDelay != 0) {
		--_pendingActionDelay;
		if (_pendingActionDelay > 0) {
			return;
		}
		switch (_previousActionVerb) {
		case kVerbTalk:
		case kVerbOpen:
		case kVerbClose:
		case kVerbUse:
			debug(3, "updateFlagsForCharPosition() set flag %d value %d", _charPositionFlagNum, _charPositionFlagValue);
			_flagsTable[_charPositionFlagNum] = _charPositionFlagValue;
			break;
		case kVerbTake:
			if (_charPositionFlagValue == 1) {
				addObjectToInventory(_charPositionFlagNum);
				_forceRedrawPanelItems = true;
			}
			break;
		default:
			break;
		}
		if (_pendingActionIndex > 0) {
			_nextAction = _pendingActionIndex;
		}
	}
}

void TuckerEngine::fadeOutPalette(int colorsCount) {
	uint8 pal[256 * 3];
	_system->getPaletteManager()->grabPalette(pal, 0, colorsCount);
	for (int color = 0; color < colorsCount; ++color) {
		for (int i = 0; i < 3; ++i) {
			const int c = int(pal[color * 3 + i]) + kFadePaletteStep * 4;
			pal[color * 3 + i] = MIN<int>(c, _currentPalette[color * 3 + i]);
		}
	}
	_system->getPaletteManager()->setPalette(pal, 0, colorsCount);
	_system->updateScreen();
}

void TuckerEngine::fadeInPalette(int colorsCount) {
	uint8 pal[256 * 3];
	_system->getPaletteManager()->grabPalette(pal, 0, colorsCount);
	for (int color = 0; color < colorsCount; ++color) {
		for (int i = 0; i < 3; ++i) {
			const int c = int(pal[color * 3 + i]) - kFadePaletteStep * 4;
			pal[color * 3 + i] = MAX<int>(c, 0);
		}
	}
	_system->getPaletteManager()->setPalette(pal, 0, colorsCount);
	_system->updateScreen();
}

void TuckerEngine::fadePaletteColor(int color, int step) {
	uint8 rgb[4];
	_system->getPaletteManager()->grabPalette(rgb, color, 1);
	for (int i = 0; i < 3; ++i) {
		const int c = _currentPalette[color * 3 + i] + step * 4;
		rgb[i] = MIN(c, 255);
	}
	_system->getPaletteManager()->setPalette(rgb, color, 1);
}

void TuckerEngine::setBlackPalette() {
	uint8 pal[256 * 3];
	memset(pal, 0, sizeof(pal));
	_system->getPaletteManager()->setPalette(pal, 0, 256);
}

void TuckerEngine::updateCursor() {
	setCursorStyle(kCursorNormal);
	if (_backgroundSpriteCurrentAnimation == -1 && !_panelLockedFlag && _selectedObject._locationObjectLocation != kLocationNone) {
		_selectedObject._locationObjectLocation = kLocationNone;
	}
	if (_locationMaskType > 0 || _selectedObject._locationObjectLocation != kLocationNone || _pendingActionDelay > 0) {
		return;
	}
	if (_rightMouseButtonPressed) {
		if (!_updateCursorFlag) {
			if (_actionVerb == kVerbLast) {
				_actionVerb = kVerbFirst;
			} else {
				_actionVerb = (Verb)(_actionVerb + 1);
			}
			_updateCursorFlag = true;
			_actionVerbLocked = true;
			_actionRequiresTwoObjects = false;
		}
	} else {
		_updateCursorFlag = false;
	}
	if (!_actionVerbLocked) {
		setActionVerbUnderCursor();
		if (_actionVerb == kVerbWalk && _location == kLocationTV) {
			_actionVerb = kVerbUse;
		}
	}
	_selectedObjectNum = 0;
	_selectedObjectType = 0;
	int num = setCharacterUnderCursor();
	if (_selectedObjectType == 0) {
		num = setLocationAnimationUnderCursor();
	}
	if (_selectedObjectType > 0) {
		_selectedObjectNum = num;
	} else {
		num = getObjectUnderCursor();
		if (num > -1) {
			_selectedObjectNum = _locationObjectsTable[num]._textNum;
		}
	}
	handleMouseClickOnInventoryObject();
	if (_actionVerb == kVerbTalk && _selectedObjectType != 2) {
		_selectedObjectNum = 0;
		_selectedObjectType = 0;
	} else if (_actionVerb == kVerbGive && _selectedObjectType != 3 && !_actionRequiresTwoObjects) {
		_selectedObjectNum = 0;
		_selectedObjectType = 0;
	}
	if (!_actionVerbLocked && _selectedObjectType == 2 && _selectedObjectNum != 21) {
		_actionVerb = kVerbTalk;
	}
	if (!_actionRequiresTwoObjects) {
		_actionObj1Num = _selectedObjectNum;
		_actionObj1Type = _selectedObjectType;
		_actionObj2Num = 0;
		_actionObj2Type = 0;
	} else if (_actionObj1Num == _selectedObjectNum && _actionObj1Type == _selectedObjectType) {
		_selectedObjectNum = 0;
		_selectedObjectType = 0;
		_actionObj2Num = 0;
		_actionObj2Type = 0;
	} else {
		_actionObj2Num = _selectedObjectNum;
		_actionObj2Type = _selectedObjectType;
	}
	if (!_leftMouseButtonPressed) {
		_mouseClick = 0;
	}
	if (_mousePosY >= 150) {
		if (_mouseWheelUp)
			moveDownInventoryObjects();
		else if (_mouseWheelDown)
			moveUpInventoryObjects();
	}
	if (_leftMouseButtonPressed && _mouseClick == 0) {
		_fadedPanel = false;
		_mouseClick = 1;
		clearItemsGfx();
		drawInfoString();
		if (_mousePosY >= 150 && _mousePosX < 212) {
			if (_mousePosX < 200) {
				setActionVerbUnderCursor();
				_actionVerbLocked = true;
				_actionRequiresTwoObjects = false;
			} else if (_mousePosY < 175) {
				moveDownInventoryObjects();
			} else {
				moveUpInventoryObjects();
			}
		} else {
			if (_selectedObjectType == 3) {
				setActionForInventoryObject();
			} else if (_actionVerb != kVerbWalk) {
				_actionVerbLocked = false;
				setActionState();
			} else if (_actionObj1Num == 261 || (_actionObj1Num == 205 && _flagsTable[143] == 0)) {
				_actionVerbLocked = false;
				setActionState();
			} else {
				_actionVerbLocked = false;
				_actionRequiresTwoObjects = false;
				_currentActionVerb = kVerbWalk;
				setSelectedObjectKey();
			}
		}
	}
}

void TuckerEngine::stopSounds() {
	for (int i = 0; i < _locationSoundsCount; ++i) {
		stopSound(i);
	}
	for (int i = 0; i < _locationMusicsCount; ++i) {
		stopMusic(i);
	}
}

void TuckerEngine::playSounds() {
	for (int i = 0; i < _locationSoundsCount; ++i) {
		if (_locationSoundsTable[i]._type == 1 || _locationSoundsTable[i]._type == 2 || _locationSoundsTable[i]._type == 5 ||
			(_locationSoundsTable[i]._type == 7 && _flagsTable[_locationSoundsTable[i]._flagNum] == _locationSoundsTable[i]._flagValueStartFx)) {
			startSound(_locationSoundsTable[i]._offset, i, _locationSoundsTable[i]._volume);
		}
	}
	for (int i = 0; i < _locationMusicsCount; ++i) {
		if (_locationMusicsTable[i]._flag > 0) {
			startMusic(_locationMusicsTable[i]._offset, i, _locationMusicsTable[i]._volume);
		}
	}
}

void TuckerEngine::updateCharactersPath() {
	if (!_panelLockedFlag) {
		return;
	}
	if (_backgroundSpriteCurrentAnimation != -1 && _location != kLocationVentSystem) {
		if (_xPosCurrent == _selectedObject._xPos && _yPosCurrent == _selectedObject._yPos) {
			_locationMaskCounter = 1;
			_panelLockedFlag = false;
		}
		return;
	}
	int xPos = _xPosCurrent;
	int yPos = _yPosCurrent;
	if (_characterFacingDirection == 5) {
		_characterPrevFacingDirection = 5;
	}
	bool flag = false;
	if (_yPosCurrent > _selectedObject._yPos) {
		if (testLocationMask(_xPosCurrent, _yPosCurrent - 1)) {
			--_yPosCurrent;
			_characterFacingDirection = 4;
			flag = true;
		}
	} else if (_yPosCurrent < _selectedObject._yPos) {
		if (testLocationMask(_xPosCurrent, _yPosCurrent + 1)) {
			++_yPosCurrent;
			_characterFacingDirection = 2;
			flag = true;
		}
	}
	if (_xPosCurrent > _selectedObject._xPos) {
		if (testLocationMask(_xPosCurrent - 1, _yPosCurrent)) {
			--_xPosCurrent;
			_characterFacingDirection = 3;
			_characterBackFrontFacing = false;
			flag = true;
		}
	} else if (_xPosCurrent < _selectedObject._xPos) {
		if (testLocationMask(_xPosCurrent + 1, _yPosCurrent)) {
			++_xPosCurrent;
			_characterFacingDirection = 1;
			_characterBackFrontFacing = true;
			flag = true;
		}
	}
	if (!flag) {
		if (_selectedObjectLocationMask) {
			_selectedObjectLocationMask = false;
			_selectedObject._xPos = _selectedObject._xDefaultPos;
			_selectedObject._yPos = _selectedObject._yDefaultPos;
		} else {
			_panelLockedFlag = false;
			_characterFacingDirection = 0;
			if (_xPosCurrent == _selectedObject._xPos && _yPosCurrent == _selectedObject._yPos) {
				_locationMaskCounter = 1;
			}
		}
	}
	if (_location == kLocationVentSystem) {
		if ((_backgroundSpriteCurrentAnimation != 3 || _characterBackFrontFacing) && (_backgroundSpriteCurrentAnimation != 6 || !_characterBackFrontFacing)) {
			_xPosCurrent = xPos;
			_yPosCurrent = yPos;
			return;
		}
	}
	if (_xPosCurrent != _selectedObject._xPos || _yPosCurrent != _selectedObject._yPos) {
		return;
	}
	if (_selectedObjectLocationMask) {
		return;
	}
	_locationMaskCounter = 1;
	_panelLockedFlag = false;
	_locationMaskIgnore = false;
	if (_characterPrevFacingDirection <= 0 || _characterPrevFacingDirection >= 5) {
		return;
	}
	if (_selectedObject._locationObjectLocation == kLocationNone) {
		_characterFacingDirection = 5;
		while (_spriteAnimationFramesTable[_spriteAnimationFrameIndex] != 999) {
			++_spriteAnimationFrameIndex;
		}
		++_spriteAnimationFrameIndex;
	}
}

void TuckerEngine::setSoundVolumeDistance() {
	int w = ABS(_xPosCurrent - _currentFxDist);
	int d = w * _currentFxScale / 10;
	int volume = (d > _currentFxVolume) ? 0 : _currentFxVolume - d;
	setVolumeSound(_currentFxIndex, volume);
}

void TuckerEngine::updateData3DrawFlag() {
	for (int i = 0; i < _locationAnimationsCount; ++i) {
		LocationAnimation *a = &_locationAnimationsTable[i];
		if (a->_flagNum > 0 && a->_flagValue != _flagsTable[a->_flagNum]) {
			a->_drawFlag = false;
		} else if (a->_getFlag == 0) {
			a->_drawFlag = true;
		} else {
			a->_drawFlag = (_inventoryItemsState[a->_inventoryNum] == 0);
		}
	}
}

void TuckerEngine::updateData3() {
	updateData3DrawFlag();
	for (int i = 0; i < _locationAnimationsCount; ++i) {
		LocationAnimation *a = &_locationAnimationsTable[i];
		if (a->_animLastCounter != 0 && a->_drawFlag) {
			if (a->_animLastCounter == a->_animCurrentCounter) {
				a->_animCurrentCounter = a->_animInitCounter;
			} else {
				++a->_animCurrentCounter;
			}
			const int index = a->_animCurrentCounter;
			if (_staticData3Table[index] == 998) {
				_flagsTable[_staticData3Table[index + 1]] = _staticData3Table[index + 2];
				a->_animCurrentCounter = a->_animInitCounter;
				a->_drawFlag = false;
			}
			if (_location == kLocationStoreRoom && i == 0) {
				// workaround bug #2872385: update fish animation sequence for correct
				// position in aquarium.
				if (a->_animInitCounter == 505 && a->_animCurrentCounter == 513) {
					a->_animCurrentCounter = 525;
				}
			}
			a->_graphicNum = _staticData3Table[a->_animCurrentCounter];
		}
	}
	updateData3DrawFlag();
}

void TuckerEngine::updateSfxData3_1() {
	for (int i = 0; i < _locationSoundsCount; ++i) {
		LocationSound *s = &_locationSoundsTable[i];
		if ((s->_type == 6 || s->_type == 7) && s->_updateType == 1) {
			for (int j = 0; j < _spritesCount; ++j) {
				if (_spritesTable[j]._animationFrame == s->_startFxSpriteNum && _spritesTable[j]._state == s->_startFxSpriteState) {
					if (s->_type == 7) {
						_flagsTable[s->_flagNum] = s->_flagValueStartFx;
					}
					startSound(s->_offset, i, s->_volume);
				} else if (s->_type == 7) {
					if (_spritesTable[j]._animationFrame == s->_stopFxSpriteNum && _spritesTable[j]._state == s->_stopFxSpriteState) {
						_flagsTable[s->_flagNum] = s->_flagValueStopFx;
						stopSound(i);
					}
				}
			}
		}
	}
}

void TuckerEngine::updateSfxData3_2() {
	for (int i = 0; i < _locationSoundsCount; ++i) {
		LocationSound *s = &_locationSoundsTable[i];
		if ((s->_type == 6 || s->_type == 7) && s->_updateType == 0) {
			if (s->_startFxSpriteNum == _backgroundSpriteCurrentFrame && s->_startFxSpriteState == _backgroundSpriteCurrentAnimation) {
				if (s->_type == 7) {
					_flagsTable[s->_flagNum] = s->_flagValueStartFx;
				}
				startSound(s->_offset, i, s->_volume);
			} else if (s->_type == 7) {
				if (s->_stopFxSpriteNum == _backgroundSpriteCurrentFrame && s->_stopFxSpriteState == _backgroundSpriteCurrentAnimation) {
					_flagsTable[s->_flagNum] = s->_flagValueStopFx;
					stopSound(i);
				}
			}
		}
	}
}

void TuckerEngine::saveOrLoad() {
	bool hasSavegame = existsSavegame();

	if (!_leftMouseButtonPressed) {
		_mouseClick = 0;
	}
	if (_currentSaveLoadGameState > 0) {
		if (_saveOrLoadGamePanel == 0 && !hasSavegame) {
			drawSpeechText(_scrollOffset + 120, 170, _infoBarBuf, _saveOrLoadGamePanel + 21, 102);
		} else {
			drawSpeechText(_scrollOffset + 120, 170, _infoBarBuf, _saveOrLoadGamePanel + 19, 102);
			int len = getStringWidth(_saveOrLoadGamePanel + 19, _infoBarBuf);
			drawStringInteger(_currentSaveLoadGameState, len / 2 + 128, 160, 2);
		}
	} else {
		drawSpeechText(_scrollOffset + 120, 170, _infoBarBuf, 21, 102);
	}
	if (_mousePosY > 140) {
		if (_mouseWheelUp && _currentSaveLoadGameState < kLastSaveSlot) {
			++_currentSaveLoadGameState;
			_forceRedrawPanelItems = true;
			return;
		} else if (_mouseWheelDown && _currentSaveLoadGameState > 1) {
			--_currentSaveLoadGameState;
			_forceRedrawPanelItems = true;
			return;
		}
	}
	if (_leftMouseButtonPressed && _mouseClick == 0) {
		_mouseClick = 1;
		if (_mousePosX > 228 && _mousePosX < 240 && _mousePosY > 154 && _mousePosY < 170) {
			if (_currentSaveLoadGameState < kLastSaveSlot) {
				++_currentSaveLoadGameState;
				_forceRedrawPanelItems = true;
			}
			return;
		}
		if (_mousePosX > 228 && _mousePosX < 240 && _mousePosY > 170 && _mousePosY < 188) {
			if (_currentSaveLoadGameState > 1) {
				--_currentSaveLoadGameState;
				_forceRedrawPanelItems = true;
			}
			return;
		}
		if (_mousePosX > 244 && _mousePosX < 310 && _mousePosY > 170 && _mousePosY < 188) {
			_forceRedrawPanelItems = true;
			_panelType = kPanelTypeLoadSavePlayQuit;
			return;
		}
		if (_mousePosX > 260 && _mousePosX < 290 && _mousePosY > 152 && _mousePosY < 168) {
			if (_saveOrLoadGamePanel == 1) {
				saveGameState(_currentSaveLoadGameState, "");
			} else if (hasSavegame && _currentSaveLoadGameState > 0) {
				loadGameState(_currentSaveLoadGameState);
			}
			_forceRedrawPanelItems = true;
			_panelType = kPanelTypeNormal;
			setCursorState(kCursorStateNormal);
			return;
		}
	}
}

void TuckerEngine::handleMouseOnPanel() {
	if (!_leftMouseButtonPressed) {
		_mouseClick = 0;
	}
	if (_leftMouseButtonPressed && _mouseClick == 0) {
		_mouseClick = 1;
		if (_mousePosY < 160 || _mousePosY > 176) {
			return;
		}
		if (_mousePosX < 45 || _mousePosX > 275) {
			return;
		}
		if (_mousePosX < 96) {
			_saveOrLoadGamePanel = 0;
			_forceRedrawPanelItems = true;
			_panelType = kPanelTypeLoadSaveSavegame;
		} else if (_mousePosX < 158) {
			_saveOrLoadGamePanel = 1;
			_forceRedrawPanelItems = true;
			_panelType = kPanelTypeLoadSaveSavegame;
		} else if (_mousePosX < 218) {
			_forceRedrawPanelItems = true;
			_panelType = kPanelTypeNormal;
			setCursorState(kCursorStateNormal);
		} else {
			_quitGame = true;
		}
	}
}

void TuckerEngine::togglePanelStyle() {
	switch (_panelState) {
	case kPanelStateShrinking:
		if (++_switchPanelCounter == 25) {
			_panelStyle = (_panelStyle == kPanelStyleVerbs) ? kPanelStyleIcons : kPanelStyleVerbs;
			loadPanel();
			_forceRedrawPanelItems = true;
			_panelState = kPanelStateExpanding;
		}
		break;

	case kPanelStateExpanding:
		if (--_switchPanelCounter == 0) {
			_panelState = kPanelStateNormal;
		}
		break;

	default:
		break;
	}
}

void TuckerEngine::redrawPanelOverBackground() {
	const uint8 *src = _itemsGfxBuf;
	uint8 *dst = _locationBackgroundGfxBuf + 640 * 140 + _scrollOffset;
	for (int y = 0; y < 10; ++y) {
		memcpy(dst, src, 320);
		src += 320;
		dst += 640;
	}
	for (int y = 0; y < _switchPanelCounter; ++y) {
		for (int x = 0; x < 320; ++x) {
			dst[x] = 0;
		}
		dst += 640;
	}
	int y2 = 50 - _switchPanelCounter * 2;
	for (int y = 0; y < y2; ++y) {
		int i = y * 50 / y2;
		memcpy(dst, src + i * 320, 320);
		dst += 640;
	}
	for (int y = 0; y < _switchPanelCounter; ++y) {
		for (int x = 0; x < 320; ++x) {
			dst[x] = 0;
		}
		dst += 640;
	}
	if (_conversationOptionsCount > 0) {
		drawConversationTexts();
	}
	addDirtyRect(_scrollOffset, 140, 320, 60);
}

void TuckerEngine::drawConversationTexts() {
	int y = 141;
	bool flag = false;
	for (int i = 0; i <  _conversationOptionsCount; ++i) {
		int color = 108;
		if ((_mousePosY > y && _mousePosY < y + 11) || _nextTableToLoadIndex == i) {
			color = 106;
		}
		drawSpeechText(0, y, _characterSpeechDataPtr, _instructionsActionsTable[i], color);
		if (_mousePosY > y && _mousePosY < _conversationOptionLinesCount * 10 + y + 1) {
			_nextTableToLoadIndex = i;
			flag = true;
		}
		y += _conversationOptionLinesCount * 10;
	}
	if (!flag) {
		_nextTableToLoadIndex = -1;
	}
}

void TuckerEngine::updateScreenScrolling() {
	int scrollPrevOffset = _scrollOffset;
	if (_locationWidthTable[_location] != 2) {
		_scrollOffset = 0;
	} else if (_validInstructionId) {
		_scrollOffset = _xPosCurrent - 200;
	} else if (_location == kLocationPark && _backgroundSpriteCurrentAnimation == 6 && _scrollOffset + 200 < _xPosCurrent) {
		++_scrollOffset;
		if (_scrollOffset > 320) {
			_scrollOffset = 320;
		}
	} else if (_scrollOffset + 120 > _xPosCurrent) {
		_scrollOffset = _xPosCurrent - 120;
		if (_scrollOffset < 0) {
			_scrollOffset = 0;
		}
	} else if (_scrollOffset + 200 < _xPosCurrent) {
		_scrollOffset = _xPosCurrent - 200;
		if (_scrollOffset > 320) {
			_scrollOffset = 320;
		}
	}
	if (scrollPrevOffset != _scrollOffset) {
		_fullRedraw = true;
	}
}

void TuckerEngine::updateGameHints() {
	if (_gameHintsIndex == 0 && _flagsTable[3] > 0) {
		_gameHintsIndex = 1;
		_gameHintsCounter = 0;
		_displayHintsText = false;
	} else if (_gameHintsIndex == 1 && _flagsTable[12] > 0) {
		_gameHintsIndex = 2;
		_gameHintsCounter = 0;
		_displayHintsText = false;
	} else if (_gameHintsIndex == 2 && _flagsTable[20] > 0) {
		_gameHintsIndex = 3;
		_gameHintsCounter = 0;
		_displayHintsText = false;
	} else if (_gameHintsIndex == 3 && _flagsTable[9] > 0) {
		_gameHintsIndex = 4;
		_gameHintsCounter = 0;
		_displayHintsText = false;
	} else if (_gameHintsIndex == 4 && _flagsTable[23] > 0) {
		_gameHintsIndex = 5;
		_gameHintsCounter = 0;
		_displayHintsText = false;
	} else if (_flagsTable[19] > 0) {
		_gameHintsIndex = 6;
		_gameHintsCounter = 0;
		_displayHintsText = false;
	}
	++_gameHintsCounter;
	if (_gameHintsCounter > 1500) {
		_displayHintsText = true;
	}
}

void TuckerEngine::startCharacterSounds() {
	if (_characterSoundFxDelayCounter != 0) {
		--_characterSoundFxDelayCounter;
		if (_characterSoundFxDelayCounter <= 0) {
			startSound(_locationSoundsTable[_characterSoundFxNum]._offset, _characterSoundFxNum, _locationSoundsTable[_characterSoundFxNum]._volume);
		}
	}
}

void TuckerEngine::updateSoundsTypes3_4() {
	if (isSoundPlaying(0)) {
		return;
	}
	for (int i = 0; i < _locationSoundsCount; ++i) {
		switch (_locationSoundsTable[i]._type) {
		case 3:
			if (getRandomNumber() >= 32300) {
				startSound(_locationSoundsTable[i]._offset, 0, _locationSoundsTable[i]._volume);
				return;
			}
			break;
		case 4:
			if (getRandomNumber() >= 32763) {
				startSound(_locationSoundsTable[i]._offset, 0, _locationSoundsTable[i]._volume);
				return;
			}
			break;
		}
	}
}

void TuckerEngine::drawData3() {
	for (int i = 0; i < _locationAnimationsCount; ++i) {
		if (_locationAnimationsTable[i]._drawFlag) {
			int num = _locationAnimationsTable[i]._graphicNum;
			const Data *d = &_dataTable[num];
			Graphics::decodeRLE(_locationBackgroundGfxBuf + d->_yDest * 640 + d->_xDest, _data3GfxBuf + d->_sourceOffset, d->_xSize, d->_ySize);
			addDirtyRect(d->_xDest, d->_yDest, d->_xSize, d->_ySize);
		}
	}
}

void TuckerEngine::execData3PreUpdate() {
	switch (_location) {
	case 1:
		execData3PreUpdate_locationNum1();
		break;
	case 2:
		execData3PreUpdate_locationNum2();
		break;
	case 3:
		execData3PreUpdate_locationNum3();
		break;
	case 4:
		execData3PreUpdate_locationNum4();
		break;
	case 6:
		execData3PreUpdate_locationNum6();
		break;
	case 9:
		execData3PreUpdate_locationNum9();
		break;
	case 10:
		execData3PreUpdate_locationNum10();
		break;
	case 12:
		execData3PreUpdate_locationNum12();
		break;
	case 13:
		execData3PreUpdate_locationNum13();
		break;
	case 14:
		execData3PreUpdate_locationNum14();
		break;
	case 15:
		execData3PreUpdate_locationNum15();
		break;
	case 16:
		execData3PreUpdate_locationNum16();
		break;
	case 19:
		execData3PreUpdate_locationNum19();
		break;
	case 21:
		execData3PreUpdate_locationNum21();
		break;
	case 22:
		execData3PreUpdate_locationNum22();
		break;
	case 24:
		execData3PreUpdate_locationNum24();
		break;
	case 25:
		execData3PreUpdate_locationNum25();
		break;
	case 26:
		execData3PreUpdate_locationNum26();
		break;
	case 27:
		execData3PreUpdate_locationNum27();
		break;
	case 28:
		execData3PreUpdate_locationNum28();
		break;
	case 29:
		execData3PreUpdate_locationNum29();
		break;
	case 30:
		execData3PreUpdate_locationNum30();
		break;
	case 31:
		execData3PreUpdate_locationNum31();
		break;
	case 32:
		execData3PreUpdate_locationNum32();
		break;
	case 33:
		execData3PreUpdate_locationNum33();
		break;
	case 34:
		execData3PreUpdate_locationNum34();
		break;
	case 35:
		execData3PreUpdate_locationNum35();
		break;
	case 36:
		execData3PreUpdate_locationNum36();
		break;
	case 38:
		execData3PreUpdate_locationNum38();
		break;
	case 41:
		execData3PreUpdate_locationNum41();
		break;
	case 42:
		execData3PreUpdate_locationNum42();
		break;
	case 43:
		execData3PreUpdate_locationNum43();
		break;
	case 44:
		execData3PreUpdate_locationNum44();
		break;
	case 49:
		execData3PreUpdate_locationNum49();
		break;
	case 52:
		execData3PreUpdate_locationNum52();
		break;
	case 53:
		execData3PreUpdate_locationNum53();
		break;
	case 57:
		execData3PreUpdate_locationNum57();
		break;
	case 58:
		execData3PreUpdate_locationNum58();
		break;
	case 61:
		execData3PreUpdate_locationNum61();
		break;
	case 63:
		execData3PreUpdate_locationNum63();
		break;
	case 64:
		execData3PreUpdate_locationNum64();
		break;
	case 65:
		execData3PreUpdate_locationNum65();
		break;
	case 66:
		execData3PreUpdate_locationNum66();
		break;
	case 70:
		execData3PreUpdate_locationNum70();
		break;
	default:
		break;
	}
}

void TuckerEngine::execData3PostUpdate() {
	switch (_location) {
	case 1:
		execData3PostUpdate_locationNum1();
		break;
	case 6:
		execData3PostUpdate_locationNum6();
		break;
	case 8:
		execData3PostUpdate_locationNum8();
		break;
	case 9:
		execData3PostUpdate_locationNum9();
		break;
	case 14:
		execData3PostUpdate_locationNum14();
		break;
	case 21:
		execData3PostUpdate_locationNum21();
		break;
	case 24:
		execData3PostUpdate_locationNum24();
		break;
	case 27:
		execData3PostUpdate_locationNum27();
		break;
	case 28:
		execData3PostUpdate_locationNum28();
		break;
	case 32:
		execData3PostUpdate_locationNum32();
		break;
	case 60:
		execData3PostUpdate_locationNum60();
		break;
	case 66:
		execData3PostUpdate_locationNum66();
		break;
	default:
		break;
	}
}

void TuckerEngine::drawBackgroundSprites() {
	if (_backgroundSpriteDataPtr && _backgroundSpriteCurrentFrame != 0 && _backgroundSpriteCurrentFrame <= _backgroundSpriteLastFrame) {
		int frameOffset = READ_LE_UINT24(_backgroundSpriteDataPtr + _backgroundSpriteCurrentFrame * 4);
		int srcW = READ_LE_UINT16(_backgroundSpriteDataPtr + frameOffset);
		int srcH = READ_LE_UINT16(_backgroundSpriteDataPtr + frameOffset + 2);
		int srcX = READ_LE_UINT16(_backgroundSpriteDataPtr + frameOffset + 8);
		int srcY = READ_LE_UINT16(_backgroundSpriteDataPtr + frameOffset + 10);
		if (_location == kLocationFishingTrawler && _backgroundSpriteCurrentAnimation > 1) {
			srcY += _mainSpritesBaseOffset;
		}
		if (_location == kLocationSubmarineHangar && _backgroundSpriteCurrentAnimation == 3) {
			srcX += 228;
		} else if (_location == kLocationInsideMuseumPartThree && _backgroundSpriteCurrentAnimation == 1) {
			srcX += 100;
		} else if (_xPosCurrent > 320 && _xPosCurrent < 640) {
			srcX += 320;
		}
		srcX += _backgroundSprOffset;
		Graphics::decodeRLE_248(_locationBackgroundGfxBuf + srcY * 640 + srcX, _backgroundSpriteDataPtr + frameOffset + 12, srcW, srcH, 0, _locationHeightTable[_location], false);
		addDirtyRect(srcX, srcY, srcW, srcH);
	}
}

void TuckerEngine::drawCurrentSprite() {
	// WORKAROUND: original game glitch
	// Locations 48 and 61 contain reserved colors from [0xE0-0xF8] in a walkable area which
	// results in a number of pixels being falsely drawn in the foreground (on top of Bud).
	// Even worse, location 61 uses some of the same colors in places which actually _should_
	// be drawn in the foreground.
	// We whitelist these colors based on location number and, in case of location 61, also
	// based on Bud's location (pun not intended).
	// This fixes Trac#10423.
	const int *whitelistReservedColors = nullptr;
	//                                                      [0xE0, ...                            ..., 0xEF]
	static const int whitelistReservedColorsLocation48[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0 };
	static const int whitelistReservedColorsLocation61[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1 };
	switch (_location) {
	case kLocationCorridor:
		whitelistReservedColors = (const int *)&whitelistReservedColorsLocation48;
		break;

	case kLocationParkPartThree:
		if (_xPosCurrent <= 565)
			whitelistReservedColors = (const int *)&whitelistReservedColorsLocation61;
		break;

	default:
		break;
	}

	SpriteFrame *chr = &_spriteFramesTable[_currentSpriteAnimationFrame];
	int yPos = _yPosCurrent + _mainSpritesBaseOffset - 54 + chr->_yOffset;
	int xPos = _xPosCurrent;
	if (!_mirroredDrawing) {
		xPos += chr->_xOffset - 14;
	} else {
		xPos -= chr->_xSize + chr->_xOffset - 14;
	}
	Graphics::decodeRLE_248(_locationBackgroundGfxBuf + yPos * 640 + xPos, _spritesGfxBuf + chr->_sourceOffset, chr->_xSize, chr->_ySize,
		chr->_yOffset, _locationHeightTable[_location], _mirroredDrawing, whitelistReservedColors);
	addDirtyRect(xPos, yPos, chr->_xSize, chr->_ySize);
	if (_currentSpriteAnimationLength > 1) {
		SpriteFrame *chr2 = &_spriteFramesTable[_currentSpriteAnimationFrame2];
		yPos = _yPosCurrent + _mainSpritesBaseOffset - 54 + chr2->_yOffset;
		xPos = _xPosCurrent;
		if (!_mirroredDrawing) {
			xPos += chr2->_xOffset - 14;
		} else {
			xPos -= chr2->_xSize + chr2->_xOffset - 14;
		}
		Graphics::decodeRLE_248(_locationBackgroundGfxBuf + yPos * 640 + xPos, _spritesGfxBuf + chr2->_sourceOffset, chr2->_xSize, chr2->_ySize,
			chr2->_yOffset, _locationHeightTable[_location], _mirroredDrawing, whitelistReservedColors);
		addDirtyRect(xPos, yPos, chr2->_xSize, chr2->_ySize);
	}
}

void TuckerEngine::setVolumeSound(int index, int volume) {
	if (volume < 0) {
		volume = 0;
	}
	_mixer->setChannelVolume(_sfxHandles[index], volume * Audio::Mixer::kMaxChannelVolume / kMaxSoundVolume);
}

void TuckerEngine::setVolumeMusic(int index, int volume) {
	if (volume < 0) {
		volume = 0;
	}
	_mixer->setChannelVolume(_musicHandles[index], volume * Audio::Mixer::kMaxChannelVolume / kMaxSoundVolume);
}

void TuckerEngine::startSound(int offset, int index, int volume) {
	bool loop = (_locationSoundsTable[index]._type == 2 || _locationSoundsTable[index]._type == 5 || _locationSoundsTable[index]._type == 7);
	loadSound(Audio::Mixer::kSFXSoundType, _locationSoundsTable[index]._num, volume, loop, &_sfxHandles[index]);
}

void TuckerEngine::stopSound(int index) {
	_mixer->stopHandle(_sfxHandles[index]);
}

bool TuckerEngine::isSoundPlaying(int index) {
	return _mixer->isSoundHandleActive(_sfxHandles[index]);
}

void TuckerEngine::startMusic(int offset, int index, int volume) {
	bool loop = (_locationMusicsTable[index]._flag == 2);
	loadSound(Audio::Mixer::kMusicSoundType, _locationMusicsTable[index]._num, volume, loop, &_musicHandles[index]);
}

void TuckerEngine::stopMusic(int index) {
	_mixer->stopHandle(_musicHandles[index]);
}

void TuckerEngine::startSpeechSound(int num, int volume) {
	loadSound(Audio::Mixer::kSpeechSoundType, num, volume, false, &_speechHandle);
}

void TuckerEngine::stopSpeechSound() {
	_mixer->stopHandle(_speechHandle);
}

bool TuckerEngine::isSpeechSoundPlaying() {
	return _mixer->isSoundHandleActive(_speechHandle);
}

void TuckerEngine::rememberSpeechSound() {
	for (int i = 4; i > 0; --i) {
		_speechHistoryTable[i] = _speechHistoryTable[i - 1];
	}
	_speechHistoryTable[0] = _part * 3000 + _ptTextOffset + _speechSoundNum - 3000;
}

void TuckerEngine::redrawPanelItems() {
	if (_forceRedrawPanelItems || (_redrawPanelItemsCounter != 0 && _panelType == kPanelTypeNormal)) {
		_forceRedrawPanelItems = false;
		if (_redrawPanelItemsCounter > 0) {
			--_redrawPanelItemsCounter;
		}

		const uint8 *src = nullptr;
		uint8 *dst = nullptr;
		int sz = 0;

		switch (_panelType) {
		case kPanelTypeNormal:
			src = _panelGfxBuf;
			dst = _itemsGfxBuf + 3200;
			sz = 16000;
			break;
		case kPanelTypeEmpty:
			src = _panelGfxBuf + 16320;
			dst = _itemsGfxBuf;
			sz = 19200;
			break;
		case kPanelTypeLoadSavePlayQuit:
			// The following offset does not match disassembly on purpose to fix a
			// "glitch" in the original game.
			// This ensures that the background image ends up in the same place as
			// in the case of kPanelTypeLoadSaveSavegame.
			// This fixes Trac#10496.
			src = _panelGfxBuf + 16000;
			dst = _itemsGfxBuf;
			sz = 19200;
			memcpy(dst, src, sz);
			src = _panelGfxBuf + 55040;
			dst = _itemsGfxBuf + 6400;
			sz = 5120;
			break;
		case kPanelTypeLoadSaveSavegame:
			src = _panelGfxBuf + 35200;
			dst = _itemsGfxBuf;
			sz = 19200;
			break;
		default:
			break;
		}
		memcpy(dst, src, sz);
		if (_panelType == kPanelTypeNormal) {
			redrawPanelItemsHelper();
		}
	}
}

void TuckerEngine::redrawPanelItemsHelper() {
	const int k = (_redrawPanelItemsCounter / 4) - ((_redrawPanelItemsCounter / 8) * 2);
	for (int i = 0; i < 2; ++i) {
		for (int j = 0; j < 3; ++j) {
			if (i * 3 + j + _inventoryObjectsOffset >= _inventoryObjectsCount) {
				continue;
			}
			if (i * 3 + j + _inventoryObjectsOffset == _lastInventoryObjectIndex && k != 0) {
				continue;
			}
			const int obj = _inventoryObjectsList[i * 3 + j + _inventoryObjectsOffset];
			const uint8 *src = _panelObjectsGfxBuf + _panelObjectsOffsetTable[obj];
			uint8 *dst = _itemsGfxBuf + 3412 + i * 8320 + j * 34;
			Graphics::decodeRLE_320(dst, src, 32, 24);
		}
	}
}

void TuckerEngine::drawSprite(int num) {
	Sprite *s = &_spritesTable[num];
	if (s->_animationFrame <= s->_firstFrame && s->_animationFrame > 0 && s->_state != -1) {
		const uint8 *p = s->_animationData;
		if (!p) {
			return;
		}
		int frameOffset = READ_LE_UINT24(p + s->_animationFrame * 4);
		int srcW = READ_LE_UINT16(p + frameOffset);
		int srcH = READ_LE_UINT16(p + frameOffset + 2);
		int srcX = READ_LE_UINT16(p + frameOffset + 8);
		int srcY = READ_LE_UINT16(p + frameOffset + 10);
		s->_gfxBackgroundOffset += s->_backgroundOffset;
		int xPos = s->_gfxBackgroundOffset + srcX;
		if (xPos < 600 && (_scrollOffset + 320 < xPos || _scrollOffset - srcW > xPos)) {
			return;
		}
		s->_xSource = srcX;
		uint8 *dstPtr = _locationBackgroundGfxBuf + srcY * 640 + xPos;
		const uint8 *srcPtr = p + frameOffset + 12;
		switch (s->_colorType) {
		case 0:
			Graphics::decodeRLE(dstPtr, srcPtr, srcW, srcH);
			break;
		case 99:
			Graphics::decodeRLE_224(dstPtr, srcPtr, srcW, srcH);
			break;
		default:
			Graphics::decodeRLE_248(dstPtr, srcPtr, srcW, srcH, 0, s->_yMaxBackground, s->_flipX);
			break;
		}
		const int xR = (srcX +  s->_gfxBackgroundOffset) % 640;
		const int yR =  srcY + (s->_gfxBackgroundOffset  / 640);
		addDirtyRect(xR, yR, srcW, srcH);
	}
}

void TuckerEngine::clearItemsGfx() {
	memset(_itemsGfxBuf, 0, 3200);
}

void TuckerEngine::drawPausedInfoBar() {
	const int len = getStringWidth(36, _infoBarBuf);
	const int x = (kScreenWidth / 2) - 1 - (len / 2);
	drawItemString(x, 36, _infoBarBuf);
}

const uint8 *TuckerEngine::getStringBuf(int type) const {
	const uint8 *p = nullptr;
	switch (type) {
	case 0:
		p = _data5Buf;
		break;
	case 1:
		p = _bgTextBuf;
		break;
	case 2:
		p = _charNameBuf;
		break;
	case 3:
		p = _objTxtBuf;
		break;
	}
	return p;
}

void TuckerEngine::drawInfoString() {
	const uint8 *obj1StrBuf = getStringBuf(_actionObj1Type);
	const uint8 *obj2StrBuf = getStringBuf(_actionObj2Type);
	int infoStringWidth = 0;
	int object1NameWidth = 0;
	int verbWidth = getStringWidth(_actionVerb + 1, _infoBarBuf);
	if (_actionObj1Num > 0 || _actionObj1Type > 0) {
		object1NameWidth = getStringWidth(_actionObj1Num + 1, obj1StrBuf) + 4;
		infoStringWidth = verbWidth + object1NameWidth;
	} else {
		infoStringWidth = verbWidth;
	}
	VerbPreposition verbPreposition = kVerbPrepositionNone;
	int verbPrepositionWidth = 0;
	if (_actionRequiresTwoObjects) {
		verbPreposition = (_actionVerb == kVerbGive) ? kVerbPrepositionTo : kVerbPrepositionWith;
		verbPrepositionWidth = getStringWidth(verbPreposition, _infoBarBuf) + 4;
		if (_gameLang != Common::EN_ANY && (_actionObj2Num > 0 || _actionObj2Type > 0) && verbPreposition != kVerbPrepositionNone) {
			infoStringWidth = 0;
			verbWidth = 0;
			object1NameWidth = 0;
		}
		infoStringWidth += verbPrepositionWidth;
		if (_actionObj2Num > 0 || _actionObj2Type > 0) {
			infoStringWidth += getStringWidth(_actionObj2Num + 1, obj2StrBuf);
		}
	}
	const int xPos = (kScreenWidth / 2) - 1 - (infoStringWidth / 2);
	if (_gameLang == Common::EN_ANY || (_actionObj2Num == 0 && _actionObj2Type == 0) || verbPreposition == kVerbPrepositionNone) {
		drawItemString(xPos, _actionVerb + 1, _infoBarBuf);
		if (_actionObj1Num > 0 || _actionObj1Type > 0) {
			drawItemString(xPos + 4 + verbWidth, _actionObj1Num + 1, obj1StrBuf);
		}
	}
	if (verbPreposition > 0) {
		drawItemString(xPos + 4 + verbWidth + object1NameWidth, verbPreposition, _infoBarBuf);
		if (_actionObj2Num > 0 || _actionObj2Type > 0) {
			drawItemString(xPos + 4 + verbWidth + object1NameWidth + verbPrepositionWidth, _actionObj2Num + 1, obj2StrBuf);
		}
	}
}

void TuckerEngine::drawGameHintString() {
	const int len = getStringWidth(_gameHintsStringNum + 29, _infoBarBuf);
	const int x = (kScreenWidth / 2) - 1 - (len / 2);
	drawItemString(x, _gameHintsStringNum + 29, _infoBarBuf);
}

void TuckerEngine::updateCharacterAnimation() {
	if (_characterAnimationIndex > -1) {
		if (_backgroundSpriteCurrentFrame == 0) {
			_backgroundSpriteCurrentAnimation = _characterAnimationsTable[_characterAnimationIndex];
			++_characterAnimationIndex;
			_backgroundSpriteDataPtr = _sprA02Table[_backgroundSpriteCurrentAnimation];
			_backgroundSpriteLastFrame = READ_LE_UINT16(_backgroundSpriteDataPtr);
			_backgroundSpriteCurrentFrame = _characterAnimationsTable[_characterAnimationIndex];
			++_characterAnimationIndex;
		} else if (_characterAnimationsTable[_characterAnimationIndex] == 99) {
			_characterAnimationIndex = -1;
			_backgroundSpriteCurrentAnimation = -1;
			if (_nextAction == 0) {
				setCursorState(kCursorStateNormal);
			}
		} else {
			_backgroundSpriteCurrentFrame = _characterAnimationsTable[_characterAnimationIndex];
			if (_noCharacterAnimationChange == 0) {
				++_characterAnimationIndex;
			}
		}
	} else if (_backgroundSpriteCurrentAnimation > -1) {
		while (_spriteAnimationFramesTable[_spriteAnimationFrameIndex] != 999) {
			++_spriteAnimationFrameIndex;
		}
		_characterFacingDirection = 0;
		if (_changeBackgroundSprite) {
			if (_backgroundSpriteCurrentFrame == 0) {
				_backgroundSpriteDataPtr = _sprA02Table[_backgroundSpriteCurrentAnimation];
				_backgroundSpriteCurrentFrame = _backgroundSpriteLastFrame = READ_LE_UINT16(_backgroundSpriteDataPtr);
			} else {
				--_backgroundSpriteCurrentFrame;
				if (_backgroundSpriteCurrentFrame < 1) {
					_backgroundSpriteCurrentAnimation = -1;
					_backgroundSpriteCurrentFrame = 0;
					_changeBackgroundSprite = false;
					if (_nextAction == 0) {
						setCursorState(kCursorStateNormal);
					}
				}
			}
		} else {
			if (_backgroundSpriteCurrentFrame == 0) {
				_backgroundSpriteCurrentFrame = 1;
				assert(_backgroundSpriteCurrentAnimation >= 0 && _backgroundSpriteCurrentAnimation < kSprA02TableSize);
				_backgroundSpriteDataPtr = _sprA02Table[_backgroundSpriteCurrentAnimation];
				_backgroundSpriteLastFrame = READ_LE_UINT16(_backgroundSpriteDataPtr);
			} else if (_location == kLocationVentSystem && !_panelLockedFlag && (_backgroundSpriteCurrentAnimation == 3 || _backgroundSpriteCurrentAnimation == 6)) {
				_backgroundSpriteCurrentFrame = 0;
				_backgroundSpriteCurrentAnimation = -1;
			} else {
				++_backgroundSpriteCurrentFrame;
				if (_backgroundSpriteCurrentFrame > _backgroundSpriteLastFrame) {
					_backgroundSpriteCurrentAnimation = -1;
					_backgroundSpriteCurrentFrame = 0;
					if (_nextAction == 0 && _panelType == kPanelTypeNormal) {
						setCursorState(kCursorStateNormal);
					}
				}
			}
		}
	}
	if (_location == kLocationStoreRoom && _flagsTable[103] == 0) {
		if (_panelLockedFlag) {
			_panelLockedFlag = false;
			_selectedObject._locationObjectLocation = kLocationNone;
			if (_actionVerb != kVerbTalk) {
				_speechSoundNum = 2236;
				startSpeechSound(_speechSoundNum, _speechVolume);
				_characterSpeechDataPtr = _ptTextBuf + getPositionForLine(_speechSoundNum, _ptTextBuf);
				_speechSoundNum = 0;
				_actionPosX = _xPosCurrent;
				_actionPosY = _yPosCurrent - 64;
				_actionTextColor = 1;
				_actionCharacterNum = 99;
				setCursorState(kCursorStateDisabledHidden);
				_charSpeechSoundCounter = kDefaultCharSpeechSoundCounter;
			}
		}
		if (_charSpeechSoundCounter == 0 || _actionCharacterNum != 99) {
			if (_backgroundSpriteCurrentAnimation == 5) {
				_backgroundSpriteCurrentFrame = 0;
			}
		} else {
			if (_backgroundSpriteCurrentAnimation != 5) {
				_backgroundSpriteCurrentFrame = 0;
			}
		}
		if (_backgroundSpriteCurrentFrame == 0) {
			if (_charSpeechSoundCounter > 0 && _actionCharacterNum == 99) {
				_backgroundSpriteCurrentAnimation = 5;
			} else {
				_backgroundSpriteCurrentAnimation = (getRandomNumber() < 33000) ? 2 : 3;
			}
			_backgroundSpriteCurrentFrame = 1;
			_backgroundSpriteDataPtr = _sprA02Table[_backgroundSpriteCurrentAnimation];
			_backgroundSpriteLastFrame = READ_LE_UINT16(_backgroundSpriteDataPtr);
		}
	} else if (_location == kLocationVentSystem) {
		if (_backgroundSpriteCurrentFrame == 0) {
			if (!_characterBackFrontFacing) {
				if (_characterBackFrontFacing != _characterPrevBackFrontFacing) {
					_backgroundSpriteCurrentAnimation = 10;
				} else if (_panelLockedFlag) {
					_backgroundSpriteCurrentAnimation = 3;
				} else if (_charSpeechSoundCounter > 0 && _actionCharacterNum == 99) {
					_backgroundSpriteCurrentAnimation = 8;
				} else {
					_backgroundSpriteCurrentAnimation = (getRandomNumber() < 32000) ? 11 : 5;
				}
			} else {
				if (_characterBackFrontFacing != _characterPrevBackFrontFacing) {
					_backgroundSpriteCurrentAnimation = 2;
				} else if (_panelLockedFlag) {
					_backgroundSpriteCurrentAnimation = 6;
				} else if (_charSpeechSoundCounter > 0 && _actionCharacterNum == 99) {
					_backgroundSpriteCurrentAnimation = 9;
				} else {
					_backgroundSpriteCurrentAnimation = (getRandomNumber() < 32000) ? 12 : 7;
				}
			}
			_characterPrevBackFrontFacing = _characterBackFrontFacing;
			_backgroundSpriteCurrentFrame = 1;
			_backgroundSpriteDataPtr = _sprA02Table[_backgroundSpriteCurrentAnimation];
			_backgroundSpriteLastFrame = READ_LE_UINT16(_backgroundSpriteDataPtr);
		}
		_backgroundSprOffset = _xPosCurrent - 160;
	} else if (_location == kLocationTV && _backgroundSpriteCurrentFrame == 0) {
		if (_charSpeechSoundCounter > 0 && _actionCharacterNum == 99) {
			_backgroundSpriteCurrentAnimation = 1;
		} else {
			_backgroundSpriteCurrentAnimation = (getRandomNumber() < 32000) ? 3 : 2;
		}
		_backgroundSpriteCurrentFrame = 1;
		_backgroundSpriteDataPtr = _sprA02Table[_backgroundSpriteCurrentAnimation];
		_backgroundSpriteLastFrame = READ_LE_UINT16(_backgroundSpriteDataPtr);
	}
	int frame = _spriteAnimationFramesTable[_spriteAnimationFrameIndex];
	if (!_panelLockedFlag && _characterFacingDirection < 5 && _selectedObject._locationObjectLocation == kLocationNone) {
		_characterFacingDirection = 0;
	}
	if (_charSpeechSoundCounter > 0 && _characterFacingDirection != 6 && _actionCharacterNum == 99) {
		_characterFacingDirection = 6;
		frame = 999;
	} else if (_characterFacingDirection == 6 && (_charSpeechSoundCounter == 0 || _actionCharacterNum != 99)) {
		_characterFacingDirection = 0;
		frame = 999;
	}
	int num = 0;
	if (frame == 999 || (_characterFacingDirection != _characterPrevFacingDirection && _characterFacingDirection < 5)) {
		_mirroredDrawing = false;
		if (_characterFacingDirection == 6) {
			if (_csDataHandled) {
				switch (_selectedCharacterDirection) {
				case 1:
					num = 17;
					break;
				case 2:
					num = 16;
					break;
				case 4:
					num = 15;
					break;
				default:
					num = 16;
					_mirroredDrawing = true;
					break;
				}
			} else {
				num = 15;
			}
		}
		if (_characterFacingDirection == 5) {
			_characterFacingDirection = 0;
		}
		if (_characterFacingDirection == 0) {
			if (_csDataHandled) {
				_mirroredDrawing = false;
				switch (_selectedCharacterDirection) {
				case 1:
					num = 3;
					break;
				case 2:
					num = 1;
					break;
				case 3:
					num = 1;
					_mirroredDrawing = true;
					break;
				default:
					num = 5;
					break;
				}
			} else if (getRandomNumber() < 2000) {
				num = 13;
			} else if (getRandomNumber() < 3000) {
				num = 14;
				if (_location == kLocationFishShopPartThree) {
					num = 18;
				}
			} else {
				num = (getRandomNumber() < 20000) ? 18 : 6;
			}
		} else {
			switch (_characterFacingDirection) {
			case 1:
				num = 0;
				break;
			case 2:
				num = 4;
				break;
			case 3:
				num = 0;
				_mirroredDrawing = true;
				break;
			case 4:
				num = 2;
				break;
			}
		}
		_currentSpriteAnimationLength = _spriteAnimationsTable[num]._numParts;
		_spriteAnimationFrameIndex = _spriteAnimationsTable[num]._firstFrameIndex;
		frame = _spriteAnimationFramesTable[_spriteAnimationFrameIndex];
	}
	if (_characterAnimationNum > 0) {
		num = _characterAnimationNum;
		_currentSpriteAnimationLength = _spriteAnimationsTable[num]._numParts;
		_spriteAnimationFrameIndex = _spriteAnimationsTable[num]._firstFrameIndex;
		frame = _spriteAnimationFramesTable[_spriteAnimationFrameIndex];
		_characterAnimationNum = 0;
	}
	_currentSpriteAnimationFrame = frame;
	++_spriteAnimationFrameIndex;
	if (_currentSpriteAnimationLength > 1) {
		_currentSpriteAnimationFrame2 = _spriteAnimationFramesTable[_spriteAnimationFrameIndex];
		++_spriteAnimationFrameIndex;
		if (_characterSpriteAnimationFrameCounter > 0) {
			++_characterSpriteAnimationFrameCounter;
			if (_characterSpriteAnimationFrameCounter > 121) {
				_characterSpriteAnimationFrameCounter = 0;
			}
			if (_selectedCharacterDirection == 1) {
				_currentSpriteAnimationFrame = (_characterSpriteAnimationFrameCounter > 2 && _characterSpriteAnimationFrameCounter < 120) ? 122 : 121;
			} else {
				_currentSpriteAnimationFrame = (_characterSpriteAnimationFrameCounter > 2 && _characterSpriteAnimationFrameCounter < 120) ? 120 : 119;
			}
		}
	}
	_characterPrevFacingDirection = _characterFacingDirection;
}

void TuckerEngine::addObjectToInventory(int num) {
	_inventoryObjectsList[_inventoryObjectsCount] = num;
	_lastInventoryObjectIndex = _inventoryObjectsCount;
	_redrawPanelItemsCounter = 50;
	++_inventoryObjectsCount;
	_inventoryItemsState[num] = 1;
	if (_inventoryObjectsOffset + 5 < _lastInventoryObjectIndex) {
		_inventoryObjectsOffset += 3;
	}
}

void TuckerEngine::removeObjectFromInventory(int num) {
	for (int i = 0; i < _inventoryObjectsCount; ++i) {
		if (_inventoryObjectsList[i] == num) {
			--_inventoryObjectsCount;
			_inventoryItemsState[num] = 2;
			const int count = _inventoryObjectsCount - i;
			if (count != 0) {
				memmove(_inventoryObjectsList + i, _inventoryObjectsList + i + 1, count * sizeof(int));
			}
			break;
		}
	}
}

void TuckerEngine::handleMap() {
	if (_handleMapCounter > 0) {
		++_handleMapCounter;
		if (_handleMapCounter > 19) {
			_handleMapCounter = 0;
			_locationMaskCounter = 1;
			_panelLockedFlag = false;
		}
	}
	if (!_panelLockedFlag && (_backgroundSpriteCurrentAnimation == -1 || _location == kLocationVentSystem) && _locationMaskType == 3) {
		setCursorState(kCursorStateNormal);
		if (_locationMaskCounter == 1) {
			_characterFacingDirection = 0;
			_locationMaskType = 0;
		}
		return;
	}
	if (_selectedObject._locationObjectLocation != kLocationNone && _locationMaskCounter != 0 && (_backgroundSpriteCurrentAnimation <= -1 || _location == kLocationVentSystem)) {
		// TODO
		// This is actually "_locationNum != 25" in disassembly. Is this a typo?
		if (_location == kLocationVentSystem || _backgroundSpriteCurrentAnimation != 4) {
			if (_locationMaskType == 0) {
				_locationMaskType = 1;
				setCursorState(kCursorStateDisabledHidden);
				if (_selectedObject._locationObjectToWalkX2 > 800) {
					_backgroundSpriteCurrentAnimation = _selectedObject._locationObjectToWalkX2 - 900;
					if (_selectedObject._locationObjectToWalkY2 > 499) {
						_changeBackgroundSprite = true;
						_backgroundSprOffset = _selectedObject._locationObjectToWalkY2 - 500;
					} else {
						_backgroundSprOffset = _selectedObject._locationObjectToWalkY2;
						_changeBackgroundSprite = false;
					}
					_backgroundSpriteCurrentFrame = 0;
					_mirroredDrawing = false;
					if (_location == kLocationVentSystem) {
						_backgroundSpriteDataPtr = _sprA02Table[_backgroundSpriteCurrentAnimation];
						_backgroundSpriteLastFrame = READ_LE_UINT16(_backgroundSpriteDataPtr);
						_backgroundSpriteCurrentFrame = 1;
					}
				} else {
					_locationMaskCounter = 0;
					_selectedObject._xPos = _selectedObject._locationObjectToWalkX2;
					_selectedObject._yPos = _selectedObject._locationObjectToWalkY2;
					_handleMapCounter = 1;
					_panelLockedFlag = true;
				}
				return;
			}
			_locationMaskType = 2;
			_panelType = kPanelTypeNormal;
			setCursorState(kCursorStateNormal);
			if (_selectedObject._locationObjectLocation == kLocationMap) {
				_noPositionChangeAfterMap = true;
				handleMapSequence();
				return;
			}
			for (int i = 0; i < 14; ++i) {
				fadeInPalette();
				redrawScreen(_scrollOffset);
				_fadePaletteCounter = 34;
			}
			_nextLocation = _selectedObject._locationObjectLocation;
			_xPosCurrent = _selectedObject._locationObjectToX;
			_yPosCurrent = _selectedObject._locationObjectToY;
			if (_selectedObject._locationObjectToX2 > 800) {
				_backgroundSpriteCurrentAnimation = _selectedObject._locationObjectToX2 - 900;
				if (_selectedObject._locationObjectToY2 > 499) {
					_changeBackgroundSprite = true;
					_backgroundSprOffset = _selectedObject._locationObjectToY2 - 500;
				} else {
					_changeBackgroundSprite = false;
					_backgroundSprOffset = _selectedObject._locationObjectToY2;
				}
				_backgroundSpriteCurrentFrame = 0;
			} else {
				_selectedObject._xPos = _selectedObject._locationObjectToX2;
				_selectedObject._yPos = _selectedObject._locationObjectToY2;
				_panelLockedFlag = true;
			}
			_scrollOffset = 0;
			_handleMapCounter = 0;
			_locationMaskCounter = 0;
			_selectedObject._locationObjectLocation = kLocationNone;
		}
	}
}

void TuckerEngine::clearSprites() {
	memset(_spritesTable, 0, sizeof(_spritesTable));
	for (int i = 0; i < kMaxCharacters; ++i) {
		_spritesTable[i]._state = -1;
		_spritesTable[i]._stateIndex = -1;
	}
}

void TuckerEngine::updateSprites() {
	const int count = (_location == kLocationMall) ? 3 : _spritesCount;
	for (int i = 0; i < count; ++i) {
		if (_spritesTable[i]._stateIndex > -1) {
			++_spritesTable[i]._stateIndex;
			if (_characterStateTable[_spritesTable[i]._stateIndex] == 99) {
				_spritesTable[i]._stateIndex = -1;
				_spritesTable[i]._state = -1;
				updateSprite(i);
			} else {
				_spritesTable[i]._animationFrame = _characterStateTable[_spritesTable[i]._stateIndex];
			}
			continue;
		}
		if (_spritesTable[i]._state == -1) {
			updateSprite(i);
			continue;
		}
		if (_charSpeechSoundCounter > 0 && i == _actionCharacterNum && !_spritesTable[i]._needUpdate) {
			updateSprite(i);
			continue;
		}
		if (_charSpeechSoundCounter == 0 && _spritesTable[i]._needUpdate) {
			updateSprite(i);
			continue;
		}
		if (_spritesTable[i]._updateDelay > 0) {
			--_spritesTable[i]._updateDelay;
			if (_spritesTable[i]._updateDelay == 0) {
				updateSprite(i);
			}
			continue;
		}
		if (_spritesTable[i]._defaultUpdateDelay > 0) {
			_spritesTable[i]._updateDelay = _spritesTable[i]._defaultUpdateDelay - 1;
			++_spritesTable[i]._animationFrame;
			if (_spritesTable[i]._animationFrame == _spritesTable[i]._firstFrame) {
				updateSprite(i);
			}
			continue;
		}
		if (!_spritesTable[i]._nextAnimationFrame) {
			++_spritesTable[i]._animationFrame;
			if (_spritesTable[i]._firstFrame - 1 < _spritesTable[i]._animationFrame) {
				if (_spritesTable[i]._prevAnimationFrame) {
					--_spritesTable[i]._animationFrame;
					_spritesTable[i]._nextAnimationFrame = true;
				} else {
					updateSprite(i);
				}
			}
			continue;
		}
		--_spritesTable[i]._animationFrame;
		if (_spritesTable[i]._animationFrame == 0) {
			updateSprite(i);
		}
	}
}

void TuckerEngine::updateSprite(int i) {
	_spritesTable[i]._prevState = _spritesTable[i]._state;
	_spritesTable[i]._prevAnimationFrame = false;
	_spritesTable[i]._nextAnimationFrame = false;
	_updateSpriteFlag1 = false;
	_updateSpriteFlag2 = false;
	_spritesTable[i]._defaultUpdateDelay = 0;
	_spritesTable[i]._updateDelay = 0;
	switch (_location) {
	case 2:
		updateSprite_locationNum2();
		break;
	case 3:
		if (i == 0) {
			updateSprite_locationNum3_0(i);
		} else if (i == 1) {
			updateSprite_locationNum3_1(i);
		} else {
			updateSprite_locationNum3_2(i);
		}
		break;
	case 4:
		updateSprite_locationNum4(0);
		break;
	case 5:
		if (i == 0) {
			updateSprite_locationNum5_0();
		} else {
			updateSprite_locationNum5_1(1);
		}
		break;
	case 6:
		if (i == 0) {
			updateSprite_locationNum6_0(0);
		} else if (i == 1) {
			updateSprite_locationNum6_1(1);
		} else {
			updateSprite_locationNum6_2(2);
		}
		break;
	case 7:
		if (i == 0) {
			updateSprite_locationNum7_0(0);
		} else {
			updateSprite_locationNum7_1(1);
		}
		break;
	case 8:
		if (i == 0) {
			updateSprite_locationNum8_0(0);
		} else {
			updateSprite_locationNum8_1(1);
		}
		break;
	case 9:
		if (i == 0) {
			updateSprite_locationNum9_0(i);
		} else if (i == 1) {
			updateSprite_locationNum9_1(i);
		} else {
			updateSprite_locationNum9_2(i);
		}
		break;
	case 10:
		updateSprite_locationNum10();
		break;
	case 11:
		if (i == 0) {
			updateSprite_locationNum11_0(0);
		} else if (i == 1) {
			updateSprite_locationNum11_1(1);
		} else if (i == 2) {
			updateSprite_locationNum11_2(2);
		} else if (i == 3) {
			updateSprite_locationNum11_3(3);
		} else if (i == 4) {
			updateSprite_locationNum11_4(4);
		}
		break;
	case 12:
		if (i == 0) {
			updateSprite_locationNum12_0(0);
		} else if (i == 1) {
			updateSprite_locationNum12_1(1);
		}
		break;
	case 13:
		updateSprite_locationNum13(0);
		break;
	case 14:
		updateSprite_locationNum14(0);
		break;
	case 15:
		if (i == 1) {
			updateSprite_locationNum15_1(1);
		} else if (i == 2) {
			updateSprite_locationNum15_2(2);
		} else if (i == 0) {
			updateSprite_locationNum15_0(0);
		}
		break;
	case 16:
		if (i == 0) {
			updateSprite_locationNum16_0(0);
		} else if (i == 1) {
			updateSprite_locationNum16_1(1);
		} else {
			updateSprite_locationNum16_2(2);
		}
		break;
	case 17:
		updateSprite_locationNum17();
		break;
	case 18:
		updateSprite_locationNum18();
		break;
	case 19:
		if (i == 0) {
			updateSprite_locationNum19_0(0);
		} else if (i == 1) {
			updateSprite_locationNum19_1(1);
		} else if (i == 2) {
			updateSprite_locationNum19_2(2);
		} else {
			updateSprite_locationNum19_3(3);
		}
		break;
	case 21:
		updateSprite_locationNum21();
		break;
	case 22:
		updateSprite_locationNum22();
		break;
	case 23:
		if (i == 0) {
			updateSprite_locationNum23_0(0);
		} else if (i == 1) {
			updateSprite_locationNum23_1(1);
		} else if (i == 2) {
			updateSprite_locationNum23_2(2);
		} else {
			updateSprite_locationNum23_3(3);
		}
		break;
	case 24:
		if (i == 0) {
			updateSprite_locationNum24_0(0);
		} else if (i == 1) {
			updateSprite_locationNum24_1(1);
		} else if (i == 2) {
			updateSprite_locationNum24_2(2);
		} else {
			updateSprite_locationNum24_3(3);
		}
		break;
	case 26:
		if (i == 0) {
			updateSprite_locationNum26_0(0);
		} else {
			updateSprite_locationNum26_1(1);
		}
		break;
	case 27:
		updateSprite_locationNum27(0);
		break;
	case 28:
		if (i == 0) {
			updateSprite_locationNum28_0(0);
		} else if (i == 1) {
			updateSprite_locationNum28_1(1);
		} else {
			updateSprite_locationNum28_2(2);
		}
		break;
	case 29:
		if (i == 0) {
			updateSprite_locationNum29_0(0);
		} else if (i == 1) {
			updateSprite_locationNum29_1(1);
		} else {
			updateSprite_locationNum29_2(2);
		}
		break;
	case 30:
		updateSprite_locationNum30_34(i);
		break;
	case 31:
		if (i == 0) {
			updateSprite_locationNum31_0(0);
		} else {
			updateSprite_locationNum31_1(1);
		}
		break;
	case 32:
		if (i == 0) {
			updateSprite_locationNum32_0(0);
		} else {
			_spritesTable[i]._state = -1;
		}
		break;
	case 33:
		if (i == 1) {
			updateSprite_locationNum33_1(1);
		} else if (i == 0) {
			updateSprite_locationNum33_0(0);
		} else if (i == 2) {
			updateSprite_locationNum33_2(2);
		} else {
			_spritesTable[i]._state = 12;
		}
		break;
	case 34:
		updateSprite_locationNum30_34(0);
		break;
	case 36:
		updateSprite_locationNum36(0);
		break;
	case 37:
		if (i == 0) {
			_spritesTable[0]._state = -1;
		} else {
			updateSprite_locationNum37(i);
		}
		break;
	case 41:
		updateSprite_locationNum41(i);
		break;
	case 42:
		updateSprite_locationNum42(i);
		break;
	case 43:
		if (i == 2) {
			updateSprite_locationNum43_2(i);
		} else if (i < 2) {
			if (_flagsTable[236] < 4) {
				_spritesTable[i]._state = i + 1;
			} else {
				_spritesTable[i]._state = -1;
			}
		} else if (i == 3) {
			updateSprite_locationNum43_3(3);
		} else if (i == 4) {
			updateSprite_locationNum43_4(4);
		} else if (i == 5) {
			updateSprite_locationNum43_5(5);
		} else {
			updateSprite_locationNum43_6(6);
		}
		break;
	case 45:
		_spritesTable[0]._state = 1;
		break;
	case 47:
		_spritesTable[i]._state = i + 1;
		break;
	case 48:
		updateSprite_locationNum48(0);
		break;
	case 49:
		updateSprite_locationNum49(0);
		break;
	case 50:
		if (i < 6) {
			updateSprite_locationNum50(i);
		} else {
			_spritesTable[i]._state = i + 1;
		}
		break;
	case 51:
		updateSprite_locationNum51(i);
		break;
	case 53:
		if (i == 0) {
			updateSprite_locationNum53_0(0);
		} else if (i == 1) {
			updateSprite_locationNum53_1(1);
		}
		break;
	case 54:
		updateSprite_locationNum54(0);
		break;
	case 55:
		updateSprite_locationNum55(0);
		break;
	case 56:
		updateSprite_locationNum56(0);
		break;
	case 57:
		if (i == 0) {
			updateSprite_locationNum57_0(0);
		} else if (i == 1) {
			updateSprite_locationNum57_1(1);
		}
		break;
	case 58:
		updateSprite_locationNum58(0);
		break;
	case 59:
		updateSprite_locationNum59(0);
		break;
	case 60:
		if (i == 0) {
			updateSprite_locationNum60_0(0);
		} else {
			updateSprite_locationNum60_1(1);
		}
		break;
	case 61:
		if (i == 0) {
			updateSprite_locationNum61_0(0);
		} else if (i == 2) {
			updateSprite_locationNum61_2(2);
		} else {
			updateSprite_locationNum61_1(1);
		}
		break;
	case 63:
		if (i == 0) {
			updateSprite_locationNum63_0(0);
		} else if (i == 1) {
			updateSprite_locationNum63_1(1);
		} else if (i == 2) {
			updateSprite_locationNum63_2(2);
		} else if (i == 3) {
			updateSprite_locationNum63_3(3);
		} else if (i == 4) {
			updateSprite_locationNum63_4(4);
		}
		break;
	case 65:
		updateSprite_locationNum65(0);
		break;
	case 66:
		if (i == 0) {
			updateSprite_locationNum66_0(0);
		} else if (i == 1) {
			updateSprite_locationNum66_1(1);
		} else if (i == 2) {
			updateSprite_locationNum66_2(2);
		} else if (i == 3) {
			updateSprite_locationNum66_3(3);
		} else {
			updateSprite_locationNum66_4(4);
		}
		break;
	case 69:
		if (i == 0) {
			_spritesTable[0]._state = 1;
		} else if (i == 1) {
			updateSprite_locationNum69_1(1);
		} else if (i == 2) {
			updateSprite_locationNum69_2(2);
		} else if (i == 3) {
			updateSprite_locationNum69_3(3);
		}
		break;
	case 71:
		updateSprite_locationNum71(0);
		break;
	case 72:
		updateSprite_locationNum72(0);
		break;
	case 74:
		updateSprite_locationNum74(i);
		break;
	case 79:
		updateSprite_locationNum79(0);
		break;
	case 81:
		if (i == 0) {
			updateSprite_locationNum81_0(0);
		} else if (i == 1) {
			updateSprite_locationNum81_1(1);
		}
		break;
	case 82:
		updateSprite_locationNum82(0);
		break;
	case 98:
		_spritesTable[0]._state = 1;
		break;
	default:
		break;
	}
	if (_spritesTable[i]._stateIndex <= -1) {
		if (!_updateSpriteFlag1) {
			_spritesTable[i]._animationFrame = 1;
		}
		if (_spritesTable[i]._state < 0 || !_sprC02Table[_spritesTable[i]._state]) {
			// WORKAROUND
			// The original game unconditionally reads into _sprC02Table[] below which
			// results in out-of-bounds reads when _spritesTable[i]._state == -1.
			// We reset the sprite's animation data in this case so sprite updates
			// are triggered correctly. This most prominently fixes a bug where Lola's
			// transition from dancing -> sitting happens too late.
			// This fixes Trac#6644.
			_spritesTable[i]._animationData = nullptr;
			_spritesTable[i]._firstFrame = 0;
			return;
		}
		_spritesTable[i]._animationData = _sprC02Table[_spritesTable[i]._state];
		_spritesTable[i]._firstFrame = READ_LE_UINT16(_spritesTable[i]._animationData);
		if (_updateSpriteFlag2) {
			_spritesTable[i]._state = _spritesTable[i]._firstFrame;
			_spritesTable[i]._nextAnimationFrame = true;
			_spritesTable[i]._prevAnimationFrame = true;
		}
	}
}

void TuckerEngine::drawStringInteger(int num, int x, int y, int digits) {
	const int xStart = x;
	char numStr[4];
	assert(num < 1000);
	sprintf(numStr, "%03d", num);
	int i = (digits > 2) ? 0 : 1;
	for (; i < 3; ++i) {
		Graphics::drawStringChar(_locationBackgroundGfxBuf, _scrollOffset + x, y, 640, numStr[i], 102, _charsetGfxBuf);
		x += 8;
	}
	addDirtyRect(_scrollOffset + xStart, y, Graphics::_charset._charW * 3, Graphics::_charset._charH);
}

void TuckerEngine::drawStringAlt(int x, int y, int color, const uint8 *str, int strLen) {
	const int xStart = x;
	int pos = 0;
	while (pos != strLen && str[pos] != '\n') {
		const uint8 chr = str[pos];
		Graphics::drawStringChar(_locationBackgroundGfxBuf, x, y, 640, chr, color, _charsetGfxBuf);
		x += _charWidthTable[chr];
		++pos;
	}
	addDirtyRect(xStart, y, x - xStart, Graphics::_charset._charH);
}

void TuckerEngine::drawItemString(int x, int num, const uint8 *str) {
	int pos = getPositionForLine(num, str);
	while (str[pos] != '\n') {
		const uint8 chr = str[pos];
		// Different versions of the game use different character set dimensions (charset.pcx).
		// The default (English) set uses a height of 8 pixels whereas others use 10 pixels.
		// This needs to be taken into consideration when drawing the language bar so text
		// gets vertically centered in all languages.
		Graphics::drawStringChar(_itemsGfxBuf, x, (10 - (Graphics::_charset._charH)) / 2, 320, chr, 1, _charsetGfxBuf);
		x += _charWidthTable[chr];
		++pos;
	}
}

void TuckerEngine::drawCreditsString(int x, int y, int num) {
	int pos = getPositionForLine(num, _ptTextBuf);
	while (_ptTextBuf[pos] != '\n') {
		const uint8 chr = _ptTextBuf[pos];
		Graphics::drawStringChar(_locationBackgroundGfxBuf, x, y, 640, chr, 1, _charsetGfxBuf);
		x += _charWidthTable[chr];
		++pos;
	}
}

void TuckerEngine::updateCharSpeechSound(bool displayText) {
	if (_charSpeechSoundCounter == 0) {
		return;
	}
	if (_displaySpeechText) {
		_charSpeechSoundCounter = 0;
	} else {
		--_charSpeechSoundCounter;
	}
	if (_charSpeechSoundCounter == 0) {
		_charSpeechSoundCounter = isSpeechSoundPlaying() ? 1 : 0;
		if (_charSpeechSoundCounter == 0) {
			_characterSpriteAnimationFrameCounter = 0;
		}
	}
	if (_charSpeechSoundCounter == 0 && !_csDataHandled) {
		setCursorState(kCursorStateNormal);
	} else if (displayText) {
		drawSpeechText(_actionPosX, _actionPosY, _characterSpeechDataPtr, _speechSoundNum, _actionTextColor);
	}
}

void TuckerEngine::updateItemsGfxColors(int color1, int color128) {
	for (int i = 0; i < 3200; ++i) {
		if (_itemsGfxBuf[i] == 1) {
			_itemsGfxBuf[i] = color1;
		} else if (_itemsGfxBuf[i] == 128) {
			_itemsGfxBuf[i] = color128;
		}
	}
}

bool TuckerEngine::testLocationMask(int x, int y) {
	if (_locationMaskType > 0 || _locationMaskIgnore) {
		return true;
	}
	if (_location == kLocationSubwayTunnel || _location == kLocationKitchen) {
		y -= 3;
	}
	const int offset = y * 640 + x;
	return (_locationBackgroundMaskBuf[offset] > 0);
}

int TuckerEngine::getStringWidth(int num, const uint8 *ptr) {
	int w = 0;
	int pos = getPositionForLine(num, ptr);
	uint8 chr;
	while ((chr = ptr[pos]) != '\n') {
		w += _charWidthTable[chr];
		++pos;
	}
	return w;
}

int TuckerEngine::getPositionForLine(int num, const uint8 *ptr) {
	int linesCount = 0;
	int i = 0;
	while (linesCount < num) {
		if (ptr[i] == '\n') {
			++linesCount;
			if (ptr[i + 1] == '\r') {
				++i;
			}
		}
		++i;
	}
	while (1) {
		if (ptr[i] != '\n' && ptr[i] != '\r') {
			break;
		}
		++i;
	}
	return i;
}

void TuckerEngine::resetCharacterAnimationIndex(int count) {
	_backgroundSpriteCurrentFrame = 0;
	_characterAnimationIndex = 0;
	for (int i = 0; i < count; ++i) {
		while (_characterAnimationsTable[_characterAnimationIndex] != 99) {
			++_characterAnimationIndex;
		}
		++_characterAnimationIndex;
	}
}

enum TableInstructionCode {
	kCode_invalid,
	kCode_pan,
	kCode_bua,
	kCode_bub,
	kCode_buc,
	kCode_bsd,
	kCode_bof,
	kCode_buh,
	kCode_bon,
	kCode_bso,
	kCode_bus,
	kCode_buw,
	kCode_bux,
	kCode_c0a,
	kCode_c0c,
	kCode_c0s,
	kCode_end,
	kCode_fad,
	kCode_fw,
	kCode_flx,
	kCode_fxx,
	kCode_fx,
	kCode_gfg,
	kCode_gv,
	kCode_loc,
	kCode_mof,
	kCode_opt,
	kCode_opf,
	kCode_ofg,
	kCode_snc,
	kCode_sse,
	kCode_ssp,
	kCode_s0p,
	kCode_sp,
	kCode_tpo,
	kCode_wa_,
	kCode_wsm,
	kCode_wat,
	kCode_was,
	kCode_wfx,
	kCode_xhr,
	kCode_xhm,
	kCode_no3 // NOOP, throw away 3-byte parameter
};

static const struct {
	const char *name;
	int code;
} _instructions[] = {
	{ "pan", kCode_pan },
	{ "bua", kCode_bua },
	{ "bub", kCode_bub },
	{ "buc", kCode_buc },
	{ "bsd", kCode_bsd },
	{ "bcd", kCode_bsd }, // only ref 6.27
	{ "bud", kCode_bsd }, // only ref 13.3
	{ "bof", kCode_bof },
	{ "buh", kCode_buh },
	{ "bon", kCode_bon },
	{ "bso", kCode_bso },
	{ "bus", kCode_bus },
	{ "b0s", kCode_bus }, // only ref 65.25
	{ "buv", kCode_no3 },
	{ "buw", kCode_buw },
	{ "bdx", kCode_bux },
	{ "bux", kCode_bux },
	{ "c0a", kCode_c0a },
	{ "c0c", kCode_c0c },
	{ "c0s", kCode_c0s },
	{ "c0v", kCode_no3 },
	{ "end", kCode_end },
	{ "fad", kCode_fad },
	{ "fw",  kCode_fw  },
	{ "flx", kCode_flx },
	{ "fxx", kCode_fxx },
	{ "fx",  kCode_fx  },
	{ "gfg", kCode_gfg },
	{ "gv",  kCode_gv  },
	{ "loc", kCode_loc },
	{ "mof", kCode_mof },
	{ "opt", kCode_opt },
	{ "opf", kCode_opf },
	{ "ofg", kCode_ofg },
	{ "snc", kCode_snc },
	{ "sse", kCode_sse },
	{ "ssp", kCode_ssp },
	{ "s0p", kCode_s0p },
	{ "sp",  kCode_sp  },
	{ "tpo", kCode_tpo },
	{ "wa+", kCode_wa_ },
	{ "wsm", kCode_wsm },
	{ "wat", kCode_wat },
	{ "was", kCode_was },
	{ "wfx", kCode_wfx },
	{ "xhr", kCode_xhr },
	{ "xhm", kCode_xhm },
	{ nullptr, 0 }
};

int TuckerEngine::readTableInstructionCode(int *index) {
	bool match = false;
	int nameLen = 0;
	for (int i = 0; _instructions[i].name; ++i) {
		nameLen = strlen(_instructions[i].name);
		if (_instructions[i].name[1] == '0') {
			if (_instructions[i].name[0] == _tableInstructionsPtr[0] && _instructions[i].name[2] == _tableInstructionsPtr[2]) {
				const char digit = _tableInstructionsPtr[1];
				assert(digit >= '0' && digit <= '9');
				*index = digit - '0';
				match = true;
			}
		} else {
			if (strncmp(_instructions[i].name, (const char *)_tableInstructionsPtr, nameLen) == 0) {
				*index = 0;
				match = true;
			}
		}
		if (match) {
			_tableInstructionsPtr += nameLen + 1;
			return _instructions[i].code;
		}
	}
	warning("Unhandled instruction '%c%c%c'", _tableInstructionsPtr[0], _tableInstructionsPtr[1], _tableInstructionsPtr[2]);
	_tableInstructionsPtr += nameLen + 1;
	return kCode_invalid;
}

int TuckerEngine::readTableInstructionParam(int len) {
	// skip duplicated minus signs (bua,--1, c0a,--1, ...)
	if (len >= 3 && memcmp(_tableInstructionsPtr, "--", 2) == 0) {
		++_tableInstructionsPtr;
		--len;
	}
	char *end = nullptr;
	const int param = strtol((const char *)_tableInstructionsPtr, &end, 10);
	if (end != (const char *)_tableInstructionsPtr + len) {
		warning("Unexpected instruction parameter length %d (%d)", (int)(end - (const char *)_tableInstructionsPtr), len);
	}
	_tableInstructionsPtr += len + 1;
	return param;
}

int TuckerEngine::executeTableInstruction() {
	int i, index = 0;

	debug(2, "executeTableInstruction() instruction %c%c%c", _tableInstructionsPtr[0], _tableInstructionsPtr[1], _tableInstructionsPtr[2]);
	const int code = readTableInstructionCode(&index);
	switch (code) {
	case kCode_pan:
		_panelType = (PanelType)readTableInstructionParam(2);
		_forceRedrawPanelItems = true;
		return 0;
	case kCode_bua:
		_backgroundSpriteCurrentAnimation = readTableInstructionParam(3);
		_backgroundSpriteCurrentFrame = 0;
		_backgroundSprOffset = 0;
		_mainLoopCounter2 = 0;
		return 0;
	case kCode_bub:
		i = readTableInstructionParam(3);
		_spriteAnimationFrameIndex = _spriteAnimationsTable[i]._firstFrameIndex;
		_characterFacingDirection = 5;
		_mainLoopCounter2 = 0;
		return 0;
	case kCode_buc:
		i = readTableInstructionParam(3);
		resetCharacterAnimationIndex(i);
		_backgroundSpriteCurrentFrame = 0;
		_backgroundSprOffset = 0;
		return 0;
	case kCode_bsd:
		_selectedCharacterDirection = readTableInstructionParam(2);
		return 0;
	case kCode_bof:
		_skipCurrentCharacterDraw = true;
		return 0;
	case kCode_buh:
		_noCharacterAnimationChange = readTableInstructionParam(2);
		return 0;
	case kCode_bon:
		_skipCurrentCharacterDraw = false;
		return 0;
	case kCode_bso:
		_backgroundSprOffset = readTableInstructionParam(3);
		return 0;
	case kCode_bus:
		_speechSoundNum = readTableInstructionParam(3) - 1;
		rememberSpeechSound();
		startSpeechSound(_part * 3000 + _ptTextOffset + _speechSoundNum - 3000, _speechVolume);
		_actionPosX = _xPosCurrent;
		_actionPosY = _yPosCurrent - 64;
		_actionTextColor = 1;
		_actionCharacterNum = 99;
		_charSpeechSoundCounter = kDefaultCharSpeechSoundCounter;
		return 0;
	case kCode_buw:
		_selectedObject._xPos = readTableInstructionParam(3);
		_selectedObject._yPos = readTableInstructionParam(3);

		// WORKAROUND: original game bug
		// When Bud is walked to specific coordinates using the 'buw' opcode the
		// walkable area is not enforced (_locationMaskIgnore == true).
		// This is usually not a problem because the player is not allowed to click,
		// however, when entering the club, this allows the player to move Bud to
		// coordinates from which he can never return, leaving him stuck there.
		// As a workaround, do not ignore the location mask during this specific
		// action when entering the club.
		// This fixes Trac#5838.
		if (!(_location == kLocationStripJoint && _nextAction == 59)) {
			_locationMaskIgnore = true;
		}
		_panelLockedFlag = true;
		return 0;
	case kCode_bux:
		_xPosCurrent = readTableInstructionParam(3);
		_yPosCurrent = readTableInstructionParam(3);
		return 0;
	case kCode_c0a:
		_spritesTable[index]._state = readTableInstructionParam(3);
		if (_spritesTable[index]._state == 999) {
			_spritesTable[index]._state = -1;
		}
		_mainLoopCounter1 = 0;
		_spritesTable[index]._updateDelay = 0;
		_spritesTable[index]._nextAnimationFrame = false;
		_spritesTable[index]._prevAnimationFrame = false;
		return 0;
	case kCode_c0c:
		setCharacterAnimation(readTableInstructionParam(3), index);
		return 0;
	case kCode_c0s:
		_speechSoundNum = readTableInstructionParam(3) - 1;
		rememberSpeechSound();
		startSpeechSound(_part * 3000 + _ptTextOffset + _speechSoundNum - 3000, kMaxSoundVolume);
		_charSpeechSoundCounter = kDefaultCharSpeechSoundCounter;
		_actionTextColor = 181 + index;
		if (!_tableInstructionFlag) {
			_actionPosX = _tableInstructionItemNum1;
			_actionPosY = _tableInstructionItemNum2;
		} else {
			_actionPosX = _tableInstructionObj1Table[index];
			_actionPosY = _tableInstructionObj2Table[index];
		}
		_actionCharacterNum = index;
		return 0;
	case kCode_end:
		return 2;
	case kCode_fad:
		_fadePaletteCounter = readTableInstructionParam(2);
		return 0;
	case kCode_fw:
		_selectedCharacterNum = readTableInstructionParam(2);
		_actionVerb = kVerbWalk;
		_selectedObjectType = 0;
		_selectedObjectNum = 1;
		setSelectedObjectKey();
		return 0;
	case kCode_flx:
		i = readTableInstructionParam(2);
		_locationSoundsTable[i]._type = 2;
		startSound(_locationSoundsTable[i]._offset, i, _locationSoundsTable[i]._volume);
		return 0;
	case kCode_fxx:
		i = readTableInstructionParam(2);
		if (isSoundPlaying(i)) {
			stopSound(i);
		}
		return 0;
	case kCode_fx:
		i = readTableInstructionParam(2);
		startSound(_locationSoundsTable[i]._offset, i, _locationSoundsTable[i]._volume);
		_soundInstructionIndex = i;
		return 0;
	case kCode_gfg:
		i = readTableInstructionParam(3);
		assert(i >= 0 && i < kFlagsTableSize);
		_flagsTable[i] = readTableInstructionParam(2);
		debug(2, "executeTableInstruction() set flag %d to %d", i, _flagsTable[i]);
		return 0;
	case kCode_gv:
		_characterAnimationNum = readTableInstructionParam(2);
		return 0;
	case kCode_loc:
		_nextLocation = (Location)readTableInstructionParam(2);
		return 1;
	case kCode_mof:
		setCursorState(kCursorStateDisabledHidden);
		return 0;
	case kCode_opt:
		_conversationOptionsCount = readTableInstructionParam(2);
		for (i = 0; i < _conversationOptionsCount; ++i) {
			_instructionsActionsTable[i] = readTableInstructionParam(3) - 1;
			_nextTableToLoadTable[i] = readTableInstructionParam(3);
		}
		_nextTableToLoadIndex = -1;
		setCursorState(kCursorStateDialog);
		return 1;
	case kCode_opf:
		_conversationOptionsCount = 0;
		for (i = readTableInstructionParam(2); i > 0; --i) {
			const int flag = readTableInstructionParam(3);
			const int value = readTableInstructionParam(2);
			debug(2, "executeTableInstruction() compare flag %d to %d (%d)", i, value, _flagsTable[i]);
			assert(flag >= 0 && flag < kFlagsTableSize);
			if (value == _flagsTable[flag]) {
				assert(_conversationOptionsCount < 6);
				_instructionsActionsTable[_conversationOptionsCount] = readTableInstructionParam(3) - 1;
				_nextTableToLoadTable[_conversationOptionsCount] = readTableInstructionParam(3);
				++_conversationOptionsCount;
			} else {
				readTableInstructionParam(3);
				readTableInstructionParam(3);
			}
		}
		_nextTableToLoadIndex = -1;
		setCursorState(kCursorStateDialog);
		return 1;
	case kCode_ofg:
		i = readTableInstructionParam(3);
		if (readTableInstructionParam(2) == 0) {
			removeObjectFromInventory(i);
		} else {
			addObjectToInventory(i);
		}
		return 0;
	case kCode_snc:
		_mainLoopCounter1 = 0;
		return 0;
	case kCode_sse:
		_nextAction = readTableInstructionParam(3);
		_csDataLoaded = false;
		return 3;
	case kCode_ssp:
		_tableInstructionFlag = false;
		_tableInstructionItemNum1 = readTableInstructionParam(3);
		_tableInstructionItemNum2 = readTableInstructionParam(3);
		return 0;
	case kCode_s0p:
		_tableInstructionFlag = true;
		_tableInstructionObj1Table[index] = readTableInstructionParam(3);
		_tableInstructionObj2Table[index] = readTableInstructionParam(3);
		return 0;
	case kCode_sp:
		_characterSpriteAnimationFrameCounter = 1;
		return 0;
	case kCode_tpo:
		_ptTextOffset = readTableInstructionParam(4);
		_characterSpeechDataPtr = _ptTextBuf + getPositionForLine(_ptTextOffset, _ptTextBuf);
		return 0;
	case kCode_wa_:
		_stopActionOnSpeechFlag = true;
		_stopActionCounter = 20;
		return 1;
	case kCode_wsm:
		_stopActionOnPanelLock = true;

		// WORKAROUND
		// Some versions have a script bug which allows you to freely click around
		// during the sequence of Bud freeing the professor in part two which even
		// allows Bud to leave the room while talking to the professor resulting in
		// general glitchiness. The Spanish and Polish versions (and possibly others)
		// fixed this by introducing the 'mof' opcode to disable the mouse during the
		// sequence.
		//
		// The difference is as follows:
		//   Buggy: 61dw buw,148,125,wsm,buw,148,132,wsm,wat,050[...]
		//   Fixed: 61dw buw,148,125,wsm,buw,148,132,wsm,mof,pan,01,wat,050[...]
		//                                               ^^^^^^^^^^
		// To work around the issue in the problematic versions we inject these two
		// instructions after the first occurence of the 'wsm' instruction (which
		// proves good enough).
		if (_location == kLocationStoreRoom && _nextAction == 61) {
			setCursorState(kCursorStateDisabledHidden);
			_panelType = kPanelTypeEmpty;
		}

		return 1;
	case kCode_wat:
		_stopActionCounter = readTableInstructionParam(3);
		return 1;
	case kCode_was:
		_stopActionOnSpeechFlag = true;
		return 1;
	case kCode_wfx:
		_stopActionOnSoundFlag = true;
		return 1;
	case kCode_xhr:
		_validInstructionId = true;
		return 0;
	case kCode_xhm:
		_validInstructionId = false;
		return 0;
	case kCode_no3:
		// opcodes mapped here are treated as NOOPs
		readTableInstructionParam(3);
		return 0;
	}
	return 2;
}

void TuckerEngine::moveUpInventoryObjects() {
	if (_inventoryObjectsOffset + 6 < _inventoryObjectsCount) {
		_inventoryObjectsOffset += 3;
		_forceRedrawPanelItems = true;
	}
}

void TuckerEngine::moveDownInventoryObjects() {
	if (_inventoryObjectsOffset > 2) {
		_inventoryObjectsOffset -= 3;
		_forceRedrawPanelItems = true;
	}
}

void TuckerEngine::setActionVerbUnderCursor() {
	if (_mousePosY < 150) {
		_actionVerb = kVerbWalk;
	} else if (_mousePosX > 195) {
		_actionVerb = kVerbLook;
	} else if (_panelStyle == kPanelStyleVerbs) {
		_actionVerb = (Verb)(((_mousePosY - 150) / 17) * 3 + (_mousePosX / 67));
	} else {
		_actionVerb = kVerbWalk;
		if (_mousePosX < 30) {
			_actionVerb = kVerbMove;
		} else if (_mousePosX > 130 && _mousePosX < 165) {
			_actionVerb = kVerbGive;
		} else {
			if (_mousePosY < 175) {
				if (_mousePosX < 67) {
					_actionVerb = kVerbOpen;
				} else if (_mousePosX > 164) {
					_actionVerb = kVerbTake;
				} else if (_mousePosX > 99) {
					_actionVerb = kVerbClose;
				}
			} else {
				if (_mousePosX < 85) {
					_actionVerb = kVerbLook;
				} else if (_mousePosX > 165) {
					_actionVerb = kVerbTalk;
				} else {
					_actionVerb = kVerbUse;
				}
			}
		}
	}
}

int TuckerEngine::getObjectUnderCursor() {
	if (_mousePosY > 140) {
		return -1;
	}
	for (int i = 0; i < _locationObjectsCount; ++i) {
		if (_mousePosX + _scrollOffset + 1 <= _locationObjectsTable[i]._xPos) {
			continue;
		}
		if (_mousePosX + _scrollOffset >= _locationObjectsTable[i]._xPos + _locationObjectsTable[i]._xSize) {
			continue;
		}
		if (_mousePosY <= _locationObjectsTable[i]._yPos) {
			continue;
		}
		if (_mousePosY >= _locationObjectsTable[i]._yPos + _locationObjectsTable[i]._ySize) {
			continue;
		}
		_selectedObjectType = 0;
		_selectedCharacterNum = i;
		setCursorStyle(_locationObjectsTable[i]._cursorStyle);
		return i;
	}
	return -1;
}

void TuckerEngine::setSelectedObjectKey() {
	const int x = _mousePosX + _scrollOffset;
	if (_mousePosY > 139 && _nextAction == 0) {
		return;
	}
	_panelLockedFlag = true;
	_locationMaskCounter = 0;
	_actionRequiresTwoObjects = false;
	_selectedObject._yPos = 0;
	_selectedObject._locationObjectLocation = kLocationNone;
	_pendingActionIndex = 0;
	if (_selectedObjectType == 0) {
		if (_selectedObjectNum == 0) {
			_selectedObject._xPos = x;
			_selectedObject._yPos = _mousePosY;
		} else {
			_selectedObject._xPos = _locationObjectsTable[_selectedCharacterNum]._standX;
			_selectedObject._yPos = _locationObjectsTable[_selectedCharacterNum]._standY;
			if (_actionVerb == kVerbWalk || _actionVerb == kVerbUse) {
				_selectedObject._locationObjectLocation = _locationObjectsTable[_selectedCharacterNum]._location;
				_selectedObject._locationObjectToX = _locationObjectsTable[_selectedCharacterNum]._toX;
				_selectedObject._locationObjectToY = _locationObjectsTable[_selectedCharacterNum]._toY;
				_selectedObject._locationObjectToX2 = _locationObjectsTable[_selectedCharacterNum]._toX2;
				_selectedObject._locationObjectToY2 = _locationObjectsTable[_selectedCharacterNum]._toY2;
				_selectedObject._locationObjectToWalkX2 = _locationObjectsTable[_selectedCharacterNum]._toWalkX2;
				_selectedObject._locationObjectToWalkY2 = _locationObjectsTable[_selectedCharacterNum]._toWalkY2;
			}
		}
	} else {
		switch (_selectedObjectType) {
		case 1:
			_selectedObject._xPos = _locationAnimationsTable[_selectedCharacterNum]._standX;
			_selectedObject._yPos = _locationAnimationsTable[_selectedCharacterNum]._standY;
			break;
		case 2:
			_selectedObject._xPos = _charPosTable[_selectedCharacterNum]._xWalkTo;
			_selectedObject._yPos = _charPosTable[_selectedCharacterNum]._yWalkTo;
			break;
		case 3:
			_selectedObject._xPos = _xPosCurrent;
			_selectedObject._yPos = _yPosCurrent;
			break;
		}
	}
	if (_selectedObject._yPos == 0) {
		_selectedObject._xPos = x;
		_selectedObject._yPos = _mousePosY;
	}
	_selectedObjectLocationMask = testLocationMask(_selectedObject._xPos, _selectedObject._yPos);
	if (!_selectedObjectLocationMask && _objectKeysLocationTable[_location] == 1) {
		if (_selectedObject._yPos < _objectKeysPosYTable[_location]) {
			while (!_selectedObjectLocationMask && _selectedObject._yPos < _objectKeysPosYTable[_location]) {
				++_selectedObject._yPos;
				_selectedObjectLocationMask = testLocationMask(_selectedObject._xPos, _selectedObject._yPos);
			}
		} else {
			while (!_selectedObjectLocationMask && _selectedObject._yPos < _objectKeysPosYTable[_location]) {
				--_selectedObject._yPos;
				_selectedObjectLocationMask = testLocationMask(_selectedObject._xPos, _selectedObject._yPos);
			}
		}
	}
	if (_selectedObjectLocationMask) {
		_selectedObjectLocationMask = testLocationMaskArea(_xPosCurrent, _yPosCurrent, _selectedObject._xPos, _selectedObject._yPos);
		if (_selectedObjectLocationMask && _objectKeysPosXTable[_location] > 0) {
			_selectedObject._xDefaultPos = _selectedObject._xPos;
			_selectedObject._yDefaultPos = _selectedObject._yPos;
			_selectedObject._xPos = _objectKeysPosXTable[_location];
			_selectedObject._yPos = _objectKeysPosYTable[_location];
			if (_objectKeysLocationTable[_location] == 1) {
				_selectedObject._xPos = _selectedObject._xDefaultPos;
			}
		}
	}
}

void TuckerEngine::setCharacterAnimation(int count, int spr) {
	_spritesTable[spr]._animationFrame = 0;
	_spritesTable[spr]._stateIndex = 0;
	for (int i = 0; i < count; ++i) {
		while (_characterStateTable[_spritesTable[spr]._stateIndex] != 99) {
			++_spritesTable[spr]._stateIndex;
		}
		++_spritesTable[spr]._stateIndex;
	}
	_spritesTable[spr]._state = _characterStateTable[_spritesTable[spr]._stateIndex];
	++_spritesTable[spr]._stateIndex;
	_spritesTable[spr]._animationFrame = _characterStateTable[_spritesTable[spr]._stateIndex];
	++_spritesTable[spr]._stateIndex;
	_spritesTable[spr]._animationData = _sprC02Table[_spritesTable[spr]._state];
	_spritesTable[spr]._firstFrame = READ_LE_UINT16(_spritesTable[spr]._animationData);
}

bool TuckerEngine::testLocationMaskArea(int xBase, int yBase, int xPos, int yPos) {
	while (true) {
		bool loop = false;
		if (yBase > yPos) {
			if (testLocationMask(xBase, yBase - 1)) {
				--yBase;
				loop = true;
			}
		} else if (yBase < yPos) {
			if (testLocationMask(xBase, yBase + 1)) {
				++yBase;
				loop = true;
			}
		}
		if (xBase > xPos) {
			if (testLocationMask(xBase - 1, yBase)) {
				--xBase;
				loop = true;
			}
		} else if (xBase < xPos) {
			if (testLocationMask(xBase + 1, yBase)) {
				++xBase;
				loop = true;
			}
		}
		if (xBase == xPos && yBase == yPos) {
			return false;
		}
		if (!loop) {
			break;
		}
	}
	return true;
}

void TuckerEngine::handleMouseClickOnInventoryObject() {
	if (_panelType != kPanelTypeNormal) {
		return;
	}
	if (_mousePosY < 150 || _mousePosX < 212) {
		return;
	}
	int pos = ((_mousePosY - 150) / 25) * 3 + (_mousePosX - 212) / 36;
	int obj = _inventoryObjectsOffset + pos;
	if (_inventoryObjectsCount - 1 < obj) {
		return;
	}
	_selectedObjectNum = _inventoryObjectsList[obj];
	_selectedObjectType = 3;
	switch (_selectedObjectNum) {
	case 30:
		if (_leftMouseButtonPressed) {
			_selectedObjectType = 0;
			_selectedObjectNum = 0;
			_actionVerb = kVerbWalk;
			_actionVerbLocked = false;
			_forceRedrawPanelItems = true;
			_panelType = kPanelTypeLoadSavePlayQuit;
			setCursorState(kCursorStateDialog);
		}
		break;
	case 1:
		if (_actionVerb == kVerbUse && _leftMouseButtonPressed) {
			if (_mapSequenceFlagsLocationTable[_location - 1] == 1) {
				handleMapSequence();
			} else {
				_actionPosX = _xPosCurrent;
				_actionPosY = _yPosCurrent - 64;
				_actionTextColor = 1;
				_actionCharacterNum = 99;
				setCursorState(kCursorStateDisabledHidden);
				_charSpeechSoundCounter = kDefaultCharSpeechSoundCounter;
				_currentActionVerb = kVerbWalk;
				_speechSoundNum = 2235;
				startSpeechSound(_speechSoundNum, _speechVolume);
				_characterSpeechDataPtr = _ptTextBuf + getPositionForLine(_speechSoundNum, _ptTextBuf);
				_speechSoundNum = 0;
				_actionVerb = kVerbWalk;
				_selectedObjectType = 0;
				_selectedObjectNum = 0;
				_actionVerbLocked = false;
			}
		}
		break;
	}
}

int TuckerEngine::setCharacterUnderCursor() {
	if (_mousePosY > 140) {
		return -1;
	}
	for (int i = 0; i < _charPosCount; ++i) {
		if (_mousePosX + _scrollOffset <= _charPosTable[i]._xPos) {
			continue;
		}
		if (_mousePosX + _scrollOffset >= _charPosTable[i]._xPos + _charPosTable[i]._xSize) {
			continue;
		}
		if (_mousePosY <= _charPosTable[i]._yPos) {
			continue;
		}
		if (_mousePosY >= _charPosTable[i]._yPos + _charPosTable[i]._ySize) {
			continue;
		}
		if (_charPosTable[i]._flagNum == 0 || _flagsTable[_charPosTable[i]._flagNum] == _charPosTable[i]._flagValue) {
			_selectedObjectType = 2;
			_selectedCharacterDirection = _charPosTable[i]._direction;
			_selectedCharacterNum = i;
			return _charPosTable[i]._name;
		}
	}
	return -1;
}

int TuckerEngine::setLocationAnimationUnderCursor() {
	if (_mousePosY > 140) {
		return -1;
	}
	for (int i = _locationAnimationsCount - 1; i >= 0; --i) {
		if (!_locationAnimationsTable[i]._drawFlag)
			continue;

		int num = _locationAnimationsTable[i]._graphicNum;
		if (_mousePosX + _scrollOffset + 1 <= _dataTable[num]._xDest) {
			continue;
		}
		if (_mousePosX + _scrollOffset >= _dataTable[num]._xDest + _dataTable[num]._xSize) {
			continue;
		}
		if (_mousePosY <= _dataTable[num]._yDest) {
			continue;
		}
		if (_mousePosY >= _dataTable[num]._yDest + _dataTable[num]._ySize) {
			continue;
		}
		if (_locationAnimationsTable[i]._selectable == 0) {
			// WORKAROUND
			// The original game does a "return -1" here which is not correct in
			// case of overlapping hotspots.
			// This most prominently fixes Trac#6645, a bug where the cellar in part three
			// could be entered without having done the cellar door puzzle first.
			continue;
		}
		_selectedObjectType = 1;
		_selectedCharacterNum = i;
		_selectedCharacter2Num = i;
		return _locationAnimationsTable[i]._selectable;
	}
	return -1;
}

void TuckerEngine::setActionForInventoryObject() {
	if (_actionVerb == kVerbWalk || _actionVerb == kVerbTalk || _actionVerb == kVerbTake || _actionVerb == kVerbMove) {
		playSpeechForAction(_actionVerb);
		_actionVerbLocked = false;
		_actionRequiresTwoObjects = false;
		return;
	}
	if (_actionVerb == kVerbOpen || _actionVerb == kVerbClose) {
		if (!(_part == kPartTwo && _selectedObjectNum == 19) && !(_part == kPartThree && _selectedObjectNum == 42)) {
			playSpeechForAction(_actionVerb);
			_actionVerbLocked = false;
			_actionRequiresTwoObjects = false;
			return;
		}
	}
	_currentActionObj1Num = _actionObj1Num;
	_currentInfoString1SourceType = _actionObj1Type;
	_currentActionObj2Num = _actionObj2Num;
	_currentInfoString2SourceType = _actionObj2Type;
	if (_actionVerb == kVerbLook && _selectedObjectType == 3) {
		if (_panelLockedFlag) {
			if (_locationMaskType != 0) {
				return;
			}
			_panelLockedFlag = false;
		}
		if (handleSpecialObjectSelectionSequence()) {
			return;
		}
		_speechSoundNum = _actionObj1Num + _speechSoundBaseNum;
		startSpeechSound(_speechSoundNum, _speechVolume);
		_characterSpeechDataPtr = _ptTextBuf + getPositionForLine(_speechSoundNum, _ptTextBuf);
		_speechSoundNum = 0;
		_actionPosX = _xPosCurrent;
		_actionPosY = _yPosCurrent - 64;
		_actionTextColor = 1;
		_actionCharacterNum = 99;
		setCursorState(kCursorStateDisabledHidden);
		_charSpeechSoundCounter = kDefaultCharSpeechSoundCounter;
		_actionVerbLocked = false;
		_actionRequiresTwoObjects = false;
		return;
	}
	// Items with unary usage i.e. "Use X", rather than "Use X on Y"
	if (
		(_part == kPartTwo && _actionObj1Num == 19) || // radio
		(_part == kPartThree && (
			 _actionObj1Num ==  3 || // pizza
			 _actionObj1Num ==  6 || // raincoat
			 _actionObj1Num == 17 || // ear plugs
			 _actionObj1Num == 18 || // glue
			 _actionObj1Num == 33 || // peg
			(_actionObj1Num == 42 && _selectedObjectNum == 18) // skate + cue
		))
	) {
		_actionVerbLocked = false;
		_actionRequiresTwoObjects = false;
		_locationMaskCounter = 1;
		setActionState();
		return;
	}
	if (!_actionRequiresTwoObjects) {
		_actionRequiresTwoObjects = true;
	} else {
		_actionVerbLocked = false;
		_actionRequiresTwoObjects = false;
		_locationMaskCounter = 1;
		setActionState();
	}
}

void TuckerEngine::setActionState() {
	_currentActionVerb = (_actionVerb == kVerbWalk) ? kVerbUse : _actionVerb;
	_currentActionObj1Num = _actionObj1Num;
	_currentInfoString1SourceType = _actionObj1Type;
	_currentActionObj2Num = _actionObj2Num;
	_currentInfoString2SourceType = _actionObj2Type;
	_actionRequiresTwoObjects = false;
	if (_selectedObjectType < 3) {
		setSelectedObjectKey();
	}
}

void TuckerEngine::playSpeechForAction(int i) {
	static const int speechActionTable[] = { 0, 2235, 2235, 2251, 2261, 2276, 2294, 2312, 2235 };
	static const int maxCounterTable[] = { 0, 1, 13, 7, 12, 15, 15, 15, 14 };
	++_speechActionCounterTable[i];
	if (_speechActionCounterTable[i] > maxCounterTable[i]) {
		_speechActionCounterTable[i] = 0;
	}
	if (speechActionTable[i] >= 2000) {
		if (_currentActionVerb == kVerbUse && _currentActionObj1Num == 6 && _currentInfoString1SourceType == 3) {
			_speechSoundNum = 2395;
		} else {
			_speechSoundNum = _speechActionCounterTable[i] + speechActionTable[i];
		}
		startSpeechSound(_speechSoundNum, _speechVolume);
		_characterSpeechDataPtr = _ptTextBuf + getPositionForLine(_speechSoundNum, _ptTextBuf);
		_speechSoundNum = 0;
		_actionPosX = _xPosCurrent;
		_actionPosY = _yPosCurrent - 64;
		_actionTextColor = 1;
		_actionCharacterNum = 99;
		setCursorState(kCursorStateDisabledHidden);
		_charSpeechSoundCounter = kDefaultCharSpeechSoundCounter;
	}
}

void TuckerEngine::drawSpeechText(int xStart, int y, const uint8 *dataPtr, int num, int color) {
	int x = (xStart - _scrollOffset) * 2;
	int offset = (_scrollOffset + 320 - xStart) * 2;
	if (_conversationOptionsCount > 0) {
		x = 304;
	} else {
		if (x > offset) {
			x = offset;
		}
		if (x > 180) {
			x = 180;
		} else if (x < 150) {
			x = 150;
		}
	}
	int count = 0;
	bool flag = false;
	struct {
		int w, count, offset;
	} lines[5];
	lines[0].offset = getPositionForLine(num, dataPtr);
	while (!flag && count < 4) {
		int lineCharsCount, lineWidth;
		flag = splitSpeechTextLines(dataPtr, lines[count].offset, x, lineCharsCount, lineWidth);
		lines[count].w = lineWidth;
		lines[count].count = lineCharsCount;
		lines[count + 1].offset = lines[count].offset + lineCharsCount + 1;
		++count;
	}
	if (count * 10 > y) {
		y = count * 10;
	}
	for (int i = 0; i < count; ++i) {
		int yPos, xPos = xStart - lines[i].w / 2;
		if (xPos < _scrollOffset) {
			xPos = _scrollOffset;
		} else if (xPos > _scrollOffset + 320 - lines[i].w) {
			xPos = _scrollOffset + 320 - lines[i].w;
		}
		if (_conversationOptionsCount != 0) {
			xPos = xStart + _scrollOffset;
			yPos = i * 10 + y;
			_conversationOptionLinesCount = count;
		} else {
			yPos = y - (count - i) * 10;
		}
		drawSpeechTextLine(dataPtr, lines[i].offset, lines[i].count, xPos, yPos, color);
	}
}

bool TuckerEngine::splitSpeechTextLines(const uint8 *dataPtr, int pos, int x, int &lineCharsCount, int &lineWidth) {
	int count = 0;
	int w = 0;
	lineCharsCount = 0;
	lineWidth = 0;
	while (x + 1 > w && dataPtr[pos] != '\n' && dataPtr[pos] != '\r') {
		if (dataPtr[pos] == ' ') {
			lineCharsCount = count;
			lineWidth = w;
		}
		w += _charWidthTable[dataPtr[pos]];
		++count;
		++pos;
	}
	bool ret = false;
	if (x + 1 > w) {
		lineCharsCount = count;
		lineWidth = w;
		ret = true;
	}
	return ret;
}

void TuckerEngine::drawSpeechTextLine(const uint8 *dataPtr, int pos, int count, int x, int y, uint8 color) {
	const int xStart = x;
	for (int i = 0; i < count && dataPtr[pos] != '\n'; ++i) {
		Graphics::drawStringChar(_locationBackgroundGfxBuf, x, y, 640, dataPtr[pos], color, _charsetGfxBuf);
		x += _charWidthTable[dataPtr[pos]];
		++pos;
	}
	// At least in the English version of the game many glyphs in the character set are one pixel
	// wider than specified in the character width table. This ensures that, when rendering text,
	// characters are overlapping one pixel (i.e. their outlines overlap).
	// This has the negative side effect that when a text line ends with a glyph whose specified
	// size is narrower than its actual size, the calculated width for the dirty rect is wrong.
	// To compensate for this we add the current character set's maximum glyph width to make sure
	// that the dirty rect always covers the whole line.
	// This fixes Bug #6370.
	addDirtyRect(xStart, y, x - xStart + Graphics::_charset._charW, Graphics::_charset._charH);
}

void TuckerEngine::redrawScreen(int offset) {
	debug(9, "redrawScreen() _fullRedraw %d offset %d _dirtyRectsCount %d", _fullRedraw, offset, _dirtyRectsCount);
	assert(offset <= kScreenWidth);
	if (_fullRedraw) {
		_fullRedraw = false;
		_system->copyRectToScreen(_locationBackgroundGfxBuf + offset, kScreenPitch, 0, 0, kScreenWidth, kScreenHeight);
	} else {
		Common::Rect clipRect(offset, 0, offset + kScreenWidth, kScreenHeight);
		for (int i = 0; i < _dirtyRectsPrevCount + _dirtyRectsCount; ++i) {
			redrawScreenRect(clipRect, _dirtyRectsTable[i]);
		}
	}
	if (_dirtyRectsPrevCount + _dirtyRectsCount < kMaxDirtyRects) {
		for (int i = 0; i < _dirtyRectsCount; ++i) {
			_dirtyRectsTable[i] = _dirtyRectsTable[_dirtyRectsPrevCount + i];
		}
		_dirtyRectsPrevCount = _dirtyRectsCount;
	} else {
		_dirtyRectsPrevCount = 0;
		_fullRedraw = true;
	}
	_dirtyRectsCount = 0;
	_system->updateScreen();
}

void TuckerEngine::redrawScreenRect(const Common::Rect &clip, const Common::Rect &dirty) {
	if (dirty.intersects(clip)) {
		Common::Rect r(dirty);
		r.clip(clip);
		const uint8 *src = _locationBackgroundGfxBuf + r.top * 640 + r.left;
		r.translate(-clip.left, -clip.top);
		const int w = r.right - r.left;
		const int h = r.bottom - r.top;
		if (w <= 0 || h <= 0) {
			return;
		}
#if 0
		static const uint8 outlineColor = 0;
		memset(_locationBackgroundGfxBuf + r.top           * 640 + r.left, outlineColor, w);
		memset(_locationBackgroundGfxBuf + (r.top + h - 1) * 640 + r.left, outlineColor, w);
		for (int y = r.top; y < r.top + h; ++y) {
			_locationBackgroundGfxBuf[y * 640 + r.left] = outlineColor;
			_locationBackgroundGfxBuf[y * 640 + r.left + w - 1] = outlineColor;
		}
#endif
		_system->copyRectToScreen(src, 640, r.left, r.top, w, h);
	}
}

void TuckerEngine::addDirtyRect(int x, int y, int w, int h) {
	if (_dirtyRectsPrevCount + _dirtyRectsCount < kMaxDirtyRects) {
		Common::Rect r(x, y, x + w, y + h);
		for (int i = 0; i < _dirtyRectsCount; ++i) {
			if (_dirtyRectsTable[_dirtyRectsPrevCount + i].contains(r)) {
				return;
			}
		}
		_dirtyRectsTable[_dirtyRectsPrevCount + _dirtyRectsCount] = r;
		++_dirtyRectsCount;
	} else {
		_fullRedraw = true;
	}
}

} // namespace Tucker
