scummvm/engines/icb/remora.cpp
2022-10-23 22:46:19 +02:00

1113 lines
35 KiB
C++

/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* Additional copyright for this file:
* Copyright (C) 1999-2000 Revolution Software Ltd.
* This code is based on source code created by Revolution Software,
* used with permission.
*
* 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 3 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, see <http://www.gnu.org/licenses/>.
*
*/
#include "engines/icb/common/px_linkeddatafile.h"
#include "engines/icb/remora.h"
#include "engines/icb/line_of_sight.h"
#include "engines/icb/sound.h"
#include "engines/icb/global_switches.h"
#include "engines/icb/custom_logics.h"
#include "engines/icb/mission.h"
#include "common/stream.h"
namespace ICB {
_remora::_remora() {
m_eGameState = INACTIVE;
m_eCurrentMode = MOTION_SCAN;
m_eLastMode = MOTION_SCAN;
m_bModeChanged = FALSE8;
m_eModeOverride = NO_OVERRIDE;
m_nNextAvailableRow = 0;
m_nFirstLineToDraw = REMORA_FIRST_SCROLLING_LINE;
m_bScrollingRequired = FALSE8;
m_bTextPictureLoaded = FALSE8;
m_bScrolling = FALSE8;
m_nStartYPixelOffset = 0;
m_nCharacterHeight = 0;
m_bFlashingTextVisible = FALSE8;
m_nFlashCounter = 0;
m_bMainHeadingSet = FALSE8;
m_nCurrentPalette = 0;
m_pDisplayBuffer = nullptr;
// Set initial zoom range and an initial zoom.
m_nMinZoom = REMORA_SCAN_ZOOM_HARD_LOWER;
m_nMaxZoom = REMORA_SCAN_ZOOM_HARD_UPPER;
m_nCurrentZoom = REMORA_SCAN_START_ZOOM;
// Name of the cluster which holds remora graphics.
Common::strcpy_s(m_pcRemoraCluster, REMORA_CLUSTER_PATH);
m_nScanPan = 0;
// Make the hash name of the remora graphics cluster.
m_nRemoraClusterHash = EngineHashString(m_pcRemoraCluster);
// No progress bar.
m_nProgressBarValue = -1;
}
void _remora::InitialiseRemora() {
// Initialise the mode control variables.
m_eGameState = INACTIVE;
m_eCurrentMode = MOTION_SCAN;
m_eLastMode = MOTION_SCAN;
m_eModeOverride = NO_OVERRIDE;
m_bModeChanged = FALSE8;
// Clear any mega speech (there shouldn't be any though).
m_pcSpeechText = nullptr;
m_nSpeechTimer = 0;
// Initially, the zoom is set at 1X and cannot be moved. This might well change.
m_nMinZoom = REMORA_SCAN_ZOOM_HARD_LOWER;
m_nMaxZoom = REMORA_SCAN_ZOOM_HARD_UPPER;
m_nCurrentZoom = REMORA_SCAN_START_ZOOM;
// Make sure there is no outstanding email.
memset(m_pcEmailID, 0, (REMORA_MAXLEN_EMAIL_ID_STRING + 1) * sizeof(char));
// Text system initialisation.
m_nNextAvailableRow = 0;
m_nFirstLineToDraw = REMORA_FIRST_SCROLLING_LINE;
m_bScrollingRequired = FALSE8;
m_bTextPictureLoaded = FALSE8;
m_nScreenFlashCount = 0;
m_bScrolling = FALSE8;
m_nStartYPixelOffset = 0;
m_nCharacterHeight = 0;
m_bFlashingTextVisible = FALSE8;
m_nFlashCounter = 0;
m_bMainHeadingSet = FALSE8;
// Reset any floor ranges that may have been set.
m_nNumFloorRangesSet = 0;
m_nNumCurrentFloorRanges = 0;
// No progress bar.
m_nProgressBarValue = -1;
// Now do any platform-specific initialisation.
DoPlatformSpecificInitialisation();
}
void _remora::DeactivateRemora(bool8 bForceShutdown) {
// In order to stop Remora scripts dropping out during pauses and speech, we enforce
// the rule here that the Remora will not quit unless it is at a point where it is
// waiting for user-input - i.e. the icon menu is active.
if (bForceShutdown || g_oIconMenu->IsActive()) {
// Play the Remora-deactivate sound.
RegisterSoundSpecial(deactivateRemoraSfx, deactivateRemoraDesc, 127, 0);
m_eGameState = SWITCHING_OFF;
ReleaseTextFormattingMemory();
}
}
void _remora::SetMode(RemoraMode eMode) {
// Ignore the function call if the Remora is not active.
if (m_eGameState == INACTIVE)
return;
// If we are setting a new mode, we need to flag the fact to make sure the current bit gets
// closed down properly.
if (m_eCurrentMode != eMode)
m_bModeChanged = TRUE8;
// Clear text in case script writers are jumping into a non-text mode like the scan.
ClearAllText();
// And set the new mode.
m_eLastMode = m_eCurrentMode;
m_eCurrentMode = eMode;
}
void _remora::SetDefaultOrOverrideMode() {
// Check if the override is set.
if (m_eModeOverride != NO_OVERRIDE) {
// There is an override set, so use it.
SetMode(m_eModeOverride);
m_eModeOverride = NO_OVERRIDE;
} else {
// No, there isn't one set, so just use the default.
SetMode(REMORA_DEFAULT_MODE);
}
}
void _remora::DisplayCharacterSpeech(uint32 nHash) {
// No character speech is displayed unless sub_titles are turned on
if (g_px->on_screen_text == TRUE8) {
const char *pcText;
// Find the text in the resources.
pcText = g_oRemora->LocateTextFromReference(nHash);
// Check we have a valid pointer.
if (!pcText)
Fatal_error("_remora::DisplayCharacterSpeech() failed to find text for hash %x", nHash);
// Text must begin with an asterisk, which we don't display.
if (pcText[0] != TS_SPOKEN_LINE)
Fatal_error("Text [%s] not marked for actors in _remora::DisplayCharacterSpeech()", pcText);
// Put the text in the buffer.
m_pcSpeechText = &pcText[1];
} else {
m_pcSpeechText = nullptr;
}
// Initialise the counter for how int32 it will be displayed.
m_nSpeechTimer = SayLineOfSpeech(nHash);
}
void _remora::SetCurrentZoom(uint32 nZoom) {
// The zoom gets clipped at the current limits.
if (nZoom > m_nMaxZoom)
m_nCurrentZoom = m_nMaxZoom;
else if (nZoom < m_nMinZoom)
m_nCurrentZoom = m_nMinZoom;
else
m_nCurrentZoom = nZoom;
}
bool8 _remora::IsThisEmailWaiting(const char *pcEmailID) const {
if ((pcEmailID == nullptr) || (strlen(m_pcEmailID) == 0))
return (FALSE8);
if (strcmp(pcEmailID, m_pcEmailID))
return (FALSE8);
else
return (TRUE8);
}
bool8 _remora::EMPEffect() {
if (m_nScreenFlashCount == 0) {
m_nScreenFlashCount = REMORA_FLASH_EFFECT_LENGTH;
return (TRUE8);
} else {
--m_nScreenFlashCount;
if (m_nScreenFlashCount == 0)
return (FALSE8);
else
return (TRUE8);
}
}
void _remora::CycleRemoraLogic(const _input &sKeyboardState) {
_logic *pPlayerObject;
uint32 nRemoraID;
// Count down the speech-display counter.
m_nSpeechTimer = (m_nSpeechTimer > 0) ? m_nSpeechTimer - 1 : 0;
// Work the counter that controls the flashing of email waiting symbols.
++m_nFlashCounter;
if (m_nFlashCounter == REMORA_TEXT_FLASH_RATE) {
// Reset the counter and toggle the visibility state of the text.
m_nFlashCounter = 0;
m_bFlashingTextVisible = (bool8)!m_bFlashingTextVisible;
}
// If text is scrolling, process the scroll.
if (m_bScrolling) {
int32 scrollAmount = (256 * m_nCharacterHeight) / REMORA_SCROLL_CYCLES;
// See which way we're scrolling.
if (m_eTextScroll == SCROLL_UP) {
m_nStartYPixelOffset -= scrollAmount;
if (m_nStartYPixelOffset <= -256 * m_nCharacterHeight) {
m_nStartYPixelOffset += 256 * m_nCharacterHeight;
m_bScrolling = FALSE8;
m_eTextScroll = SCROLL_NONE;
++m_nFirstLineToDraw;
}
} else {
m_nStartYPixelOffset += scrollAmount;
if (m_nStartYPixelOffset >= 256 * m_nCharacterHeight) {
m_nStartYPixelOffset -= 256 * m_nCharacterHeight;
m_bScrolling = FALSE8;
m_eTextScroll = SCROLL_NONE;
--m_nFirstLineToDraw;
}
}
}
// This function works a bit like a Windows message handler in that is has to be reentrant. That's why it has
// initialisation, normal operation, and close-down sections.
switch (m_eGameState) {
case SWITCHING_ON:
// This creates sprites etc., ready to be blitted to the working buffer.
SetUpRemora();
// This initialises the Remora's menu control variables.
InitialiseMenuControlVariables();
// Now run any initialisation code for each of the modes.
switch (m_eCurrentMode) {
case MOTION_SCAN:
SetUpWideScan();
break;
case INFRA_RED_LINK:
break;
case DATABASE:
break;
case COMMUNICATIONS:
break;
case M08_LOCK_CONTROL:
SetUpM08LockControl();
break;
default:
// This should never happen.
Fatal_error("Invalid Remora mode %d while initialising", m_eCurrentMode);
} // end switch
// Remora is now active.
m_eGameState = ACTIVE;
break;
case ACTIVE:
// If there has been a mode change, we need to close down the old mode and start up the new one.
if (m_bModeChanged) {
// We're handling the mode change, so clear the trigger nFlag.
m_bModeChanged = FALSE8;
// Do the close down for the old mode.
switch (m_eLastMode) {
case MOTION_SCAN:
break;
case INFRA_RED_LINK:
break;
case DATABASE:
break;
case COMMUNICATIONS:
break;
case MAP:
break;
case M08_LOCK_CONTROL:
break;
default:
// This should never happen.
Fatal_error("Invalid Remora mode %d switching Remora modes", m_eCurrentMode);
}
// Do the set-up for the new mode.
switch (m_eCurrentMode) {
case MOTION_SCAN:
SetUpWideScan();
break;
case INFRA_RED_LINK:
break;
case DATABASE:
break;
case COMMUNICATIONS:
break;
case M08_LOCK_CONTROL:
SetUpM08LockControl();
break;
default:
// This should never happen.
Fatal_error("Invalid Remora mode %d switching to new Remora mode.", m_eCurrentMode);
}
} else {
// There was no mode change, so just run any logic associated with the current mode.
switch (m_eCurrentMode) {
case MOTION_SCAN:
// Update player position (x and z only).
pPlayerObject = MS->logic_structs[MS->player.Fetch_player_id()];
m_nPlayerX = (int32)pPlayerObject->mega->actor_xyz.x;
m_nPlayerZ = (int32)pPlayerObject->mega->actor_xyz.z;
// Check if the local pan has been updated, which means the Remora is being 'swung'
// from side to side.
if (sKeyboardState.IsButtonSet(__SIDESTEP)) {
if (sKeyboardState.turn == __LEFT)
m_fPlayerPan = remainder(m_fPlayerPan - REMORA_SCAN_PAN_STEP, FULL_TURN, HALF_TURN);
else if (sKeyboardState.turn == __RIGHT)
m_fPlayerPan = remainder(m_fPlayerPan + REMORA_SCAN_PAN_STEP, FULL_TURN, HALF_TURN);
} else {
ProcessUpDownZoomKeys(sKeyboardState);
}
// Check for the palette-change key.
if (sKeyboardState.IsButtonSet(__ATTACK))
m_nCurrentPalette = (uint8)(((int32)m_nCurrentPalette + 1) % REMORA_NUM_COLOUR_SCHEMES);
// Update the beam position.
m_nScanPan = (m_nScanPan + REMORA_SCAN_SPEED) % 360;
break;
case INFRA_RED_LINK:
ProcessUpDownTextKeys(sKeyboardState);
break;
case DATABASE:
ProcessUpDownTextKeys(sKeyboardState);
break;
case COMMUNICATIONS:
break;
case M08_LOCK_CONTROL:
break;
default:
// This should never happen.
Fatal_error("Invalid Remora mode %d while Remora active", m_eCurrentMode);
} // end switch
} // end if
// Fire button quits the speech timer.
if (sKeyboardState.IsButtonSet(__ATTACK))
m_nSpeechTimer = 0;
break;
case SWITCHING_OFF:
// Remora is closing down. First close down according to which mode it is in.
switch (m_eCurrentMode) {
case MOTION_SCAN:
break;
case INFRA_RED_LINK:
break;
case DATABASE:
break;
case COMMUNICATIONS:
break;
case MAP:
break;
case M08_LOCK_CONTROL:
break;
default:
// This should never happen.
Fatal_error("Invalid Remora mode %d switching Remora off", m_eCurrentMode);
} // end switch
// Close down the icon menu.
g_oIconMenu->CloseDownIconMenu();
// Now close down the base graphic.
CloseDownRemora();
// Remora is now inactive.
m_eGameState = INACTIVE;
nRemoraID = LinkedDataObject::Fetch_item_number_by_name(MS->objects, REMORA_NAME);
if (nRemoraID == PX_LINKED_DATA_FILE_ERROR)
Fatal_error("No logic object for Remora in _remora::CycleRemoraLogic()");
g_oEventManager->PostNamedEventToObject(EVENT_LOGIC_RERUN, nRemoraID, nRemoraID);
break;
default:
// This should never happen.
Fatal_error("Invalid Remora state");
}
}
void _remora::SetText(const char *pcText, uint8 nAttribute, uint8 nIndent, _pin_position ePosition) {
uint32 i;
_TSparams *psTextParameters;
const char *pcParsePos;
uint32 nNumRemoraFormattedLines;
uint32 nLineLength;
bool8 bMainHeading;
uint32 nEffectiveWidth;
// Ignore the function call if the Remora is not active.
if (m_eGameState == INACTIVE)
return;
// If this is a blank line, deal with it now.
if (nAttribute == 0) {
// Check we haven't run out of lines.
if (m_nNextAvailableRow == REMORA_TEXT_BUFFER_ROWS)
Fatal_error("Run out of adding blank line in Remora text - limit %d", REMORA_TEXT_BUFFER_ROWS);
// Insert single blank line.
m_pDisplayBuffer[m_nNextAvailableRow++].s_nAttribute = 0;
// Nothing more to do with blank lines.
return;
}
// We have sub-headings and main headings, which share a text colour (and as a bad left-over from the old code,
// also share a text attribute value). Main headings are centred though, so we can easily differentiate them.
if (((nAttribute & REMORA_TEXT_HEADING) != 0) && (ePosition == PIN_AT_CENTRE))
bMainHeading = TRUE8;
else
bMainHeading = FALSE8;
// Work out the effective print width accounting for tabs.The X-coordinate we pin the text to depends on whether or not the text is centred or not.
if (ePosition == PIN_AT_CENTRE) {
nEffectiveWidth = REMORA_DISPLAY_WIDTH;
} else {
// Left-justification must also handle tabs.
nEffectiveWidth = REMORA_DISPLAY_WIDTH - nIndent * (REMORA_TEXT_TAB_ONE);
// Check we haven't tabbed down to a too-small column of text.
if (nEffectiveWidth < 50)
Fatal_error("Too many tabs in [%s] in _remora::SetText()", pcText);
}
// First we need to ask the text sprite drawer what the text will format like, so we can work out if it will fit.
MS->Format_remora_text(pcText, REMORA_ROW_SPACING, REMORA_CHARACTER_SPACING, nEffectiveWidth);
nNumRemoraFormattedLines = MS->text_bloc->GetLineInfo()->noOfLines;
// The Format_remora_text() function sets up the parameter block for the text sprite. Since this block
// has to be set up to get the height of a character and since a heading has to be set before anything
// else, this is a convenient place for the Remora to initialise its character-height variable.
if (m_nCharacterHeight == 0) {
psTextParameters = MS->text_bloc->GetParams();
m_nCharacterHeight = (uint8)MS->text_bloc->CharHeight(psTextParameters->fontResource, psTextParameters->fontResource_hash);
// To have ceiling based rounding
m_nPictureHeightCorrection = (uint8)(m_nCharacterHeight - 1);
}
// Check if we are setting the main heading for the screen.
if (bMainHeading) {
// This is a heading, so we are starting the display afresh.
m_nNextAvailableRow = 0;
// First scrolling line to draw is one past the heading.
m_nFirstLineToDraw = REMORA_FIRST_SCROLLING_LINE;
// We now have a heading.
m_bMainHeadingSet = TRUE8;
} else {
// Must have set a heading for the screen before any paragraph text can be set.
if (m_nNextAvailableRow == 0)
Fatal_error("You cannot set text on the Remora screen until a heading has been set for the screen.");
}
// Copy the text into the buffer.
pcParsePos = pcText;
for (i = 0; i < nNumRemoraFormattedLines; ++i) {
// Get length of line.
nLineLength = MS->text_bloc->GetLineInfo()->line[i].length;
// Copy one line.
strncpy(m_pDisplayBuffer[m_nNextAvailableRow].s_pcText, const_cast<char *>(pcParsePos), nLineLength);
// Make sure we haven't lost the terminator (but I'm sure he'll be back, if we have).
m_pDisplayBuffer[m_nNextAvailableRow].s_pcText[nLineLength] = '\0';
// Fill in the attributes for the line.
m_pDisplayBuffer[m_nNextAvailableRow].s_uPos.s_ePinPosition = ePosition;
m_pDisplayBuffer[m_nNextAvailableRow].s_nAttribute = nAttribute;
m_pDisplayBuffer[m_nNextAvailableRow].s_uXY.s_nIndent = nIndent;
// Move the line count on 1.
++m_nNextAvailableRow;
// And move the parse pointer past the line we have just dealt with.
pcParsePos += nLineLength;
// And skip any spaces.
while (*pcParsePos == ' ')
++pcParsePos;
}
// Check if the scroll controls need activating.
if (m_nNextAvailableRow > REMORA_DISPLAYED_TEXT_ROWS)
m_bScrollingRequired = TRUE8;
}
void _remora::SetupPicture(uint32 nXPixelOffset, const char *pcPictureName) {
uint32 i;
uint32 nPictureHeight, nPictureWidth;
uint32 nNumPictureRows;
const char *pcFullPictureNameAndPath;
// Ignore the function call if the Remora is not active.
if (m_eGameState == INACTIVE)
return;
// Check there is not a picture loaded already.
if (m_bTextPictureLoaded)
return;
// Flag the fact that there is now a picture loaded.
m_bTextPictureLoaded = TRUE8;
// Must have set a heading for the screen before anything else can be set.
if (m_nNextAvailableRow == 0)
Fatal_error("You cannot put a picture in the Remora until a heading has been set for the screen.");
// Load the picture.
pcFullPictureNameAndPath = MakeRemoraGraphicsPath(pcPictureName);
m_oTextPicture.InitialiseFromBitmapName(pcFullPictureNameAndPath, m_pcRemoraCluster, m_nRemoraClusterHash);
// Get the size of the image.
nPictureHeight = m_oTextPicture.GetHeight();
nPictureWidth = m_oTextPicture.GetWidth();
// Check it isn't too big.
if ((nPictureWidth > REMORA_MAX_PICTURE_WIDTH) || (nPictureHeight > REMORA_MAX_PICTURE_HEIGHT))
Fatal_error("Picture [%s] is %d wide X %d high (maximum is %d X %d)", pcPictureName, nPictureWidth, nPictureHeight, REMORA_MAX_PICTURE_WIDTH,
REMORA_MAX_PICTURE_HEIGHT);
// Need to know how many rows it occupies, so we can leave a gap in the text.
nNumPictureRows = (nPictureHeight + m_nPictureHeightCorrection) / m_nCharacterHeight;
// Check it fits in the virtual screen buffer.
if (m_nNextAvailableRow + nNumPictureRows >= REMORA_TEXT_BUFFER_ROWS)
Fatal_error("Picture [%s] at row %d will go over the end of the Remora's buffer", pcPictureName, m_nNextAvailableRow);
for (i = 0; i < nNumPictureRows; ++i) {
// Fill in the attributes for the line.
m_pDisplayBuffer[m_nNextAvailableRow].s_uPos.s_nXOffset = nXPixelOffset;
m_pDisplayBuffer[m_nNextAvailableRow].s_nAttribute = REMORA_TEXT_PICTURE;
// This is a count maintained in the rows occupied by the picture of how far into the
// picture we currently are. Makes working out a Y-draw-height easy later.
m_pDisplayBuffer[m_nNextAvailableRow].s_uXY.s_nPictureRow = (uint8)i;
// Move the line count on 1.
++m_nNextAvailableRow;
}
// Check if the scroll controls need activating.
if (m_nNextAvailableRow > REMORA_DISPLAYED_TEXT_ROWS)
m_bScrollingRequired = TRUE8;
}
void _remora::AddFloorRange(uint32 nLower, uint32 nUpper) {
LinkedDataFile *pSlices;
// Check that top value is within the available slices (bottom one must be because it is unsigned). First,
// get the pointer to the slices (this will already have been loaded by the line-of-sight engine).
pSlices = g_oLineOfSight->GetSlicesPointer();
if (nUpper >= LinkedDataObject::Fetch_number_of_items(pSlices))
nUpper = LinkedDataObject::Fetch_number_of_items(pSlices) - 1;
// Upper must be greater than lower, or it isn't a range.
if (nUpper <= nLower)
return;
// And there is a fixed maximum range that can be accommodated.
if (((nUpper - nLower) + 1) > REMORA_MAX_INCLUDED_SLICES)
Fatal_error("Range (%d-%d) greater than maximum %d in _remora::AddFloorRange()", nLower, nUpper, REMORA_MAX_INCLUDED_SLICES);
// Okay, it's a valid range now, so let's add it.
m_pFloorRanges[m_nNumFloorRangesSet].s_nLower = (uint8)nLower;
m_pFloorRanges[m_nNumFloorRangesSet].s_nUpper = (uint8)nUpper;
// Update count of how many we have.
++m_nNumFloorRangesSet;
}
void _remora::Save(Common::WriteStream *stream) const {
// Save any outstanding email. Just write the whole string since it is small and there's only one.
stream->write(m_pcEmailID, sizeof(char) * (REMORA_MAXLEN_EMAIL_ID_STRING + 1));
}
void _remora::Restore(Common::SeekableReadStream *stream) {
// Save any outstanding email. Just write the whole string since it is small and there's only one.
if (stream->read(m_pcEmailID, sizeof(char) * (REMORA_MAXLEN_EMAIL_ID_STRING + 1)) != sizeof(char) * (REMORA_MAXLEN_EMAIL_ID_STRING + 1))
Fatal_error("Error restoring email ID string in _remora::Restore()");
// Inform the icon menu whether or not there is an email waiting now we have restored.
if (strlen(m_pcEmailID) > 0)
g_oIconMenu->SetEmailArrived();
else
g_oIconMenu->ClearEmailArrived();
}
void _remora::ProcessUpDownTextKeys(const _input &sKeyboardState) {
// If the text does not extend over the edge of the screen then there is nothing to do.
if (!m_bScrollingRequired)
return;
// Also do nothing if text is already in process of scrolling.
if (m_bScrolling)
return;
// Check for up and down keys.
if (sKeyboardState.momentum == __STILL) {
return;
} else if (sKeyboardState.momentum == __BACKWARD_1) {
// Moving down through the text, so text will scroll upwards on the screen. Check that
// we can still scroll in this direction.
if ((m_nFirstLineToDraw + REMORA_DISPLAYED_TEXT_ROWS) < m_nNextAvailableRow) {
m_nStartYPixelOffset = 0;
m_eTextScroll = SCROLL_UP;
m_bScrolling = TRUE8;
} else {
m_eTextScroll = SCROLL_NONE;
m_bScrolling = FALSE8;
}
} else {
// Moving up through the text, so text will scroll down on the screen. Check that we
// can still scroll in this direction.
if (m_nFirstLineToDraw > REMORA_FIRST_SCROLLING_LINE) {
m_nStartYPixelOffset = 0;
m_eTextScroll = SCROLL_DOWN;
m_bScrolling = TRUE8;
} else {
m_eTextScroll = SCROLL_NONE;
m_bScrolling = FALSE8;
}
}
}
void _remora::ProcessUpDownZoomKeys(const _input &sKeyboardState) {
int32 nNewZoom;
// Check for up and down keys.
if (sKeyboardState.momentum == __STILL) {
return;
} else if (sKeyboardState.momentum == __BACKWARD_1) {
// Zooming out.
nNewZoom = m_nCurrentZoom - REMORA_SCAN_ZOOM_STEP;
} else {
// Zooming in.
nNewZoom = m_nCurrentZoom + REMORA_SCAN_ZOOM_STEP;
}
// Apply the new zoom value (this function keeps it range-capped to the current limits).
SetCurrentZoom(nNewZoom);
}
const char *_remora::LocateTextFromReference(uint32 nHashRef) {
const char *pcTextLine;
// Look for the reference.
pcTextLine = (const char *)LinkedDataObject::Try_fetch_item_by_hash(MS->text, nHashRef);
// If we found it, return it.
if (pcTextLine)
return (pcTextLine);
// Look in the global text file.
pcTextLine = (const char *)LinkedDataObject::Try_fetch_item_by_hash(global_text, nHashRef);
// Return the pointer regardless.
return (pcTextLine);
}
void _remora::DrawScreenText() {
int32 nStartXPixel, nStartYPixel;
int32 nVirtualRow, nScreenRow;
uint8 nRed, nGreen, nBlue;
uint32 nEffectiveWidth, nTabWidth;
_rs_params sParams;
uint32 nBaseY;
int32 nDisplayedTextRows;
// We always start drawing at row 0 on the screen.
nScreenRow = 0;
// The place we start taking stuff to draw depends on how far down the virtual buffer we are. If
// we are scrolling down then we start drawing one line early to make it look smooth.
switch (m_eTextScroll) {
case SCROLL_DOWN:
nVirtualRow = m_nFirstLineToDraw - 1;
nBaseY = REMORA_TEXT_TOP_MARGIN - m_nCharacterHeight;
nDisplayedTextRows = REMORA_DISPLAYED_TEXT_ROWS + 2;
break;
case SCROLL_UP:
nVirtualRow = m_nFirstLineToDraw;
nBaseY = REMORA_TEXT_TOP_MARGIN;
nDisplayedTextRows = REMORA_DISPLAYED_TEXT_ROWS + 2;
break;
default:
nVirtualRow = m_nFirstLineToDraw;
nBaseY = REMORA_TEXT_TOP_MARGIN;
nDisplayedTextRows = REMORA_DISPLAYED_TEXT_ROWS;
break;
}
// Loop for each line in view.
while ((nScreenRow < nDisplayedTextRows) && (nVirtualRow < m_nNextAvailableRow)) {
// Work out how far down the screen to start drawing this line
nStartYPixel = nBaseY + (nScreenRow * m_nCharacterHeight);
nStartYPixel += (m_nStartYPixelOffset / 256); // scaled down
// Check to see if it is a picture or text.
if (m_pDisplayBuffer[nVirtualRow].s_nAttribute & REMORA_TEXT_PICTURE) {
// We have a picture to display. If the picture goes off the top of the screen then we
// need to move the top edge of the picture to make it look like we are displaying the
// picture from part-way down.
nStartYPixel -= (m_nCharacterHeight * m_pDisplayBuffer[nVirtualRow].s_uXY.s_nPictureRow);
// Draw it.
sParams.bCentre = FALSE8;
sParams.bUpdate = FALSE8;
sParams.nW = 0;
sParams.nH = 0;
sParams.bAllFrames = TRUE8;
nStartXPixel = REMORA_TEXT_LEFT_MARGIN + m_pDisplayBuffer[nVirtualRow].s_uPos.s_nXOffset;
m_oTextPicture.DrawXYSprite(nStartXPixel + REMORA_SCREEN_ORIGIN_X, nStartYPixel + REMORA_SCREEN_ORIGIN_Y, &sParams);
// Picture gets drawn in one go, so we can simply skip forward past the whole thing now.
while (m_pDisplayBuffer[nVirtualRow].s_nAttribute & REMORA_TEXT_PICTURE) {
++nVirtualRow;
++nScreenRow;
}
} else {
// Displaying a line of text, not a picture. Check if it's just a blank line.
if (m_pDisplayBuffer[nVirtualRow].s_nAttribute == 0) {
++nVirtualRow;
++nScreenRow;
continue;
}
// Set the colour to draw this text in.
ColourToRGB(m_pDisplayBuffer[nVirtualRow].s_nAttribute, nRed, nGreen, nBlue);
SetTextColour(nRed, nGreen, nBlue);
// The X-coordinate we pin the text to depends on whether or not the text is centred or not.
if (m_pDisplayBuffer[nVirtualRow].s_uPos.s_ePinPosition == PIN_AT_CENTRE) {
nStartXPixel = REMORA_TEXT_CENTRE;
nEffectiveWidth = REMORA_DISPLAY_WIDTH;
} else {
// Left-justification must also handle tabs.
nTabWidth = m_pDisplayBuffer[nVirtualRow].s_uXY.s_nIndent * REMORA_TEXT_TAB_ONE;
nStartXPixel = REMORA_TEXT_LEFT_MARGIN + nTabWidth;
nEffectiveWidth = REMORA_DISPLAY_WIDTH - nTabWidth;
}
// Draw the line of text.
MS->Create_remora_text(nStartXPixel, nStartYPixel, m_pDisplayBuffer[nVirtualRow].s_pcText, 0, m_pDisplayBuffer[nVirtualRow].s_uPos.s_ePinPosition,
REMORA_ROW_SPACING, REMORA_CHARACTER_SPACING, nEffectiveWidth);
MS->Render_speech(MS->text_bloc);
MS->Kill_remora_text();
// Moved on on the screen and in the virtual buffer.
++nVirtualRow;
++nScreenRow;
}
}
}
void _remora::DrawEmailWaiting() {
uint32 nHashRef;
const char *pcEmailWaitingText;
// Check if there is a message waiting and the flash counter is currently on.
if ((strlen(m_pcEmailID) > 0) && m_bFlashingTextVisible) {
// Find the text message to display.
nHashRef = HashString("email_waiting");
pcEmailWaitingText = LocateTextFromReference(nHashRef);
MS->Create_remora_text(REMORA_EMAIL_WAITING_X, REMORA_EMAIL_WAITING_Y, pcEmailWaitingText, 0, PIN_AT_TOP_LEFT, 0, 0, REMORA_DISPLAY_WIDTH);
MS->Render_speech(MS->text_bloc);
MS->Kill_remora_text();
}
}
void _remora::ClearAllText() {
// Ignore the function call if the Remora is not active.
if (m_eGameState == INACTIVE)
return;
// Reset the text control variables.
m_nNextAvailableRow = 0;
m_nFirstLineToDraw = 0;
m_bScrollingRequired = FALSE8;
m_bScrolling = FALSE8; // these three need resetting so when we go to a new page we don't try scrolling off screen
m_nStartYPixelOffset = 0;
m_eTextScroll = SCROLL_NONE;
m_bTextPictureLoaded = FALSE8;
m_bMainHeadingSet = FALSE8;
// These control the spoken text display.
m_pcSpeechText = nullptr;
m_nSpeechTimer = 0;
}
void _remora::DrawVoiceOverText() const {
// Here we draw the text that characters may say inside the Remora.
if (g_px->on_screen_text && (m_nSpeechTimer > 0) && (strlen(m_pcSpeechText) > 0)) {
// Yes we need to display speech text.
MS->Create_remora_text(REMORA_SPEECH_X_POSITION, REMORA_SPEECH_Y_POSITION, m_pcSpeechText, 0, PIN_AT_CENTRE, 0, 0, REMORA_DISPLAY_WIDTH);
MS->Render_speech(MS->text_bloc);
MS->Kill_remora_text();
}
}
void _remora::SetCommonActivateInfo(RemoraMode eMode) {
uint32 i, j;
_logic *pPlayerObject;
LinkedDataFile *pSlices;
BarrierSlice *pSlice;
int32 nSlice;
uint32 nNumSlices;
bool8 bInFloorRange;
// Set the mode flags.
m_eGameState = SWITCHING_ON;
m_eCurrentMode = eMode;
m_bModeChanged = FALSE8;
// Cancel the flash counter if it was left running last time.
m_nScreenFlashCount = 0;
// Get position of player. The scanner modes need this information.
pPlayerObject = MS->logic_structs[MS->player.Fetch_player_id()];
m_nPlayerY = (int32)pPlayerObject->mega->actor_xyz.y;
m_nPlayerX = (int32)pPlayerObject->mega->actor_xyz.x;
m_nPlayerZ = (int32)pPlayerObject->mega->actor_xyz.z;
// If we are in the special one-off M08 interface or the map mode, the pan is fixed, otherwise we
// get it from the player.
if ((m_eCurrentMode == M08_LOCK_CONTROL) || (m_eCurrentMode == MAP))
m_fPlayerPan = REAL_ZERO;
else
m_fPlayerPan = pPlayerObject->pan;
// Just knowing the y height of the player is now not enough for the scan modes because they may need to
// include floors and objects from different heights. Here we work out the slices and range of heights
// that need including in scan displays.
pSlices = g_oLineOfSight->GetSlicesPointer();
// Find out which slice we're in.
nNumSlices = LinkedDataObject::Fetch_number_of_items(pSlices);
nSlice = 0;
for (i = 0; i < nNumSlices; ++i) {
// Get the slice.
pSlice = (BarrierSlice *)LinkedDataObject::Fetch_item_by_number(pSlices, i);
// See if the player's feet are in this slice.
if ((m_nPlayerY >= pSlice->bottom) && (m_nPlayerY < pSlice->top))
nSlice = i;
}
// Right, we know which slice the player is in. Check to see if this is in one of the ranges.
bInFloorRange = FALSE8;
m_nNumCurrentFloorRanges = 0;
i = 0;
while (!bInFloorRange && (i < m_nNumFloorRangesSet)) {
if ((nSlice >= m_pFloorRanges[i].s_nLower) && (nSlice <= m_pFloorRanges[i].s_nUpper)) {
// Yes, the slice is contained in a range, so when we do a scan, we need to include everything
// in the range.
bInFloorRange = TRUE8;
for (j = m_pFloorRanges[i].s_nLower; j <= m_pFloorRanges[i].s_nUpper; ++j) {
m_pSlices[m_nNumCurrentFloorRanges] = (BarrierSlice *)LinkedDataObject::Fetch_item_by_number(pSlices, j);
m_pnSlices[m_nNumCurrentFloorRanges] = j;
++m_nNumCurrentFloorRanges;
}
}
++i;
}
// If we didn't set a floor range then we must set a single floor slice.
if (!bInFloorRange) {
// Only one slice required to be displayed.
m_pSlices[0] = (BarrierSlice *)LinkedDataObject::Fetch_item_by_number(pSlices, nSlice);
m_pnSlices[0] = nSlice;
m_nNumCurrentFloorRanges = 1;
}
// Set an absolute floor and ceiling.
m_nIncludedFloor = (int32)m_pSlices[0]->bottom;
m_nIncludedCeiling = (int32)m_pSlices[m_nNumCurrentFloorRanges - 1]->top;
// Clear the text buffer.
ClearAllText();
}
void _remora::AccessMenuLevelVariables(int32 *pnParams, MenuVariableAccessMode eRetrieve) {
uint32 i, j;
CGame *pGameObject;
char pcVarName[] = REMORA_MENU_LEVEL_NAME;
uint32 nDigitPos;
// Get the Remora's game object.
pGameObject = (CGame *)LinkedDataObject::Fetch_item_by_name(MS->objects, REMORA_NAME);
// Get the position where we need to add the digit to the menu variable name.
nDigitPos = strlen(pcVarName) - 1;
// System currently supports 5 menu levels. We must set these variables.
for (i = 0; i < REMORA_MENU_DEPTH; ++i) {
// Make the name of the variable.
pcVarName[nDigitPos] = (char)(i + '1');
// Find the variable in the Remora's game object.
j = 0;
while ((j < CGameObject::GetNoLvars(pGameObject)) && strcmp(pcVarName, CGameObject::GetScriptVariableName(pGameObject, j)))
++j;
// If we ran out of variables, this is an error because we haven't found the one we're looking for.
if (j == CGameObject::GetNoLvars(pGameObject))
Fatal_error("Failed to find menu variable %s in _remora::AccessMenuLevelVariables()", pcVarName);
// Found it, so get or set it.
if (eRetrieve == GET)
pnParams[i] = CGameObject::GetIntegerVariable(pGameObject, j);
else
CGameObject::SetIntegerVariable(pGameObject, j, pnParams[i]);
}
}
_remora::ScreenSymbol _remora::GetSymbolToDrawObject(_logic *pObject, uint32 nID) const {
__object_type eObjectType;
CGame *pGameObject;
uint32 nScriptVar, nVarVal;
// If it's player, always return same symbol.
if (nID == MS->player.Fetch_player_id())
return (SS_REMORA);
// Get the type field stored in the object.
eObjectType = pObject->object_type;
// Decide what colour it should be.
switch (eObjectType) {
case (__BUTTON_OPERATED_DOOR):
// In the case of doors, we must work out whether or not they are closed.
if (pObject->list[BOD_STATE_INDEX])
return (DOOR_CLOSED);
else
return (DOOR_OPEN);
break;
case (__ORGANIC_MEGA):
// Need to find out if the human is alive or dead.
pGameObject = (CGame *)LinkedDataObject::Fetch_item_by_number(MS->objects, nID);
nScriptVar = CGameObject::GetVariable(pGameObject, "state");
nVarVal = CGameObject::GetIntegerVariable(pGameObject, nScriptVar);
if (nVarVal == 1)
return (DEAD_HUMAN);
else
return (ALIVE_HUMAN);
break;
case (__NON_ORGANIC_MEGA):
// Need to find out if the robot is alive or dead.
pGameObject = (CGame *)LinkedDataObject::Fetch_item_by_number(MS->objects, nID);
nScriptVar = CGameObject::GetVariable(pGameObject, "state");
nVarVal = CGameObject::GetIntegerVariable(pGameObject, nScriptVar);
if (nVarVal == 1)
return (DEAD_ROBOT);
else
return (ALIVE_ROBOT);
break;
case (__REMORA_CARRIER):
// This is an object carrying a Remora, but only the player gets a special symbol now.
pGameObject = (CGame *)LinkedDataObject::Fetch_item_by_number(MS->objects, nID);
nScriptVar = CGameObject::GetVariable(pGameObject, "state");
nVarVal = CGameObject::GetIntegerVariable(pGameObject, nScriptVar);
if (nVarVal == 1)
return (DEAD_HUMAN);
else
return (ALIVE_HUMAN);
break;
case (__RECHARGE_POINT):
pGameObject = (CGame *)LinkedDataObject::Fetch_item_by_number(MS->objects, nID);
nScriptVar = CGameObject::GetVariable(pGameObject, "set_mine");
nVarVal = CGameObject::GetIntegerVariable(pGameObject, nScriptVar);
if (nVarVal == 1)
return (RECHARGE_ARMED);
else
return (RECHARGE_UNARMED);
break;
case (__AUTO_DOOR):
// In the case of doors, we must work out whether or not they are closed.
if (pObject->list[CAD_STATE_INDEX])
return (DOOR_CLOSED);
else
return (DOOR_OPEN);
break;
default:
// If the object has no type set then don't display it.
return (DO_NOT_DISPLAY);
}
}
void _remora::BuildM08DoorList() {
m_pnDoorIDs[0] = MS->Fetch_named_objects_id("door_corridor1_to_interorgation");
m_pnDoorIDs[1] = MS->Fetch_named_objects_id("cell_door");
m_pnDoorIDs[2] = MS->Fetch_named_objects_id("door_corridor1_to_liftstart");
m_pnDoorIDs[3] = MS->Fetch_named_objects_id("door_corridor1_to_security");
m_pnDoorIDs[4] = MS->Fetch_named_objects_id("door_cell_to_lukyan");
m_pnDoorIDs[5] = MS->Fetch_named_objects_id("door_security_to_lukyan");
m_pnDoorIDs[6] = MS->Fetch_named_objects_id("door_corridor2_to_security");
m_pnDoorIDs[7] = MS->Fetch_named_objects_id("door_security_to_exitlift");
m_pnDoorIDs[8] = MS->Fetch_named_objects_id("door_security_to_doorcontrol");
m_pnDoorIDs[9] = MS->Fetch_named_objects_id("door_corridor2_to_doorcontrol");
m_pnDoorIDs[10] = MS->Fetch_named_objects_id("door_exitlift_to_mainlift_sec");
m_pnDoorIDs[11] = MS->Fetch_named_objects_id("door_robot");
}
} // End of namespace ICB