/* 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 . * */ #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(ConfMan.getInt("walk_threshold"), 0, 255); _runThreshold = CLIP(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 oldgumplist; Std::list 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::iterator olditer = oldgumplist.begin(); Std::list::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