Merge branch 'DFHack:develop' into quickfort-alias

develop
Will H 2023-03-11 16:14:06 +11:00 committed by GitHub
commit 874518b26c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 3771 additions and 5358 deletions

@ -10,10 +10,9 @@ jobs:
fail-fast: false
matrix:
os:
- ubuntu-18.04
- ubuntu-22.04
gcc:
- 4.8
- 7
- 10
plugins:
- default
include:
@ -159,7 +158,7 @@ jobs:
path: build/win64-cross/output/*
docs:
runs-on: ubuntu-18.04
runs-on: ubuntu-22.04
steps:
- name: Set up Python 3
uses: actions/setup-python@v2
@ -182,14 +181,14 @@ jobs:
path: docs/html
lint:
runs-on: ubuntu-18.04
runs-on: ubuntu-22.04
steps:
- name: Set up Python 3
uses: actions/setup-python@v2
with:
python-version: 3
- name: Set up Ruby 2.7
uses: actions/setup-ruby@v1
uses: ruby/setup-ruby@v1
with:
ruby-version: 2.7
- name: Install Lua

@ -0,0 +1,57 @@
name: Deploy to Steam
on:
workflow_dispatch:
inputs:
commit_hash:
description: Commit hash
type: string
required: true
version:
description: Version
type: string
required: true
release_channel:
description: Release channel
type: string
required: true
default: beta
jobs:
deploy-to-steam:
name: Deploy to Steam
runs-on: ubuntu-22.04
steps:
- name: Clone DFHack
uses: actions/checkout@v3
with:
submodules: true
fetch-depth: 0
ref: ${{ github.event.inputs.commit_hash }}
- name: Fetch ccache
uses: actions/cache@v3
with:
path: build/win64-cross/ccache
key: ccache-win64-cross-msvc-${{ github.event.inputs.commit_hash }}
restore-keys: |
ccache-win64-cross-msvc-develop-${{ github.event.inputs.commit_hash }}
ccache-win64-cross-msvc
- name: Cross-compile win64 artifacts
env:
CMAKE_EXTRA_ARGS: '-DBUILD_STONESENSE:BOOL=1'
run: |
cd build
bash -x build-win64-from-linux.sh
- name: Steam deploy
uses: game-ci/steam-deploy@v2
with:
username: ${{ secrets.STEAM_USERNAME }}
password: ${{ secrets.STEAM_PASSWORD }}
configVdf: ${{ secrets.STEAM_CONFIG_VDF}}
ssfnFileName: ${{ secrets.STEAM_SSFN_FILE_NAME }}
ssfnFileContents: ${{ secrets.STEAM_SSFN_FILE_CONTENTS }}
appId: 2346660
buildDescription: ${{ github.event.inputs.version }}
rootPath: build
depot1Path: win64-cross/output
releaseBranch: ${{ github.event.inputs.release_channel }}

@ -615,3 +615,5 @@ if(BUILD_SIZECHECK)
add_subdirectory(depends/sizecheck)
add_dependencies(dfhack sizecheck)
endif()
add_subdirectory(package/windows)

@ -5,19 +5,19 @@ Quickstart guide
Welcome to DFHack! This guide will help get you oriented with the DFHack system
and teach you how to find and use the tools productively. If you're reading this
in `quickstart-guide`, hit the right arrow key or click on the hotkey hint in
the lower right corner of the window to go to the next page.
in the in-game `quickstart-guide` reader, hit the right arrow key or click on
the hotkey hint in the lower right corner of the window to go to the next page.
What is DFHack?
---------------
DFHack is a framework for Dwarf Fortress that provides a unified, cross-platform
environment that enables mods and tools to significantly extend the game. The
default DFHack distribution contains a wide variety of tools, including bugfixes,
interface improvements, automation agents, design blueprints, modding building
blocks, and more. Third-party tools (e.g. mods downloaded from Steam Workshop or
the forums) can also seamlessly integrate with the DFHack framework and extend
the game far beyond what can be done by just modding the raws.
DFHack is an add-on for Dwarf Fortress that enables mods and tools to
significantly extend the game. The default DFHack distribution contains a wide
variety of tools, including bugfixes, interface improvements, automation agents,
design blueprints, modding building blocks, and more. Third-party tools (e.g.
mods downloaded from Steam Workshop or the forums) can also seamlessly integrate
with the DFHack framework and extend the game far beyond what can be done by
just modding the raws.
DFHack's mission is to provide tools and interfaces for players and modders to:

@ -18,6 +18,14 @@ An automated labor management tool that only addressed hauling labors, leaving t
of skilled labors entirely up to the player. Fundamentally incompatible with the work detail
system of labor management in v50 of Dwarf Fortress.
.. _automaterial:
automaterial
============
Moved frequently used materials to the top of the materials list when building
buildings. Also offered extended options when building constructions. All
functionality has been merged into `buildingplan`.
.. _combine-drinks:
combine-drinks

@ -38,8 +38,10 @@ changelog.txt uses a syntax similar to RST, with a few special sequences:
## Fixes
-@ ``widgets.HotkeyLabel``: don't trigger on click if the widget is disabled
- ``dfhack.job.isSuitableMaterial``: now properly detects lack of fire and magma safety for vulnerable materials with high melting points
- `dig-now`: fixed multi-layer channel designations only channeling every second layer
## Misc Improvements
- `dig-now`: added handling of dig designations that have been converted into active jobs
## Documentation
@ -56,6 +58,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences:
## Removed
-@ ``gui.THIN_FRAME``: replaced by ``gui.INTERIOR_FRAME``
- `automaterial`: all functionality has been merged into `buildingplan`
# 50.07-alpha2

@ -1,53 +0,0 @@
automaterial
============
.. dfhack-tool::
:summary: Sorts building materials by recent usage.
:tags: untested fort design productivity buildings map
:no-command:
This plugin makes building constructions (walls, floors, fortifications, etc)
much easier by saving you from having to trawl through long lists of materials
each time you place one.
It moves the last used material for a given construction type to the top of the
list, if there are any left. So if you build a wall with chalk blocks, the next
time you place a wall the chalk blocks will be at the top of the list,
regardless of distance (it only does this in "grouped" mode, as individual item
lists could be huge). This means you can place most constructions without having
to search for your preferred material type.
Usage
-----
::
enable automaterial
.. image:: ../images/automaterial-mat.png
Pressing :kbd:`a` while highlighting any material will enable that material for
"auto select" for this construction type. You can enable multiple materials. Now
the next time you place this type of construction, the plugin will automatically
choose materials for you from the kinds you enabled. If there is enough to
satisfy the whole placement, you won't be prompted with the material screen at
all -- the construction will be placed and you will be back in the construction
menu.
When choosing the construction placement, you will see a couple of options:
.. image:: ../images/automaterial-pos.png
Use :kbd:`a` here to temporarily disable the material autoselection, e.g. if you
need to go to the material selection screen so you can toggle some materials on
or off.
The other option (auto type selection, off by default) can be toggled on with
:kbd:`t`. If you toggle this option on, instead of returning you to the main
construction menu after selecting materials, it returns you back to this screen.
If you use this along with several autoselect enabled materials, you should be
able to place complex constructions more conveniently.
The ``automaterial`` plugin also enables extra construction placement modes,
such as designating areas larger than 10x10 and allowing you to designate hollow
rectangles instead of the default filled ones.

@ -1 +1 @@
Subproject commit 8ae81f8d8f1f96d82b9074b205073bb8e8d29f96
Subproject commit 267eb20f75e891fc0ade166e2d316498f454d913

@ -0,0 +1,7 @@
project(package_windows)
if(WIN32)
add_executable(launchdf WIN32 launchdf.c)
install(TARGETS launchdf
DESTINATION ${DFHACK_DATA_DESTINATION})
endif()

@ -0,0 +1,28 @@
#include <windows.h>
int WINAPI wWinMain(HINSTANCE hi, HINSTANCE hpi, PWSTR cmd, int ns)
{
STARTUPINFOA si;
PROCESS_INFORMATION pi;
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
ZeroMemory(&pi, sizeof(pi));
if (CreateProcessA("Dwarf Fortress.exe",
NULL,
NULL,
NULL,
FALSE,
0,
NULL,
NULL,
&si,
&pi) == 0)
{
MessageBoxA(NULL, "could not launch 'Dwarf Fortress.exe'", NULL, 0);
exit(1);
}
exit(0);
}

@ -82,7 +82,6 @@ dfhack_plugin(autodump autodump.cpp)
dfhack_plugin(autofarm autofarm.cpp)
#dfhack_plugin(autogems autogems.cpp LINK_LIBRARIES jsoncpp_static)
add_subdirectory(autolabor)
#dfhack_plugin(automaterial automaterial.cpp LINK_LIBRARIES lua)
dfhack_plugin(automelt automelt.cpp LINK_LIBRARIES lua)
dfhack_plugin(autonestbox autonestbox.cpp LINK_LIBRARIES lua)
#dfhack_plugin(autotrade autotrade.cpp)

File diff suppressed because it is too large Load Diff

@ -259,7 +259,7 @@ static void validate_config(color_ostream &out, bool verbose = false) {
static void clear_state(color_ostream &out) {
call_buildingplan_lua(&out, "signal_reset");
call_buildingplan_lua(&out, "reload_cursors");
call_buildingplan_lua(&out, "reload_pens");
planned_buildings.clear();
tasks.clear();
cur_heat_safety.clear();
@ -318,14 +318,6 @@ DFhackCExport command_result plugin_load_data (color_ostream &out) {
return CR_OK;
}
DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) {
if (event == SC_WORLD_UNLOADED) {
DEBUG(status,out).print("world unloaded; clearing state for %s\n", plugin_name);
clear_state(out);
}
return CR_OK;
}
static bool cycle_requested = false;
static void do_cycle(color_ostream &out) {
@ -524,16 +516,20 @@ static void printStatus(color_ostream &out) {
out.print(" use bars: %s\n", get_config_bool(config, CONFIG_BARS) ? "yes" : "no");
out.print("\n");
size_t bld_count = 0;
map<string, int32_t> counts;
int32_t total = 0;
for (auto &entry : planned_buildings) {
auto &pb = entry.second;
auto bld = pb.getBuildingIfValidOrRemoveIfNot(out);
// don't actually remove bad buildings from the list while we're
// actively iterating through that list
auto bld = pb.getBuildingIfValidOrRemoveIfNot(out, true);
if (!bld || bld->jobs.size() != 1)
continue;
auto &job_items = bld->jobs[0]->job_items;
if (job_items.size() != pb.vector_ids.size())
continue;
++bld_count;
int job_item_idx = 0;
for (auto &vec_ids : pb.vector_ids) {
auto &jitem = job_items[job_item_idx++];
@ -545,9 +541,9 @@ static void printStatus(color_ostream &out) {
}
}
if (planned_buildings.size()) {
if (bld_count) {
out.print("Waiting for %d item(s) to be produced for %zd building(s):\n",
total, planned_buildings.size());
total, bld_count);
for (auto &count : counts)
out.print(" %3d %s%s\n", count.second, count.first.c_str(), count.second == 1 ? "" : "s");
} else {
@ -851,11 +847,25 @@ static int getMaterialFilter(lua_State *L) {
const auto &mat_filter = filters[index].getMaterials();
map<MaterialInfo, int32_t> counts;
scanAvailableItems(*out, type, subtype, custom, index, NULL, &counts);
// name -> {count=int, enabled=bool, category=string}
HeatSafety heat = get_heat_safety_filter(key);
df::job_item jitem_cur_heat = getJobItemWithHeatSafety(
get_job_items(*out, key)[index], heat);
df::job_item jitem_fire = getJobItemWithHeatSafety(
get_job_items(*out, key)[index], HEAT_SAFETY_FIRE);
df::job_item jitem_magma = getJobItemWithHeatSafety(
get_job_items(*out, key)[index], HEAT_SAFETY_MAGMA);
// name -> {count=int, enabled=bool, category=string, heat=string}
map<string, map<string, string>> ret;
for (auto & entry : mat_cache) {
auto &name = entry.first;
auto &mat = entry.second.first;
if (!mat.matches(jitem_cur_heat))
continue;
string heat_safety = "";
if (mat.matches(jitem_magma))
heat_safety = "magma-safe";
else if (mat.matches(jitem_fire))
heat_safety = "fire-safe";
auto &name = entry.first;
auto &cat = entry.second.second;
map<string, string> props;
string count = "0";
@ -957,7 +967,7 @@ static string getDescString(color_ostream &out, df::building *bld, int index) {
PlannedBuilding &pb = planned_buildings.at(bld->id);
auto &jitem = bld->jobs[0]->job_items[index];
return get_desc_string(out, jitem, pb.vector_ids[index]);
return int_to_string(jitem->quantity) + " " + get_desc_string(out, jitem, pb.vector_ids[index]);
}
static int getQueuePosition(color_ostream &out, df::building *bld, int index) {

@ -47,6 +47,7 @@ void set_config_bool(DFHack::PersistentDataItem &c, int index, bool value);
std::vector<df::job_item_vector_id> getVectorIds(DFHack::color_ostream &out, const df::job_item *job_item);
bool itemPassesScreen(df::item * item);
df::job_item getJobItemWithHeatSafety(const df::job_item *job_item, HeatSafety heat);
bool matchesFilters(df::item * item, const df::job_item * job_item, HeatSafety heat, const ItemFilter &item_filter);
bool isJobReady(DFHack::color_ostream &out, const std::vector<df::job_item *> &jitems);
void finalizeBuilding(DFHack::color_ostream &out, df::building *bld);

@ -48,6 +48,16 @@ bool itemPassesScreen(df::item * item) {
&& !item->isAssignedToStockpile();
}
df::job_item getJobItemWithHeatSafety(const df::job_item *job_item, HeatSafety heat) {
df::job_item jitem = *job_item;
if (heat >= HEAT_SAFETY_MAGMA) {
jitem.flags2.bits.magma_safe = true;
jitem.flags2.bits.fire_safe = false;
} else if (heat == HEAT_SAFETY_FIRE && !jitem.flags2.bits.magma_safe)
jitem.flags2.bits.fire_safe = true;
return jitem;
}
bool matchesFilters(df::item * item, const df::job_item * job_item, HeatSafety heat, const ItemFilter &item_filter) {
// check the properties that are not checked by Job::isSuitableItem()
if (job_item->item_type > -1 && job_item->item_type != item->getType())
@ -67,12 +77,7 @@ bool matchesFilters(df::item * item, const df::job_item * job_item, HeatSafety h
&& !item->hasToolUse(job_item->has_tool_use))
return false;
df::job_item jitem = *job_item;
if (heat == HEAT_SAFETY_MAGMA) {
jitem.flags2.bits.magma_safe = true;
jitem.flags2.bits.fire_safe = false;
} else if (heat == HEAT_SAFETY_FIRE && !jitem.flags2.bits.magma_safe)
jitem.flags2.bits.fire_safe = true;
df::job_item jitem = getJobItemWithHeatSafety(job_item, heat);
return Job::isSuitableItem(
&jitem, item->getType(), item->getSubtype())

@ -99,11 +99,12 @@ PlannedBuilding::PlannedBuilding(color_ostream &out, PersistentDataItem &bld_con
// Ensure the building still exists and is in a valid state. It can disappear
// for lots of reasons, such as running the game with the buildingplan plugin
// disabled, manually removing the building, modifying it via the API, etc.
df::building * PlannedBuilding::getBuildingIfValidOrRemoveIfNot(color_ostream &out) {
df::building * PlannedBuilding::getBuildingIfValidOrRemoveIfNot(color_ostream &out, bool skip_remove) {
auto bld = df::building::find(id);
bool valid = bld && bld->getBuildStage() == 0;
if (!valid) {
remove(out);
if (!skip_remove)
remove(out);
return NULL;
}
return bld;

@ -29,7 +29,7 @@ public:
// Ensure the building still exists and is in a valid state. It can disappear
// for lots of reasons, such as running the game with the buildingplan plugin
// disabled, manually removing the building, modifying it via the API, etc.
df::building * getBuildingIfValidOrRemoveIfNot(DFHack::color_ostream &out);
df::building * getBuildingIfValidOrRemoveIfNot(DFHack::color_ostream &out, bool skip_remove = false);
private:
DFHack::PersistentDataItem bld_config;

@ -6,6 +6,7 @@
#include "PluginManager.h"
#include "TileTypes.h"
#include "LuaTools.h"
#include "Debug.h"
#include "modules/Buildings.h"
#include "modules/Gui.h"
@ -14,6 +15,8 @@
#include "modules/Random.h"
#include "modules/Units.h"
#include "modules/World.h"
#include "modules/EventManager.h"
#include "modules/Job.h"
#include <df/historical_entity.h>
#include <df/map_block.h>
@ -26,12 +29,132 @@
#include <df/world.h>
#include <df/world_site.h>
#include <cinttypes>
#include <unordered_set>
#include <unordered_map>
DFHACK_PLUGIN("dig-now");
REQUIRE_GLOBAL(plotinfo);
REQUIRE_GLOBAL(world);
// Debugging
namespace DFHack {
DBG_DECLARE(dignow, general, DebugCategory::LINFO);
DBG_DECLARE(dignow, channels, DebugCategory::LINFO);
}
#define COORD "%" PRIi16 " %" PRIi16 " %" PRIi16
#define COORDARGS(id) id.x, id.y, id.z
using namespace DFHack;
struct designation{
df::coord pos;
df::tile_designation type;
df::tile_occupancy occupancy;
designation() = default;
designation(const df::coord &c, const df::tile_designation &td, const df::tile_occupancy &to) : pos(c), type(td), occupancy(to) {}
bool operator==(const designation &rhs) const {
return pos == rhs.pos;
}
bool operator!=(const designation &rhs) const {
return !(rhs == *this);
}
};
namespace std {
template<>
struct hash<designation> {
std::size_t operator()(const designation &c) const {
std::hash<df::coord> hash_coord;
return hash_coord(c.pos);
}
};
}
class DesignationJobs {
private:
std::unordered_map<df::coord, designation> designations;
std::unordered_map<df::coord, df::job*> jobs;
public:
void load(MapExtras::MapCache &map) {
designations.clear();
DEBUG(general).print("DesignationJobs: reading jobs list\n");
df::job_list_link* node = df::global::world->jobs.list.next;
while (node) {
df::job* job = node->item;
node = node->next;
if(!job || !Maps::isValidTilePos(job->pos))
continue;
df::tile_designation td = map.designationAt(job->pos);
df::tile_occupancy to = map.occupancyAt(job->pos);
const auto ctd = td.whole;
const auto cto = to.whole;
switch (job->job_type){
case job_type::Dig:
td.bits.dig = tile_dig_designation::Default;
break;
case job_type::DigChannel:
td.bits.dig = tile_dig_designation::Channel;
break;
case job_type::CarveRamp:
td.bits.dig = tile_dig_designation::Ramp;
break;
case job_type::CarveUpwardStaircase:
td.bits.dig = tile_dig_designation::UpStair;
break;
case job_type::CarveDownwardStaircase:
td.bits.dig = tile_dig_designation::DownStair;
break;
case job_type::CarveUpDownStaircase:
td.bits.dig = tile_dig_designation::UpDownStair;
break;
case job_type::DetailWall:
case job_type::DetailFloor: {
df::tiletype tt = map.tiletypeAt(job->pos);
if (tileSpecial(tt) != df::tiletype_special::SMOOTH) {
td.bits.smooth = 1;
}
break;
}
case job_type::CarveTrack:
to.bits.carve_track_north = (job->item_category.whole >> 18) & 1;
to.bits.carve_track_south = (job->item_category.whole >> 19) & 1;
to.bits.carve_track_west = (job->item_category.whole >> 20) & 1;
to.bits.carve_track_east = (job->item_category.whole >> 21) & 1;
break;
default:
break;
}
if (ctd != td.whole || cto != to.whole) {
// we found a designation job
designations.emplace(job->pos, designation(job->pos, td, to));
jobs.emplace(job->pos, job);
}
}
DEBUG(general).print("DesignationJobs: DONE reading jobs list\n");
}
void remove(const df::coord &pos) {
if(jobs.count(pos)) {
Job::removeJob(jobs[pos]);
jobs.erase(pos);
}
}
designation get(const df::coord &pos) {
if (designations.count(pos)) {
return designations[pos];
}
return {};
}
bool count(const df::coord &pos) {
return jobs.count(pos);
}
};
struct boulder_percent_options {
// percent chance ([0..100]) for creating a boulder for the given rock type
uint32_t layer;
@ -320,8 +443,19 @@ static bool dig_tile(color_ostream &out, MapExtras::MapCache &map,
std::vector<dug_tile_info> &dug_tiles) {
df::tiletype tt = map.tiletypeAt(pos);
if (!is_diggable(map, pos, tt))
if (!is_diggable(map, pos, tt)) {
DEBUG(general).print("dig_tile: not diggable\n");
return false;
}
/** The algorithm process seems to be:
* for each tile
* check for a designation
* if a designation exists send it to dig_tile
*
* dig_tile (below) then digs the layer below the channel designated tile
* thereby changing it and causing its designation to be lost
* */
df::tiletype target_type = df::tiletype::Void;
switch(designation) {
@ -341,19 +475,22 @@ static bool dig_tile(color_ostream &out, MapExtras::MapCache &map,
DFCoord pos_below(pos.x, pos.y, pos.z-1);
if (can_dig_channel(tt) && map.ensureBlockAt(pos_below)
&& is_diggable(map, pos_below, map.tiletypeAt(pos_below))) {
TRACE(channels).print("dig_tile: channeling at (" COORD ") [can_dig_channel: true]\n",COORDARGS(pos_below));
target_type = df::tiletype::OpenSpace;
DFCoord pos_above(pos.x, pos.y, pos.z+1);
if (map.ensureBlockAt(pos_above))
if (map.ensureBlockAt(pos_above)) {
remove_ramp_top(map, pos_above);
df::tile_dig_designation td_below =
map.designationAt(pos_below).bits.dig;
if (dig_tile(out, map, pos_below,
df::tile_dig_designation::Ramp, dug_tiles)) {
}
df::tile_dig_designation td_below = map.designationAt(pos_below).bits.dig;
if (dig_tile(out, map, pos_below, df::tile_dig_designation::Ramp, dug_tiles)) {
clean_ramps(map, pos_below);
if (td_below == df::tile_dig_designation::Default)
if (td_below == df::tile_dig_designation::Default) {
dig_tile(out, map, pos_below, td_below, dug_tiles);
}
return true;
}
} else {
DEBUG(channels).print("dig_tile: failed to channel at (" COORD ") [can_dig_channel: false]\n", COORDARGS(pos_below));
}
break;
}
@ -407,7 +544,8 @@ static bool dig_tile(color_ostream &out, MapExtras::MapCache &map,
if (target_type == df::tiletype::Void || target_type == tt)
return false;
dug_tiles.push_back(dug_tile_info(map, pos));
dug_tiles.emplace_back(map, pos);
TRACE(general).print("dig_tile: digging the designation tile at (" COORD ")\n",COORDARGS(pos));
dig_type(map, pos, target_type);
// let light filter down to newly exposed tiles
@ -594,9 +732,14 @@ static void do_dig(color_ostream &out, std::vector<DFCoord> &dug_coords,
item_coords_t &item_coords, const dig_now_options &options) {
MapExtras::MapCache map;
Random::MersenneRNG rng;
DesignationJobs jobs;
DEBUG(general).print("do_dig(): starting..\n");
jobs.load(map);
rng.init();
DEBUG(general).print("do_dig(): reading map..\n");
std::unordered_set<designation> buffer;
// go down levels instead of up so stacked ramps behave as expected
for (int16_t z = options.end.z; z >= options.start.z; --z) {
for (int16_t y = options.start.y; y <= options.end.y; ++y) {
@ -609,49 +752,73 @@ static void do_dig(color_ostream &out, std::vector<DFCoord> &dug_coords,
DFCoord pos(x, y, z);
df::tile_designation td = map.designationAt(pos);
df::tile_occupancy to = map.occupancyAt(pos);
if (td.bits.dig != df::tile_dig_designation::No &&
!to.bits.dig_marked) {
std::vector<dug_tile_info> dug_tiles;
if (dig_tile(out, map, pos, td.bits.dig, dug_tiles)) {
for (auto info : dug_tiles) {
td = map.designationAt(info.pos);
td.bits.dig = df::tile_dig_designation::No;
map.setDesignationAt(info.pos, td);
dug_coords.push_back(info.pos);
refresh_adjacent_smooth_walls(map, info.pos);
if (info.imat < 0)
continue;
if (produces_item(options.boulder_percents,
map, rng, info)) {
auto k = std::make_pair(info.itype, info.imat);
item_coords[k].push_back(info.pos);
}
}
}
} else if (td.bits.smooth == 1) {
if (smooth_tile(out, map, pos)) {
td = map.designationAt(pos);
td.bits.smooth = 0;
map.setDesignationAt(pos, td);
}
} else if (to.bits.carve_track_north == 1
|| to.bits.carve_track_east == 1
|| to.bits.carve_track_south == 1
|| to.bits.carve_track_west == 1) {
if (carve_tile(map, pos, to)) {
to = map.occupancyAt(pos);
to.bits.carve_track_north = 0;
to.bits.carve_track_east = 0;
to.bits.carve_track_south = 0;
to.bits.carve_track_west = 0;
map.setOccupancyAt(pos, to);
if (jobs.count(pos)) {
buffer.emplace(jobs.get(pos));
jobs.remove(pos);
// if it does get removed, then we're gonna buffer the jobs info then remove the job
} else if ((td.bits.dig != df::tile_dig_designation::No && !to.bits.dig_marked)
|| td.bits.smooth == 1
|| to.bits.carve_track_north == 1
|| to.bits.carve_track_east == 1
|| to.bits.carve_track_south == 1
|| to.bits.carve_track_west == 1) {
// we're only buffering designations, so that processing doesn't affect what we're buffering
buffer.emplace(pos, td, to);
}
}
}
}
DEBUG(general).print("do_dig(): processing designations..\n");
// process designations
for(auto &d : buffer) {
auto pos = d.pos;
auto td = d.type;
auto to = d.occupancy;
if (td.bits.dig != df::tile_dig_designation::No && !to.bits.dig_marked) {
std::vector<dug_tile_info> dug_tiles;
if (dig_tile(out, map, pos, td.bits.dig, dug_tiles)) {
for (auto info: dug_tiles) {
td = map.designationAt(info.pos);
td.bits.dig = df::tile_dig_designation::No;
map.setDesignationAt(info.pos, td);
dug_coords.push_back(info.pos);
refresh_adjacent_smooth_walls(map, info.pos);
if (info.imat < 0)
continue;
if (produces_item(options.boulder_percents,
map, rng, info)) {
auto k = std::make_pair(info.itype, info.imat);
item_coords[k].push_back(info.pos);
}
}
}
} else if (td.bits.smooth == 1) {
if (smooth_tile(out, map, pos)) {
td = map.designationAt(pos);
td.bits.smooth = 0;
map.setDesignationAt(pos, td);
}
} else if (to.bits.carve_track_north == 1
|| to.bits.carve_track_east == 1
|| to.bits.carve_track_south == 1
|| to.bits.carve_track_west == 1) {
if (carve_tile(map, pos, to)) {
to = map.occupancyAt(pos);
to.bits.carve_track_north = 0;
to.bits.carve_track_east = 0;
to.bits.carve_track_south = 0;
to.bits.carve_track_west = 0;
map.setOccupancyAt(pos, to);
}
}
}
DEBUG(general).print("do_dig(): write changes to map..\n");
map.WriteAll();
}

@ -1,23 +0,0 @@
local _ENV = mkmodule('plugins.automaterial')
local buildingplan = require('plugins.buildingplan')
-- construct the building and register it with buildingplan for item selection
function build_with_buildingplan_box_select(subtype, x, y, z)
local pos = xyz2pos(x, y, z)
local bld, err = dfhack.buildings.constructBuilding{
type=df.building_type.Construction, subtype=subtype, pos=pos}
-- it's not a user error if we can't place a building here; just indicate
-- that no building was placed by returning false.
if err then return false end
buildingplan.addPlannedBuilding(bld)
return true
end
function build_with_buildingplan_ui()
for _,bld in ipairs(buildingplan.construct_buildings_from_ui_state()) do
buildingplan.addPlannedBuilding(bld)
end
end
return _ENV

File diff suppressed because it is too large Load Diff

@ -0,0 +1,731 @@
local _ENV = mkmodule('plugins.buildingplan.filterselection')
local gui = require('gui')
local pens = require('plugins.buildingplan.pens')
local widgets = require('gui.widgets')
local uibs = df.global.buildreq
local to_pen = dfhack.pen.parse
local function get_cur_filters()
return dfhack.buildings.getFiltersByType({}, uibs.building_type,
uibs.building_subtype, uibs.custom_type)
end
--------------------------------
-- Slider
--
Slider = defclass(Slider, widgets.Widget)
Slider.ATTRS{
num_stops=DEFAULT_NIL,
get_left_idx_fn=DEFAULT_NIL,
get_right_idx_fn=DEFAULT_NIL,
on_left_change=DEFAULT_NIL,
on_right_change=DEFAULT_NIL,
}
function Slider:preinit(init_table)
init_table.frame = init_table.frame or {}
init_table.frame.h = init_table.frame.h or 1
end
function Slider:init()
if self.num_stops < 2 then error('too few Slider stops') end
self.is_dragging_target = nil -- 'left', 'right', or 'both'
self.is_dragging_idx = nil -- offset from leftmost dragged tile
end
local function slider_get_width_per_idx(self)
return math.max(5, (self.frame_body.width-7) // (self.num_stops-1))
end
function Slider:onInput(keys)
if not keys._MOUSE_L_DOWN then return false end
local x = self:getMousePos()
if not x then return false end
local left_idx, right_idx = self.get_left_idx_fn(), self.get_right_idx_fn()
local width_per_idx = slider_get_width_per_idx(self)
local left_pos = width_per_idx*(left_idx-1)
local right_pos = width_per_idx*(right_idx-1) + 4
if x < left_pos then
self.on_left_change(self.get_left_idx_fn() - 1)
elseif x < left_pos+3 then
self.is_dragging_target = 'left'
self.is_dragging_idx = x - left_pos
elseif x < right_pos then
self.is_dragging_target = 'both'
self.is_dragging_idx = x - left_pos
elseif x < right_pos+3 then
self.is_dragging_target = 'right'
self.is_dragging_idx = x - right_pos
else
self.on_right_change(self.get_right_idx_fn() + 1)
end
return true
end
local function slider_do_drag(self, width_per_idx)
local x = self.frame_body:localXY(dfhack.screen.getMousePos())
local cur_pos = x - self.is_dragging_idx
cur_pos = math.max(0, cur_pos)
cur_pos = math.min(width_per_idx*(self.num_stops-1)+7, cur_pos)
local offset = self.is_dragging_target == 'right' and -2 or 1
local new_idx = math.max(0, cur_pos+offset)//width_per_idx + 1
local new_left_idx, new_right_idx
if self.is_dragging_target == 'right' then
new_right_idx = new_idx
else
new_left_idx = new_idx
if self.is_dragging_target == 'both' then
new_right_idx = new_left_idx + self.get_right_idx_fn() - self.get_left_idx_fn()
if new_right_idx > self.num_stops then
return
end
end
end
if new_left_idx and new_left_idx ~= self.get_left_idx_fn() then
self.on_left_change(new_left_idx)
end
if new_right_idx and new_right_idx ~= self.get_right_idx_fn() then
self.on_right_change(new_right_idx)
end
end
local SLIDER_LEFT_END = to_pen{ch=198, fg=COLOR_GREY, bg=COLOR_BLACK}
local SLIDER_TRACK = to_pen{ch=205, fg=COLOR_GREY, bg=COLOR_BLACK}
local SLIDER_TRACK_SELECTED = to_pen{ch=205, fg=COLOR_LIGHTGREEN, bg=COLOR_BLACK}
local SLIDER_TRACK_STOP = to_pen{ch=216, fg=COLOR_GREY, bg=COLOR_BLACK}
local SLIDER_TRACK_STOP_SELECTED = to_pen{ch=216, fg=COLOR_LIGHTGREEN, bg=COLOR_BLACK}
local SLIDER_RIGHT_END = to_pen{ch=181, fg=COLOR_GREY, bg=COLOR_BLACK}
local SLIDER_TAB_LEFT = to_pen{ch=60, fg=COLOR_BLACK, bg=COLOR_YELLOW}
local SLIDER_TAB_CENTER = to_pen{ch=9, fg=COLOR_BLACK, bg=COLOR_YELLOW}
local SLIDER_TAB_RIGHT = to_pen{ch=62, fg=COLOR_BLACK, bg=COLOR_YELLOW}
function Slider:onRenderBody(dc, rect)
local left_idx, right_idx = self.get_left_idx_fn(), self.get_right_idx_fn()
local width_per_idx = slider_get_width_per_idx(self)
-- draw track
dc:seek(1,0)
dc:char(nil, SLIDER_LEFT_END)
dc:char(nil, SLIDER_TRACK)
for stop_idx=1,self.num_stops-1 do
local track_stop_pen = SLIDER_TRACK_STOP_SELECTED
local track_pen = SLIDER_TRACK_SELECTED
if left_idx > stop_idx or right_idx < stop_idx then
track_stop_pen = SLIDER_TRACK_STOP
track_pen = SLIDER_TRACK
elseif right_idx == stop_idx then
track_pen = SLIDER_TRACK
end
dc:char(nil, track_stop_pen)
for i=2,width_per_idx do
dc:char(nil, track_pen)
end
end
if right_idx >= self.num_stops then
dc:char(nil, SLIDER_TRACK_STOP_SELECTED)
else
dc:char(nil, SLIDER_TRACK_STOP)
end
dc:char(nil, SLIDER_TRACK)
dc:char(nil, SLIDER_RIGHT_END)
-- draw tabs
dc:seek(width_per_idx*(left_idx-1))
dc:char(nil, SLIDER_TAB_LEFT)
dc:char(nil, SLIDER_TAB_CENTER)
dc:char(nil, SLIDER_TAB_RIGHT)
dc:seek(width_per_idx*(right_idx-1)+4)
dc:char(nil, SLIDER_TAB_LEFT)
dc:char(nil, SLIDER_TAB_CENTER)
dc:char(nil, SLIDER_TAB_RIGHT)
-- manage dragging
if self.is_dragging_target then
slider_do_drag(self, width_per_idx)
end
if df.global.enabler.mouse_lbut == 0 then
self.is_dragging_target = nil
self.is_dragging_idx = nil
end
end
--------------------------------
-- QualityAndMaterialsPage
--
QualityAndMaterialsPage = defclass(QualityAndMaterialsPage, widgets.Panel)
QualityAndMaterialsPage.ATTRS{
frame={t=0, l=0},
index=DEFAULT_NIL,
desc=DEFAULT_NIL,
}
local TYPE_COL_WIDTH = 20
local HEADER_HEIGHT = 7
local QUALITY_HEIGHT = 9
local FOOTER_HEIGHT = 4
-- returns whether the items matched by the specified filter can have a quality
-- rating. This also conveniently indicates whether an item can be decorated.
local function can_be_improved(idx)
local filter = get_cur_filters()[idx]
if filter.flags2 and filter.flags2.building_material then
return false;
end
return filter.item_type ~= df.item_type.WOOD and
filter.item_type ~= df.item_type.BLOCKS and
filter.item_type ~= df.item_type.BAR and
filter.item_type ~= df.item_type.BOULDER
end
local function mat_sort_by_name(a, b)
return a.name < b.name
end
local function mat_sort_by_quantity(a, b)
return a.quantity > b.quantity or
(a.quantity == b.quantity and mat_sort_by_name(a, b))
end
function QualityAndMaterialsPage:init()
self.dirty = true
self.summary = ''
local enable_item_quality = can_be_improved(self.index)
self:addviews{
widgets.Panel{
view_id='header',
frame={l=0, t=0, h=HEADER_HEIGHT, r=0},
frame_inset={l=1},
subviews={
widgets.Label{
frame={l=0, t=0},
text='Current filter:',
},
widgets.WrappedLabel{
frame={l=16, t=0, h=2, r=0},
text_pen=COLOR_LIGHTCYAN,
text_to_wrap=function() return self.summary end,
auto_height=false,
},
widgets.CycleHotkeyLabel{
view_id='mat_sort',
frame={l=0, t=3, w=21},
label='Sort by:',
key='CUSTOM_SHIFT_R',
options={
{label='name', value=mat_sort_by_name},
{label='available', value=mat_sort_by_quantity}
},
on_change=function() self.dirty = true end,
},
widgets.ToggleHotkeyLabel{
view_id='hide_zero',
frame={l=0, t=4, w=24},
label='Hide unavailable:',
key='CUSTOM_SHIFT_H',
initial_option=false,
on_change=function() self.dirty = true end,
},
widgets.EditField{
view_id='search',
frame={l=26, t=3},
label_text='Search: ',
on_char=function(ch) return ch:match('[%l -]') end,
},
widgets.Label{
frame={l=1, b=0},
text='Type',
text_pen=COLOR_LIGHTRED,
},
widgets.Label{
frame={l=TYPE_COL_WIDTH, b=0},
text='Material',
text_pen=COLOR_LIGHTRED,
},
},
},
widgets.Panel{
view_id='materials_lists',
frame={l=0, t=HEADER_HEIGHT, r=0, b=FOOTER_HEIGHT+QUALITY_HEIGHT},
frame_style=gui.INTERIOR_FRAME,
subviews={
widgets.List{
view_id='materials_categories',
frame={l=1, t=0, b=0, w=TYPE_COL_WIDTH-3},
scroll_keys={},
icon_width=2,
cursor_pen=COLOR_CYAN,
on_submit=self:callback('toggle_category'),
},
widgets.FilteredList{
view_id='materials_mats',
frame={l=TYPE_COL_WIDTH, t=0, r=0, b=0},
icon_width=2,
on_submit=self:callback('toggle_material'),
},
},
},
widgets.Panel{
view_id='divider',
frame={l=TYPE_COL_WIDTH-1, t=HEADER_HEIGHT, b=FOOTER_HEIGHT+QUALITY_HEIGHT, w=1},
on_render=self:callback('draw_divider'),
},
widgets.Panel{
view_id='quality_panel',
frame={l=0, r=0, h=QUALITY_HEIGHT, b=FOOTER_HEIGHT},
frame_style=gui.INTERIOR_FRAME,
frame_title='Item quality',
subviews={
widgets.CycleHotkeyLabel{
view_id='decorated',
frame={l=0, t=1, w=23},
key='CUSTOM_SHIFT_D',
label='Decorated only:',
options={
{label='No', value=false},
{label='Yes', value=true},
},
enabled=enable_item_quality,
on_change=self:callback('set_decorated'),
},
widgets.CycleHotkeyLabel{
view_id='min_quality',
frame={l=0, t=3, w=18},
label='Min quality:',
label_below=true,
key_back='CUSTOM_SHIFT_Z',
key='CUSTOM_SHIFT_X',
options={
{label='Ordinary', value=0},
{label='Well Crafted', value=1},
{label='Finely Crafted', value=2},
{label='Superior', value=3},
{label='Exceptional', value=4},
{label='Masterful', value=5},
{label='Artifact', value=6},
},
enabled=enable_item_quality,
on_change=function(val) self:set_min_quality(val+1) end,
},
widgets.CycleHotkeyLabel{
view_id='max_quality',
frame={r=1, t=3, w=18},
label='Max quality:',
label_below=true,
key_back='CUSTOM_SHIFT_Q',
key='CUSTOM_SHIFT_W',
options={
{label='Ordinary', value=0},
{label='Well Crafted', value=1},
{label='Finely Crafted', value=2},
{label='Superior', value=3},
{label='Exceptional', value=4},
{label='Masterful', value=5},
{label='Artifact', value=6},
},
enabled=enable_item_quality,
on_change=function(val) self:set_max_quality(val+1) end,
},
Slider{
frame={l=0, t=6},
num_stops=7,
get_left_idx_fn=function()
return self.subviews.min_quality:getOptionValue() + 1
end,
get_right_idx_fn=function()
return self.subviews.max_quality:getOptionValue() + 1
end,
on_left_change=self:callback('set_min_quality'),
on_right_change=self:callback('set_max_quality'),
active=enable_item_quality,
},
},
},
widgets.Panel{
view_id='footer',
frame={l=0, r=0, b=0, h=FOOTER_HEIGHT},
frame_inset={t=1, l=1},
subviews={
widgets.HotkeyLabel{
frame={l=0, t=0},
label='Toggle',
auto_width=true,
key='SELECT',
},
widgets.HotkeyLabel{
frame={l=0, t=2},
label='Done',
auto_width=true,
key='LEAVESCREEN',
},
widgets.HotkeyLabel{
frame={l=30, t=0},
label='Invert selection',
auto_width=true,
key='CUSTOM_SHIFT_I',
on_activate=self:callback('invert_materials'),
},
widgets.HotkeyLabel{
frame={l=30, t=2},
label='Reset filter',
auto_width=true,
key='CUSTOM_SHIFT_X',
on_activate=self:callback('clear_filter'),
},
},
}
}
-- replace the FilteredList's built-in EditField with our own
self.subviews.materials_mats.list.frame.t = 0
self.subviews.materials_mats.edit.visible = false
self.subviews.materials_mats.edit = self.subviews.search
self.subviews.search.on_change = self.subviews.materials_mats:callback('onFilterChange')
end
local MAT_ENABLED_PEN = to_pen{ch=string.char(251), fg=COLOR_LIGHTGREEN}
local MAT_DISABLED_PEN = to_pen{ch='x', fg=COLOR_RED}
local function make_cat_choice(label, cat, key, cats)
local enabled = cats[cat]
local icon = nil
if not cats.unset then
icon = enabled and MAT_ENABLED_PEN or MAT_DISABLED_PEN
end
return {
text=label,
key=key,
enabled=enabled,
cat=cat,
icon=icon,
}
end
local function make_mat_choice(name, props, enabled, cats)
local quantity = tonumber(props.count)
local text = ('%5d - %s'):format(quantity, name)
local icon = nil
if not cats.unset then
icon = enabled and MAT_ENABLED_PEN or MAT_DISABLED_PEN
end
return {
text=text,
enabled=enabled,
icon=icon,
name=name,
cat=props.category,
quantity=quantity,
}
end
function QualityAndMaterialsPage:refresh()
local summary = self.desc
local subviews = self.subviews
local buildingplan = require('plugins.buildingplan')
local heat = buildingplan.getHeatSafetyFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type)
if heat >= 2 then summary = 'Magma safe ' .. summary
elseif heat == 1 then summary = 'Fire safe ' .. summary
else summary = 'Any ' .. summary
end
local quality = buildingplan.getQualityFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1)
subviews.decorated:setOption(quality.decorated ~= 0)
subviews.min_quality:setOption(quality.min_quality)
subviews.max_quality:setOption(quality.max_quality)
local cats = buildingplan.getMaterialMaskFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1)
local category_choices={
make_cat_choice('Stone', 'stone', 'CUSTOM_SHIFT_S', cats),
make_cat_choice('Wood', 'wood', 'CUSTOM_SHIFT_O', cats),
make_cat_choice('Metal', 'metal', 'CUSTOM_SHIFT_M', cats),
make_cat_choice('Glass', 'glass', 'CUSTOM_SHIFT_G', cats),
}
self.subviews.materials_categories:setChoices(category_choices)
local mats = buildingplan.getMaterialFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1)
local mat_choices = {}
local hide_zero = self.subviews.hide_zero:getOptionValue()
local enabled_mat_names = {}
for name,props in pairs(mats) do
local enabled = props.enabled == 'true' and cats[props.category]
if not cats.unset and enabled then
table.insert(enabled_mat_names, name)
end
if not hide_zero or tonumber(props.count) > 0 then
table.insert(mat_choices, make_mat_choice(name, props, enabled, cats))
end
end
table.sort(mat_choices, self.subviews.mat_sort:getOptionValue())
local prev_filter = self.subviews.search.text
self.subviews.materials_mats:setChoices(mat_choices)
self.subviews.materials_mats:setFilter(prev_filter)
if #enabled_mat_names > 0 then
table.sort(enabled_mat_names)
summary = summary .. (' of %s'):format(table.concat(enabled_mat_names, ', '))
end
self.summary = summary
self.dirty = false
self:updateLayout()
end
function QualityAndMaterialsPage:toggle_category(_, choice)
local cats = {}
if not choice.icon then
-- toggling from unset to something is set
table.insert(cats, choice.cat)
else
choice.enabled = not choice.enabled
for _,c in ipairs(self.subviews.materials_categories:getChoices()) do
if c.enabled then
table.insert(cats, c.cat)
end
end
end
require('plugins.buildingplan').setMaterialMaskFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1, cats)
self.dirty = true
end
function QualityAndMaterialsPage:toggle_material(_, choice)
local mats = {}
if not choice.icon then
-- toggling from unset to something is set
table.insert(mats, choice.name)
else
for _,c in ipairs(self.subviews.materials_mats:getChoices()) do
local enabled = c.enabled
if choice.name == c.name then
enabled = not c.enabled
end
if enabled then
table.insert(mats, c.name)
end
end
end
require('plugins.buildingplan').setMaterialFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1, mats)
self.dirty = true
end
function QualityAndMaterialsPage:invert_materials()
local mats = {}
for _,c in ipairs(self.subviews.materials_mats:getChoices()) do
if not c.icon then return end
if not c.enabled then
table.insert(mats, c.name)
end
end
require('plugins.buildingplan').setMaterialFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1, mats)
self.dirty = true
end
function QualityAndMaterialsPage:clear_filter()
require('plugins.buildingplan').clearFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1)
self.dirty = true
end
function QualityAndMaterialsPage:set_decorated(decorated)
local subviews = self.subviews
require('plugins.buildingplan').setQualityFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1,
decorated and 1 or 0, subviews.min_quality:getOptionValue(), subviews.max_quality:getOptionValue())
self.dirty = true
end
function QualityAndMaterialsPage:set_min_quality(idx)
idx = math.min(6, math.max(0, idx-1))
local subviews = self.subviews
subviews.min_quality:setOption(idx)
if subviews.max_quality:getOptionValue() < idx then
subviews.max_quality:setOption(idx)
end
require('plugins.buildingplan').setQualityFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1,
subviews.decorated:getOptionValue() and 1 or 0, idx, subviews.max_quality:getOptionValue())
self.dirty = true
end
function QualityAndMaterialsPage:set_max_quality(idx)
idx = math.min(6, math.max(0, idx-1))
local subviews = self.subviews
subviews.max_quality:setOption(idx)
if subviews.min_quality:getOptionValue() > idx then
subviews.min_quality:setOption(idx)
end
require('plugins.buildingplan').setQualityFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1,
subviews.decorated:getOptionValue() and 1 or 0, subviews.min_quality:getOptionValue(), idx)
self.dirty = true
end
function QualityAndMaterialsPage:draw_divider(dc)
local y2 = dc.height - 1
for y=0,y2 do
dc:seek(0, y)
if y == 0 then
dc:char(nil, pens.VERT_TOP_PEN)
elseif y == y2 then
dc:char(nil, pens.VERT_BOT_PEN)
else
dc:char(nil, pens.VERT_MID_PEN)
end
end
end
function QualityAndMaterialsPage:onRenderFrame(dc, rect)
QualityAndMaterialsPage.super.onRenderFrame(self, dc, rect)
if self.dirty then
self:refresh()
end
end
--------------------------------
-- GlobalSettingsPage
--
GlobalSettingsPage = defclass(GlobalSettingsPage, widgets.ResizingPanel)
GlobalSettingsPage.ATTRS{
autoarrange_subviews=true,
frame={t=0, l=0},
frame_style=gui.INTERIOR_FRAME,
}
function GlobalSettingsPage:init()
self:addviews{
widgets.WrappedLabel{
frame={l=0},
text_to_wrap='These options will affect the selection of "Generic Materials" for all future buildings.',
},
widgets.Panel{
frame={h=1},
},
widgets.ToggleHotkeyLabel{
view_id='blocks',
frame={l=0},
key='CUSTOM_B',
label='Blocks',
label_width=8,
on_change=self:callback('update_setting', 'blocks'),
},
widgets.ToggleHotkeyLabel{
view_id='logs',
frame={l=0},
key='CUSTOM_L',
label='Logs',
label_width=8,
on_change=self:callback('update_setting', 'logs'),
},
widgets.ToggleHotkeyLabel{
view_id='boulders',
frame={l=0},
key='CUSTOM_O',
label='Boulders',
label_width=8,
on_change=self:callback('update_setting', 'boulders'),
},
widgets.ToggleHotkeyLabel{
view_id='bars',
frame={l=0},
key='CUSTOM_R',
label='Bars',
label_width=8,
on_change=self:callback('update_setting', 'bars'),
},
}
self:init_settings()
end
function GlobalSettingsPage:init_settings()
local settings = require('plugins.buildingplan').getGlobalSettings()
local subviews = self.subviews
subviews.blocks:setOption(settings.blocks)
subviews.logs:setOption(settings.logs)
subviews.boulders:setOption(settings.boulders)
subviews.bars:setOption(settings.bars)
end
function GlobalSettingsPage:update_setting(setting, val)
dfhack.run_command('buildingplan', 'set', setting, tostring(val))
self:init_settings()
end
--------------------------------
-- FilterSelection
--
FilterSelection = defclass(FilterSelection, widgets.Window)
FilterSelection.ATTRS{
frame_title='Choose filters',
frame={w=55, h=53, l=30, t=8},
frame_inset={t=1},
resizable=true,
index=DEFAULT_NIL,
desc=DEFAULT_NIL,
autoarrange_subviews=true,
}
function FilterSelection:init()
self:addviews{
widgets.TabBar{
frame={t=0},
labels={
'Quality and materials',
'Global settings',
},
on_select=function(idx)
self.subviews.pages:setSelected(idx)
self:updateLayout()
end,
get_cur_page=function() return self.subviews.pages:getSelected() end,
key='CUSTOM_CTRL_T',
},
widgets.Widget{
frame={h=1},
},
widgets.Pages{
view_id='pages',
frame={t=5, l=0, b=0, r=0},
subviews={
QualityAndMaterialsPage{
index=self.index,
desc=self.desc
},
GlobalSettingsPage{},
},
},
}
end
FilterSelectionScreen = defclass(FilterSelectionScreen, gui.ZScreen)
FilterSelectionScreen.ATTRS {
focus_path='dwarfmode/Building/Placement/dfhack/lua/buildingplan/filterselection',
pass_movement_keys=true,
pass_mouse_clicks=false,
defocusable=false,
index=DEFAULT_NIL,
desc=DEFAULT_NIL,
}
function FilterSelectionScreen:init()
self:addviews{
FilterSelection{
index=self.index,
desc=self.desc
}
}
end
function FilterSelectionScreen:onShow()
-- don't let the building "shadow" follow the mouse cursor while this screen is open
df.global.game.main_interface.bottom_mode_selected = -1
end
function FilterSelectionScreen:onDismiss()
-- re-enable building shadow
df.global.game.main_interface.bottom_mode_selected = df.main_bottom_mode_type.BUILDING_PLACEMENT
end
return _ENV

@ -0,0 +1,148 @@
local _ENV = mkmodule('plugins.buildingplan.inspectoroverlay')
local gui = require('gui')
local overlay = require('plugins.overlay')
local widgets = require('gui.widgets')
reset_inspector_flag = false
local function get_building_filters()
local bld = dfhack.gui.getSelectedBuilding()
return dfhack.buildings.getFiltersByType({},
bld:getType(), bld:getSubtype(), bld:getCustomType())
end
--------------------------------
-- InspectorLine
--
InspectorLine = defclass(InspectorLine, widgets.Panel)
InspectorLine.ATTRS{
idx=DEFAULT_NIL,
}
function InspectorLine:init()
self.frame.h = 2
self.visible = function() return #get_building_filters() >= self.idx end
self:addviews{
widgets.Label{
frame={t=0, l=0},
text={{text=self:callback('get_desc_string')}},
},
widgets.Label{
frame={t=1, l=2},
text={{text=self:callback('get_status_line')}},
},
}
end
function InspectorLine:get_desc_string()
if self.desc then return self.desc end
self.desc = require('plugins.buildingplan').getDescString(dfhack.gui.getSelectedBuilding(), self.idx-1)
return self.desc
end
function InspectorLine:get_status_line()
if self.status then return self.status end
local queue_pos = require('plugins.buildingplan').getQueuePosition(dfhack.gui.getSelectedBuilding(), self.idx-1)
if queue_pos <= 0 then
return 'Item attached'
end
self.status = ('Position in line: %d'):format(queue_pos)
return self.status
end
function InspectorLine:reset()
self.desc = nil
self.status = nil
end
--------------------------------
-- InspectorOverlay
--
InspectorOverlay = defclass(InspectorOverlay, overlay.OverlayWidget)
InspectorOverlay.ATTRS{
default_pos={x=-41,y=14},
default_enabled=true,
viewscreens='dwarfmode/ViewSheets/BUILDING',
frame={w=30, h=15},
frame_style=gui.MEDIUM_FRAME,
frame_background=gui.CLEAR_PEN,
}
function InspectorOverlay:init()
self:addviews{
widgets.Label{
frame={t=0, l=0},
text='Waiting for items:',
},
InspectorLine{view_id='item1', frame={t=2, l=0}, idx=1},
InspectorLine{view_id='item2', frame={t=4, l=0}, idx=2},
InspectorLine{view_id='item3', frame={t=6, l=0}, idx=3},
InspectorLine{view_id='item4', frame={t=8, l=0}, idx=4},
widgets.HotkeyLabel{
frame={t=11, l=0},
label='adjust filters',
key='CUSTOM_CTRL_F',
visible=false, -- until implemented
},
widgets.HotkeyLabel{
frame={t=12, l=0},
label='make top priority',
key='CUSTOM_CTRL_T',
on_activate=self:callback('make_top_priority'),
},
}
end
function InspectorOverlay:reset()
self.subviews.item1:reset()
self.subviews.item2:reset()
self.subviews.item3:reset()
self.subviews.item4:reset()
reset_inspector_flag = false
end
function InspectorOverlay:make_top_priority()
require('plugins.buildingplan').makeTopPriority(dfhack.gui.getSelectedBuilding())
self:reset()
end
local RESUME_BUTTON_FRAME = {t=15, h=3, r=73, w=25}
local function mouse_is_over_resume_button(rect)
local x,y = dfhack.screen.getMousePos()
if not x then return false end
if y < RESUME_BUTTON_FRAME.t or y > RESUME_BUTTON_FRAME.t + RESUME_BUTTON_FRAME.h - 1 then
return false
end
if x > rect.x2 - RESUME_BUTTON_FRAME.r + 1 or x < rect.x2 - RESUME_BUTTON_FRAME.r - RESUME_BUTTON_FRAME.w + 2 then
return false
end
return true
end
function InspectorOverlay:onInput(keys)
if not require('plugins.buildingplan').isPlannedBuilding(dfhack.gui.getSelectedBuilding()) then
return false
end
if keys._MOUSE_L_DOWN and mouse_is_over_resume_button(self.frame_parent_rect) then
return true
elseif keys._MOUSE_L_DOWN or keys._MOUSE_R_DOWN or keys.LEAVESCREEN then
self:reset()
end
return InspectorOverlay.super.onInput(self, keys)
end
function InspectorOverlay:render(dc)
if not require('plugins.buildingplan').isPlannedBuilding(dfhack.gui.getSelectedBuilding()) then
return
end
if reset_inspector_flag then
self:reset()
end
InspectorOverlay.super.render(self, dc)
end
return _ENV

@ -0,0 +1,347 @@
local _ENV = mkmodule('plugins.buildingplan.itemselection')
local gui = require('gui')
local pens = require('plugins.buildingplan.pens')
local utils = require('utils')
local widgets = require('gui.widgets')
local uibs = df.global.buildreq
local to_pen = dfhack.pen.parse
local BUILD_TEXT_PEN = to_pen{fg=COLOR_BLACK, bg=COLOR_GREEN, keep_lower=true}
local BUILD_TEXT_HPEN = to_pen{fg=COLOR_WHITE, bg=COLOR_GREEN, keep_lower=true}
-- map of building type -> {set=set of recently used, list=list of recently used}
-- most recent entries are at the *end* of the list
local recently_used = {}
local function sort_by_type(a, b)
local ad, bd = a.data, b.data
return ad.item_type < bd.item_type or
(ad.item_type == bd.item_type and ad.item_subtype < bd.item_subtype) or
(ad.item_type == bd.item_type and ad.item_subtype == bd.item_subtype and a.search_key < b.search_key) or
(ad.item_type == bd.item_type and ad.item_subtype == bd.item_subtype and a.search_key == b.search_key and ad.quality > bd.quality)
end
local function sort_by_recency(a, b)
local tracker = recently_used[uibs.building_type]
if not tracker then return sort_by_type(a, b) end
local recent_a, recent_b = tracker.set[a.search_key], tracker.set[b.search_key]
-- if they're both in the set, return the one with the greater index,
-- indicating more recent
if recent_a and recent_b then return recent_a > recent_b end
if recent_a and not recent_b then return true end
if not recent_a and recent_b then return false end
return sort_by_type(a, b)
end
local function sort_by_name(a, b)
return a.search_key < b.search_key or
(a.search_key == b.search_key and sort_by_type(a, b))
end
local function sort_by_quantity(a, b)
local ad, bd = a.data, b.data
return ad.quantity > bd.quantity or
(ad.quantity == bd.quantity and sort_by_type(a, b))
end
ItemSelection = defclass(ItemSelection, widgets.Window)
ItemSelection.ATTRS{
frame_title='Choose items',
frame={w=56, h=20, l=4, t=8},
resizable=true,
index=DEFAULT_NIL,
desc=DEFAULT_NIL,
quantity=DEFAULT_NIL,
on_submit=DEFAULT_NIL,
on_cancel=DEFAULT_NIL,
}
function ItemSelection:init()
self.num_selected = 0
self.selected_set = {}
local plural = self.quantity == 1 and '' or 's'
self:addviews{
widgets.Label{
frame={t=0, l=0, r=10},
text={
self.desc,
plural,
NEWLINE,
('Select up to %d item%s ('):format(self.quantity, plural),
{text=function() return self.num_selected end},
' selected)',
},
},
widgets.Label{
frame={r=0, w=9, t=0, h=3},
text_pen=BUILD_TEXT_PEN,
text_hpen=BUILD_TEXT_HPEN,
text={
' ', NEWLINE,
' Build ', NEWLINE,
' ',
},
on_click=self:callback('submit'),
},
widgets.FilteredList{
view_id='flist',
frame={t=3, l=0, r=0, b=4},
case_sensitive=false,
choices=self:get_choices(sort_by_recency),
icon_width=2,
on_submit=self:callback('toggle_group'),
edit_on_char=function(ch) return ch:match('[%l -]') end,
},
widgets.CycleHotkeyLabel{
frame={l=0, b=2},
key='CUSTOM_SHIFT_R',
label='Sort by:',
options={
{label='Recently used', value=sort_by_recency},
{label='Name', value=sort_by_name},
{label='Amount', value=sort_by_quantity},
},
on_change=self:callback('on_sort'),
},
widgets.HotkeyLabel{
frame={l=0, b=1},
key='SELECT',
label='Use all/none',
auto_width=true,
on_activate=function() self:toggle_group(self.subviews.flist.list:getSelected()) end,
},
widgets.HotkeyLabel{
frame={l=22, b=1},
key='CUSTOM_SHIFT_B',
label='Build',
auto_width=true,
on_activate=self:callback('submit'),
},
widgets.HotkeyLabel{
frame={l=38, b=1},
key='LEAVESCREEN',
label='Go back',
auto_width=true,
on_activate=self:callback('on_cancel'),
},
widgets.HotkeyLabel{
frame={l=0, b=0},
key='KEYBOARD_CURSOR_RIGHT_FAST',
key_sep=' : ',
label='Use one',
auto_width=true,
on_activate=function() self:increment_group(self.subviews.flist.list:getSelected()) end,
},
widgets.Label{
frame={l=6, b=0, w=5},
text_pen=COLOR_LIGHTGREEN,
text='Right',
},
widgets.HotkeyLabel{
frame={l=23, b=0},
key='KEYBOARD_CURSOR_LEFT_FAST',
key_sep=' : ',
label='Use one fewer',
auto_width=true,
on_activate=function() self:decrement_group(self.subviews.flist.list:getSelected()) end,
},
widgets.Label{
frame={l=29, b=0, w=4},
text_pen=COLOR_LIGHTGREEN,
text='Left',
},
}
end
-- resort and restore selection
function ItemSelection:on_sort(sort_fn)
local flist = self.subviews.flist
local saved_filter = flist:getFilter()
flist:setFilter('')
flist:setChoices(self:get_choices(sort_fn), flist:getSelected())
flist:setFilter(saved_filter)
end
local function make_search_key(str)
local out = ''
for c in str:gmatch("[%w%s]") do
out = out .. c
end
return out
end
function ItemSelection:get_choices(sort_fn)
local item_ids = require('plugins.buildingplan').getAvailableItems(uibs.building_type,
uibs.building_subtype, uibs.custom_type, self.index-1)
local buckets = {}
for _,item_id in ipairs(item_ids) do
local item = df.item.find(item_id)
if not item then goto continue end
local desc = dfhack.items.getDescription(item, 0, true)
if buckets[desc] then
local bucket = buckets[desc]
table.insert(bucket.data.item_ids, item_id)
bucket.data.quantity = bucket.data.quantity + 1
else
local entry = {
search_key=make_search_key(desc),
icon=self:callback('get_entry_icon', item_id),
data={
item_ids={item_id},
item_type=item:getType(),
item_subtype=item:getSubtype(),
quantity=1,
quality=item:getQuality(),
selected=0,
},
}
buckets[desc] = entry
end
::continue::
end
local choices = {}
for desc,choice in pairs(buckets) do
local data = choice.data
choice.text = {
{width=10, text=function() return ('[%d/%d]'):format(data.selected, data.quantity) end},
{gap=2, text=desc},
}
table.insert(choices, choice)
end
table.sort(choices, sort_fn)
return choices
end
function ItemSelection:increment_group(idx, choice)
local data = choice.data
if self.quantity <= self.num_selected then return false end
if data.selected >= data.quantity then return false end
data.selected = data.selected + 1
self.num_selected = self.num_selected + 1
local item_id = data.item_ids[data.selected]
self.selected_set[item_id] = true
return true
end
function ItemSelection:decrement_group(idx, choice)
local data = choice.data
if data.selected <= 0 then return false end
local item_id = data.item_ids[data.selected]
self.selected_set[item_id] = nil
self.num_selected = self.num_selected - 1
data.selected = data.selected - 1
return true
end
function ItemSelection:toggle_group(idx, choice)
local data = choice.data
if data.selected > 0 then
while self:decrement_group(idx, choice) do end
else
while self:increment_group(idx, choice) do end
end
end
function ItemSelection:get_entry_icon(item_id)
return self.selected_set[item_id] and pens.SELECTED_ITEM_PEN or nil
end
local function track_recently_used(choices)
-- use same set for all subtypes
local tracker = ensure_key(recently_used, uibs.building_type)
for _,choice in ipairs(choices) do
local data = choice.data
if data.selected <= 0 then goto continue end
local key = choice.search_key
local recent_set = ensure_key(tracker, 'set')
local recent_list = ensure_key(tracker, 'list')
if recent_set[key] then
if recent_list[#recent_list] ~= key then
for i,v in ipairs(recent_list) do
if v == key then
table.remove(recent_list, i)
table.insert(recent_list, key)
break
end
end
tracker.set = utils.invert(recent_list)
end
else
-- only keep most recent 10
if #recent_list >= 10 then
-- remove least recently used from list and set
recent_set[table.remove(recent_list, 1)] = nil
end
table.insert(recent_list, key)
recent_set[key] = #recent_list
end
::continue::
end
end
function ItemSelection:submit()
local selected_items = {}
for item_id in pairs(self.selected_set) do
table.insert(selected_items, item_id)
end
if #selected_items > 0 then
track_recently_used(self.subviews.flist:getChoices())
end
self.on_submit(selected_items)
end
function ItemSelection:onInput(keys)
if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then
self.on_cancel()
return true
elseif keys._MOUSE_L_DOWN then
local list = self.subviews.flist.list
local idx = list:getIdxUnderMouse()
if idx then
list:setSelected(idx)
local modstate = dfhack.internal.getModstate()
if modstate & 2 > 0 then -- ctrl
local choice = list:getChoices()[idx]
if modstate & 1 > 0 then -- shift
self:decrement_group(idx, choice)
else
self:increment_group(idx, choice)
end
return true
end
end
end
return ItemSelection.super.onInput(self, keys)
end
ItemSelectionScreen = defclass(ItemSelectionScreen, gui.ZScreen)
ItemSelectionScreen.ATTRS {
focus_path='dwarfmode/Building/Placement/dfhack/lua/buildingplan/itemselection',
force_pause=true,
pass_movement_keys=true,
pass_pause=false,
pass_mouse_clicks=false,
defocusable=false,
index=DEFAULT_NIL,
desc=DEFAULT_NIL,
quantity=DEFAULT_NIL,
on_submit=DEFAULT_NIL,
on_cancel=DEFAULT_NIL,
}
function ItemSelectionScreen:init()
self:addviews{
ItemSelection{
index=self.index,
desc=self.desc,
quantity=self.quantity,
on_submit=self.on_submit,
on_cancel=self.on_cancel,
}
}
end
return _ENV

@ -0,0 +1,31 @@
local _ENV = mkmodule('plugins.buildingplan.pens')
GOOD_TILE_PEN, BAD_TILE_PEN = nil, nil
VERT_TOP_PEN, VERT_MID_PEN, VERT_BOT_PEN = nil, nil, nil
BUTTON_START_PEN, BUTTON_END_PEN = nil, nil
SELECTED_ITEM_PEN = nil
local to_pen = dfhack.pen.parse
local tp = function(base, offset)
if base == -1 then return nil end
return base + offset
end
function reload_pens()
GOOD_TILE_PEN = to_pen{ch='o', fg=COLOR_GREEN, tile=dfhack.screen.findGraphicsTile('CURSORS', 1, 2)}
BAD_TILE_PEN = to_pen{ch='X', fg=COLOR_RED, tile=dfhack.screen.findGraphicsTile('CURSORS', 3, 0)}
local tb_texpos = dfhack.textures.getThinBordersTexposStart()
VERT_TOP_PEN = to_pen{tile=tp(tb_texpos, 10), ch=194, fg=COLOR_GREY, bg=COLOR_BLACK}
VERT_MID_PEN = to_pen{tile=tp(tb_texpos, 4), ch=192, fg=COLOR_GREY, bg=COLOR_BLACK}
VERT_BOT_PEN = to_pen{tile=tp(tb_texpos, 11), ch=179, fg=COLOR_GREY, bg=COLOR_BLACK}
local cp_texpos = dfhack.textures.getControlPanelTexposStart()
BUTTON_START_PEN = to_pen{tile=tp(cp_texpos, 13), ch='[', fg=COLOR_YELLOW}
BUTTON_END_PEN = to_pen{tile=tp(cp_texpos, 15), ch=']', fg=COLOR_YELLOW}
SELECTED_ITEM_PEN = to_pen{tile=tp(cp_texpos, 9), ch=string.char(251), fg=COLOR_YELLOW}
end
reload_pens()
return _ENV

@ -0,0 +1,772 @@
local _ENV = mkmodule('plugins.buildingplan.planneroverlay')
local itemselection = require('plugins.buildingplan.itemselection')
local filterselection = require('plugins.buildingplan.filterselection')
local gui = require('gui')
local guidm = require('gui.dwarfmode')
local overlay = require('plugins.overlay')
local pens = require('plugins.buildingplan.pens')
local utils = require('utils')
local widgets = require('gui.widgets')
require('dfhack.buildings')
local uibs = df.global.buildreq
reset_counts_flag = false
local function get_cur_filters()
return dfhack.buildings.getFiltersByType({}, uibs.building_type,
uibs.building_subtype, uibs.custom_type)
end
local function is_choosing_area()
return uibs.selection_pos.x >= 0
end
local function get_cur_area_dims(placement_data)
if not placement_data and not is_choosing_area() then return 1, 1, 1 end
local selection_pos = placement_data and placement_data.p1 or uibs.selection_pos
local pos = placement_data and placement_data.p2 or uibs.pos
return math.abs(selection_pos.x - pos.x) + 1,
math.abs(selection_pos.y - pos.y) + 1,
math.abs(selection_pos.z - pos.z) + 1
end
local function is_pressure_plate()
return uibs.building_type == df.building_type.Trap
and uibs.building_subtype == df.trap_type.PressurePlate
end
local function is_weapon_trap()
return uibs.building_type == df.building_type.Trap
and uibs.building_subtype == df.trap_type.WeaponTrap
end
-- adjusted from CycleHotkeyLabel on the planner panel
local weapon_quantity = 1
local function get_quantity(filter, hollow, placement_data)
if is_pressure_plate() then
local flags = uibs.plate_info.flags
return (flags.units and 1 or 0) + (flags.water and 1 or 0) +
(flags.magma and 1 or 0) + (flags.track and 1 or 0)
elseif is_weapon_trap() and filter.vector_id == df.job_item_vector_id.ANY_WEAPON then
return weapon_quantity
end
local quantity = filter.quantity or 1
local dimx, dimy, dimz = get_cur_area_dims(placement_data)
if quantity < 1 then
return (((dimx * dimy) // 4) + 1) * dimz
end
if hollow and dimx > 2 and dimy > 2 then
return quantity * (2*dimx + 2*dimy - 4) * dimz
end
return quantity * dimx * dimy * dimz
end
local function cur_building_has_no_area()
if uibs.building_type == df.building_type.Construction then return false end
local filters = dfhack.buildings.getFiltersByType({},
uibs.building_type, uibs.building_subtype, uibs.custom_type)
-- this works because all variable-size buildings have either no item
-- filters or a quantity of -1 for their first (and only) item
return filters and filters[1] and (not filters[1].quantity or filters[1].quantity > 0)
end
local function is_plannable()
return get_cur_filters() and
not (uibs.building_type == df.building_type.Construction
and uibs.building_subtype == df.construction_type.TrackNSEW)
end
local function is_construction()
return uibs.building_type == df.building_type.Construction
end
local function is_stairs()
return is_construction()
and uibs.building_subtype == df.construction_type.UpDownStair
end
local direction_panel_frame = {t=4, h=13, w=46, r=28}
local direction_panel_types = utils.invert{
df.building_type.Bridge,
df.building_type.ScrewPump,
df.building_type.WaterWheel,
df.building_type.AxleHorizontal,
df.building_type.Rollers,
}
local function has_direction_panel()
return direction_panel_types[uibs.building_type]
or (uibs.building_type == df.building_type.Trap
and uibs.building_subtype == df.trap_type.TrackStop)
end
local pressure_plate_panel_frame = {t=4, h=37, w=46, r=28}
local function has_pressure_plate_panel()
return is_pressure_plate()
end
local function is_over_options_panel()
local frame = nil
if has_direction_panel() then
frame = direction_panel_frame
elseif has_pressure_plate_panel() then
frame = pressure_plate_panel_frame
else
return false
end
local v = widgets.Widget{frame=frame}
local rect = gui.mkdims_wh(0, 0, dfhack.screen.getWindowSize())
v:updateLayout(gui.ViewRect{rect=rect})
return v:getMousePos()
end
--------------------------------
-- ItemLine
--
ItemLine = defclass(ItemLine, widgets.Panel)
ItemLine.ATTRS{
idx=DEFAULT_NIL,
is_selected_fn=DEFAULT_NIL,
is_hollow_fn=DEFAULT_NIL,
on_select=DEFAULT_NIL,
on_filter=DEFAULT_NIL,
on_clear_filter=DEFAULT_NIL,
}
function ItemLine:init()
self.frame.h = 1
self.visible = function() return #get_cur_filters() >= self.idx end
self:addviews{
widgets.Label{
frame={t=0, l=0},
text='*',
auto_width=true,
visible=self.is_selected_fn,
},
widgets.Label{
frame={t=0, l=25},
text={
{tile=pens.BUTTON_START_PEN},
{gap=6, tile=pens.BUTTON_END_PEN},
},
auto_width=true,
on_click=function() self.on_filter(self.idx) end,
},
widgets.Label{
frame={t=0, l=33},
text={
{tile=pens.BUTTON_START_PEN},
{gap=1, tile=pens.BUTTON_END_PEN},
},
auto_width=true,
on_click=function() self.on_clear_filter(self.idx) end,
},
widgets.Label{
frame={t=0, l=2},
text={
{width=21, text=self:callback('get_item_line_text')},
{gap=3, text='filter', pen=COLOR_GREEN},
{gap=2, text='x', pen=self:callback('get_x_pen')},
{gap=3, text=function() return self.note end,
pen=function() return self.note_pen end},
},
},
}
end
function ItemLine:reset()
self.desc = nil
self.available = nil
end
function ItemLine:onInput(keys)
if keys._MOUSE_L_DOWN and self:getMousePos() then
self.on_select(self.idx)
end
return ItemLine.super.onInput(self, keys)
end
function ItemLine:get_x_pen()
return require('plugins.buildingplan').hasFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.idx-1) and
COLOR_GREEN or COLOR_GREY
end
function ItemLine:get_item_line_text()
local idx = self.idx
local filter = get_cur_filters()[idx]
local quantity = get_quantity(filter, self.is_hollow_fn())
local buildingplan = require('plugins.buildingplan')
self.desc = self.desc or buildingplan.get_desc(filter)
self.available = self.available or buildingplan.countAvailableItems(
uibs.building_type, uibs.building_subtype, uibs.custom_type, idx - 1)
if self.available >= quantity then
self.note_pen = COLOR_GREEN
self.note = 'Available now'
else
self.note_pen = COLOR_YELLOW
self.note = 'Will link later'
end
return ('%d %s%s'):format(quantity, self.desc, quantity == 1 and '' or 's')
end
function ItemLine:reduce_quantity(used_quantity)
if not self.available then return end
local filter = get_cur_filters()[self.idx]
used_quantity = used_quantity or get_quantity(filter, self.is_hollow_fn())
self.available = math.max(0, self.available - used_quantity)
end
local function get_placement_errors()
local out = ''
for _,str in ipairs(uibs.errors) do
if #out > 0 then out = out .. NEWLINE end
out = out .. str.value
end
return out
end
--------------------------------
-- PlannerOverlay
--
PlannerOverlay = defclass(PlannerOverlay, overlay.OverlayWidget)
PlannerOverlay.ATTRS{
default_pos={x=5,y=9},
default_enabled=true,
viewscreens='dwarfmode/Building/Placement',
frame={w=56, h=20},
}
function PlannerOverlay:init()
self.selected = 1
local main_panel = widgets.Panel{
view_id='main',
frame={t=0, l=0, r=0, h=14},
frame_style=gui.MEDIUM_FRAME,
frame_background=gui.CLEAR_PEN,
}
local function make_is_selected_fn(idx)
return function() return self.selected == idx end
end
local function on_select_fn(idx)
self.selected = idx
end
local function is_hollow_fn()
return self.subviews.hollow:getOptionValue()
end
local buildingplan = require('plugins.buildingplan')
main_panel:addviews{
widgets.Label{
frame={},
auto_width=true,
text='No items required.',
visible=function() return #get_cur_filters() == 0 end,
},
ItemLine{view_id='item1', frame={t=0, l=0, r=0}, idx=1,
is_selected_fn=make_is_selected_fn(1), is_hollow_fn=is_hollow_fn,
on_select=on_select_fn, on_filter=self:callback('set_filter'),
on_clear_filter=self:callback('clear_filter')},
ItemLine{view_id='item2', frame={t=2, l=0, r=0}, idx=2,
is_selected_fn=make_is_selected_fn(2), is_hollow_fn=is_hollow_fn,
on_select=on_select_fn, on_filter=self:callback('set_filter'),
on_clear_filter=self:callback('clear_filter')},
ItemLine{view_id='item3', frame={t=4, l=0, r=0}, idx=3,
is_selected_fn=make_is_selected_fn(3), is_hollow_fn=is_hollow_fn,
on_select=on_select_fn, on_filter=self:callback('set_filter'),
on_clear_filter=self:callback('clear_filter')},
ItemLine{view_id='item4', frame={t=6, l=0, r=0}, idx=4,
is_selected_fn=make_is_selected_fn(4), is_hollow_fn=is_hollow_fn,
on_select=on_select_fn, on_filter=self:callback('set_filter'),
on_clear_filter=self:callback('clear_filter')},
widgets.CycleHotkeyLabel{
view_id='hollow',
frame={t=3, l=4},
key='CUSTOM_H',
label='Hollow area:',
visible=is_construction,
options={
{label='No', value=false},
{label='Yes', value=true},
},
},
widgets.CycleHotkeyLabel{
view_id='stairs_top_subtype',
frame={t=4, l=4},
key='CUSTOM_R',
label='Top Stair Type: ',
visible=is_stairs,
options={
{label='Auto', value='auto'},
{label='UpDown', value=df.construction_type.UpDownStair},
{label='Down', value=df.construction_type.DownStair},
},
},
widgets.CycleHotkeyLabel {
view_id='stairs_bottom_subtype',
frame={t=5, l=4},
key='CUSTOM_B',
label='Bottom Stair Type:',
visible=is_stairs,
options={
{label='Auto', value='auto'},
{label='UpDown', value=df.construction_type.UpDownStair},
{label='Up', value=df.construction_type.UpStair},
},
},
widgets.CycleHotkeyLabel {
view_id='weapons',
frame={t=5, l=4},
key='CUSTOM_T',
key_back='CUSTOM_SHIFT_T',
label='Num weapons:',
visible=is_weapon_trap,
options={1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
on_change=function(val) weapon_quantity = val end,
},
widgets.Label{
frame={b=3, l=17},
text={
'Selected area: ',
{text=function()
return ('%dx%dx%d'):format(get_cur_area_dims(self.saved_placement))
end
},
},
visible=function()
return not cur_building_has_no_area() and (self.saved_placement or is_choosing_area())
end,
},
widgets.Panel{
visible=function() return #get_cur_filters() > 0 end,
subviews={
widgets.HotkeyLabel{
frame={b=1, l=0},
key='STRING_A042',
auto_width=true,
enabled=function() return #get_cur_filters() > 1 end,
on_activate=function() self.selected = ((self.selected - 2) % #get_cur_filters()) + 1 end,
},
widgets.HotkeyLabel{
frame={b=1, l=1},
key='STRING_A047',
label='Prev/next item',
auto_width=true,
enabled=function() return #get_cur_filters() > 1 end,
on_activate=function() self.selected = (self.selected % #get_cur_filters()) + 1 end,
},
widgets.HotkeyLabel{
frame={b=1, l=21},
key='CUSTOM_F',
label='Set filter',
auto_width=true,
on_activate=function() self:set_filter(self.selected) end,
},
widgets.HotkeyLabel{
frame={b=1, l=37},
key='CUSTOM_X',
label='Clear filter',
auto_width=true,
on_activate=function() self:clear_filter(self.selected) end,
enabled=function()
return buildingplan.hasFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.selected - 1)
end
},
widgets.CycleHotkeyLabel{
view_id='choose',
frame={b=0, l=0, w=25},
key='CUSTOM_I',
label='Choose from items:',
options={{label='Yes', value=true},
{label='No', value=false}},
initial_option=false,
enabled=function()
for idx = 1,4 do
if (self.subviews['item'..idx].available or 0) > 0 then
return true
end
end
end,
},
widgets.CycleHotkeyLabel{
view_id='safety',
frame={b=0, l=29, w=25},
key='CUSTOM_G',
label='Building safety:',
options={
{label='Any', value=0},
{label='Magma', value=2, pen=COLOR_RED},
{label='Fire', value=1, pen=COLOR_LIGHTRED},
},
initial_option=0,
on_change=function(heat)
buildingplan.setHeatSafetyFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, heat)
end,
},
},
},
}
local error_panel = widgets.ResizingPanel{
view_id='errors',
frame={t=14, l=0, r=0},
frame_style=gui.MEDIUM_FRAME,
frame_background=gui.CLEAR_PEN,
}
error_panel:addviews{
widgets.WrappedLabel{
frame={t=0, l=0, r=0},
text_pen=COLOR_LIGHTRED,
text_to_wrap=get_placement_errors,
visible=function() return #uibs.errors > 0 end,
},
widgets.Label{
frame={t=0, l=0, r=0},
text_pen=COLOR_GREEN,
text='OK to build',
visible=function() return #uibs.errors == 0 end,
},
}
self:addviews{
main_panel,
error_panel,
}
end
function PlannerOverlay:reset()
self.subviews.item1:reset()
self.subviews.item2:reset()
self.subviews.item3:reset()
self.subviews.item4:reset()
reset_counts_flag = false
end
function PlannerOverlay:set_filter(idx)
filterselection.FilterSelectionScreen{index=idx, desc=require('plugins.buildingplan').get_desc(get_cur_filters()[idx])}:show()
end
function PlannerOverlay:clear_filter(idx)
clearFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, idx-1)
end
local function get_placement_data()
local pos = uibs.pos
local direction = uibs.direction
local width, height, depth = get_cur_area_dims()
local _, adjusted_width, adjusted_height = dfhack.buildings.getCorrectSize(
width, height, uibs.building_type, uibs.building_subtype,
uibs.custom_type, direction)
-- get the upper-left corner of the building/area at min z-level
local has_selection = is_choosing_area()
local start_pos = xyz2pos(
has_selection and math.min(uibs.selection_pos.x, pos.x) or pos.x - adjusted_width//2,
has_selection and math.min(uibs.selection_pos.y, pos.y) or pos.y - adjusted_height//2,
has_selection and math.min(uibs.selection_pos.z, pos.z) or pos.z
)
if uibs.building_type == df.building_type.ScrewPump then
if direction == df.screw_pump_direction.FromSouth then
start_pos.y = start_pos.y + 1
elseif direction == df.screw_pump_direction.FromEast then
start_pos.x = start_pos.x + 1
end
end
local min_x, max_x = start_pos.x, start_pos.x
local min_y, max_y = start_pos.y, start_pos.y
local min_z, max_z = start_pos.z, start_pos.z
if adjusted_width == 1 and adjusted_height == 1
and (width > 1 or height > 1 or depth > 1) then
max_x = min_x + width - 1
max_y = min_y + height - 1
max_z = math.max(uibs.selection_pos.z, pos.z)
end
return {
p1=xyz2pos(min_x, min_y, min_z),
p2=xyz2pos(max_x, max_y, max_z),
width=adjusted_width,
height=adjusted_height
}
end
function PlannerOverlay:save_placement()
self.saved_placement = get_placement_data()
if (uibs.selection_pos:isValid()) then
self.saved_selection_pos_valid = true
self.saved_selection_pos = copyall(uibs.selection_pos)
self.saved_pos = copyall(uibs.pos)
uibs.selection_pos:clear()
else
self.saved_selection_pos = copyall(self.saved_placement.p1)
self.saved_pos = copyall(self.saved_placement.p2)
self.saved_pos.x = self.saved_pos.x + self.saved_placement.width - 1
self.saved_pos.y = self.saved_pos.y + self.saved_placement.height - 1
end
end
function PlannerOverlay:restore_placement()
if self.saved_selection_pos_valid then
uibs.selection_pos = self.saved_selection_pos
self.saved_selection_pos_valid = nil
else
uibs.selection_pos:clear()
end
self.saved_selection_pos = nil
self.saved_pos = nil
local placement_data = self.saved_placement
self.saved_placement = nil
return placement_data
end
function PlannerOverlay:onInput(keys)
if not is_plannable() then return false end
if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then
if uibs.selection_pos:isValid() then
uibs.selection_pos:clear()
return true
end
self.selected = 1
self.subviews.hollow:setOption(false)
self.subviews.choose:setOption(false)
self:reset()
reset_counts_flag = true
return false
end
if PlannerOverlay.super.onInput(self, keys) then
return true
end
if keys._MOUSE_L_DOWN then
if is_over_options_panel() then return false end
local detect_rect = copyall(self.frame_rect)
detect_rect.height = self.subviews.main.frame_rect.height +
self.subviews.errors.frame_rect.height
detect_rect.y2 = detect_rect.y1 + detect_rect.height - 1
if self.subviews.main:getMousePos(gui.ViewRect{rect=detect_rect})
or self.subviews.errors:getMousePos() then
return true
end
if not is_construction() and #uibs.errors > 0 then return true end
if dfhack.gui.getMousePos() then
if is_choosing_area() or cur_building_has_no_area() then
local filters = get_cur_filters()
local num_filters = #filters
local choose = self.subviews.choose
if choose.enabled() and choose:getOptionValue() then
self:save_placement()
local is_hollow = self.subviews.hollow:getOptionValue()
local chosen_items, active_screens = {}, {}
local pending = num_filters
df.global.game.main_interface.bottom_mode_selected = -1
for idx = num_filters,1,-1 do
chosen_items[idx] = {}
if (self.subviews['item'..idx].available or 0) > 0 then
local filter = filters[idx]
active_screens[idx] = itemselection.ItemSelectionScreen{
index=idx,
desc=require('plugins.buildingplan').get_desc(filter),
quantity=get_quantity(filter, is_hollow,
self.saved_placement),
on_submit=function(items)
chosen_items[idx] = items
active_screens[idx]:dismiss()
active_screens[idx] = nil
pending = pending - 1
if pending == 0 then
df.global.game.main_interface.bottom_mode_selected = df.main_bottom_mode_type.BUILDING_PLACEMENT
self:place_building(self:restore_placement(), chosen_items)
end
end,
on_cancel=function()
for i,scr in pairs(active_screens) do
scr:dismiss()
end
df.global.game.main_interface.bottom_mode_selected = df.main_bottom_mode_type.BUILDING_PLACEMENT
self:restore_placement()
end,
}:show()
else
pending = pending - 1
end
end
else
self:place_building(get_placement_data())
end
return true
elseif not is_choosing_area() then
return false
end
end
end
return keys._MOUSE_L or keys.SELECT
end
function PlannerOverlay:render(dc)
if not is_plannable() then return end
self.subviews.errors:updateLayout()
PlannerOverlay.super.render(self, dc)
end
local ONE_BY_ONE = xy2pos(1, 1)
function PlannerOverlay:onRenderFrame(dc, rect)
PlannerOverlay.super.onRenderFrame(self, dc, rect)
if reset_counts_flag then
self:reset()
self.subviews.safety:setOption(require('plugins.buildingplan').getHeatSafetyFilter(
uibs.building_type, uibs.building_subtype, uibs.custom_type))
end
local selection_pos = self.saved_selection_pos or uibs.selection_pos
if not selection_pos or selection_pos.x < 0 then return end
local pos = self.saved_pos or uibs.pos
local bounds = {
x1 = math.max(0, math.min(selection_pos.x, pos.x)),
x2 = math.min(df.global.world.map.x_count-1, math.max(selection_pos.x, pos.x)),
y1 = math.max(0, math.min(selection_pos.y, pos.y)),
y2 = math.min(df.global.world.map.y_count-1, math.max(selection_pos.y, pos.y)),
}
local hollow = self.subviews.hollow:getOptionValue()
local default_pen = (self.saved_selection_pos or #uibs.errors == 0) and pens.GOOD_TILE_PEN or pens.BAD_TILE_PEN
local get_pen_fn = is_construction() and function(pos)
return dfhack.buildings.checkFreeTiles(pos, ONE_BY_ONE) and pens.GOOD_TILE_PEN or pens.BAD_TILE_PEN
end or function()
return default_pen
end
local function get_overlay_pen(pos)
if not hollow then return get_pen_fn(pos) end
if pos.x == bounds.x1 or pos.x == bounds.x2 or
pos.y == bounds.y1 or pos.y == bounds.y2 then
return get_pen_fn(pos)
end
return gui.TRANSPARENT_PEN
end
guidm.renderMapOverlay(get_overlay_pen, bounds)
end
function PlannerOverlay:get_stairs_subtype(pos, corner1, corner2)
local subtype = uibs.building_subtype
if pos.z == corner1.z then
local opt = self.subviews.stairs_bottom_subtype:getOptionValue()
if opt == 'auto' then
local tt = dfhack.maps.getTileType(pos)
local shape = df.tiletype.attrs[tt].shape
if shape ~= df.tiletype_shape.STAIR_DOWN then
subtype = df.construction_type.UpStair
end
else
subtype = opt
end
elseif pos.z == corner2.z then
local opt = self.subviews.stairs_top_subtype:getOptionValue()
if opt == 'auto' then
local tt = dfhack.maps.getTileType(pos)
local shape = df.tiletype.attrs[tt].shape
if shape ~= df.tiletype_shape.STAIR_UP then
subtype = df.construction_type.DownStair
end
else
subtype = opt
end
end
return subtype
end
function PlannerOverlay:place_building(placement_data, chosen_items)
local p1, p2 = placement_data.p1, placement_data.p2
local blds = {}
local hollow = self.subviews.hollow:getOptionValue()
local subtype = uibs.building_subtype
local filters = get_cur_filters()
if is_pressure_plate() then
filters[1].quantity = get_quantity(filters[1])
elseif is_weapon_trap() then
filters[2].quantity = get_quantity(filters[2])
end
for z=p1.z,p2.z do for y=p1.y,p2.y do for x=p1.x,p2.x do
if hollow and x ~= p1.x and x ~= p2.x and y ~= p1.y and y ~= p2.y then
goto continue
end
local pos = xyz2pos(x, y, z)
if is_stairs() then
subtype = self:get_stairs_subtype(pos, p1, p2)
end
local bld, err = dfhack.buildings.constructBuilding{pos=pos,
type=uibs.building_type, subtype=subtype, custom=uibs.custom_type,
width=placement_data.width, height=placement_data.height,
direction=uibs.direction, filters=filters}
if err then
-- it's ok if some buildings fail to build
goto continue
end
-- assign fields for the types that need them. we can't pass them all in
-- to the call to constructBuilding since attempting to assign unrelated
-- fields to building types that don't support them causes errors.
for k,v in pairs(bld) do
if k == 'friction' then bld.friction = uibs.friction end
if k == 'use_dump' then bld.use_dump = uibs.use_dump end
if k == 'dump_x_shift' then bld.dump_x_shift = uibs.dump_x_shift end
if k == 'dump_y_shift' then bld.dump_y_shift = uibs.dump_y_shift end
if k == 'speed' then bld.speed = uibs.speed end
if k == 'plate_info' then utils.assign(bld.plate_info, uibs.plate_info) end
end
table.insert(blds, bld)
::continue::
end end end
local used_quantity = is_construction() and #blds or false
self.subviews.item1:reduce_quantity(used_quantity)
self.subviews.item2:reduce_quantity(used_quantity)
self.subviews.item3:reduce_quantity(used_quantity)
self.subviews.item4:reduce_quantity(used_quantity)
local buildingplan = require('plugins.buildingplan')
for _,bld in ipairs(blds) do
-- attach chosen items and reduce job_item quantity
if chosen_items then
local job = bld.jobs[0]
local jitems = job.job_items
local num_filters = #get_cur_filters()
for idx=1,num_filters do
local item_ids = chosen_items[idx]
local jitem = jitems[num_filters-idx]
while jitem.quantity > 0 and #item_ids > 0 do
local item_id = item_ids[#item_ids]
local item = df.item.find(item_id)
if not item then
dfhack.printerr(('item no longer available: %d'):format(item_id))
break
end
if not dfhack.job.attachJobItem(job, item, df.job_item_ref.T_role.Hauled, idx-1, -1) then
dfhack.printerr(('cannot attach item: %d'):format(item_id))
break
end
jitem.quantity = jitem.quantity - 1
item_ids[#item_ids] = nil
end
end
end
buildingplan.addPlannedBuilding(bld)
end
buildingplan.scheduleCycle()
uibs.selection_pos:clear()
end
return _ENV

@ -13,6 +13,7 @@
#include "df/viewscreen_titlest.h"
#include "df/viewscreen_update_regionst.h"
#include "df/viewscreen_worldst.h"
#include "df/world.h"
#include "Debug.h"
#include "LuaTools.h"
@ -27,6 +28,8 @@ using namespace DFHack;
DFHACK_PLUGIN("overlay");
DFHACK_PLUGIN_IS_ENABLED(is_enabled);
REQUIRE_GLOBAL(world);
namespace DFHack {
DBG_DECLARE(overlay, control, DebugCategory::LINFO);
DBG_DECLARE(overlay, event, DebugCategory::LINFO);
@ -67,13 +70,15 @@ struct viewscreen_overlay : T {
}
DEFINE_VMETHOD_INTERPOSE(void, feed, (std::set<df::interface_key> *input)) {
bool input_is_handled = false;
call_overlay_lua(NULL, "feed_viewscreen_widgets", 2, 1,
[&](lua_State *L) {
Lua::Push(L, T::_identity.getName());
Lua::PushInterfaceKeys(L, *input);
}, [&](lua_State *L) {
input_is_handled = lua_toboolean(L, -1);
});
// don't send input to the overlays if there is a modal dialog up
if (!world->status.popups.size())
call_overlay_lua(NULL, "feed_viewscreen_widgets", 2, 1,
[&](lua_State *L) {
Lua::Push(L, T::_identity.getName());
Lua::PushInterfaceKeys(L, *input);
}, [&](lua_State *L) {
input_is_handled = lua_toboolean(L, -1);
});
if (!input_is_handled)
INTERPOSE_NEXT(feed)(input);
}

@ -1,154 +1,126 @@
#include "OrganicMatLookup.h"
#include "StockpileUtils.h"
#include "modules/Materials.h"
#include "MiscUtils.h"
#include "df/world.h"
#include "df/world_data.h"
#include "Debug.h"
#include "df/creature_raw.h"
#include "df/caste_raw.h"
#include "df/material.h"
#include "df/world.h"
using namespace DFHack;
using namespace df::enums;
using df::global::world;
using std::endl;
namespace DFHack {
DBG_EXTERN(stockpiles, log);
}
/**
* Helper class for mapping the various organic mats between their material indices
* and their index in the stockpile_settings structures.
*/
void OrganicMatLookup::food_mat_by_idx ( std::ostream &out, organic_mat_category::organic_mat_category mat_category, std::vector<int16_t>::size_type food_idx, FoodMat & food_mat )
{
out << "food_lookup: food_idx(" << food_idx << ") ";
df::world_raws &raws = world->raws;
void OrganicMatLookup::food_mat_by_idx(organic_mat_category::organic_mat_category mat_category, std::vector<int16_t>::size_type food_idx, FoodMat& food_mat) {
DEBUG(log).print("food_lookup: food_idx(%zd) ", food_idx);
df::world_raws& raws = world->raws;
df::special_mat_table table = raws.mat_table;
int32_t main_idx = table.organic_indexes[mat_category][food_idx];
int16_t type = table.organic_types[mat_category][food_idx];
if ( mat_category == organic_mat_category::Fish ||
mat_category == organic_mat_category::UnpreparedFish ||
mat_category == organic_mat_category::Eggs )
{
if (mat_category == organic_mat_category::Fish ||
mat_category == organic_mat_category::UnpreparedFish ||
mat_category == organic_mat_category::Eggs) {
food_mat.creature = raws.creatures.all[type];
food_mat.caste = food_mat.creature->caste[main_idx];
out << " special creature type(" << type << ") caste("<< main_idx <<")" <<endl;
DEBUG(log).print("special creature type(%d) caste(%d)", type, main_idx);
}
else
{
food_mat.material.decode ( type, main_idx );
out << " type(" << type << ") index("<< main_idx <<") token(" << food_mat.material.getToken() << ")" << endl;
else {
food_mat.material.decode(type, main_idx);
DEBUG(log).print("type(%d) index(%d) token(%s)", type, main_idx, food_mat.material.getToken().c_str());
}
}
std::string OrganicMatLookup::food_token_by_idx ( std::ostream &out, organic_mat_category::organic_mat_category mat_category, std::vector<int16_t>::size_type idx )
{
std::string OrganicMatLookup::food_token_by_idx(organic_mat_category::organic_mat_category mat_category, std::vector<int16_t>::size_type idx) {
FoodMat food_mat;
food_mat_by_idx ( out, mat_category, idx, food_mat );
if ( food_mat.material.isValid() )
{
food_mat_by_idx(mat_category, idx, food_mat);
if (food_mat.material.isValid()) {
return food_mat.material.getToken();
}
else if ( food_mat.creature )
{
else if (food_mat.creature) {
return food_mat.creature->creature_id + ":" + food_mat.caste->caste_id;
}
return std::string();
}
size_t OrganicMatLookup::food_max_size ( organic_mat_category::organic_mat_category mat_category )
{
size_t OrganicMatLookup::food_max_size(organic_mat_category::organic_mat_category mat_category) {
return world->raws.mat_table.organic_types[mat_category].size();
}
void OrganicMatLookup::food_build_map ( std::ostream &out )
{
if ( index_built )
void OrganicMatLookup::food_build_map() {
if (index_built)
return;
df::world_raws &raws = world->raws;
df::world_raws& raws = world->raws;
df::special_mat_table table = raws.mat_table;
using df::enums::organic_mat_category::organic_mat_category;
using traits = df::enum_traits<organic_mat_category>;
for ( int32_t mat_category = traits::first_item_value; mat_category <= traits::last_item_value; ++mat_category )
{
for ( size_t i = 0; i < table.organic_indexes[mat_category].size(); ++i )
{
int16_t type = table.organic_types[mat_category].at ( i );
int32_t index = table.organic_indexes[mat_category].at ( i );
food_index[mat_category].insert ( std::make_pair ( std::make_pair ( type,index ), i ) ); // wtf.. only in c++
for (int32_t mat_category = traits::first_item_value; mat_category <= traits::last_item_value; ++mat_category) {
for (size_t i = 0; i < table.organic_indexes[mat_category].size(); ++i) {
int16_t type = table.organic_types[mat_category].at(i);
int32_t index = table.organic_indexes[mat_category].at(i);
food_index[mat_category].insert(std::make_pair(std::make_pair(type, index), i)); // wtf.. only in c++
}
}
index_built = true;
}
int16_t OrganicMatLookup::food_idx_by_token ( std::ostream &out, organic_mat_category::organic_mat_category mat_category, const std::string & token )
{
int16_t food_idx = -1;
df::world_raws &raws = world->raws;
int16_t OrganicMatLookup::food_idx_by_token(organic_mat_category::organic_mat_category mat_category, const std::string& token) {
df::world_raws& raws = world->raws;
df::special_mat_table table = raws.mat_table;
out << "food_idx_by_token: ";
if ( mat_category == organic_mat_category::Fish ||
mat_category == organic_mat_category::UnpreparedFish ||
mat_category == organic_mat_category::Eggs )
{
DEBUG(log).print("food_idx_by_token: ");
if (mat_category == organic_mat_category::Fish ||
mat_category == organic_mat_category::UnpreparedFish ||
mat_category == organic_mat_category::Eggs) {
std::vector<std::string> tokens;
split_string ( &tokens, token, ":" );
if ( tokens.size() != 2 )
{
out << "creature " << "invalid CREATURE:CASTE token: " << token << endl;
split_string(&tokens, token, ":");
if (tokens.size() != 2) {
WARN(log).print("creature invalid CREATURE:CASTE token: %s\n", token.c_str());
return -1;
}
else
{
int16_t creature_idx = find_creature ( tokens[0] );
if ( creature_idx < 0 )
{
out << " creature invalid token " << tokens[0];
}
else
{
food_idx = linear_index ( table.organic_types[mat_category], creature_idx );
if ( tokens[1] == "MALE" )
food_idx += 1;
if ( table.organic_types[mat_category][food_idx] == creature_idx )
out << "creature " << token << " caste " << tokens[1] << " creature_idx(" << creature_idx << ") food_idx("<< food_idx << ")" << endl;
else
{
out << "ERROR creature caste not found: " << token << " caste " << tokens[1] << " creature_idx(" << creature_idx << ") food_idx("<< food_idx << ")" << endl;
food_idx = -1;
}
}
int16_t creature_idx = find_creature(tokens[0]);
if (creature_idx < 0) {
WARN(log).print("creature invalid token %s\n", tokens[0].c_str());
return -1;
}
}
else
{
if ( !index_built )
food_build_map ( out );
MaterialInfo mat_info = food_mat_by_token ( out, token );
int16_t type = mat_info.type;
int32_t index = mat_info.index;
auto it = food_index[mat_category].find ( std::make_pair ( type, index ) );
if ( it != food_index[mat_category].end() )
{
out << "matinfo: " << token << " type(" << mat_info.type << ") idx(" << mat_info.index << ") food_idx(" << it->second << ")" << endl;
food_idx = it->second;
}
else
{
out << "matinfo: " << token << " type(" << mat_info.type << ") idx(" << mat_info.index << ") food_idx not found :(" << endl;
int16_t food_idx = linear_index(table.organic_types[mat_category], creature_idx);
if (tokens[1] == "MALE")
food_idx += 1;
if (table.organic_types[mat_category][food_idx] == creature_idx) {
DEBUG(log).print("creature %s caste %s creature_idx(%d) food_idx(%d)\n", token.c_str(), tokens[1].c_str(), creature_idx, food_idx);
return food_idx;
}
WARN(log).print("creature caste not found: %s caste %s creature_idx(%d) food_idx(%d)\n", token.c_str(), tokens[1].c_str(), creature_idx, food_idx);
return -1;
}
return food_idx;
if (!index_built)
food_build_map();
MaterialInfo mat_info = food_mat_by_token(token);
int16_t type = mat_info.type;
int32_t index = mat_info.index;
auto it = food_index[mat_category].find(std::make_pair(type, index));
if (it != food_index[mat_category].end()) {
DEBUG(log).print("matinfo: %s type(%d) idx(%d) food_idx(%zd)\n", token.c_str(), mat_info.type, mat_info.index, it->second);
return it->second;
}
WARN(log).print("matinfo: %s type(%d) idx(%d) food_idx not found :(\n", token.c_str(), mat_info.type, mat_info.index);
return -1;
}
MaterialInfo OrganicMatLookup::food_mat_by_token ( std::ostream &out, const std::string & token )
{
MaterialInfo OrganicMatLookup::food_mat_by_token(const std::string& token) {
MaterialInfo mat_info;
mat_info.find ( token );
mat_info.find(token);
return mat_info;
}
bool OrganicMatLookup::index_built = false;
std::vector<OrganicMatLookup::FoodMatMap> OrganicMatLookup::food_index = std::vector<OrganicMatLookup::FoodMatMap> ( df::enum_traits< df::organic_mat_category >::last_item_value + 1 );
std::vector<OrganicMatLookup::FoodMatMap> OrganicMatLookup::food_index = std::vector<OrganicMatLookup::FoodMatMap>(df::enum_traits< df::organic_mat_category >::last_item_value + 1);

@ -5,42 +5,42 @@
#include "df/organic_mat_category.h"
namespace df {
struct creature_raw;
struct caste_raw;
struct creature_raw;
struct caste_raw;
}
/**
* Helper class for mapping the various organic mats between their material indices
* and their index in the stockpile_settings structures.
*/
class OrganicMatLookup
{
class OrganicMatLookup {
// pair of material type and index
// pair of material type and index
typedef std::pair<int16_t, int32_t> FoodMatPair;
// map for using type,index pairs to find the food index
// map for using type,index pairs to find the food index
typedef std::map<FoodMatPair, size_t> FoodMatMap;
public:
struct FoodMat
{
struct FoodMat {
DFHack::MaterialInfo material;
df::creature_raw *creature;
df::caste_raw * caste;
FoodMat() : material ( -1 ), creature ( 0 ), caste ( 0 ) {}
df::creature_raw* creature;
df::caste_raw* caste;
FoodMat(): material(-1), creature(0), caste(0) { }
};
static void food_mat_by_idx ( std::ostream &out, df::enums::organic_mat_category::organic_mat_category mat_category, std::vector<int16_t>::size_type food_idx, FoodMat & food_mat );
static std::string food_token_by_idx ( std::ostream &out, df::enums::organic_mat_category::organic_mat_category mat_category, std::vector<int16_t>::size_type idx );
static size_t food_max_size ( df::enums::organic_mat_category::organic_mat_category mat_category );
static void food_build_map ( std::ostream &out );
static void food_mat_by_idx(df::enums::organic_mat_category::organic_mat_category mat_category, std::vector<int16_t>::size_type food_idx, FoodMat& food_mat);
static std::string food_token_by_idx(df::enums::organic_mat_category::organic_mat_category mat_category, std::vector<int16_t>::size_type idx);
static int16_t food_idx_by_token ( std::ostream &out, df::enums::organic_mat_category::organic_mat_category mat_category, const std::string & token );
static size_t food_max_size(df::enums::organic_mat_category::organic_mat_category mat_category);
static void food_build_map();
static DFHack::MaterialInfo food_mat_by_token ( std::ostream &out, const std::string & token );
static int16_t food_idx_by_token(df::enums::organic_mat_category::organic_mat_category mat_category, const std::string& token);
static DFHack::MaterialInfo food_mat_by_token(const std::string& token);
static bool index_built;
static std::vector<FoodMatMap> food_index;
private:
OrganicMatLookup();
};

File diff suppressed because it is too large Load Diff

@ -1,349 +1,283 @@
#pragma once
// stockpiles plugin
#include "proto/stockpiles.pb.h"
// dfhack
#include "modules/Materials.h"
#include "modules/Items.h"
// df
#include "df/world.h"
#include "df/world_data.h"
#include "df/itemdef.h"
#include "df/organic_mat_category.h"
#include "df/furniture_type.h"
#include "df/item_quality.h"
#include "df/item_type.h"
// stl
#include <functional>
#include <vector>
#include <ostream>
#include <istream>
namespace df {
struct building_stockpilest;
}
#include "proto/stockpiles.pb.h"
/**
* Null buffer that acts like /dev/null for when debug is disabled
*/
class NullBuffer : public std::streambuf
{
public:
int overflow ( int c );
};
#include <functional>
class NullStream : public std::ostream
namespace df
{
public:
NullStream();
private:
NullBuffer m_sb;
};
struct building_stockpilest;
}
/**
* Class for serializing the stockpile_settings structure into a Google protobuf
*/
class StockpileSerializer
{
class StockpileSerializer {
public:
/**
* @param out for debugging
* @param stockpile stockpile to read or write settings to
*/
StockpileSerializer ( df::building_stockpilest * stockpile );
~StockpileSerializer();
/**
* @param out for debugging
* @param stockpile stockpile to read or write settings to
*/
void enable_debug ( std::ostream &out );
StockpileSerializer(df::building_stockpilest* stockpile);
/**
* Since we depend on protobuf-lite, not the full lib, we copy this function from
* protobuf message.cc
*/
bool serialize_to_ostream(std::ostream* output);
~StockpileSerializer();
/**
* Will serialize stockpile settings to a file (overwrites existing files)
* @return success/failure
*/
bool serialize_to_file ( const std::string & file );
/**
* Since we depend on protobuf-lite, not the full lib, we copy this function from
* protobuf message.cc
*/
bool serialize_to_ostream(std::ostream* output);
/**
* Again, copied from message.cc
*/
bool parse_from_istream(std::istream* input);
/**
* Will serialize stockpile settings to a file (overwrites existing files)
* @return success/failure
*/
bool serialize_to_file(const std::string& file);
/**
* Again, copied from message.cc
*/
bool parse_from_istream(std::istream* input);
/**
* Read stockpile settings from file
*/
bool unserialize_from_file ( const std::string & file );
/**
* Read stockpile settings from file
*/
bool unserialize_from_file(const std::string& file);
private:
df::building_stockpilest* mPile;
dfstockpiles::StockpileSettings mBuffer;
std::map<int, std::string> mOtherMatsFurniture;
std::map<int, std::string> mOtherMatsFinishedGoods;
std::map<int, std::string> mOtherMatsBars;
std::map<int, std::string> mOtherMatsBlocks;
std::map<int, std::string> mOtherMatsWeaponsArmor;
/**
read memory structures and serialize to protobuf
*/
void write();
// parse serialized data into ui indices
void read();
/**
* Find an enum's value based off the string label.
* @param traits the enum's trait struct
* @param token the string value in key_table
* @return the enum's value, -1 if not found
*/
template <typename E>
static typename df::enum_traits<E>::base_type linear_index(df::enum_traits<E> traits, const std::string& token) {
auto j = traits.first_item_value;
auto limit = traits.last_item_value;
// sometimes enums start at -1, which is bad news for array indexing
if (j < 0) {
j += abs(traits.first_item_value);
limit += abs(traits.first_item_value);
}
for (; j <= limit; ++j) {
if (token.compare(traits.key_table[j]) == 0)
return j;
}
return -1;
}
// read the token from the serailized list during import
typedef std::function<std::string(const size_t&)> FuncReadImport;
// add the token to the serialized list during export
typedef std::function<void(const std::string&)> FuncWriteExport;
// are item's of item_type allowed?
typedef std::function<bool(df::enums::item_type::item_type)> FuncItemAllowed;
// is this material allowed?
typedef std::function<bool(const DFHack::MaterialInfo&)> FuncMaterialAllowed;
// convenient struct for parsing food stockpile items
struct food_pair {
// exporting
FuncWriteExport set_value;
std::vector<char>* stockpile_values;
// importing
FuncReadImport get_value;
size_t serialized_count;
bool valid;
food_pair(FuncWriteExport s, std::vector<char>* sp_v, FuncReadImport g, size_t count)
: set_value(s), stockpile_values(sp_v), get_value(g), serialized_count(count), valid(true) { }
food_pair(): valid(false) { }
};
/**
* There are many repeated (un)serialization cases throughout the stockpile_settings structure,
* so the most common cases have been generalized into generic functions using lambdas.
*
* The basic process to serialize a stockpile_settings structure is:
* 1. loop through the list
* 2. for every element that is TRUE:
* 3. map the specific stockpile_settings index into a general material, creature, etc index
* 4. verify that type is allowed in the list (e.g., no stone in gems stockpiles)
* 5. add it to the protobuf using FuncWriteExport
*
* The unserialization process is the same in reverse.
*/
void serialize_list_organic_mat(FuncWriteExport add_value, const std::vector<char>* list, df::enums::organic_mat_category::organic_mat_category cat);
/**
* @see serialize_list_organic_mat
*/
void unserialize_list_organic_mat(FuncReadImport get_value, size_t list_size, std::vector<char>* pile_list, df::enums::organic_mat_category::organic_mat_category cat);
/**
* @see serialize_list_organic_mat
*/
void serialize_list_item_type(FuncItemAllowed is_allowed, FuncWriteExport add_value, const std::vector<char>& list);
/**
* @see serialize_list_organic_mat
*/
void unserialize_list_item_type(FuncItemAllowed is_allowed, FuncReadImport read_value, int32_t list_size, std::vector<char>* pile_list);
/**
* @see serialize_list_organic_mat
*/
void serialize_list_material(FuncMaterialAllowed is_allowed, FuncWriteExport add_value, const std::vector<char>& list);
/**
* @see serialize_list_organic_mat
*/
void unserialize_list_material(FuncMaterialAllowed is_allowed, FuncReadImport read_value, int32_t list_size, std::vector<char>* pile_list);
/**
* @see serialize_list_organic_mat
*/
void serialize_list_quality(FuncWriteExport add_value, const bool(&quality_list)[7]);
/**
* Set all values in a bool[7] to false
*/
void quality_clear(bool(&pile_list)[7]);
/**
* @see serialize_list_organic_mat
*/
void unserialize_list_quality(FuncReadImport read_value, int32_t list_size, bool(&pile_list)[7]);
/**
* @see serialize_list_organic_mat
*/
void serialize_list_other_mats(const std::map<int, std::string> other_mats, FuncWriteExport add_value, std::vector<char> list);
/**
* @see serialize_list_organic_mat
*/
void unserialize_list_other_mats(const std::map<int, std::string> other_mats, FuncReadImport read_value, int32_t list_size, std::vector<char>* pile_list);
/**
* @see serialize_list_organic_mat
*/
void serialize_list_itemdef(FuncWriteExport add_value, std::vector<char> list, std::vector<df::itemdef*> items, df::enums::item_type::item_type type);
/**
* @see serialize_list_organic_mat
*/
void unserialize_list_itemdef(FuncReadImport read_value, int32_t list_size, std::vector<char>* pile_list, df::enums::item_type::item_type type);
/**
* Given a list of other_materials and an index, return its corresponding token
* @return empty string if not found
* @see other_mats_token
*/
std::string other_mats_index(const std::map<int, std::string> other_mats, int idx);
/**
* Given a list of other_materials and a token, return its corresponding index
* @return -1 if not found
* @see other_mats_index
*/
int other_mats_token(const std::map<int, std::string> other_mats, const std::string& token);
void write_general();
void read_general();
void write_animals();
void read_animals();
bool mDebug;
std::ostream * mOut;
NullStream mNull;
df::building_stockpilest * mPile;
dfstockpiles::StockpileSettings mBuffer;
std::map<int, std::string> mOtherMatsFurniture;
std::map<int, std::string> mOtherMatsFinishedGoods;
std::map<int, std::string> mOtherMatsBars;
std::map<int, std::string> mOtherMatsBlocks;
std::map<int, std::string> mOtherMatsWeaponsArmor;
std::ostream & debug();
/**
read memory structures and serialize to protobuf
*/
void write();
// parse serialized data into ui indices
void read ();
/**
* Find an enum's value based off the string label.
* @param traits the enum's trait struct
* @param token the string value in key_table
* @return the enum's value, -1 if not found
*/
template<typename E>
static typename df::enum_traits<E>::base_type linear_index ( std::ostream & out, df::enum_traits<E> traits, const std::string &token )
{
auto j = traits.first_item_value;
auto limit = traits.last_item_value;
// sometimes enums start at -1, which is bad news for array indexing
if ( j < 0 )
{
j += abs ( traits.first_item_value );
limit += abs ( traits.first_item_value );
}
for ( ; j <= limit; ++j )
{
// out << " linear_index("<< token <<") = table["<<j<<"/"<<limit<<"]: " <<traits.key_table[j] << endl;
if ( token.compare ( traits.key_table[j] ) == 0 )
return j;
}
return -1;
}
// read the token from the serailized list during import
typedef std::function<std::string ( const size_t& ) > FuncReadImport;
// add the token to the serialized list during export
typedef std::function<void ( const std::string & ) > FuncWriteExport;
// are item's of item_type allowed?
typedef std::function<bool ( df::enums::item_type::item_type ) > FuncItemAllowed;
// is this material allowed?
typedef std::function<bool ( const DFHack::MaterialInfo & ) > FuncMaterialAllowed;
// convenient struct for parsing food stockpile items
struct food_pair
{
// exporting
FuncWriteExport set_value;
std::vector<char> * stockpile_values;
// importing
FuncReadImport get_value;
size_t serialized_count;
bool valid;
food_pair ( FuncWriteExport s, std::vector<char>* sp_v, FuncReadImport g, size_t count )
: set_value ( s )
, stockpile_values ( sp_v )
, get_value ( g )
, serialized_count ( count )
, valid ( true )
{}
food_pair(): valid( false ) {}
};
/**
* There are many repeated (un)serialization cases throughout the stockpile_settings structure,
* so the most common cases have been generalized into generic functions using lambdas.
*
* The basic process to serialize a stockpile_settings structure is:
* 1. loop through the list
* 2. for every element that is TRUE:
* 3. map the specific stockpile_settings index into a general material, creature, etc index
* 4. verify that type is allowed in the list (e.g., no stone in gems stockpiles)
* 5. add it to the protobuf using FuncWriteExport
*
* The unserialization process is the same in reverse.
*/
void serialize_list_organic_mat ( FuncWriteExport add_value, const std::vector<char> * list, df::enums::organic_mat_category::organic_mat_category cat );
/**
* @see serialize_list_organic_mat
*/
void unserialize_list_organic_mat ( FuncReadImport get_value, size_t list_size, std::vector<char> *pile_list, df::enums::organic_mat_category::organic_mat_category cat );
/**
* @see serialize_list_organic_mat
*/
void serialize_list_item_type ( FuncItemAllowed is_allowed, FuncWriteExport add_value, const std::vector<char> &list );
/**
* @see serialize_list_organic_mat
*/
void unserialize_list_item_type ( FuncItemAllowed is_allowed, FuncReadImport read_value, int32_t list_size, std::vector<char> *pile_list );
/**
* @see serialize_list_organic_mat
*/
void serialize_list_material ( FuncMaterialAllowed is_allowed, FuncWriteExport add_value, const std::vector<char> &list );
/**
* @see serialize_list_organic_mat
*/
void unserialize_list_material ( FuncMaterialAllowed is_allowed, FuncReadImport read_value, int32_t list_size, std::vector<char> *pile_list );
/**
* @see serialize_list_organic_mat
*/
void serialize_list_quality ( FuncWriteExport add_value, const bool ( &quality_list ) [7] );
/**
* Set all values in a bool[7] to false
*/
void quality_clear ( bool ( &pile_list ) [7] );
food_pair food_map(df::enums::organic_mat_category::organic_mat_category cat);
void write_food();
void read_food();
/**
* @see serialize_list_organic_mat
*/
void unserialize_list_quality ( FuncReadImport read_value, int32_t list_size, bool ( &pile_list ) [7] );
void furniture_setup_other_mats();
void write_furniture();
bool furniture_mat_is_allowed(const DFHack::MaterialInfo& mi);
void read_furniture();
bool refuse_creature_is_allowed(const df::creature_raw* raw);
/**
* @see serialize_list_organic_mat
*/
void serialize_list_other_mats ( const std::map<int, std::string> other_mats, FuncWriteExport add_value, std::vector<char> list );
void refuse_write_helper(std::function<void(const std::string&)> add_value, const std::vector<char>& list);
/**
* @see serialize_list_organic_mat
*/
void unserialize_list_other_mats ( const std::map<int, std::string> other_mats, FuncReadImport read_value, int32_t list_size, std::vector<char> *pile_list );
bool refuse_type_is_allowed(df::enums::item_type::item_type type);
void write_refuse();
void refuse_read_helper(std::function<std::string(const size_t&)> get_value, size_t list_size, std::vector<char>* pile_list);
/**
* @see serialize_list_organic_mat
*/
void serialize_list_itemdef ( FuncWriteExport add_value, std::vector<char> list, std::vector<df::itemdef *> items, df::enums::item_type::item_type type );
void read_refuse();
bool stone_is_allowed(const DFHack::MaterialInfo& mi);
/**
* @see serialize_list_organic_mat
*/
void unserialize_list_itemdef ( FuncReadImport read_value, int32_t list_size, std::vector<char> *pile_list, df::enums::item_type::item_type type );
void write_stone();
void read_stone();
/**
* Given a list of other_materials and an index, return its corresponding token
* @return empty string if not found
* @see other_mats_token
*/
std::string other_mats_index ( const std::map<int, std::string> other_mats, int idx );
bool ammo_mat_is_allowed(const DFHack::MaterialInfo& mi);
/**
* Given a list of other_materials and a token, return its corresponding index
* @return -1 if not found
* @see other_mats_index
*/
int other_mats_token ( const std::map<int, std::string> other_mats, const std::string & token );
void write_ammo();
void read_ammo();
bool coins_mat_is_allowed(const DFHack::MaterialInfo& mi);
void write_general();
void read_general();
void write_coins();
void read_coins();
void write_animals();
void read_animals();
void bars_blocks_setup_other_mats();
bool bars_mat_is_allowed(const DFHack::MaterialInfo& mi);
food_pair food_map ( df::enums::organic_mat_category::organic_mat_category cat );
bool blocks_mat_is_allowed(const DFHack::MaterialInfo& mi);
void write_bars_blocks();
void read_bars_blocks();
bool gem_mat_is_allowed(const DFHack::MaterialInfo& mi);
bool gem_cut_mat_is_allowed(const DFHack::MaterialInfo& mi);
bool gem_other_mat_is_allowed(DFHack::MaterialInfo& mi);
void write_food();
void read_food();
void write_gems();
void furniture_setup_other_mats();
void write_furniture();
bool furniture_mat_is_allowed ( const DFHack::MaterialInfo &mi );
void read_furniture();
void read_gems();
bool refuse_creature_is_allowed ( const df::creature_raw *raw );
void refuse_write_helper ( std::function<void ( const std::string & ) > add_value, const std::vector<char> & list );
bool refuse_type_is_allowed ( df::enums::item_type::item_type type );
void write_refuse();
void refuse_read_helper ( std::function<std::string ( const size_t& ) > get_value, size_t list_size, std::vector<char>* pile_list );
void read_refuse();
bool stone_is_allowed ( const DFHack::MaterialInfo &mi );
void write_stone();
void read_stone();
bool ammo_mat_is_allowed ( const DFHack::MaterialInfo &mi );
void write_ammo();
void read_ammo();
bool coins_mat_is_allowed ( const DFHack::MaterialInfo &mi );
void write_coins();
void read_coins();
void bars_blocks_setup_other_mats();
bool bars_mat_is_allowed ( const DFHack::MaterialInfo &mi );
bool blocks_mat_is_allowed ( const DFHack::MaterialInfo &mi );
void write_bars_blocks();
void read_bars_blocks();
bool gem_mat_is_allowed ( const DFHack::MaterialInfo &mi );
bool gem_cut_mat_is_allowed ( const DFHack::MaterialInfo &mi );
bool gem_other_mat_is_allowed(DFHack::MaterialInfo &mi );
void write_gems();
void read_gems();
bool finished_goods_type_is_allowed ( df::enums::item_type::item_type type );
void finished_goods_setup_other_mats();
bool finished_goods_mat_is_allowed ( const DFHack::MaterialInfo &mi );
void write_finished_goods();
void read_finished_goods();
void write_leather();
void read_leather();
void write_cloth();
bool finished_goods_type_is_allowed(df::enums::item_type::item_type type);
void finished_goods_setup_other_mats();
bool finished_goods_mat_is_allowed(const DFHack::MaterialInfo& mi);
void write_finished_goods();
void read_finished_goods();
void write_leather();
void read_leather();
void write_cloth();
void read_cloth();
bool wood_mat_is_allowed ( const df::plant_raw * plant );
bool wood_mat_is_allowed(const df::plant_raw* plant);
void write_wood();
void read_wood();
bool weapons_mat_is_allowed ( const DFHack::MaterialInfo &mi );
bool weapons_mat_is_allowed(const DFHack::MaterialInfo& mi);
void write_weapons();
void read_weapons();
void weapons_armor_setup_other_mats();
bool armor_mat_is_allowed ( const DFHack::MaterialInfo &mi );
bool armor_mat_is_allowed(const DFHack::MaterialInfo& mi);
void write_armor();
void read_armor();
};

@ -3,26 +3,16 @@
#include "MiscUtils.h"
#include "df/world.h"
#include "df/world_data.h"
#include "df/creature_raw.h"
#include "df/plant_raw.h"
#include <string>
#include <algorithm>
#include <functional>
// os
#include <sys/stat.h>
// Utility Functions {{{
// A set of convenience functions for doing common lookups
/**
* Retrieve creature raw from index
*/
static inline df::creature_raw* find_creature ( int32_t idx )
{
static inline df::creature_raw* find_creature(int32_t idx) {
return df::global::world->raws.creatures.all[idx];
}
@ -30,16 +20,14 @@ static inline df::creature_raw* find_creature ( int32_t idx )
* Retrieve creature index from id string
* @return -1 if not found
*/
static inline int16_t find_creature ( const std::string &creature_id )
{
return linear_index ( df::global::world->raws.creatures.all, &df::creature_raw::creature_id, creature_id );
static inline int16_t find_creature(const std::string& creature_id) {
return linear_index(df::global::world->raws.creatures.all, &df::creature_raw::creature_id, creature_id);
}
/**
* Retrieve plant raw from index
*/
static inline df::plant_raw* find_plant ( size_t idx )
{
static inline df::plant_raw* find_plant(size_t idx) {
return df::global::world->raws.plants.all[idx];
}
@ -47,32 +35,26 @@ static inline df::plant_raw* find_plant ( size_t idx )
* Retrieve plant index from id string
* @return -1 if not found
*/
static inline size_t find_plant ( const std::string &plant_id )
{
return linear_index ( df::global::world->raws.plants.all, &df::plant_raw::id, plant_id );
static inline size_t find_plant(const std::string& plant_id) {
return linear_index(df::global::world->raws.plants.all, &df::plant_raw::id, plant_id);
}
struct less_than_no_case
{
bool operator () (char x, char y) const
{
return toupper( static_cast< unsigned char >(x)) < toupper( static_cast< unsigned char >(y));
}
struct less_than_no_case {
bool operator () (char x, char y) const {
return toupper(static_cast<unsigned char>(x)) < toupper(static_cast<unsigned char>(y));
}
};
static inline bool CompareNoCase(const std::string &a, const std::string &b)
{
return std::lexicographical_compare( a.begin(),a.end(), b.begin(),b.end(), less_than_no_case() );
static inline bool CompareNoCase(const std::string& a, const std::string& b) {
return std::lexicographical_compare(a.begin(), a.end(), b.begin(), b.end(), less_than_no_case());
}
/**
* Checks if the parameter has the dfstock extension.
* Doesn't check if the file exists or not.
*/
static inline bool is_dfstockfile ( const std::string& filename )
{
return filename.rfind ( ".dfstock" ) != std::string::npos;
static inline bool is_dfstockfile(const std::string& filename) {
return filename.rfind(".dfstock") != std::string::npos;
}
// }}} utility Functions

@ -1,3 +1,4 @@
#include "Debug.h"
#include "PluginManager.h"
#include "StockpileUtils.h"
#include "StockpileSerializer.h"
@ -12,11 +13,16 @@ using namespace DFHack;
DFHACK_PLUGIN("stockpiles");
static command_result savestock ( color_ostream &out, vector <string> & parameters );
static command_result loadstock ( color_ostream &out, vector <string> & parameters );
REQUIRE_GLOBAL(world);
DFhackCExport command_result plugin_init ( color_ostream &out, std::vector <PluginCommand> &commands )
{
namespace DFHack {
DBG_DECLARE(stockpiles, log, DebugCategory::LINFO);
}
static command_result savestock(color_ostream& out, vector <string>& parameters);
static command_result loadstock(color_ostream& out, vector <string>& parameters);
DFhackCExport command_result plugin_init(color_ostream& out, std::vector <PluginCommand>& commands) {
commands.push_back(PluginCommand(
"savestock",
"Save the active stockpile's settings to a file.",
@ -31,61 +37,46 @@ DFhackCExport command_result plugin_init ( color_ostream &out, std::vector <Plug
return CR_OK;
}
DFhackCExport command_result plugin_shutdown ( color_ostream &out )
{
DFhackCExport command_result plugin_shutdown(color_ostream& out) {
return CR_OK;
}
// exporting
static command_result savestock ( color_ostream &out, vector <string> & parameters )
{
df::building_stockpilest *sp = Gui::getSelectedStockpile(out, true);
if ( !sp )
{
out.printerr ( "Selected building isn't a stockpile.\n" );
static command_result savestock(color_ostream& out, vector <string>& parameters) {
df::building_stockpilest* sp = Gui::getSelectedStockpile(out, true);
if (!sp) {
out.printerr("Selected building isn't a stockpile.\n");
return CR_WRONG_USAGE;
}
if ( parameters.size() > 2 )
{
out.printerr ( "Invalid parameters\n" );
if (parameters.size() > 2) {
out.printerr("Invalid parameters\n");
return CR_WRONG_USAGE;
}
bool debug = false;
std::string file;
for ( size_t i = 0; i < parameters.size(); ++i )
{
const std::string o = parameters.at ( i );
if ( o == "--debug" || o == "-d" )
debug = true;
else if ( !o.empty() && o[0] != '-' )
{
for (size_t i = 0; i < parameters.size(); ++i) {
const std::string o = parameters.at(i);
if (!o.empty() && o[0] != '-') {
file = o;
}
}
if ( file.empty() )
{
out.printerr ( "You must supply a valid filename.\n" );
if (file.empty()) {
out.printerr("You must supply a valid filename.\n");
return CR_WRONG_USAGE;
}
StockpileSerializer cereal ( sp );
if ( debug )
cereal.enable_debug ( out );
StockpileSerializer cereal(sp);
if ( !is_dfstockfile ( file ) ) file += ".dfstock";
try
{
if ( !cereal.serialize_to_file ( file ) )
{
out.printerr ( "could not save to %s\n", file.c_str() );
if (!is_dfstockfile(file)) file += ".dfstock";
try {
if (!cereal.serialize_to_file(file)) {
out.printerr("could not save to %s\n", file.c_str());
return CR_FAILURE;
}
}
catch ( std::exception &e )
{
out.printerr ( "serialization failed: protobuf exception: %s\n", e.what() );
catch (std::exception& e) {
out.printerr("serialization failed: protobuf exception: %s\n", e.what());
return CR_FAILURE;
}
@ -94,59 +85,45 @@ static command_result savestock ( color_ostream &out, vector <string> & paramete
// importing
static command_result loadstock ( color_ostream &out, vector <string> & parameters )
{
df::building_stockpilest *sp = Gui::getSelectedStockpile(out, true);
if ( !sp )
{
out.printerr ( "Selected building isn't a stockpile.\n" );
static command_result loadstock(color_ostream& out, vector <string>& parameters) {
df::building_stockpilest* sp = Gui::getSelectedStockpile(out, true);
if (!sp) {
out.printerr("Selected building isn't a stockpile.\n");
return CR_WRONG_USAGE;
}
if ( parameters.size() < 1 || parameters.size() > 2 )
{
out.printerr ( "Invalid parameters\n" );
if (parameters.size() < 1 || parameters.size() > 2) {
out.printerr("Invalid parameters\n");
return CR_WRONG_USAGE;
}
bool debug = false;
std::string file;
for ( size_t i = 0; i < parameters.size(); ++i )
{
const std::string o = parameters.at ( i );
if ( o == "--debug" || o == "-d" )
debug = true;
else if ( !o.empty() && o[0] != '-' )
{
for (size_t i = 0; i < parameters.size(); ++i) {
const std::string o = parameters.at(i);
if (!o.empty() && o[0] != '-') {
file = o;
}
}
if ( file.empty() ) {
out.printerr ( "ERROR: missing .dfstock file parameter\n");
if (file.empty()) {
out.printerr("ERROR: missing .dfstock file parameter\n");
return DFHack::CR_WRONG_USAGE;
}
if ( !is_dfstockfile ( file ) )
if (!is_dfstockfile(file))
file += ".dfstock";
if ( !Filesystem::exists ( file ) )
{
out.printerr ( "ERROR: the .dfstock file doesn't exist: %s\n", file.c_str());
if (!Filesystem::exists(file)) {
out.printerr("ERROR: the .dfstock file doesn't exist: %s\n", file.c_str());
return CR_WRONG_USAGE;
}
StockpileSerializer cereal ( sp );
if ( debug )
cereal.enable_debug ( out );
try
{
if ( !cereal.unserialize_from_file ( file ) )
{
out.printerr ( "unserialization failed: %s\n", file.c_str() );
StockpileSerializer cereal(sp);
try {
if (!cereal.unserialize_from_file(file)) {
out.printerr("unserialization failed: %s\n", file.c_str());
return CR_FAILURE;
}
}
catch ( std::exception &e )
{
out.printerr ( "unserialization failed: protobuf exception: %s\n", e.what() );
catch (std::exception& e) {
out.printerr("unserialization failed: protobuf exception: %s\n", e.what());
return CR_FAILURE;
}
return CR_OK;

@ -1 +1 @@
Subproject commit 288b38c9e9d8fabf1a0934ffbe23104274c39883
Subproject commit 89a345d66d70eb02293bd2e46dcfad65f0229ee6