There were couple reports of doors being generated in a corner of a room. This happened for randomly generated irregular rooms, because the code that was deciding if a corridor starting or ending location was good did not handle irregular rooms at all. This changes the corridor code so it can now generate start and end points properly for any valid position in irregular rooms, instead of only on the edges. This means the corridor sometimes meanders a bit more than before, because it tries to find the end point away from the edge of the room rectangle. Also added is sanity checking for the randomly generated rooms and corridors level, testing for door placement and room connectivity. And another fix for a rare special case where dig_corridor created a zero-tile long corridor; the entrance door was placed, but there was nothing behind it. Fixes github #1269 and #1385
489 lines
15 KiB
C
489 lines
15 KiB
C
/* NetHack 3.7 mkmap.c $NHDT-Date: 1717432093 2024/06/03 16:28:13 $ $NHDT-Branch: NetHack-3.7 $:$NHDT-Revision: 1.40 $ */
|
|
/* Copyright (c) J. C. Collet, M. Stephenson and D. Cohrs, 1992 */
|
|
/* NetHack may be freely redistributed. See license for details. */
|
|
|
|
#include "hack.h"
|
|
#include "sp_lev.h"
|
|
|
|
#define HEIGHT (ROWNO - 1)
|
|
#define WIDTH (COLNO - 2)
|
|
|
|
staticfn void init_map(schar);
|
|
staticfn void init_fill(schar, schar);
|
|
staticfn schar get_map(coordxy, coordxy, schar);
|
|
staticfn void pass_one(schar, schar);
|
|
staticfn void pass_two(schar, schar);
|
|
staticfn void pass_three(schar, schar);
|
|
staticfn void join_map_cleanup(void);
|
|
staticfn void join_map(schar, schar);
|
|
staticfn void finish_map(schar, schar, boolean, boolean, boolean);
|
|
staticfn void remove_room(unsigned);
|
|
void mkmap(lev_init *);
|
|
|
|
staticfn void
|
|
init_map(schar bg_typ)
|
|
{
|
|
coordxy x, y;
|
|
|
|
for (x = 1; x < COLNO; x++)
|
|
for (y = 0; y < ROWNO; y++) {
|
|
levl[x][y].roomno = NO_ROOM;
|
|
levl[x][y].typ = bg_typ;
|
|
levl[x][y].lit = FALSE;
|
|
}
|
|
}
|
|
|
|
staticfn void
|
|
init_fill(schar bg_typ, schar fg_typ)
|
|
{
|
|
coordxy x, y;
|
|
long limit, count;
|
|
|
|
limit = (WIDTH * HEIGHT * 2) / 5;
|
|
count = 0;
|
|
while (count < limit) {
|
|
x = (coordxy) rn1(WIDTH - 1, 2);
|
|
y = (coordxy) rnd(HEIGHT - 1);
|
|
if (levl[x][y].typ == bg_typ) {
|
|
levl[x][y].typ = fg_typ;
|
|
count++;
|
|
}
|
|
}
|
|
}
|
|
|
|
staticfn schar
|
|
get_map(coordxy col, coordxy row, schar bg_typ)
|
|
{
|
|
if (col <= 0 || row < 0 || col > WIDTH || row >= HEIGHT)
|
|
return bg_typ;
|
|
return levl[col][row].typ;
|
|
}
|
|
|
|
staticfn const int dirs[16] = {
|
|
-1, -1 /**/, -1, 0 /**/, -1, 1 /**/, 0, -1 /**/,
|
|
0, 1 /**/, 1, -1 /**/, 1, 0 /**/, 1, 1
|
|
};
|
|
|
|
staticfn void
|
|
pass_one(schar bg_typ, schar fg_typ)
|
|
{
|
|
coordxy x, y;
|
|
short count, dr;
|
|
|
|
for (x = 2; x <= WIDTH; x++)
|
|
for (y = 1; y < HEIGHT; y++) {
|
|
for (count = 0, dr = 0; dr < 8; dr++)
|
|
if (get_map(x + dirs[dr * 2], y + dirs[(dr * 2) + 1], bg_typ)
|
|
== fg_typ)
|
|
count++;
|
|
|
|
switch (count) {
|
|
case 0: /* death */
|
|
case 1:
|
|
case 2:
|
|
levl[x][y].typ = bg_typ;
|
|
break;
|
|
case 5:
|
|
case 6:
|
|
case 7:
|
|
case 8:
|
|
levl[x][y].typ = fg_typ;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
#define new_loc(i, j) *(gn.new_locations + ((j) * (WIDTH + 1)) + (i))
|
|
|
|
staticfn void
|
|
pass_two(schar bg_typ, schar fg_typ)
|
|
{
|
|
coordxy x, y;
|
|
short count, dr;
|
|
|
|
for (x = 2; x <= WIDTH; x++)
|
|
for (y = 1; y < HEIGHT; y++) {
|
|
for (count = 0, dr = 0; dr < 8; dr++)
|
|
if (get_map(x + dirs[dr * 2], y + dirs[(dr * 2) + 1], bg_typ)
|
|
== fg_typ)
|
|
count++;
|
|
if (count == 5)
|
|
new_loc(x, y) = bg_typ;
|
|
else
|
|
new_loc(x, y) = get_map(x, y, bg_typ);
|
|
}
|
|
|
|
for (x = 2; x <= WIDTH; x++)
|
|
for (y = 1; y < HEIGHT; y++)
|
|
levl[x][y].typ = new_loc(x, y);
|
|
}
|
|
|
|
staticfn void
|
|
pass_three(schar bg_typ, schar fg_typ)
|
|
{
|
|
coordxy x, y;
|
|
short count, dr;
|
|
|
|
for (x = 2; x <= WIDTH; x++)
|
|
for (y = 1; y < HEIGHT; y++) {
|
|
for (count = 0, dr = 0; dr < 8; dr++)
|
|
if (get_map(x + dirs[dr * 2], y + dirs[(dr * 2) + 1], bg_typ)
|
|
== fg_typ)
|
|
count++;
|
|
if (count < 3)
|
|
new_loc(x, y) = bg_typ;
|
|
else
|
|
new_loc(x, y) = get_map(x, y, bg_typ);
|
|
}
|
|
|
|
for (x = 2; x <= WIDTH; x++)
|
|
for (y = 1; y < HEIGHT; y++)
|
|
levl[x][y].typ = new_loc(x, y);
|
|
}
|
|
|
|
/*
|
|
* use a flooding algorithm to find all locations that should
|
|
* have the same rm number as the current location.
|
|
* if anyroom is TRUE, use IS_ROOM to check room membership instead of
|
|
* exactly matching levl[sx][sy].typ and walls are included as well.
|
|
*/
|
|
void
|
|
flood_fill_rm(
|
|
coordxy sx,
|
|
coordxy sy,
|
|
int rmno,
|
|
boolean lit,
|
|
boolean anyroom)
|
|
{
|
|
coordxy i, nx;
|
|
schar fg_typ = levl[sx][sy].typ;
|
|
|
|
/* back up to find leftmost uninitialized location */
|
|
while (sx > 0 && (anyroom ? IS_ROOM(levl[sx][sy].typ)
|
|
: levl[sx][sy].typ == fg_typ)
|
|
&& (int) levl[sx][sy].roomno != rmno)
|
|
sx--;
|
|
sx++; /* compensate for extra decrement */
|
|
|
|
/* assume sx,sy is valid */
|
|
if (sx < gm.min_rx)
|
|
gm.min_rx = sx;
|
|
if (sy < gm.min_ry)
|
|
gm.min_ry = sy;
|
|
|
|
for (i = sx; i <= WIDTH && levl[i][sy].typ == fg_typ; i++) {
|
|
levl[i][sy].roomno = rmno;
|
|
levl[i][sy].lit = lit;
|
|
if (anyroom) {
|
|
/* add walls to room as well */
|
|
coordxy ii, jj;
|
|
for (ii = (i == sx ? i - 1 : i); ii <= i + 1; ii++)
|
|
for (jj = sy - 1; jj <= sy + 1; jj++)
|
|
if (isok(ii, jj) && (IS_WALL(levl[ii][jj].typ)
|
|
|| IS_DOOR(levl[ii][jj].typ)
|
|
|| levl[ii][jj].typ == SDOOR)) {
|
|
levl[ii][jj].edge = 1;
|
|
if (lit)
|
|
levl[ii][jj].lit = lit;
|
|
|
|
if (levl[ii][jj].roomno == NO_ROOM)
|
|
levl[ii][jj].roomno = rmno;
|
|
else if ((int) levl[ii][jj].roomno != rmno)
|
|
levl[ii][jj].roomno = SHARED;
|
|
}
|
|
}
|
|
gn.n_loc_filled++;
|
|
}
|
|
nx = i;
|
|
|
|
if (isok(sx, sy - 1)) {
|
|
for (i = sx; i < nx; i++)
|
|
if (levl[i][sy - 1].typ == fg_typ) {
|
|
if ((int) levl[i][sy - 1].roomno != rmno)
|
|
flood_fill_rm(i, sy - 1, rmno, lit, anyroom);
|
|
} else {
|
|
if ((i > sx || isok(i - 1, sy - 1))
|
|
&& levl[i - 1][sy - 1].typ == fg_typ) {
|
|
if ((int) levl[i - 1][sy - 1].roomno != rmno)
|
|
flood_fill_rm(i - 1, sy - 1, rmno, lit, anyroom);
|
|
}
|
|
if ((i < nx - 1 || isok(i + 1, sy - 1))
|
|
&& levl[i + 1][sy - 1].typ == fg_typ) {
|
|
if ((int) levl[i + 1][sy - 1].roomno != rmno)
|
|
flood_fill_rm(i + 1, sy - 1, rmno, lit, anyroom);
|
|
}
|
|
}
|
|
}
|
|
if (isok(sx, sy + 1)) {
|
|
for (i = sx; i < nx; i++)
|
|
if (levl[i][sy + 1].typ == fg_typ) {
|
|
if ((int) levl[i][sy + 1].roomno != rmno)
|
|
flood_fill_rm(i, sy + 1, rmno, lit, anyroom);
|
|
} else {
|
|
if ((i > sx || isok(i - 1, sy + 1))
|
|
&& levl[i - 1][sy + 1].typ == fg_typ) {
|
|
if ((int) levl[i - 1][sy + 1].roomno != rmno)
|
|
flood_fill_rm(i - 1, sy + 1, rmno, lit, anyroom);
|
|
}
|
|
if ((i < nx - 1 || isok(i + 1, sy + 1))
|
|
&& levl[i + 1][sy + 1].typ == fg_typ) {
|
|
if ((int) levl[i + 1][sy + 1].roomno != rmno)
|
|
flood_fill_rm(i + 1, sy + 1, rmno, lit, anyroom);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (nx > gm.max_rx)
|
|
gm.max_rx = nx - 1; /* nx is just past valid region */
|
|
if (sy > gm.max_ry)
|
|
gm.max_ry = sy;
|
|
}
|
|
|
|
/* join_map uses temporary rooms; clean up after it */
|
|
staticfn void
|
|
join_map_cleanup(void)
|
|
{
|
|
coordxy x, y;
|
|
|
|
for (x = 1; x < COLNO; x++)
|
|
for (y = 0; y < ROWNO; y++)
|
|
levl[x][y].roomno = NO_ROOM;
|
|
svn.nroom = gn.nsubroom = 0;
|
|
svr.rooms[svn.nroom].hx = gs.subrooms[gn.nsubroom].hx = -1;
|
|
}
|
|
|
|
staticfn void
|
|
join_map(schar bg_typ, schar fg_typ)
|
|
{
|
|
struct mkroom *croom, *croom2;
|
|
|
|
coordxy x, y, sx, sy;
|
|
coord sm, em;
|
|
|
|
/* first, use flood filling to find all of the regions that need joining
|
|
*/
|
|
for (x = 2; x <= WIDTH; x++)
|
|
for (y = 1; y < HEIGHT; y++) {
|
|
if (levl[x][y].typ == fg_typ && levl[x][y].roomno == NO_ROOM) {
|
|
gm.min_rx = gm.max_rx = x;
|
|
gm.min_ry = gm.max_ry = y;
|
|
gn.n_loc_filled = 0;
|
|
flood_fill_rm(x, y, svn.nroom + ROOMOFFSET, FALSE, FALSE);
|
|
if (gn.n_loc_filled > 3) {
|
|
add_room(gm.min_rx, gm.min_ry, gm.max_rx, gm.max_ry,
|
|
FALSE, OROOM, TRUE);
|
|
svr.rooms[svn.nroom - 1].irregular = TRUE;
|
|
if (svn.nroom >= (MAXNROFROOMS * 2))
|
|
goto joinm;
|
|
} else {
|
|
/*
|
|
* it's a tiny hole; erase it from the map to avoid
|
|
* having the player end up here with no way out.
|
|
*/
|
|
for (sx = gm.min_rx; sx <= gm.max_rx; sx++)
|
|
for (sy = gm.min_ry; sy <= gm.max_ry; sy++)
|
|
if ((int) levl[sx][sy].roomno
|
|
== svn.nroom + ROOMOFFSET) {
|
|
levl[sx][sy].typ = bg_typ;
|
|
levl[sx][sy].roomno = NO_ROOM;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
joinm:
|
|
/*
|
|
* Ok, now we can actually join the regions with fg_typ's.
|
|
* The rooms are already sorted due to the previous loop,
|
|
* so don't call sort_rooms(), which can screw up the roomno's
|
|
* validity in the levl structure.
|
|
*/
|
|
for (croom = &svr.rooms[0], croom2 = croom + 1;
|
|
croom2 < &svr.rooms[svn.nroom]; ) {
|
|
/* pick random starting and end locations for "corridor" */
|
|
if (!somexy(croom, &sm) || !somexy(croom2, &em)) {
|
|
/* ack! -- the level is going to be busted */
|
|
/* arbitrarily pick centers of both rooms and hope for the best */
|
|
impossible("No start/end room loc in join_map.");
|
|
sm.x = croom->lx + ((croom->hx - croom->lx) / 2);
|
|
sm.y = croom->ly + ((croom->hy - croom->ly) / 2);
|
|
em.x = croom2->lx + ((croom2->hx - croom2->lx) / 2);
|
|
em.y = croom2->ly + ((croom2->hy - croom2->ly) / 2);
|
|
}
|
|
|
|
(void) dig_corridor(&sm, &em, NULL, FALSE, fg_typ, bg_typ);
|
|
|
|
/* choose next region to join */
|
|
/* only increment croom if croom and croom2 are non-overlapping */
|
|
if (croom2->lx > croom->hx
|
|
|| ((croom2->ly > croom->hy || croom2->hy < croom->ly)
|
|
&& rn2(3))) {
|
|
croom = croom2;
|
|
}
|
|
croom2++; /* always increment the next room */
|
|
}
|
|
join_map_cleanup();
|
|
}
|
|
|
|
staticfn void
|
|
finish_map(
|
|
schar fg_typ,
|
|
schar bg_typ,
|
|
boolean lit,
|
|
boolean walled,
|
|
boolean icedpools)
|
|
{
|
|
coordxy x, y;
|
|
|
|
if (walled)
|
|
wallify_map(1, 0, COLNO-1, ROWNO-1);
|
|
|
|
if (lit) {
|
|
for (x = 1; x < COLNO; x++)
|
|
for (y = 0; y < ROWNO; y++)
|
|
if ((!IS_OBSTRUCTED(fg_typ) && levl[x][y].typ == fg_typ)
|
|
|| (!IS_OBSTRUCTED(bg_typ) && levl[x][y].typ == bg_typ)
|
|
|| (bg_typ == TREE && levl[x][y].typ == bg_typ)
|
|
|| (walled && IS_WALL(levl[x][y].typ)))
|
|
levl[x][y].lit = TRUE;
|
|
for (x = 0; x < svn.nroom; x++)
|
|
svr.rooms[x].rlit = 1;
|
|
}
|
|
/* light lava even if everything's otherwise unlit;
|
|
ice might be frozen pool rather than frozen moat */
|
|
for (x = 1; x < COLNO; x++)
|
|
for (y = 0; y < ROWNO; y++) {
|
|
if (levl[x][y].typ == LAVAPOOL)
|
|
levl[x][y].lit = TRUE;
|
|
else if (levl[x][y].typ == ICE)
|
|
levl[x][y].icedpool = icedpools ? ICED_POOL : ICED_MOAT;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* TODO: If we really want to remove rooms after a map is plopped down
|
|
* in a special level, this needs to be rewritten - the maps may have
|
|
* holes in them ("x" mapchar), leaving parts of rooms still on the map.
|
|
*
|
|
* When level processed by join_map is overlaid by a MAP, some rooms may no
|
|
* longer be valid. All rooms in the region lx <= x < hx, ly <= y < hy are
|
|
* removed. Rooms partially in the region are truncated. This function
|
|
* must be called before the REGIONs or ROOMs of the map are processed, or
|
|
* those rooms will be removed as well. Assumes roomno fields in the
|
|
* region are already cleared, and roomno and irregular fields outside the
|
|
* region are all set.
|
|
*/
|
|
void
|
|
remove_rooms(coordxy lx, coordxy ly, coordxy hx, coordxy hy)
|
|
{
|
|
int i;
|
|
struct mkroom *croom;
|
|
|
|
for (i = svn.nroom - 1; i >= 0; --i) {
|
|
croom = &svr.rooms[i];
|
|
if (croom->hx < lx || croom->lx >= hx || croom->hy < ly
|
|
|| croom->ly >= hy)
|
|
continue; /* no overlap */
|
|
|
|
if (croom->lx < lx || croom->hx >= hx || croom->ly < ly
|
|
|| croom->hy >= hy) { /* partial overlap */
|
|
/* TODO: ensure remaining parts of room are still joined */
|
|
|
|
if (!croom->irregular)
|
|
impossible("regular room in joined map");
|
|
} else {
|
|
/* total overlap, remove the room */
|
|
remove_room((unsigned) i);
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Remove roomno from the rooms array, decrementing nroom.
|
|
* The last room is swapped with the being-removed room and locations
|
|
* within it have their roomno field updated. Other rooms are unaffected.
|
|
* Assumes level structure contents corresponding to roomno have already
|
|
* been reset.
|
|
* Currently handles only the removal of rooms that have no subrooms.
|
|
*/
|
|
staticfn void
|
|
remove_room(unsigned int roomno)
|
|
{
|
|
struct mkroom *croom = &svr.rooms[roomno];
|
|
struct mkroom *maxroom = &svr.rooms[--svn.nroom];
|
|
coordxy x, y;
|
|
unsigned oroomno;
|
|
|
|
if (croom != maxroom) {
|
|
/* since the order in the array only matters for making corridors,
|
|
* copy the last room over the one being removed on the assumption
|
|
* that corridors have already been dug. */
|
|
*croom = *maxroom;
|
|
|
|
/* since maxroom moved, update affected level roomno values */
|
|
oroomno = svn.nroom + ROOMOFFSET;
|
|
roomno += ROOMOFFSET;
|
|
for (x = croom->lx; x <= croom->hx; ++x)
|
|
for (y = croom->ly; y <= croom->hy; ++y) {
|
|
if (levl[x][y].roomno == oroomno)
|
|
levl[x][y].roomno = roomno;
|
|
}
|
|
}
|
|
|
|
maxroom->hx = -1; /* just like add_room */
|
|
}
|
|
|
|
#define N_P1_ITER 1 /* tune map generation via this value */
|
|
#define N_P2_ITER 1 /* tune map generation via this value */
|
|
#define N_P3_ITER 2 /* tune map smoothing via this value */
|
|
|
|
boolean
|
|
litstate_rnd(int litstate)
|
|
{
|
|
if (litstate < 0)
|
|
return (rnd(1 + abs(depth(&u.uz))) < 11 && rn2(77)) ? TRUE : FALSE;
|
|
return (boolean) litstate;
|
|
}
|
|
|
|
void
|
|
mkmap(lev_init *init_lev)
|
|
{
|
|
schar bg_typ = init_lev->bg, fg_typ = init_lev->fg;
|
|
boolean smooth = init_lev->smoothed, join = init_lev->joined;
|
|
xint16 lit = init_lev->lit, walled = init_lev->walled;
|
|
int i;
|
|
|
|
lit = litstate_rnd(lit);
|
|
|
|
gn.new_locations = (char *) alloc((WIDTH + 1) * HEIGHT);
|
|
|
|
init_map(bg_typ);
|
|
init_fill(bg_typ, fg_typ);
|
|
|
|
for (i = 0; i < N_P1_ITER; i++)
|
|
pass_one(bg_typ, fg_typ);
|
|
|
|
for (i = 0; i < N_P2_ITER; i++)
|
|
pass_two(bg_typ, fg_typ);
|
|
|
|
if (smooth)
|
|
for (i = 0; i < N_P3_ITER; i++)
|
|
pass_three(bg_typ, fg_typ);
|
|
|
|
if (join)
|
|
join_map(bg_typ, fg_typ);
|
|
|
|
finish_map(fg_typ, bg_typ, (boolean) lit, (boolean) walled,
|
|
init_lev->icedpools);
|
|
/* a walled, joined level is cavernous, not mazelike -dlc */
|
|
if (walled && join) {
|
|
svl.level.flags.is_maze_lev = FALSE;
|
|
svl.level.flags.is_cavernous_lev = TRUE;
|
|
}
|
|
free(gn.new_locations);
|
|
}
|
|
|
|
/*mkmap.c*/
|