Files
scummvm-cursorfix/engines/ultima/nuvie/core/map.cpp
2026-02-02 04:50:13 +01:00

824 lines
21 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 "ultima/shared/std/string.h"
#include "ultima/nuvie/core/nuvie_defs.h"
#include "ultima/nuvie/files/nuvie_io_file.h"
#include "ultima/nuvie/conf/configuration.h"
#include "ultima/nuvie/core/game.h"
#include "ultima/nuvie/core/tile_manager.h"
#include "ultima/nuvie/actors/actor_manager.h"
#include "ultima/nuvie/core/map.h"
#include "ultima/nuvie/gui/widgets/map_window.h"
#include "ultima/nuvie/misc/u6_misc.h"
namespace Ultima {
namespace Nuvie {
Map::Map(const Configuration *cfg) : config(cfg), tile_manager(nullptr),
obj_manager(nullptr), actor_manager(nullptr), surface(nullptr),
roof_surface(nullptr) {
ARRAYCLEAR(dungeons);
config->value(config_get_game_key(config) + "/roof_mode", roof_mode, false);
}
Map::~Map() {
if (surface == nullptr)
return;
free(surface);
for (int i = 0; i < 5; i++)
free(dungeons[i]);
if (roof_surface)
free(roof_surface);
}
byte *Map::get_map_data(uint8 level) {
if (level == 0)
return surface;
if (level > 5)
return nullptr;
return dungeons[level - 1];
}
uint16 *Map::get_roof_data(uint8 level) {
if (level == 0)
return roof_surface;
return nullptr;
}
const Tile *Map::get_tile(uint16 x, uint16 y, uint8 level, bool original_tile) {
if (level > 5)
return nullptr;
WRAP_COORD(x, level);
WRAP_COORD(y, level);
const uint8 *ptr = get_map_data(level);
const Tile *map_tile;
if (original_tile)
map_tile = tile_manager->get_original_tile(ptr[y * get_width(level) + x]);
else
map_tile = tile_manager->get_tile(ptr[y * get_width(level) + x]);
return map_tile;
}
uint16 Map::get_width(uint8 level) const {
if (level == 0)
return 1024; // surface
return 256; // dungeon
}
bool Map::is_passable(uint16 x, uint16 y, uint8 level) {
WRAP_COORD(x, level);
WRAP_COORD(y, level);
uint8 obj_status = obj_manager->is_passable(x, y, level);
if (obj_status == OBJ_NOT_PASSABLE) {
return false;
}
//special case for bridges, hacked doors and dungeon entrances etc.
if (obj_status != OBJ_NO_OBJ && obj_manager->is_forced_passable(x, y, level))
return true;
const uint8 *ptr = get_map_data(level);
const Tile *map_tile = tile_manager->get_original_tile(ptr[y * get_width(level) + x]);
return map_tile->passable;
}
/***
* Can we enter this map location by traveling in a given direction?
* Used by MD
*/
bool Map::is_passable(uint16 x, uint16 y, uint8 level, NuvieDir dir) {
if (is_passable_from_dir(x, y, level, get_reverse_direction(dir))) {
sint16 rel_x, rel_y;
uint16 tx, ty;
get_relative_dir(get_reverse_direction(dir), &rel_x, &rel_y);
tx = wrap_signed_coord((sint16)x + rel_x, level);
ty = wrap_signed_coord((sint16)y + rel_y, level);
return is_passable_from_dir(tx, ty, level, dir);
}
return false;
}
bool Map::is_passable_from_dir(uint16 x, uint16 y, uint8 level, NuvieDir dir) {
WRAP_COORD(x, level);
WRAP_COORD(y, level);
uint8 obj_status = obj_manager->is_passable(x, y, level);
if (obj_status == OBJ_NOT_PASSABLE) {
return false;
}
//special case for bridges, hacked doors and dungeon entrances etc.
if (obj_status != OBJ_NO_OBJ && obj_manager->is_forced_passable(x, y, level))
return true;
const uint8 *ptr = get_map_data(level);
const Tile *map_tile = tile_manager->get_original_tile(ptr[y * get_width(level) + x]);
if (!map_tile->passable && !(map_tile->flags1 & TILEFLAG_WALL)) {
switch (dir) {
case NUVIE_DIR_W :
return (map_tile->flags1 & TILEFLAG_WALL_WEST);
case NUVIE_DIR_S :
return (map_tile->flags1 & TILEFLAG_WALL_SOUTH);
case NUVIE_DIR_E :
return (map_tile->flags1 & TILEFLAG_WALL_EAST);
case NUVIE_DIR_N :
return (map_tile->flags1 & TILEFLAG_WALL_NORTH);
case NUVIE_DIR_NE :
return !(!(map_tile->flags1 & TILEFLAG_WALL_NORTH) || !(map_tile->flags1 & TILEFLAG_WALL_EAST));
case NUVIE_DIR_NW :
return !(!(map_tile->flags1 & TILEFLAG_WALL_NORTH) || !(map_tile->flags1 & TILEFLAG_WALL_WEST));
case NUVIE_DIR_SE :
return !(!(map_tile->flags1 & TILEFLAG_WALL_SOUTH) || !(map_tile->flags1 & TILEFLAG_WALL_EAST));
case NUVIE_DIR_SW :
return !(!(map_tile->flags1 & TILEFLAG_WALL_SOUTH) || !(map_tile->flags1 & TILEFLAG_WALL_WEST));
default:
error("Invalid direction in Map::is_passable_from_dir");
}
}
return map_tile->passable;
}
/* Returns true if an entire area is_passable(). */
bool Map::is_passable(uint16 x1, uint16 y1, uint16 x2, uint16 y2, uint8 level) {
for (int x = x1; x <= x2; x++)
for (int y = y1; y <= y2; y++)
if (!is_passable((uint16)x, (uint16)y, level))
return false;
return true;
}
bool Map::is_boundary(uint16 x, uint16 y, uint8 level) {
WRAP_COORD(x, level);
WRAP_COORD(y, level);
uint8 *ptr = get_map_data(level);
Tile *map_tile = tile_manager->get_tile(ptr[y * get_width(level) + x]);
if (map_tile->boundary && obj_manager->is_forced_passable(x, y, level) == false)
return true;
if (obj_manager->is_boundary(x, y, level))
return true;
return false;
}
bool Map::is_missile_boundary(uint16 x, uint16 y, uint8 level, Obj *excluded_obj) {
WRAP_COORD(x, level);
WRAP_COORD(y, level);
uint8 *ptr = get_map_data(level);
Tile *map_tile = tile_manager->get_tile(ptr[y * get_width(level) + x]);
if ((map_tile->flags2 & TILEFLAG_MISSILE_BOUNDARY) != 0 && obj_manager->is_forced_passable(x, y, level) == false)
return true;
if (obj_manager->is_boundary(x, y, level, TILEFLAG_MISSILE_BOUNDARY, excluded_obj))
return true;
return false;
}
bool Map::is_water(uint16 x, uint16 y, uint16 level, bool ignore_objects) {
WRAP_COORD(x, level);
WRAP_COORD(y, level);
if (!ignore_objects) {
const Obj *obj = obj_manager->get_obj(x, y, level);
if (obj != nullptr)
return false;
}
const uint8 *ptr = get_map_data(level);
const Tile *map_tile = tile_manager->get_original_tile(ptr[y * get_width(level) + x]);
if (map_tile->water)
return true;
return false;
}
bool Map::is_damaging(uint16 x, uint16 y, uint8 level, bool ignore_objects) {
const uint8 *ptr = get_map_data(level);
WRAP_COORD(x, level);
WRAP_COORD(y, level);
const Tile *map_tile = tile_manager->get_original_tile(ptr[y * get_width(level) + x]);
if (map_tile->damages)
return true;
if (!ignore_objects) {
if (obj_manager->is_damaging(x, y, level))
return true;
}
return false;
}
bool Map::can_put_obj(uint16 x, uint16 y, uint8 level) {
LineTestResult lt;
if (lineTest(x, y, x, y, level, LT_HitActors | LT_HitUnpassable, lt)) {
if (lt.hitObj) {
// We can place an object on a bench or table. Or on any other object if
// the object is passable and not on a boundary.
Tile *obj_tile = obj_manager->get_obj_tile(lt.hitObj->obj_n, lt.hitObj->frame_n);
if ((obj_tile->flags3 & TILEFLAG_CAN_PLACE_ONTOP) ||
(obj_tile->passable && !is_boundary(lt.hit_x, lt.hit_y, lt.hit_level))) {
return true;
} else {
return false;
}
}
}
if (is_missile_boundary(x, y, level))
return false;
return true;
}
uint8 Map::get_impedance(uint16 x, uint16 y, uint8 level, bool ignore_objects) {
uint8 *ptr = get_map_data(level);
WRAP_COORD(x, level);
WRAP_COORD(y, level);
const Tile *map_tile = tile_manager->get_original_tile(ptr[y * get_width(level) + x]);
uint8 impedance = 0;
if (!ignore_objects) {
const U6LList *obj_list = obj_manager->get_obj_list(x, y, level);
if (obj_list) {
for (const U6Link *link = obj_list->start(); link != nullptr; link = link->next) {
const Obj *obj = (Obj *)link->data;
if (obj != nullptr) {
uint8 tile_flag = obj_manager->get_obj_tile(obj->obj_n, obj->frame_n)->flags1;
if ((tile_flag & TILEFLAG_BLOCKING) == 0) {
impedance += (tile_flag & TILEFLAG_IMPEDANCE) >> TILEFLAG_IMPEDANCE_SHIFT;
}
}
}
}
}
if ((map_tile->flags1 & TILEFLAG_BLOCKING) == 0)
impedance += (map_tile->flags1 & TILEFLAG_IMPEDANCE) >> TILEFLAG_IMPEDANCE_SHIFT;
return impedance;
}
const Tile *Map::get_dmg_tile(uint16 x, uint16 y, uint8 level) {
const Tile *tile = get_tile(x, y, level);
if (tile->damages)
return tile;
return obj_manager->get_obj_dmg_tile(x, y, level);
}
bool Map::actor_at_location(uint16 x, uint16 y, uint8 level, bool inc_surrounding_objs) {
WRAP_COORD(x, level);
WRAP_COORD(y, level);
//check for blocking Actor at location.
if (actor_manager->get_actor(x, y, level, inc_surrounding_objs) != nullptr)
return true;
return false;
}
/* Return pointer to actor standing at map coordinates.
*/
Actor *Map::get_actor(uint16 x, uint16 y, uint8 z, bool inc_surrounding_objs) {
WRAP_COORD(x, z);
WRAP_COORD(y, z);
return (actor_manager->get_actor(x, y, z, inc_surrounding_objs));
}
const char *Map::look(uint16 x, uint16 y, uint8 level) {
unsigned char *ptr;
uint16 qty = 0;
if (level == 0) {
ptr = surface;
} else
ptr = dungeons[level - 1];
WRAP_COORD(x, level);
WRAP_COORD(y, level);
Obj *obj = obj_manager->get_obj(x, y, level);
if (obj != nullptr && !(obj->status & OBJ_STATUS_INVISIBLE) //only show visible objects.
&& !Game::get_game()->get_map_window()->tile_is_black(obj->x, obj->y, obj)) {
// tile = tile_manager->get_original_tile(obj_manager->get_obj_tile_num(obj->obj_n)+obj->frame_n);
// tile_num = tile->tile_num;
// qty = obj->qty;
return obj_manager->look_obj(obj);
}
uint16 tile_num = ptr[y * get_width(level) + x];
return tile_manager->lookAtTile(tile_num, qty, true);
}
bool Map::loadMap(TileManager *tm, ObjManager *om) {
Common::Path filename;
NuvieIOFileRead map_file;
NuvieIOFileRead chunks_file;
uint8 i;
tile_manager = tm;
obj_manager = om;
config_get_path(config, "map", filename);
if (map_file.open(filename) == false)
return false;
config_get_path(config, "chunks", filename);
if (chunks_file.open(filename) == false)
return false;
unsigned char *map_data = map_file.readAll();
if (map_data == nullptr)
return false;
unsigned char *chunk_data = chunks_file.readAll();
if (chunk_data == nullptr)
return false;
unsigned char *map_ptr = map_data;
surface = (unsigned char *)malloc(1024 * 1024);
if (surface == nullptr)
return false;
for (i = 0; i < 64; i++) {
insertSurfaceSuperChunk(map_ptr, chunk_data, i);
map_ptr += 384;
}
for (i = 0; i < 5; i++) {
dungeons[i] = (unsigned char *)malloc(256 * 256);
if (dungeons[i] == nullptr)
return false;
insertDungeonSuperChunk(map_ptr, chunk_data, i);
map_ptr += 1536;
}
free(map_data);
free(chunk_data);
if (roof_mode)
loadRoofData();
/* ERIC Useful for testing map wrapping
I plan to add a map patch function
to allow custom map changes to be
loaded easily into nuvie.
uint16 mx,my;
for(my=100;my<130;my++)
for(mx=0;mx<30;mx++)
surface[my * 1024 + mx] = 1;
for(my=100;my<130;my++)
for(mx=1000;mx<1024;mx++)
surface[my * 1024 + mx] = 111;
*/
/*
printf("\n\n\n\n\n\n\n");
uint16 mx,my;
for(my=0;my<1024;my++)
{
for(mx=0;mx<1024;mx++)
{
printf("%3d,", surface[my * 1024 + mx]+1);
}
printf("\n");
}
*/
return true;
}
bool Map::has_roof(uint16 x, uint16 y, uint8 level) const {
if (!roof_mode || level != 0)
return false;
if (roof_surface[y * 1024 + x] != 0)
return true;
return false;
}
Common::Path Map::getRoofDataFilename() const {
Std::string game_type, tmp;
Common::Path datadir, path, mapfile;
config->value("config/datadir", tmp, "");
config->value("config/GameID", game_type);
datadir = Common::Path(tmp);
build_path(datadir, "maps", path);
datadir = path;
build_path(datadir, game_type, path);
datadir = path;
build_path(datadir, "roof_map_00.dat", mapfile);
return mapfile;
}
Common::Path Map::getRoofTilesetFilename() const {
Std::string tmp;
Common::Path datadir;
Common::Path imagefile;
Common::Path path;
config->value("config/datadir", tmp, "");
datadir = Common::Path(tmp);
build_path(datadir, "images", path);
datadir = path;
build_path(datadir, "roof_tiles.bmp", imagefile);
return imagefile;
}
void Map::set_roof_mode(bool roofs) {
roof_mode = roofs;
if (roof_mode) {
if (roof_surface)
return;
else
loadRoofData();
} else {
if (roof_surface) {
free(roof_surface);
roof_surface = nullptr;
}
}
}
void Map::loadRoofData() {
NuvieIOFileRead file;
roof_surface = (uint16 *)malloc(1024 * 1024 * 2);
if (file.open(getRoofDataFilename())) {
memset(roof_surface, 0, 1024 * 1024 * 2);
uint16 *ptr = roof_surface;
while (!file.is_eof()) {
uint16 offset = file.read2();
ptr += offset;
uint8 run_len = file.read1();
for (uint8 i = 0; i < run_len; i++) {
*ptr = file.read2();
ptr++;
}
}
} else {
if (roof_surface) {
free(roof_surface);
roof_surface = nullptr;
}
roof_mode = false;
}
}
void Map::saveRoofData() {
NuvieIOFileWrite file;
uint32 prev_offset = 0;
uint32 cur_offset = 0;
uint16 run_length = 0;
if (roof_surface && file.open(getRoofDataFilename())) {
for (; cur_offset < 1048576;) {
for (; cur_offset < prev_offset + 65535 && cur_offset < 1048576;) {
if (roof_surface[cur_offset] != 0) {
file.write2((uint16)(cur_offset - prev_offset));
for (run_length = 0; run_length < 256; run_length++) {
if (roof_surface[cur_offset + run_length] == 0)
break;
}
if (run_length == 256)
run_length--;
file.write1((uint8)run_length);
for (uint8 i = 0; i < run_length; i++) {
file.write2(roof_surface[cur_offset + i]);
}
cur_offset += run_length;
break;
}
cur_offset++;
if (cur_offset == prev_offset + 65535) {
//write blank.
file.write2(65535);
file.write1(0);
}
}
prev_offset = cur_offset;
}
}
}
void Map::insertSurfaceSuperChunk(const unsigned char *schunk, const unsigned char *chunk_data, uint8 schunk_num) {
uint16 world_x, world_y;
uint16 c1, c2;
uint8 i, j;
world_x = schunk_num % 8;
world_y = (schunk_num - world_x) / 8;
world_x *= 128;
world_y *= 128;
for (i = 0; i < 16; i++) {
for (j = 0; j < 16; j += 2) {
c1 = ((schunk[1] & 0xf) << 8) | schunk[0];
c2 = (schunk[2] << 4) | (schunk[1] >> 4);
insertSurfaceChunk(&chunk_data[c1 * 64], world_x + j * 8, world_y + i * 8);
insertSurfaceChunk(&chunk_data[c2 * 64], world_x + (j + 1) * 8, world_y + i * 8);
schunk += 3;
}
}
}
void Map::insertSurfaceChunk(const unsigned char *chunk, uint16 x, uint16 y) {
unsigned char *map_ptr;
map_ptr = &surface[y * 1024 + x];
for (int i = 0; i < 8; i++) {
memcpy(map_ptr, chunk, 8);
map_ptr += 1024;
chunk += 8;
}
}
void Map::insertDungeonSuperChunk(const unsigned char *schunk, const unsigned char *chunk_data, uint8 level) {
uint16 c1, c2;
uint8 i, j;
for (i = 0; i < 32; i++) {
for (j = 0; j < 32; j += 2) {
c1 = ((schunk[1] & 0xf) << 8) | schunk[0];
c2 = (schunk[2] << 4) | (schunk[1] >> 4);
insertDungeonChunk(&chunk_data[c1 * 64], j * 8, i * 8, level);
insertDungeonChunk(&chunk_data[c2 * 64], (j + 1) * 8, i * 8, level);
schunk += 3;
}
}
}
void Map::insertDungeonChunk(const unsigned char *chunk, uint16 x, uint16 y, uint8 level) {
unsigned char *map_ptr;
map_ptr = &dungeons[level][y * 256 + x];
for (int i = 0; i < 8; i++) {
memcpy(map_ptr, chunk, 8);
map_ptr += 256;
chunk += 8;
}
}
/* Get absolute coordinates for relative destination from MapCoord.
*/
MapCoord MapCoord::abs_coords(sint16 dx, sint16 dy) const {
// uint16 pitch = Map::get_width(z); cannot call function without object
uint16 pitch = (z == 0) ? 1024 : 256;
dx += x;
dy += y;
// wrap on map boundary for MD
if (dx < 0)
dx = pitch + dx;
else if (dx >= pitch)
dx = pitch - dx;
if (dy < 0)
dy = 0;
else if (dy >= pitch)
dy = pitch - 1;
return (MapCoord(dx, dy, z));
}
/* Returns true if this map coordinate is visible in the game window.
*/
bool MapCoord::is_visible() const {
return (Game::get_game()->get_map_window()->in_window(x, y, z));
}
bool Map::testIntersection(int x, int y, uint8 level, uint8 flags, LineTestResult &Result, Obj *excluded_obj) {
/* more checks added, may need more testing (SB-X) */
#if 0
if (flags & LT_HitUnpassable) {
if (!is_passable(x, y, level)) {
Result.init(x, y, level, nullptr, obj_manager->get_obj(x, y, level, true));
return true;
}
}
if (flags & LT_HitForcedPassable) {
if (obj_manager->is_forced_passable(x, y, level)) {
Result.init(x, y, level, nullptr, obj_manager->get_obj(x, y, level, true));
return true;
}
}
if (flags & LT_HitActors) {
// TODO:
}
return false;
#else
if (flags & LT_HitUnpassable) {
if (!is_passable(x, y, level)) {
Obj *obj_hit = obj_manager->get_obj(x, y, level);
if (!obj_hit || !excluded_obj || obj_hit != excluded_obj) {
Result.init(x, y, level, nullptr, obj_manager->get_obj(x, y, level, true));
return true;
}
}
}
if (flags & LT_HitMissileBoundary) {
if (is_missile_boundary(x, y, level, excluded_obj)) {
Result.init(x, y, level, nullptr, obj_manager->get_obj(x, y, level, true));
return true;
}
}
if (flags & LT_HitForcedPassable) {
if (obj_manager->is_forced_passable(x, y, level)) {
Result.init(x, y, level, nullptr, obj_manager->get_obj(x, y, level, true));
return true;
}
}
if (flags & LT_HitActors) {
if (actor_manager->get_actor(x, y, level)) {
Result.init(x, y, level, actor_manager->get_actor(x, y, level), nullptr);
return true;
}
}
if ((flags & LT_HitLocation) && Result.loc_to_hit) {
if (x == Result.loc_to_hit->x && y == Result.loc_to_hit->y) {
Result.init(x, y, level, nullptr, nullptr);
Result.loc_to_hit->z = level;
Result.hitLoc = Result.loc_to_hit;
return true;
}
}
if (flags & LT_HitObjects) {
if (obj_manager->get_obj(x, y, level)) {
Result.init(x, y, level, nullptr, obj_manager->get_obj(x, y, level, true));
return true;
}
}
return false;
#endif
}
// returns true if a line hits something travelling from (start_x, start_y) to
// (end_x, end_y). If a hit occurs Result is filled in with the relevant info.
// If want_screen_space is true input tile coordinates are multiplied by 16 for
// line calculation and scaled back down before testing for collisions. The
// original game does this for projectiles.
bool Map::lineTest(int start_x, int start_y, int end_x, int end_y, uint8 level,
uint8 flags, LineTestResult &Result, uint32 skip, Obj *excluded_obj,
bool want_screen_space) {
// standard Bresenham's algorithm.
uint8 scale_factor_log2 = 0;
if (want_screen_space)
scale_factor_log2 = 4; // set scale factor to 16
int deltax = abs(end_x - start_x) << scale_factor_log2;
int deltay = abs(end_y - start_y) << scale_factor_log2;
int x = (start_x << scale_factor_log2);
int y = (start_y << scale_factor_log2);
x += ((1 << scale_factor_log2) >> 1); // start at the center of the tile
y += ((1 << scale_factor_log2) >> 1); // when in screen space
int d;
int xinc1, xinc2;
int yinc1, yinc2;
int dinc1, dinc2;
uint32 count;
int xtile = start_x;
int ytile = start_y;
int xtile_prev = xtile;
int ytile_prev = ytile;
if (deltax >= deltay) {
d = (deltay << 1) - deltax;
count = deltax + 1;
dinc1 = deltay << 1;
dinc2 = (deltay - deltax) << 1;
xinc1 = 1;
xinc2 = 1;
yinc1 = 0;
yinc2 = 1;
} else {
d = (deltax << 1) - deltay;
count = deltay + 1;
dinc1 = deltax << 1;
dinc2 = (deltax - deltay) << 1;
xinc1 = 0;
xinc2 = 1;
yinc1 = 1;
yinc2 = 1;
}
if (start_x > end_x) {
xinc1 = -xinc1;
xinc2 = -xinc2;
}
if (start_y > end_y) {
yinc1 = -yinc1;
yinc2 = -yinc2;
}
for (uint32 i = 0; i < count; i++) {
// only test for collision if tile coordinates have changed
if ((scale_factor_log2 == 0 || x >> scale_factor_log2 != xtile || y >> scale_factor_log2 != ytile)) {
xtile_prev = xtile;
ytile_prev = ytile;
xtile = x >> scale_factor_log2; // scale back down to tile
ytile = y >> scale_factor_log2; // space if necessary
// test the current location
if ((i >= skip) && (testIntersection(xtile, ytile, level, flags, Result, excluded_obj) == true)) {
Result.pre_hit_x = xtile_prev;
Result.pre_hit_y = ytile_prev;
return true;
}
}
if (d < 0) {
d += dinc1;
x += xinc1;
y += yinc1;
} else {
d += dinc2;
x += xinc2;
y += yinc2;
}
}
return false;
}
} // End of namespace Nuvie
} // End of namespace Ultima