/* 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 "ultima/ultima.h" #include "ultima/ultima8/misc/debugger.h" #include "ultima/ultima8/world/current_map.h" #include "ultima/ultima8/world/map.h" #include "ultima/ultima8/world/actors/actor.h" #include "ultima/ultima8/world/world.h" #include "ultima/ultima8/world/world_point.h" #include "ultima/ultima8/world/coord_utils.h" #include "ultima/ultima8/usecode/uc_list.h" #include "ultima/ultima8/usecode/uc_machine.h" #include "ultima/ultima8/world/teleport_egg.h" #include "ultima/ultima8/world/egg_hatcher_process.h" #include "ultima/ultima8/kernel/kernel.h" #include "ultima/ultima8/games/game_data.h" #include "ultima/ultima8/gfx/main_shape_archive.h" #include "ultima/ultima8/gumps/game_map_gump.h" #include "ultima/ultima8/misc/box.h" #include "ultima/ultima8/misc/direction_util.h" #include "ultima/ultima8/world/get_object.h" // Uncomment to check that a single object doesn't appear in multiple chunks // during updates //#define VALIDATE_CHUNKS 1 namespace Ultima { namespace Ultima8 { typedef Std::list item_list; const int INT_MAX_VALUE = 0x7fffffff; const int INT_MIN_VALUE = -INT_MAX_VALUE - 1; CurrentMap::CurrentMap() : _currentMap(0), _eggHatcher(0), _fastXMin(-1), _fastYMin(-1), _fastXMax(-1), _fastYMax(-1) { for (unsigned int i = 0; i < MAP_NUM_CHUNKS; i++) { memset(_fast[i], false, sizeof(uint32)*MAP_NUM_CHUNKS / 32); } if (GAME_IS_U8) { _mapChunkSize = 512; } else if (GAME_IS_CRUSADER) { _mapChunkSize = 1024; } else { warning("Unknown game type in CurrentMap constructor."); } for (unsigned int i = 0; i < MAP_NUM_TARGET_ITEMS; i++) { _targets[i] = 0; } } CurrentMap::~CurrentMap() { // clear(); } void CurrentMap::clear() { for (unsigned int i = 0; i < MAP_NUM_CHUNKS; i++) { for (unsigned int j = 0; j < MAP_NUM_CHUNKS; j++) { for (auto *item : _items[i][j]) delete item; _items[i][j].clear(); } memset(_fast[i], false, sizeof(uint32)*MAP_NUM_CHUNKS / 32); } _fastXMin = _fastYMin = _fastXMax = _fastYMax = -1; _currentMap = nullptr; Process *ehp = Kernel::get_instance()->getProcess(_eggHatcher); if (ehp) ehp->terminate(); _eggHatcher = 0; } uint32 CurrentMap::getNum() const { if (_currentMap == nullptr) return 0; return _currentMap->_mapNum; } void CurrentMap::createEggHatcher() { // get rid of old one, if any Process *ehp = Kernel::get_instance()->getProcess(_eggHatcher); if (ehp) ehp->terminate(); ehp = new EggHatcherProcess(); _eggHatcher = Kernel::get_instance()->addProcess(ehp); } void CurrentMap::writeback() { if (!_currentMap) return; for (unsigned int i = 0; i < MAP_NUM_CHUNKS; i++) { for (unsigned int j = 0; j < MAP_NUM_CHUNKS; j++) { for (auto *item : _items[i][j]) { // item is being removed from the CurrentMap item lists item->clearExtFlag(Item::EXT_INCURMAP); // delete all fast only and disposable _items if (item->hasFlags(Item::FLG_FAST_ONLY | Item::FLG_DISPOSABLE)) { delete item; continue; } // Reset the egg Egg *egg = dynamic_cast(item); if (egg) { egg->reset(); } // this item isn't from the Map. (like NPCs) if (item->hasFlags(Item::FLG_IN_NPC_LIST)) continue; item->clearObjId(); if (item->hasExtFlags(Item::EXT_FIXED)) { // item came from fixed _currentMap->_fixedItems.push_back(item); } else { _currentMap->_dynamicItems.push_back(item); } } _items[i][j].clear(); } } // delete _eggHatcher Process *ehp = Kernel::get_instance()->getProcess(_eggHatcher); if (ehp) ehp->terminate(); _eggHatcher = 0; } void CurrentMap::loadItems(const Std::list &itemlist, bool callCacheIn) { for (auto *item : itemlist) { item->assignObjId(); // No fast area for you! item->clearFlag(Item::FLG_FASTAREA); // add item to internal object list addItemToEnd(item); if (callCacheIn) item->callUsecodeEvent_cachein(); } } void CurrentMap::loadMap(Map *map) { // Don't call the cachein events at startup or when loading a savegame // in u8. Always call them in Crusader. // TODO: This may not work for loading games in Crusader - need to check. bool callCacheIn = (_currentMap != nullptr || GAME_IS_CRUSADER); _currentMap = map; createEggHatcher(); // Clear fast area for (unsigned int i = 0; i < MAP_NUM_CHUNKS; i++) { memset(_fast[i], false, sizeof(uint32)*MAP_NUM_CHUNKS / 32); } _fastXMin = -1; _fastYMin = -1; _fastXMax = -1; _fastYMax = -1; // Clear target items for (unsigned int i = 0; i < MAP_NUM_TARGET_ITEMS; i++) { _targets[i] = 0; } loadItems(map->_fixedItems, callCacheIn); loadItems(map->_dynamicItems, callCacheIn); // we take control of the items in map, so clear the pointers map->_fixedItems.clear(); map->_dynamicItems.clear(); // load relevant NPCs to the item lists // !constant for (uint16 i = 0; i < 256; ++i) { Actor *actor = getActor(i); if (!actor) continue; // Schedule // CHECKME: is this the right time to pass? if (callCacheIn) actor->schedule(Ultima8Engine::get_instance()->getGameTimeInSeconds() / 60); if (actor->getMapNum() == getNum()) { addItemToEnd(actor); // the avatar's cachein function is very strange in U8; disabled for now if (callCacheIn && GAME_IS_CRUSADER) actor->callUsecodeEvent_cachein(); } } } void CurrentMap::addItem(Item *item) { Point3 pt = item->getLocation(); if (pt.x < 0 || pt.x >= _mapChunkSize * MAP_NUM_CHUNKS || pt.y < 0 || pt.y >= _mapChunkSize * MAP_NUM_CHUNKS) { //warning("Skipping item %u: out of range (%d, %d)", item->getObjId(), pt.x, pt.y); return; } int32 cx = pt.x / _mapChunkSize; int32 cy = pt.y / _mapChunkSize; #ifdef VALIDATE_CHUNKS for (int32 ccy = 0; ccy < MAP_NUM_CHUNKS; ccy++) { for (int32 ccx = 0; ccx < MAP_NUM_CHUNKS; ccx++) { for (const auto *existing : _items[ccx][ccy]) if (existing == item) { warning("item %d already exists in map chunk (%d, %d)", item->getObjId(), ccx, ccy); } } } } #endif _items[cx][cy].push_front(item); item->setExtFlag(Item::EXT_INCURMAP); Egg *egg = dynamic_cast(item); if (egg) { EggHatcherProcess *ehp = dynamic_cast(Kernel::get_instance()->getProcess(_eggHatcher)); assert(ehp); ehp->addEgg(egg); } } void CurrentMap::addItemToEnd(Item *item) { Point3 pt = item->getLocation(); if (pt.x < 0 || pt.x >= _mapChunkSize * MAP_NUM_CHUNKS || pt.y < 0 || pt.y >= _mapChunkSize * MAP_NUM_CHUNKS) { //warning("Skipping item %u: out of range (%d, %d)", item->getObjId(), pt.x, pt.y); return; } int32 cx = pt.x / _mapChunkSize; int32 cy = pt.y / _mapChunkSize; #ifdef VALIDATE_CHUNKS for (int32 ccy = 0; ccy < MAP_NUM_CHUNKS; ccy++) { for (int32 ccx = 0; ccx < MAP_NUM_CHUNKS; ccx++) { for (const auto *existing : _items[ccx][ccy]) if (existing == item) { warning("item %d already exists in map chunk (%d, %d)", item->getObjId(), ccx, ccy); } } } } #endif _items[cx][cy].push_back(item); item->setExtFlag(Item::EXT_INCURMAP); Egg *egg = dynamic_cast(item); if (egg) { EggHatcherProcess *ehp = dynamic_cast(Kernel::get_instance()->getProcess(_eggHatcher)); assert(ehp); ehp->addEgg(egg); } } void CurrentMap::removeItem(Item *item) { Point3 pt = item->getLocation(); removeItemFromList(item, pt.x, pt.y); } void CurrentMap::addTargetItem(const Item *item) { assert(item); // The game also maintains a count of non-zero targets, but it // seems to serve little purpose so we just update. ObjId id = item->getObjId(); for (int i = 0; i < MAP_NUM_TARGET_ITEMS; i++) { if (_targets[i] == 0) { _targets[i] = id; return; } } } void CurrentMap::removeTargetItem(const Item *item) { assert(item); ObjId id = item->getObjId(); for (int i = 0; i < MAP_NUM_TARGET_ITEMS; i++) { if (_targets[i] == id) { _targets[i] = 0; return; } } } Item *CurrentMap::findBestTargetItem(int32 x, int32 y, int32 z, Direction dir, DirectionMode dirmode) { // "best" means: // Shape info SI_OCCL // isNPC // Closest // in that order. bool bestisnpc = false; bool bestisoccl = false; Item *bestitem = nullptr; int bestdist = 0xffff; const uint16 controllednpc = World::get_instance()->getControlledNPCNum(); for (int i = 0; i < MAP_NUM_TARGET_ITEMS; i++) { if (_targets[i] == 0 || _targets[i] == controllednpc) continue; Item *item = getItem(_targets[i]); // FIXME: this should probably always be non-null, // but if it's not the item disappeared - remove it from the list. // If we fix this, this function can be const. if (!item) { _targets[i] = 0; continue; } if (item->hasFlags(Item::FLG_BROKEN)) continue; const ShapeInfo *si = item->getShapeInfo(); bool isoccl = si->_flags & ShapeInfo::SI_OCCL; Point3 pt = item->getLocation(); Direction itemdir = Direction_GetWorldDir(pt.y - y, pt.x - x, dirmode); if (itemdir != dir) continue; const Actor *actor = dynamic_cast(item); if ((bestisoccl && !isoccl) || (bestisnpc && !actor) || !item->isPartlyOnScreen()) continue; int xdiff = abs(x - pt.x); int ydiff = abs(y - pt.y); int zdiff = abs(z - pt.z); int dist = MAX(MAX(xdiff, ydiff), zdiff); if (dist < bestdist) { bestitem = item; bestdist = dist; bestisoccl = isoccl; bestisnpc = (actor != nullptr); } } return bestitem; } void CurrentMap::removeItemFromList(Item *item, int32 oldx, int32 oldy) { //! This might a bit too inefficient // if it's really a problem we could change the item lists into sets // or something, but let's see how it turns out if (oldx < 0 || oldx >= _mapChunkSize * MAP_NUM_CHUNKS || oldy < 0 || oldy >= _mapChunkSize * MAP_NUM_CHUNKS) { //warning("Skipping item %u: out of range (%d, %d)", item->getObjId(), oldx, oldy); return; } int32 cx = oldx / _mapChunkSize; int32 cy = oldy / _mapChunkSize; _items[cx][cy].remove(item); item->clearExtFlag(Item::EXT_INCURMAP); } // Check to see if the chunk is on the screen static inline bool ChunkOnScreen(int32 cx, int32 cy, int32 sleft, int32 stop, int32 sright, int32 sbot, int mapChunkSize) { int32 scx = (cx * mapChunkSize - cy * mapChunkSize) / 4; int32 scy = (cx * mapChunkSize + cy * mapChunkSize) / 8; // Screenspace bounding box left extent (LNT x coord) int32 cxleft = scx - mapChunkSize / 4; // Screenspace bounding box right extent (RFT x coord) int32 cxright = scx + mapChunkSize / 4; // Screenspace bounding box top extent (LFT y coord) int32 cytop = scy - mapChunkSize / 2; // Screenspace bounding box bottom extent (RNB y coord) int32 cybot = scy + mapChunkSize / 4; const bool right_clear = cxright <= sleft; const bool left_clear = cxleft >= sright; const bool top_clear = cytop >= sbot; const bool bot_clear = cybot <= stop; const bool clear = right_clear || left_clear || top_clear || bot_clear; return !clear; } static inline void CalcFastAreaLimits(int32 &sx_limit, int32 &sy_limit, int32 &xy_limit, const Common::Rect32 &dims, int mapChunkSize) { // By default the fastArea is the screensize rounded down to the nearest // map chunk, plus 3 wide and 7 high. // In the original games, the fast area is +/- 3,7 in U8 // map chunks and +/- 3,5 for Cruasder. We have to do it a // bit differently because the screen size is adjustable. sx_limit = dims.width() / (mapChunkSize / 2) + 3; sy_limit = dims.height() / (mapChunkSize / 4) + 7; xy_limit = (sy_limit + sx_limit) / 2; } void CurrentMap::updateFastArea(const Point3 &from, const Point3 &to) { int x_min = MIN(from.x, to.x); int x_max = MAX(from.x, to.x); int y_min = MIN(from.y, to.y); int y_max = MAX(from.y, to.y); int z_min = MIN(from.z, to.z); int z_max = MAX(from.z, to.z); // Work out Fine (screenspace) Limits of chunks with half chunk border Common::Rect32 dims = Ultima8Engine::get_instance()->getGameMapGump()->getDims(); int32 sleft = ((x_min - y_min) / 4) - (dims.width() / 2 + _mapChunkSize / 4); int32 stop = ((x_min + y_min) / 8 - z_max) - (dims.height() / 2 + _mapChunkSize / 8); int32 sright = ((x_max - y_max) / 4) + (dims.width() / 2 + _mapChunkSize / 4); int32 sbot = ((x_max + y_max) / 8 - z_min) + (dims.height() / 2 + _mapChunkSize / 8); // Don't do anything IF the regions are the same if (_fastXMin == sleft && _fastYMin == stop && _fastXMax == sright && _fastYMax == sbot) return; // Update the saved region _fastXMin = sleft; _fastYMin = stop; _fastXMax = sright; _fastYMax = sbot; // Get Coarse Limits int32 sx_limit; int32 sy_limit; int32 xy_limit; CalcFastAreaLimits(sx_limit, sy_limit, xy_limit, dims, _mapChunkSize); x_min = x_min / _mapChunkSize - xy_limit; x_max = x_max / _mapChunkSize + xy_limit; y_min = y_min / _mapChunkSize - xy_limit; y_max = y_max / _mapChunkSize + xy_limit; for (int32 cy = 0; cy < MAP_NUM_CHUNKS; cy++) { for (int32 cx = 0; cx < MAP_NUM_CHUNKS; cx++) { // Coarse bool want_fast = cx >= x_min && cx <= x_max && cy >= y_min && cy <= y_max; // Fine if (want_fast) want_fast = ChunkOnScreen(cx, cy, sleft, stop, sright, sbot, _mapChunkSize); bool currently_fast = isChunkFast(cx, cy); // Don't do anything, they are the same if (want_fast == currently_fast) continue; // leave fast area if (!want_fast) unsetChunkFast(cx, cy); // Enter fast area else setChunkFast(cx, cy); } } } void CurrentMap::setChunkFast(int32 cx, int32 cy) { _fast[cy][cx / 32] |= 1 << (cx & 31); for (auto *item : _items[cx][cy]) { item->enterFastArea(); } } void CurrentMap::unsetChunkFast(int32 cx, int32 cy) { _fast[cy][cx / 32] &= ~(1 << (cx & 31)); item_list::iterator iter = _items[cx][cy].begin(); while (iter != _items[cx][cy].end()) { Item *item = *iter; ++iter; #ifdef VALIDATE_CHUNKS int32 x, y, z; item->getLocation(x, y, z); if (x / _mapChunkSize != cx || y / _mapChunkSize != cy) { warning("Item leaving fast area in chunk (%d, %d), should be (%d, %d)", cx, cy, x / _mapChunkSize, y / _mapChunkSize); } #endif item->leaveFastArea(); // Can destroy the item } } inline void CurrentMap::clipMapChunks(int &minx, int &maxx, int &miny, int &maxy) { minx = CLIP(minx, 0, MAP_NUM_CHUNKS - 1); maxx = CLIP(maxx, 0, MAP_NUM_CHUNKS - 1); miny = CLIP(miny, 0, MAP_NUM_CHUNKS - 1); maxy = CLIP(maxy, 0, MAP_NUM_CHUNKS - 1); } void CurrentMap::areaSearch(UCList *itemlist, const uint8 *loopscript, uint32 scriptsize, const Item *check, uint16 range, bool recurse, int32 x, int32 y) const { int32 xd = 0, yd = 0; // if item != 0, search an area around item. Otherwise, search an area // around (x,y) if (check) { int32 zd; Point3 pt = check->getLocationAbsolute(); x = pt.x; y = pt.y; check->getFootpadWorld(xd, yd, zd); } // // Original games consider items at <= range to match. Add a pixel // to range to match that behavior (eg, KEYPAD in Crusader : No Remorse // Mission 12 (map 19) at (17118,34878) uses range 6400 and a VALBOX // exactly 6400 from the KEYPAD. // // box size is negative in x and y, so range is from x+range to x-range. // const Box searchrange(x + range, y + range, 0, xd + range * 2 + 1, yd + range * 2 + 1, INT_MAX_VALUE); int minx = ((x - xd - range) / _mapChunkSize) - 1; int maxx = ((x + range) / _mapChunkSize) + 1; int miny = ((y - yd - range) / _mapChunkSize) - 1; int maxy = ((y + range) / _mapChunkSize) + 1; clipMapChunks(minx, maxx, miny, maxy); // // NOTE: Iteration order of chunks here is important for // usecode compatibility! // // eg, No Remorse Mission 2 has a camera which searches for // trigger with qlo = 5, but there are 2 of those on the map. // We need to return in the correct order or it triggers // the wrong one and breaks the game. // for (int cy = miny; cy <= maxy; cy++) { for (int cx = minx; cx <= maxx; cx++) { for (const auto *item : _items[cx][cy]) { if (item->hasExtFlags(Item::EXT_SPRITE)) continue; // check if item is in range Point3 pt = item->getLocation(); if (searchrange.containsXY(pt.x, pt.y)) { // check item against loopscript if (item->checkLoopScript(loopscript, scriptsize)) { assert(itemlist->getElementSize() == 2); itemlist->appenduint16(item->getObjId()); } if (recurse) { // recurse into child-containers const Container *container = dynamic_cast(item); if (container) container->containerSearch(itemlist, loopscript, scriptsize, recurse); } } } } } } void CurrentMap::surfaceSearch(UCList *itemlist, const uint8 *loopscript, uint32 scriptsize, const Item *check, bool above, bool below, bool recurse) const { int32 xd, yd, zd; Point3 pt = check->getLocationAbsolute(); check->getFootpadWorld(xd, yd, zd); const Box searchrange(pt.x, pt.y, pt.z, xd, yd, zd); int minx = ((pt.x - xd) / _mapChunkSize) - 1; int maxx = (pt.x / _mapChunkSize) + 1; int miny = ((pt.y - yd) / _mapChunkSize) - 1; int maxy = (pt.y / _mapChunkSize) + 1; clipMapChunks(minx, maxx, miny, maxy); for (int cy = miny; cy <= maxy; cy++) { for (int cx = minx; cx <= maxx; cx++) { for (const auto *item : _items[cx][cy]) { if (item->getObjId() == check->getObjId()) continue; if (item->hasExtFlags(Item::EXT_SPRITE)) continue; // check if item is in range? const Box ib = item->getWorldBox(); if (searchrange.overlapsXY(ib)) { bool ok = false; if (above && ib._z == (searchrange._z + searchrange._zd)) { ok = true; // Only recursive if tops aren't same (i.e. NOT flat) if (recurse && (ib._zd + ib._z != searchrange._z + searchrange._zd)) surfaceSearch(itemlist, loopscript, scriptsize, item, true, false, true); } if (below && searchrange._z == (ib._z + ib._zd)) { ok = true; // Only recursive if bottoms aren't same (i.e. NOT flat) if (recurse && (ib._z != searchrange._z)) surfaceSearch(itemlist, loopscript, scriptsize, item, false, true, true); } if (ok) { // check item against loopscript if (item->checkLoopScript(loopscript, scriptsize)) { assert(itemlist->getElementSize() == 2); itemlist->appenduint16(item->getObjId()); } } } } } } } TeleportEgg *CurrentMap::findDestination(uint16 id) { for (unsigned int i = 0; i < MAP_NUM_CHUNKS; i++) { for (unsigned int j = 0; j < MAP_NUM_CHUNKS; j++) { for (auto *item : _items[i][j]) { TeleportEgg *egg = dynamic_cast(item); if (egg) { if (!egg->isTeleporter() && egg->getTeleportId() == id) return egg; } } } } return nullptr; } const Std::list *CurrentMap::getItemList(int32 gx, int32 gy) const { if (gx < 0 || gy < 0 || gx >= MAP_NUM_CHUNKS || gy >= MAP_NUM_CHUNKS) return nullptr; return &_items[gx][gy]; } PositionInfo CurrentMap::getPositionInfo(int32 x, int32 y, int32 z, uint32 shape, ObjId id) const { const ShapeInfo *si = GameData::get_instance()->getMainShapes()->getShapeInfo(shape); int32 xd, yd, zd; // Note: this assumes the shape to be placed is not flipped si->getFootpadWorld(xd, yd, zd, 0); Box target(x, y, z, xd, yd, zd); Box empty; return getPositionInfo(target, empty, si->_flags, id); } PositionInfo CurrentMap::getPositionInfo(const Box &target, const Box &start, uint32 shapeflags, ObjId id) const { PositionInfo info; static const uint32 flagmask = (ShapeInfo::SI_SOLID | ShapeInfo::SI_DAMAGING | ShapeInfo::SI_LAND | ShapeInfo::SI_ROOF); static const uint32 supportmask = (ShapeInfo::SI_SOLID | ShapeInfo::SI_LAND | ShapeInfo::SI_ROOF); static const uint32 landmask = (ShapeInfo::SI_LAND | ShapeInfo::SI_ROOF); static const uint32 blockmask = (ShapeInfo::SI_SOLID | ShapeInfo::SI_DAMAGING); int32 supportz = INT_MIN_VALUE; int32 landz = INT_MIN_VALUE; int32 roofz = INT_MAX_VALUE; int32 midx = target._x - target._xd / 2; int32 midy = target._y - target._yd / 2; int minx = ((target._x - target._xd) / _mapChunkSize) - 1; int maxx = (target._x / _mapChunkSize) + 1; int miny = ((target._y - target._yd) / _mapChunkSize) - 1; int maxy = (target._y / _mapChunkSize) + 1; clipMapChunks(minx, maxx, miny, maxy); for (int cx = minx; cx <= maxx; cx++) { for (int cy = miny; cy <= maxy; cy++) { for (const auto *item : _items[cx][cy]) { if (item->getObjId() == id) continue; if (item->hasExtFlags(Item::EXT_SPRITE)) continue; const ShapeInfo *si = item->getShapeInfo(); if (!(si->_flags & flagmask)) continue; // not an interesting item Box ib = item->getWorldBox(); // check overlap if ((si->_flags & shapeflags & blockmask) && target.overlaps(ib) && !start.overlaps(ib)) { // overlapping an item. Invalid position #if 0 debugC(kDebugObject, "%s", item->dumpInfo().c_str()); #endif if (info.blocker == nullptr) { info.blocker = item; } } if (target.overlapsXY(ib)) { // check support if (si->_flags & supportmask && ib._z + ib._zd > supportz && ib._z + ib._zd <= target._z) { supportz = ib._z + ib._zd; } // check roof if (si->is_roof() && ib._z < roofz && ib._z >= target._z + target._zd) { info.roof = item; roofz = ib._z; } } // check bottom center if (ib.isBelow(midx, midy, target._z)) { // check land if (si->_flags & landmask && ib._z + ib._zd > landz) { info.land = item; landz = ib._z + ib._zd; } } } } } info.valid = info.blocker == nullptr; // Partial support allowed if land is close. Allow up to 8 to match the // position adjustments in scanForValidPosition. In Crusader, we don't // require land - just support. if (supportz == target._z && (landz + 8 >= target._z || GAME_IS_CRUSADER)) info.supported = true; // Mark supported at minimum z if (target._z <= 0) info.supported = true; return info; } bool CurrentMap::scanForValidPosition(int32 x, int32 y, int32 z, const Item *item, Direction movedir, bool wantsupport, int32 &tx, int32 &ty, int32 &tz) { // TODO: clean this up. Currently the mask arrays are filled with more // data than is actually used. const uint32 blockflagmask = (ShapeInfo::SI_SOLID | ShapeInfo::SI_DAMAGING) & item->getShapeInfo()->_flags; if (!blockflagmask) warning("scanForValidPosition non-solid object, should be valid anywhere"); Direction searchdir = static_cast(((int)movedir + 4) % 8); bool xdir = (Direction_XFactor(searchdir) != 0); bool ydir = (Direction_YFactor(searchdir) != 0); int searchtype = ((int)searchdir / 2); // The number of grid points on each side of x/y/z to scan. const int scansize = GAME_IS_CRUSADER ? 10 : 8; // Mark everything as valid, but without support. We only use SCANSIZE * 2 + 1 bits of the mask // but fill them all for simplicity. Mask arrays are bigger than needed for either game. uint32 validmask[24]; uint32 supportmask[24]; for (int i = 0; i < scansize * 2 + 1; ++i) { validmask[i] = 0xFFFFFFFF; supportmask[i] = 0; } int32 xd, yd, zd; item->getFootpadWorld(xd, yd, zd); // Note on scan direction: // The 'horiz' variable below will always mean a direction in // the positive x/y directions, with the exception of searchtype 1, // in which case positive horiz points in the (positive x, negative y) // direction. // next, we'll loop over all objects in the area, and mark the areas // overlapped and supported by each object int minx = ((x - xd) / _mapChunkSize) - 1; int maxx = (x / _mapChunkSize) + 1; int miny = ((y - yd) / _mapChunkSize) - 1; int maxy = (y / _mapChunkSize) + 1; clipMapChunks(minx, maxx, miny, maxy); for (int cx = minx; cx <= maxx; cx++) { for (int cy = miny; cy <= maxy; cy++) { for (const auto *citem : _items[cx][cy]) { if (citem->getObjId() == item->getObjId()) continue; if (citem->hasExtFlags(Item::EXT_SPRITE)) continue; const ShapeInfo *si = citem->getShapeInfo(); //!! need to check is_sea() and is_land() maybe? if (!(si->_flags & blockflagmask)) continue; // not an interesting item int32 ixd, iyd, izd; Point3 pt = citem->getLocation(); citem->getFootpadWorld(ixd, iyd, izd); int minv = pt.z - z - zd + 1; int maxv = pt.z + izd - z - 1; if (minv < -scansize) minv = -scansize; if (maxv > scansize) maxv = scansize; int sminx = pt.x - ixd + 1 - x; int smaxx = pt.x + xd - 1 - x; int sminy = pt.y - iyd + 1 - y; int smaxy = pt.y + yd - 1 - y; int minh = -100; int maxh = 100; if (!xdir && (sminx > 0 || smaxx < 0)) continue; if (!ydir && (sminy > 0 || smaxy < 0)) continue; if (xdir && minh < sminx) minh = sminx; if (xdir && maxh > smaxx) maxh = smaxx; if ((ydir && searchtype != 1) && minh < sminy) minh = sminy; if ((ydir && searchtype != 1) && maxh > smaxy) maxh = smaxy; if (searchtype == 1 && minh < -smaxy) minh = -smaxy; if (searchtype == 1 && maxh > -sminy) maxh = -sminy; if (minh < -scansize) minh = -scansize; if (maxh > scansize) maxh = scansize; for (int j = minv; j <= maxv; ++j) for (int i = minh; i <= maxh; ++i) validmask[j + scansize] &= ~(1 << (i + scansize)); if (wantsupport && si->is_solid() && pt.z + izd >= z - scansize && pt.z + izd <= z + scansize) { for (int i = minh; i <= maxh; ++i) supportmask[pt.z + izd - z + scansize] |= (1 << (i + scansize)); } } } } bool foundunsupported = false; #if 0 debugC(kDebugCollision, "valid | support") for (unsigned int i = 0; i < SCANSIZE * 2 + 1; ++i) { debugC(kDebugCollision, "%05x | %05x", validmask[SCANSIZE * 2 - i], supportmask[SCANSIZE * 2 - i]); } debugC(kDebugCollision, ("-----------"); #endif // // Crusader also has some floor pieces and elevators which are 1 z-pixel offset, // presumably to fix render-order issues. We need to be able to step up/down // to them smoothly, so check 1 px either side of the step sizes as well. Also // check +/- 2 px which can happen on walkways // static const int SEARCH_OFFSETS_U8[] = {0, -4, 4, -8, 8}; static const int SEARCH_OFFSETS_CRU[] = {0, -1, 1, -2, 2, -3, 3, -4, 4, -5, 5, -7, 7, -8, 8, -9, 9}; const int *search_offsets = GAME_IS_CRUSADER ? SEARCH_OFFSETS_CRU : SEARCH_OFFSETS_U8; const unsigned int nhoriz = GAME_IS_CRUSADER ? 11 : 3; const unsigned int nvert = GAME_IS_CRUSADER ? ARRAYSIZE(SEARCH_OFFSETS_CRU) : ARRAYSIZE(SEARCH_OFFSETS_U8); for (unsigned int i = 0; i < nhoriz; ++i) { const int horiz = search_offsets[i]; for (unsigned int j = 0; j < nvert; ++j) { const int vert = search_offsets[j]; const uint32 checkbit = (1 << (horiz + scansize)); if (validmask[vert + scansize] & checkbit) { if (!wantsupport || !foundunsupported || (supportmask[vert + scansize] & checkbit)) { tz = z + vert; tx = x; if (searchtype != 0) tx += horiz; ty = y; if (searchtype == 1) ty -= horiz; else if (searchtype != 2) ty += horiz; } if (!wantsupport || (supportmask[vert + scansize] & checkbit)) return true; foundunsupported = true; } } } // no supported location found, so return unsupported unblocked one if (foundunsupported) return true; return false; } // Do a sweepTest of an item from start to end point. // dims is the bounding box size. // item is the item that we are checking to move // blocking_only forces us to check against blocking items only. // Returns item hit or 0 if no hit. // end is set to the colision point bool CurrentMap::sweepTest(const Point3 &start, const Point3 &end, const int32 dims[3], uint32 shapeflags, ObjId item, bool blocking_only, Std::list *hit) const { const uint32 blockflagmask = (ShapeInfo::SI_SOLID | ShapeInfo::SI_DAMAGING | ShapeInfo::SI_LAND); int minx = ((start.x - dims[0]) / _mapChunkSize) - 1; int maxx = (start.x / _mapChunkSize) + 1; int miny = ((start.y - dims[1]) / _mapChunkSize) - 1; int maxy = (start.y / _mapChunkSize) + 1; { int dminx = ((end.x - dims[0]) / _mapChunkSize) - 1; int dmaxx = (end.x / _mapChunkSize) + 1; int dminy = ((end.y - dims[1]) / _mapChunkSize) - 1; int dmaxy = (end.y / _mapChunkSize) + 1; if (dminx < minx) minx = dminx; if (dmaxx > maxx) maxx = dmaxx; if (dminy < miny) miny = dminy; if (dmaxy > maxy) maxy = dmaxy; } clipMapChunks(minx, maxx, miny, maxy); // Get velocity, extents, and centre of item int32 vel[3]; int32 ext[3]; Point3 centre; vel[0] = end.x - start.x; vel[1] = end.y - start.y; vel[2] = end.z - start.z; ext[0] = dims[0] / 2; ext[1] = dims[1] / 2; ext[2] = dims[2] / 2; centre.x = start.x - ext[0]; centre.y = start.y - ext[1]; // Z is opposite direction to x and y.. centre.z = start.z + ext[2]; debugC(kDebugCollision, "Sweeping from (%d, %d, %d) - (%d, %d, %d) to (%d, %d, %d) - (%d, %d, %d)", -ext[0], -ext[1], -ext[2], ext[0], ext[1], ext[2], vel[0] - ext[0], vel[1] - ext[1], vel[2] - ext[2], vel[0] + ext[0], vel[1] + ext[1], vel[2] + ext[2]); Std::list::iterator sw_it; if (hit) sw_it = hit->end(); for (int cx = minx; cx <= maxx; cx++) { for (int cy = miny; cy <= maxy; cy++) { for (const auto *other_item : _items[cx][cy]) { if (other_item->getObjId() == item) continue; if (other_item->hasExtFlags(Item::EXT_SPRITE)) continue; uint32 othershapeflags = other_item->getShapeInfo()->_flags; bool blocking = (othershapeflags & shapeflags & blockflagmask) != 0; // This WILL hit everything and return them unless // blocking_only is set if (blocking_only && !blocking) continue; int32 other[3], oext[3]; Point3 opt = other_item->getLocation(); other[0] = opt.x; other[1] = opt.y; other[2] = opt.z; other_item->getFootpadWorld(oext[0], oext[1], oext[2]); // If the objects overlapped at the start, ignore collision. // The -1 and +1 portions are to still consider collisions // for items which were merely touching at the start for all // intents and purposes, but partially overlapped due to an // off-by-one error (hypothetically, but they do happen so // protect against it). if ( /* not non-overlapping start position */ !(start.x <= other[0] - (oext[0] - 1) || start.x - dims[0] >= other[0] - 1 || start.y <= other[1] - (oext[1] - 1) || start.y - dims[1] >= other[1] - 1 || start.z + dims[2] <= other[2] + 1 || start.z >= other[2] + (oext[2] - 1))) { // Overlapped at the start, and not just touching so // ignore collision continue; } // Make oext the distance to midpoint in each dim oext[0] /= 2; oext[1] /= 2; oext[2] /= 2; // Put other into our coord frame other[0] -= oext[0] + centre.x; other[1] -= oext[1] + centre.y; other[2] += oext[2] - centre.z; //first times of overlap along each axis int32 u_1[3] = {0, 0, 0}; //last times of overlap along each axis int32 u_0[3] = {0x4000, 0x4000, 0x4000}; // CONSTANTS bool touch = false; bool touch_floor = false; //find the possible first and last times //of overlap along each axis for (int i = 0 ; i < 3; i++) { int32 A_max = ext[i]; int32 A_min = -ext[i]; int32 B_max = other[i] + oext[i]; int32 B_min = other[i] - oext[i]; if (vel[i] < 0 && A_max >= B_min) { // A_max>=B_min not required // Special case: if moving item has zero height and // other item is a 128x128 flat, then moving item is // considered blocked by the flat // FIXME: it might be better to make this extra check // identical to the 'flats' special case in ItemSorter if (A_max == B_min && ! (i == 2 && ext[i] == 0 && oext[i] == 0 && oext[0] == 64 && oext[1] == 64)) touch = true; // touch at start if (A_min + vel[i] == B_max) { touch = true; // touch at end if (i == 2) touch_floor = true; } // - want to know when rear of A passes front of B u_0[i] = ((B_max - A_min) * 0x4000) / vel[i]; // - want to know when front of A passes rear of B u_1[i] = ((B_min - A_max) * 0x4000) / vel[i]; } else if (vel[i] > 0 && A_min <= B_max) { // A_min<=B_max not required if (A_min == B_max) touch = true; // touch at start if (A_max + vel[i] == B_min) touch = true; // touch at end // + want to know when front of A passes rear of B u_0[i] = ((B_min - A_max) * 0x4000) / vel[i]; // + want to know when rear of A passes front of B u_1[i] = ((B_max - A_min) * 0x4000) / vel[i]; } else if (vel[i] == 0 && A_max >= B_min && A_min <= B_max) { if (A_min == B_max || A_max == B_min) touch = true; if (i == 2 && A_min == B_max) touch_floor = true; u_0[i] = -1; u_1[i] = 0x4000; } else { u_0[i] = 0x4001; u_1[i] = -1; } if (u_1[i] >= u_0[i] && (u_0[i] > 0x4000 || u_1[i] < 0)) { u_0[i] = 0x4001; u_1[i] = -1; } } //possible first time of overlap int32 first = u_0[0]; if (u_0[1] > first) first = u_0[1]; if (u_0[2] > first) first = u_0[2]; //possible last time of overlap int32 last = u_1[0]; if (u_1[1] < last) last = u_1[1]; if (u_1[2] < last) last = u_1[2]; // store directions in which we're being blocked uint8 dirs = 0; for (int i = 0; i <= 2; ++i) { if (first == u_0[i]) dirs |= (1 << i); } //they could have only collided if //the first time of overlap occurred //before the last time of overlap if (first <= last) { if (!hit) return true; // Clamp if (first < -1) first = -1; if (last > 0x4000) last = 0x4000; // Ok, what we want to do here is add to the list. // Sorted by _hitTime. // Small speed up. if (sw_it != hit->end()) { if (sw_it->_hitTime > first) sw_it = hit->begin(); } else sw_it = hit->begin(); for (; sw_it != hit->end(); ++sw_it) { if (sw_it->_hitTime > first || (sw_it->_hitTime == first && sw_it->_endTime > last)) break; } // Now add it hit->insert(sw_it, SweepItem(other_item->getObjId(), first, last, touch, touch_floor, blocking, dirs)); //debugC(kDebugCollision, "Hit item %u (%d, %d, %d) at first: %d, last: %d", // other_item->getObjId(), other[0], other[1], other[2], first, last); //debugC(kDebugCollision, "hit item time (%d-%d) (%d-%d) (%d-%d)", // u_0[0], u_1[0], u_0[1], u_1[1], u_0[2], u_1[2]); //debugC(kDebugCollision, "touch: %d, floor: %d, block: %d", touch, touch_floor, blocking); } } } } return hit && hit->size(); } void CurrentMap::setFastAtPoint(const Point3 &pt) { int32 cx = pt.x / _mapChunkSize; int32 cy = pt.y / _mapChunkSize; if (!isChunkFast(cx, cy)) setChunkFast(cx, cy); } void CurrentMap::setWholeMapFast() { for (unsigned int i = 0; i < MAP_NUM_CHUNKS; ++i) { for (unsigned int j = 0; j < MAP_NUM_CHUNKS; ++j) { if (!isChunkFast(j, i)) setChunkFast(j, i); } } _fastXMin = -1; _fastYMin = -1; _fastXMax = -1; _fastYMax = -1; } void CurrentMap::save(Common::WriteStream *ws) { for (unsigned int i = 0; i < MAP_NUM_CHUNKS; ++i) { for (unsigned int j = 0; j < MAP_NUM_CHUNKS / 32; ++j) { ws->writeUint32LE(_fast[i][j]); } } if (GAME_IS_CRUSADER) { for (int i = 0; i < MAP_NUM_TARGET_ITEMS; i++) ws->writeUint16LE(_targets[i]); } } bool CurrentMap::load(Common::ReadStream *rs, uint32 version) { for (unsigned int i = 0; i < MAP_NUM_CHUNKS; ++i) { for (unsigned int j = 0; j < MAP_NUM_CHUNKS / 32; ++j) { _fast[i][j] = rs->readUint32LE(); } } _fastXMin = -1; _fastYMin = -1; _fastXMax = -1; _fastYMax = -1; if (GAME_IS_CRUSADER) { for (int i = 0; i < MAP_NUM_TARGET_ITEMS; i++) _targets[i] = rs->readUint16LE(); } return true; } uint32 CurrentMap::I_canExistAt(const uint8 *args, unsigned int argsize) { ARG_UINT16(shape); ARG_UINT16(x); ARG_UINT16(y); ARG_UINT8(z); if (argsize > 8) { //!! TODO: figure these out ARG_NULL16(); // is either 1 or 4. moves? ARG_NULL16(); // some objid? ARG_NULL16(); // always zero } World_FromUsecodeXY(x, y); // // TODO: The crusader version of this function actually checks by // changing the shapeinfo of shape 0x31A to match the target // shape. For a first level approximation, this is the same. // const CurrentMap *cm = World::get_instance()->getCurrentMap(); PositionInfo info = cm->getPositionInfo(x, y, z, shape, 0); if (info.valid) return 1; else return 0; } uint32 CurrentMap::I_canExistAtPoint(const uint8 *args, unsigned int /*argsize*/) { ARG_ITEM_FROM_PTR(other); ARG_UINT16(shape); ARG_WORLDPOINT(pt); if (other) { debugC(kDebugObject, "I_canExistAtPoint other object: %s", other->dumpInfo().c_str()); } else { debugC(kDebugObject, "I_canExistAtPoint other object null."); } if (shape > 0x800) return 0; int32 x = pt.getX(); int32 y = pt.getY(); int32 z = pt.getZ(); World_FromUsecodeXY(x, y); const CurrentMap *cm = World::get_instance()->getCurrentMap(); PositionInfo info = cm->getPositionInfo(x, y, z, shape, 0); if (info.valid) return 1; else return 0; } } // End of namespace Ultima8 } // End of namespace Ultima