Files
scummvm-cursorfix/engines/ultima/ultima8/kernel/mouse.cpp
2026-02-02 04:50:13 +01:00

592 lines
16 KiB
C++

/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 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 "common/config-manager.h"
#include "graphics/cursorman.h"
#include "ultima/ultima.h"
#include "ultima/ultima8/kernel/mouse.h"
#include "ultima/ultima8/games/game_data.h"
#include "ultima/ultima8/gfx/render_surface.h"
#include "ultima/ultima8/gumps/gump.h"
#include "ultima/ultima8/kernel/kernel.h"
#include "ultima/ultima8/misc/direction_util.h"
#include "ultima/ultima8/world/get_object.h"
#include "ultima/ultima8/world/actors/avatar_mover_process.h"
#include "ultima/ultima8/world/actors/main_actor.h"
#include "ultima/ultima8/gfx/shape.h"
#include "ultima/ultima8/gfx/shape_frame.h"
#include "ultima/ultima8/gfx/palette.h"
namespace Ultima {
namespace Ultima8 {
Mouse *Mouse::_instance = nullptr;
Mouse::Mouse() : _lastMouseFrame(-1), _flashingCursorTime(0), _mouseOverGump(0),
_dragging(DRAG_NOT), _dragging_objId(0), _draggingItem_startGump(0),
_draggingItem_lastGump(0) {
_instance = this;
_cursors.push(MOUSE_NONE);
CursorMan.showMouse(false);
// The original game switches cursors from small -> medium -> large on
// rectangles - in x, ~30px and ~130px away from the avatar (center) on
// the 320px screen, and approximately the same proportions in y.
//
// These cursors correspond to the player movement of step -> walk -> run.
//
// Modern players may be in a window so give them a little bit more
// space to make the large cursor without having to hit the edge.
// Walk & run threshold range of 0-255
ConfMan.registerDefault("walk_threshold", 50);
ConfMan.registerDefault("run_threshold", 160);
_walkThreshold = CLIP<int>(ConfMan.getInt("walk_threshold"), 0, 255);
_runThreshold = CLIP<int>(ConfMan.getInt("run_threshold"), 0, 255);
}
Mouse::~Mouse() {
_instance = nullptr;
}
bool Mouse::buttonDown(MouseButton button) {
assert(button != MOUSE_LAST);
bool handled = false;
MButton &mbutton = _mouseButton[button];
Gump *desktopGump = Ultima8Engine::get_instance()->getDesktopGump();
Gump *mousedowngump = desktopGump->onMouseDown(button, _mousePos.x, _mousePos.y);
if (mousedowngump) {
mbutton._downGump = mousedowngump->getObjId();
handled = true;
} else {
mbutton._downGump = 0;
}
mbutton._lastDown = mbutton._curDown;
mbutton._curDown = g_system->getMillis();
mbutton._downPoint = _mousePos;
mbutton.setState(MBS_DOWN);
mbutton.clearState(MBS_HANDLED);
uint32 timeout = getDoubleClickTime();
if (mbutton.isUnhandledDoubleClick(timeout)) {
if (_dragging == Mouse::DRAG_NOT) {
Gump *gump = getGump(mbutton._downGump);
if (gump) {
int32 mx2 = _mousePos.x, my2 = _mousePos.y;
Gump *parent = gump->GetParent();
if (parent) parent->ScreenSpaceToGump(mx2, my2);
gump->onMouseDouble(button, mx2, my2);
}
mbutton.setState(MBS_HANDLED);
mbutton._lastDown = 0;
}
}
return handled;
}
bool Mouse::buttonUp(MouseButton button) {
assert(button != MOUSE_LAST);
bool handled = false;
_mouseButton[button].clearState(MBS_DOWN);
// Need to store the last down position of the mouse
// when the button is released.
_mouseButton[button]._downPoint = _mousePos;
// Always send mouse up to the gump
Gump *gump = getGump(_mouseButton[button]._downGump);
if (gump) {
int32 mx2 = _mousePos.x, my2 = _mousePos.y;
Gump *parent = gump->GetParent();
if (parent)
parent->ScreenSpaceToGump(mx2, my2);
gump->onMouseUp(button, mx2, my2);
handled = true;
}
if (button == BUTTON_LEFT && _dragging != Mouse::DRAG_NOT) {
stopDragging(_mousePos.x, _mousePos.y);
handled = true;
}
return handled;
}
void Mouse::popAllCursors() {
_cursors.clear();
_cursors.push(MOUSE_NONE);
update();
}
bool Mouse::isMouseDownEvent(MouseButton button) const {
return _mouseButton[button].isState(MBS_DOWN);
}
int Mouse::getMouseLength(int mx, int my) const {
Ultima8Engine *engine = Ultima8Engine::get_instance();
AvatarMoverProcess *proc = engine->getAvatarMoverProcess();
if (proc) {
if (proc->hasMovementFlags(AvatarMoverProcess::MOVE_STEP))
return 0;
if (proc->hasMovementFlags(AvatarMoverProcess::MOVE_RUN))
return 2;
}
RenderSurface *screen = engine->getRenderScreen();
Common::Rect32 dims = screen->getSurfaceDims();
// Reference point is the center of the screen
int dx = abs(mx - dims.width() / 2);
int dy = abs((dims.height() / 2) - my);
int xmed = dims.width() / 2 * _runThreshold / 255;
int ymed = dims.height() / 2 * _runThreshold / 255;
if (dx > xmed || dy > ymed)
return 2;
// For short cursor, reference point is near the avatar's feet
dy = abs((dims.height() / 2 + 14) - my); //! constant
int xshort = dims.width() / 2 * _walkThreshold / 255;
int yshort = dims.height() / 2 * _walkThreshold / 255;
if (dx > xshort || dy > yshort)
return 1;
return 0;
}
Direction Mouse::getMouseDirectionWorld(int mx, int my) const {
RenderSurface *screen = Ultima8Engine::get_instance()->getRenderScreen();
Common::Rect32 dims = screen->getSurfaceDims();
// For now, reference point is (near) the center of the screen
int dx = mx - dims.width() / 2;
int dy = (dims.height() / 2 + 14) - my; //! constant
return Direction_Get(dy * 2, dx, dirmode_8dirs);
}
Direction Mouse::getMouseDirectionScreen(int mx, int my) const {
return Direction_OneRight(getMouseDirectionWorld(mx, my), dirmode_8dirs);
}
int Mouse::getMouseFrame() {
// Ultima 8 mouse cursors:
// 0-7 = short (0 = up, 1 = up-right, 2 = right, ...)
// 8-15 = medium
// 16-23 = long
// 24 = blue dot
// 25-32 = combat
// 33 = red dot
// 34 = target
// 35 = pentagram
// 36 = skeletal hand
// 38 = quill
// 39 = magnifying glass
// 40 = red cross
if (_cursors.empty())
return -1;
MouseCursor cursor = _cursors.top();
if (_flashingCursorTime > 0) {
if (g_system->getMillis() < _flashingCursorTime + 250)
cursor = MOUSE_CROSS;
else
_flashingCursorTime = 0;
}
switch (cursor) {
case MOUSE_NORMAL: {
if (GAME_IS_CRUSADER)
return -1;
bool combat = false;
bool combatRun = false;
const MainActor *av = getMainActor();
if (av) {
combat = av->isInCombat();
combatRun = av->hasActorFlags(Actor::ACT_COMBATRUN);
}
// Calculate frame based on direction
Direction mousedir = getMouseDirectionScreen();
int frame = mouseFrameForDir(mousedir);
/** length --- frame offset
* 0 0
* 1 8
* 2 16
* combat 25
**/
int offset = 25;
if (!combat || combatRun) //combat mouse is off if running
offset = getMouseLength() * 8;
return frame + offset;
}
//!! constants...
case MOUSE_NONE:
return -1;
case MOUSE_TARGET:
return 34;
case MOUSE_WAIT:
return 35;
case MOUSE_HAND:
return 36;
case MOUSE_QUILL:
return 38;
case MOUSE_MAGGLASS:
return 39;
case MOUSE_CROSS:
return 40;
default:
return -1;
}
}
int Mouse::mouseFrameForDir(Direction mousedir) const {
switch (mousedir) {
case dir_north: return 0;
case dir_northeast: return 1;
case dir_east: return 2;
case dir_southeast: return 3;
case dir_south: return 4;
case dir_southwest: return 5;
case dir_west: return 6;
case dir_northwest: return 7;
default: return 0;
}
}
void Mouse::setMouseCoords(int mx, int my) {
RenderSurface *screen = Ultima8Engine::get_instance()->getRenderScreen();
Common::Rect32 dims = screen->getSurfaceDims();
if (mx < dims.left)
mx = dims.left;
else if (mx >= dims.right)
mx = dims.right - 1;
if (my < dims.top)
my = dims.top;
else if (my >= dims.bottom)
my = dims.bottom - 1;
_mousePos.x = mx;
_mousePos.y = my;
Gump *desktopGump = Ultima8Engine::get_instance()->getDesktopGump();
Gump *gump = desktopGump->onMouseMotion(mx, my);
if (gump && _mouseOverGump != gump->getObjId()) {
Gump *oldGump = getGump(_mouseOverGump);
Std::list<Gump *> oldgumplist;
Std::list<Gump *> newgumplist;
// create lists of parents of old and new 'mouseover' gumps
if (oldGump) {
while (oldGump) {
oldgumplist.push_front(oldGump);
oldGump = oldGump->GetParent();
}
}
Gump *newGump = gump;
while (newGump) {
newgumplist.push_front(newGump);
newGump = newGump->GetParent();
}
Std::list<Gump *>::iterator olditer = oldgumplist.begin();
Std::list<Gump *>::iterator newiter = newgumplist.begin();
// strip common prefix from lists
while (olditer != oldgumplist.end() &&
newiter != newgumplist.end() &&
*olditer == *newiter) {
++olditer;
++newiter;
}
// send events to remaining gumps
for (; olditer != oldgumplist.end(); ++olditer)
(*olditer)->onMouseLeft();
_mouseOverGump = gump->getObjId();
for (; newiter != newgumplist.end(); ++newiter)
(*newiter)->onMouseOver();
}
if (_dragging == DRAG_NOT) {
if (_mouseButton[BUTTON_LEFT].isState(MBS_DOWN)) {
int startx = _mouseButton[BUTTON_LEFT]._downPoint.x;
int starty = _mouseButton[BUTTON_LEFT]._downPoint.y;
if (ABS(startx - mx) > 2 ||
ABS(starty - my) > 2) {
startDragging(startx, starty);
}
}
}
if (_dragging == DRAG_OK || _dragging == DRAG_TEMPFAIL) {
moveDragging(mx, my);
}
}
void Mouse::setMouseCursor(MouseCursor cursor) {
_cursors.pop();
_cursors.push(cursor);
update();
}
void Mouse::flashCrossCursor() {
_flashingCursorTime = g_system->getMillis();
}
void Mouse::pushMouseCursor(MouseCursor cursor) {
_cursors.push(cursor);
update();
}
void Mouse::popMouseCursor() {
_cursors.pop();
update();
}
void Mouse::startDragging(int startx, int starty) {
setDraggingOffset(0, 0); // initialize
Gump *desktopGump = Ultima8Engine::get_instance()->getDesktopGump();
_dragging_objId = desktopGump->TraceObjId(startx, starty);
Gump *gump = getGump(_dragging_objId);
Item *item = getItem(_dragging_objId);
// for a Gump, notify the Gump's parent that we started _dragging:
if (gump) {
debugC(kDebugObject, "Dragging gump %u (class=%s)", _dragging_objId, gump->GetClassType()._className);
Gump *parent = gump->GetParent();
assert(parent); // can't drag root gump
int32 px = startx, py = starty;
parent->ScreenSpaceToGump(px, py);
if (gump->IsDraggable() && gump->onDragStart(px, py))
_dragging = DRAG_OK;
else {
_dragging_objId = 0;
return;
}
} else if (item) {
// for an Item, notify the gump the item is in that we started _dragging
debugC(kDebugObject, "Dragging item %u (class=%s)", _dragging_objId, item->GetClassType()._className);
// find gump item was in
gump = desktopGump->FindGump(startx, starty);
int32 gx = startx, gy = starty;
gump->ScreenSpaceToGump(gx, gy);
bool ok = !Ultima8Engine::get_instance()->isAvatarInStasis() &&
gump->StartDraggingItem(item, gx, gy);
if (!ok) {
_dragging = DRAG_INVALID;
} else {
_dragging = DRAG_OK;
// this is the gump that'll get StopDraggingItem
_draggingItem_startGump = gump->getObjId();
// this is the gump the item is currently over
_draggingItem_lastGump = gump->getObjId();
}
} else {
_dragging = DRAG_INVALID;
}
pushMouseCursor(MOUSE_NORMAL);
// pause the kernel
Kernel::get_instance()->pause();
_mouseButton[BUTTON_LEFT].setState(MBS_HANDLED);
if (_dragging == DRAG_INVALID) {
setMouseCursor(MOUSE_CROSS);
}
}
void Mouse::moveDragging(int mx, int my) {
Gump *gump = getGump(_dragging_objId);
Item *item = getItem(_dragging_objId);
setMouseCursor(MOUSE_NORMAL);
if (gump) {
// for a gump, notify Gump's parent that it was dragged
Gump *parent = gump->GetParent();
assert(parent); // can't drag root gump
int32 px = mx, py = my;
parent->ScreenSpaceToGump(px, py);
gump->onDrag(px, py);
} else if (item) {
// for an item, notify the gump it's on
Gump *desktopGump = Ultima8Engine::get_instance()->getDesktopGump();
gump = desktopGump->FindGump(mx, my);
assert(gump);
if (gump->getObjId() != _draggingItem_lastGump) {
// item switched gump, so notify previous gump item left
Gump *last = getGump(_draggingItem_lastGump);
if (last) last->DraggingItemLeftGump(item);
}
_draggingItem_lastGump = gump->getObjId();
int32 gx = mx, gy = my;
gump->ScreenSpaceToGump(gx, gy);
bool ok = gump->DraggingItem(item, gx, gy);
if (!ok) {
_dragging = DRAG_TEMPFAIL;
} else {
_dragging = DRAG_OK;
}
} else {
warning("Unknown object id on mouse drag");
}
if (_dragging == DRAG_TEMPFAIL) {
setMouseCursor(MOUSE_CROSS);
}
}
void Mouse::stopDragging(int mx, int my) {
debugC(kDebugObject, "Dropping object %u", _dragging_objId);
Gump *gump = getGump(_dragging_objId);
Item *item = getItem(_dragging_objId);
// for a Gump: notify parent
if (gump) {
Gump *parent = gump->GetParent();
assert(parent); // can't drag root gump
int32 px = mx, py = my;
parent->ScreenSpaceToGump(px, py);
gump->onDragStop(px, py);
} else if (item) {
// for an item: notify gumps
if (_dragging != DRAG_INVALID) {
Gump *startgump = getGump(_draggingItem_startGump);
assert(startgump); // can't have disappeared
bool moved = (_dragging == DRAG_OK);
if (_dragging != DRAG_OK) {
Gump *last = getGump(_draggingItem_lastGump);
if (last && last != startgump)
last->DraggingItemLeftGump(item);
}
startgump->StopDraggingItem(item, moved);
}
if (_dragging == DRAG_OK) {
item->movedByPlayer();
Gump *desktopGump = Ultima8Engine::get_instance()->getDesktopGump();
gump = desktopGump->FindGump(mx, my);
int32 gx = mx, gy = my;
gump->ScreenSpaceToGump(gx, gy);
gump->DropItem(item, gx, gy);
}
} else {
assert(_dragging == DRAG_INVALID);
}
_dragging = DRAG_NOT;
Kernel::get_instance()->unpause();
popMouseCursor();
}
uint32 Mouse::getDoubleClickTime() const {
uint32 timeout = g_system->getDoubleClickTime();
return timeout > 0 ? timeout : 400;
}
void Mouse::handleDelayedEvents() {
uint32 now = g_system->getMillis();
uint32 timeout = getDoubleClickTime();
for (int button = 0; button < MOUSE_LAST; ++button) {
if (!_mouseButton[button].isState(MBS_DOWN) &&
_mouseButton[button].isUnhandledPastTimeout(now, timeout)) {
Gump *gump = getGump(_mouseButton[button]._downGump);
if (gump) {
int32 mx = _mouseButton[button]._downPoint.x;
int32 my = _mouseButton[button]._downPoint.y;
Gump *parent = gump->GetParent();
if (parent)
parent->ScreenSpaceToGump(mx, my);
gump->onMouseClick(button, mx, my);
}
_mouseButton[button]._downGump = 0;
_mouseButton[button].setState(MBS_HANDLED);
}
}
}
Gump *Mouse::getMouseOverGump() const {
return getGump(_mouseOverGump);
}
void Mouse::update() {
GameData *gamedata = GameData::get_instance();
if (!gamedata)
return;
const Shape *mouse = gamedata->getMouse();
if (mouse) {
int frame = getMouseFrame();
if (frame != _lastMouseFrame) {
_lastMouseFrame = frame;
if (frame >= 0 && (uint)frame < mouse->frameCount()) {
const ShapeFrame *f = mouse->getFrame(frame);
CursorMan.replaceCursor(f->getSurface(), f->_xoff, f->_yoff, f->_keycolor);
CursorMan.replaceCursorPalette(mouse->getPalette()->data(), 0, 256);
CursorMan.showMouse(true);
} else {
CursorMan.showMouse(false);
}
}
}
}
} // End of namespace Ultima8
} // End of namespace Ultima