Skip to content

Draft: dig-now plugin: remove dependencies on MapCache module.#5832

Draft
SilasD wants to merge 9 commits into
DFHack:developfrom
SilasD:dig-now
Draft

Draft: dig-now plugin: remove dependencies on MapCache module.#5832
SilasD wants to merge 9 commits into
DFHack:developfrom
SilasD:dig-now

Conversation

@SilasD

@SilasD SilasD commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

I request code review on this draft PR, with a focus on dig-now.cpp lines 60 to 380 or so, which contain new code, and the dig_type() function in lines 624-682, which is a complete reimplementation.

I would like this code to be up to modern C++ standards, and I know that my C++ knowledge and skills are not the best.

The remainder of the file is old code updated with different types, e.g. df::coord instead of DFCoord, and removal of all references to the MapExtras::MapCache &map object.

There are also a couple of minor bugfixes, in particular in the dig_tile() function Channel case.

I don't think that part of the plugin needs full review yet, but I do welcome comments.

LuaLS annotations were added to dig-now.lua. No code changes.

General notes:

This PR is intended to be a naive-but-correct reimplementation of dig-now using the Maps module instead of the MapCache module. Note that it does not have the caching that MapCache provided. This results in a lot of unnecessary info lookups.

This PR is known to have bugs. These bugs already existed in the old version. They will be fixed in the near future.

This code has been verified to be almost bug-for-bug compatible with the old version. It results in the same map state with these exceptions:

  • Blueprint-mode designations for carving fortifications and tracks: the old version improperly carves them even though they are set as blueprints, and it also doesn't clear map_block.flags.designated. This new version replicates the buggy behavior, but it properly clears that flag. I can't be bothered to track down the exact line and state to replicate that tiny bug, when I expect to fix the larger bug soon.
  • Undiggable ramps are no longer improperly removed by the clean_ramp() function. This has been seen on tower-cap cap ramps, but has not been verified for all undiggable ramps.

Intended future directions, roughly in this order:

  • Extend the Lua API calls or create new parallel calls to allow Lua scripts to process a list of tiles instead of processing one tile per invocation.
  • The Lua API calls must be able to request dig/smooth operations instead of depending on map tile designations. This is because one of the expected uses is for adventure-mode mods to alter the map, and adventure mode uses the dig and smooth bits for other purposes. The current code does not support this use case.
  • Fix many, many ways that dig-now doesn't leave the map in the same state as actual mining does. See bug-report issue #TODO.
  • Make the core loop map-block-centric, perhaps using Maps::cuboid::forBlock(). Currently it is a naive
    layer-by-layer, column-by-column, row-by-row scan.
  • Re-add caching of region data, biome and geo-biome data, and material data. This reimplementation does a lot of unnecessary info lookups.

SilasD added 7 commits May 22, 2026 10:33
this is intended to be a naive direct-replacement with
minimal changes to algorithms.

this commit is the easy stuff:

remove MapExtras::MapCache &map parameter from functions
change DFCoord to df::coord
change MapCache::ensureBlockAt to Maps::ensureTileBlock
change MapCache::BlockAtTile to Maps::getTileBlock
change MapCache::tiletypeAt to *Maps::getTileType
change MapCache::getDesignationAt to *Maps::getTileDesignation
change MapCache::setDesignationAt to *Maps::getTileDesignation
change MapCache::occupancyAt to *Maps::getTileOccupancy
flag MapCache::layerMaterialAt for further work
flag MapCache::baseMaterialAt for further work
flag Block::veinTypeAt for further work
flag Block::setStoneAt for further work
flag Block::setSoilAt for further work
flag DesignationJobs for further work

add several now-necessary headers.
this is a naive-but-correct implementation with no caching.
@SilasD SilasD marked this pull request as draft June 19, 2026 19:31
@SilasD SilasD self-assigned this Jun 19, 2026
SilasD added 2 commits June 19, 2026 18:11
turns out I'm not allowed to have unused functions.
so much for forward planning.

also, apparently 0 is signed!  who would have known?

In file included from /home/runner/work/dfhack/dfhack/library/include/Error.h:31,
                 from /home/runner/work/dfhack/dfhack/library/include/BitArray.h:26,
                 from /home/runner/work/dfhack/dfhack/library/include/DataDefs.h:37,
                 from /home/runner/work/dfhack/dfhack/library/include/DataIdentity.h:38,
                 from /home/runner/work/dfhack/dfhack/library/include/DataFuncs.h:30,
                 from /home/runner/work/dfhack/dfhack/plugins/dig-now.cpp:5:
/home/runner/work/dfhack/dfhack/library/include/MiscUtils.h: In instantiation of ‘T clip_range(T, T1, T2) [with T = long unsigned int; T1 = int; T2 = long unsigned int]’:
/home/runner/work/dfhack/dfhack/plugins/dig-now.cpp:108:23:   required from here
/home/runner/work/dfhack/dfhack/library/include/MiscUtils.h:553:11: error: comparison of integer expressions of different signedness: ‘long unsigned int’ and ‘int’ [-Werror=sign-compare]
  553 |     if (a < minv) return minv;
      |         ~~^~~~~~
Comment thread plugins/dig-now.cpp
return t_matpair(INORGANIC, geo_biome->layers[layer]->mat_index);
}

#if false // gcc warns about unused functions, and warnings are errors.

@ab9rf ab9rf Jun 20, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use [[maybe_unused]] here to suppress the warning - the compiler will not warn about the function being unused but will still validate its syntax which doesn't happen with the #if

long term, if this is actually dead code it should simply be removed, there's no need to preserve it for reference or legacy purposes as it'll be forever in the repo change history

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the compiler directive.

I'm uncertain if it is dead code or not; I originally wrote these functions wiuth the code combined into one function each, then decided to split out the map_block and local pos cases.

I will maintain it for the moment, but my long-term intent is to move all the code to block-centric algorithms, so this will likely be permanently dead code and I will remove it then.

Comment thread plugins/dig-now.cpp
Comment on lines +64 to +389
static const df::region_map_entry* biome_at(const df::map_block &block, const df::coord2d p)
{
auto blockptr = &static_cast<df::map_block &>(const_cast<df::map_block &>(block));
auto biome = Maps::getRegionBiome(Maps::getBlockTileBiomeRgn(blockptr, p));
return biome;
}

#if false // gcc warns about unused functions, and warnings are errors.
static const df::region_map_entry* biome_at(const df::coord pos)
{
auto block = Maps::getTileBlock(pos);
if (!block)
return nullptr;
auto blockref = static_cast<const df::map_block &>(*block);
return biome_at(blockref, df::coord2d(pos) & 15);
}
#endif

static const df::world_geo_biome* geo_biome_at(const df::map_block &block, const df::coord2d p)
{
auto biome = biome_at(block, p);
if (!biome)
return nullptr;
if (biome->geo_index < 0)
return nullptr;
if (static_cast<size_t>(biome->geo_index) >= df::world_geo_biome::get_vector().size())
return nullptr;
auto geo_biome = df::world_geo_biome::get_vector()[biome->geo_index];
return geo_biome;
}

#if false // gcc warns about unused functions, and warnings are errors.
static const df::world_geo_biome* geo_biome_at(const df::coord pos)
{
auto block = Maps::getTileBlock(pos);
if (!block)
return nullptr;
auto blockref = static_cast<const df::map_block &>(*block);
return geo_biome_at(blockref, df::coord2d(pos) & 15);
}
#endif

static const t_matpair layer_inorganic_n(const df::map_block &block, const df::coord2d p, size_t layer)
{
using namespace df::enums::builtin_mats;
auto geo_biome = geo_biome_at(block, p);
if (!geo_biome)
return t_matpair(INORGANIC, -1); // can't happen, generic "rock"
// gcc-11 complained that 0 was of different signed-ness than size_t! trying to cast it to unsigned.
layer = clip_range(layer, static_cast<size_t>(0), geo_biome->layers.size() - 1);
return t_matpair(INORGANIC, geo_biome->layers[layer]->mat_index);
}

#if false // gcc warns about unused functions, and warnings are errors.
static const t_matpair layer_inorganic_n(const df::coord pos, size_t layer)
{
using namespace df::enums::builtin_mats;
auto block = Maps::getTileBlock(pos);
if (!block)
return t_matpair(INORGANIC, -1); // can't happen, generic "rock"
auto blockref = static_cast<const df::map_block &>(*block);
return layer_inorganic_n(blockref, df::coord2d(pos) & 15, layer);
}
#endif

static const size_t geolayer_at(const df::map_block &block, df::coord2d p)
{
return index_tile(block.designation, p).bits.geolayer_index;
}

static const t_matpair layer_inorganic_at(const df::map_block &block, df::coord2d p)
{
return layer_inorganic_n(block, p, geolayer_at(block, p));
}

#if false // gcc warns about unused functions, and warnings are errors.
static const t_matpair layer_inorganic_at(const df::coord pos)
{
using namespace df::enums::builtin_mats;
auto block = Maps::getTileBlock(pos);
if (!block)
return t_matpair(INORGANIC, -1); // generic "rock"
auto blockref = const_cast<const df::map_block &>(static_cast<df::map_block &>(*block));
return layer_inorganic_at(blockref, df::coord2d(pos) & 15);
}
#endif

static const df::enums::tiletype_material::tiletype_material getGroundType(int32_t mat_index)
{
if (isSoilInorganic(mat_index))
return df::enums::tiletype_material::SOIL;
if (isStoneInorganic(mat_index))
return df::enums::tiletype_material::STONE;
return df::enums::tiletype_material::NONE;
}

static const df::enums::tiletype_material::tiletype_material getGroundType(const t_matpair matpair)
{
using namespace df::enums::builtin_mats;
if (matpair.mat_type != INORGANIC)
return df::enums::tiletype_material::NONE;
return getGroundType(matpair.mat_index);
}

// copied from Maps, used by getBiomeRgnPos()
static df::coord2d biome_offsets[9] = {
df::coord2d(-1,-1), df::coord2d(0,-1), df::coord2d(1,-1),
df::coord2d(-1,0), df::coord2d(0,0), df::coord2d(1,0),
df::coord2d(-1,1), df::coord2d(0,1), df::coord2d(1,1)
};

// copied from Maps. arguably this should be exported from Maps; it's useful.
inline df::coord2d getBiomeRgnPos(const df::coord2d base, BiomeOffset idx)
{
auto r = base + biome_offsets[idx];

int world_width = world->world_data->world_width;
int world_height = world->world_data->world_height;

return df::coord2d(clip_range(r.x, 0, world_width-1),
clip_range(r.y, 0, world_height-1));
}

// copied and modified from MapCache getBaseMaterial().
// returns the material that should be used for boulders/gems,
// or the invalid material t_matpair(-1).
static const t_matpair baseMaterialAt(const df::map_block &block, df::coord2d p)
{
using namespace df::enums::builtin_mats;
using namespace df::enums::tiletype_material;

p = p & 15;
t_matpair rv;
auto tt = index_tile(block.tiletype, p);
switch (tileMaterial(tt)) {
// not diggable, should not drop boulders.
case df::enums::tiletype_material::NONE:
case AIR:
case CONSTRUCTION:
case HFS:
case CAMPFIRE:
case FIRE:
case MAGMA:
case POOL:
case RIVER:
case TREE:
case ROOT: // TODO this is a bug; DF can dig roots.
case MUSHROOM:
case UNDERWORLD_GATE:
rv = t_matpair(-1);
break;

// diggable but should not drop boulders, so return the first layer, presumably soil.
// (these won't drop anyway because they are not WALL or FORTIFICATION shaped, but safety first.)
case DRIFTWOOD:
case BROOK:
case ASHES:
case PLANT:
case GRASS_LIGHT:
case GRASS_DARK:
case GRASS_DRY:
case GRASS_DEAD:
rv = layer_inorganic_n(block, p, 0);
break;

case STONE:
{
rv = t_matpair(INORGANIC, -1); // fallback: generic "rock"
auto geo_biome = geo_biome_at(block, p);
if (!geo_biome)
break;

// IS THIS CODE CORRECT?
// intent here is to index 0..15 into a vector with 13 or 16 elements
// and get the element at min(index, vector length-1) .
// ranges::advance seems like the right tool.
auto geo_layer_it = begin(geo_biome->layers);
std::ranges::advance(geo_layer_it, geolayer_at(block, p), end(geo_biome->layers) - 1);
rv = t_matpair(INORGANIC, (*geo_layer_it)->mat_index);
if (getGroundType(rv) == STONE)
break;

// fall back to the first stone layer, or the very last layer.

// IS TNIS CODE CORRECT?
// intent here is to search a vector with 13 or 16 elements
// for the first element matching a predicate, and get that element
// OR the last element in the vector, whether or not it matches.
// all this is a lot of faffing around for something that's more
// concise in C style C++; see the SOIL code below.
auto is_stone = [&](df::world_geo_layer *geo_layer)
{ return getGroundType(geo_layer->mat_index) == STONE; };
// this vector typically has 16 entries, but I have seen 13 in oceans.
auto &geo_layers = geo_biome->layers;
geo_layer_it = std::find_if(begin(geo_layers), end(geo_layers) - 1, is_stone);
rv = t_matpair(INORGANIC, (*geo_layer_it)->mat_index);
break;
}

case SOIL:
rv = layer_inorganic_at(block, p);
if (getGroundType(rv) != SOIL) {
// fall back to the last soil layer, or the very first layer.
for (auto i = 15; i >= 0; i--) {
rv = layer_inorganic_n(block, p, i); // this call clips to vector size.
if (getGroundType(rv) == SOIL)
break;
}
}
break;

case FEATURE:
{
// fallback: no feature flag, or no event? no drops.
rv = t_matpair(-1);
auto des = index_tile(block.designation, p);
t_feature feature;
feature.type = df::enums::feature_type::feature_type::NONE;
if (des.bits.feature_local)
Maps::ReadFeatures(
&(static_cast<df::map_block &>(const_cast<df::map_block &>(block))),
&feature, nullptr);
else if (des.bits.feature_global)
Maps::ReadFeatures(
&(static_cast<df::map_block &>(const_cast<df::map_block &>(block))),
nullptr, &feature);
if (feature.type != df::enums::feature_type::feature_type::NONE)
rv = t_matpair(feature.main_material, feature.sub_material);
break;
}

case LAVA_STONE:
{
auto region_details = world->world_data->midmap_data.region_details;
auto des = index_tile(block.designation, p);
auto block_region_offset_idx = static_cast<BiomeOffset>(des.bits.biome);
if (block_region_offset_idx >= eBiomeCount)
// "can't happen", fall back to the local region tile's biome.
block_region_offset_idx = eHere;
auto blockptr = &static_cast<df::map_block &>(const_cast<df::map_block &>(block));
auto region_details_coord2d = Maps::getBlockTileBiomeRgn(blockptr, p);
auto region_details_idx = linear_index(region_details,
&df::world_region_details::pos, region_details_coord2d);
if (region_details_idx == -1)
{
// "can't happen"; fall back to the first region in the region_details vector.
DEBUG(general).print("BaseMaterialAt(block {}, tile {}, case "
"LAVA_STONE, didn't find region_details for region coord {}\n",
block.map_pos, p, region_details_coord2d);
region_details_idx = 0;
}
rv = t_matpair(INORGANIC, region_details[region_details_idx]->lava_stone);
break;
}

case MINERAL:
// fall back to the layer material, whether SOIL or STONE.
rv = layer_inorganic_at(block, p);
for (auto event : block.block_events) {
if (event->getType() == df::block_square_event_type::mineral) {
auto vein = (df::block_square_event_mineralst *)event;
if (vein->getassignment(p)) {
rv = t_matpair(INORGANIC, vein->inorganic_mat);
// do not early-out. later mineral events can override earlier ones.
}
}
}
break;

case FROZEN_LIQUID:
rv = t_matpair(WATER, 0);
break;

// no default so we get a compiler warning.
}

if (rv == t_matpair(-1))
return t_matpair(-1);
MaterialInfo mi;
mi.decode(rv);
if (mi.isValid() && mi.material->flags.is_set(df::material_flags::UNDIGGABLE))
return t_matpair(-1);
return rv;
}

static const t_matpair baseMaterialAt(df::coord pos)
{
auto block = Maps::getTileBlock(pos);
if (!block)
return t_matpair(-1);
auto blockref = const_cast<const df::map_block &>(static_cast<df::map_block &>(*block));
return baseMaterialAt(blockref, df::coord2d(pos) & 15);
}

static const df::inclusion_type veinTypeAt(df::map_block &block, df::coord2d p)
{
using namespace df::enums::inclusion_type;
auto veintype = TOTAL;
for (auto event : block.block_events) {
if (event->getType() == df::block_square_event_type::mineral) {
auto vein = (df::block_square_event_mineralst *)event;
if (vein->getassignment(p)) {
if (vein->flags.bits.cluster_one)
veintype = CLUSTER_ONE;
else if (vein->flags.bits.cluster_small)
veintype = CLUSTER_SMALL;
else if (vein->flags.bits.vein)
veintype = VEIN;
else if (vein->flags.bits.cluster)
veintype = CLUSTER;
// do not early-out. later mineral events can override earlier ones.
}
}
}
return veintype;
}

static const df::inclusion_type veinTypeAt(df::coord pos)
{
auto block = Maps::getTileBlock(pos);
if (!block)
return df::enums::inclusion_type::TOTAL;
auto blockref = const_cast<const df::map_block &>(static_cast<df::map_block &>(*block));
return veinTypeAt(blockref, df::coord2d(pos) & 15);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we be adding some or all of these as custom methods to the relevant codegen types? This is something we can do.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants