Initial work on fortplan plugin, including separating out code that needs to be shared with buildingplan
parent
5fc8a1f51f
commit
5547722414
@ -0,0 +1,658 @@
|
||||
#include "buildingplan-lib.h"
|
||||
|
||||
#define PLUGIN_VERSION 0.00
|
||||
static void debug(const string &msg)
|
||||
{
|
||||
if (!show_debugging)
|
||||
return;
|
||||
|
||||
color_ostream_proxy out(Core::getInstance().getConsole());
|
||||
out << "DEBUG (" << PLUGIN_VERSION << "): " << msg << endl;
|
||||
}
|
||||
|
||||
/*
|
||||
* Material Choice Screen
|
||||
*/
|
||||
|
||||
static std::string material_to_string_fn(DFHack::MaterialInfo m) { return m.toString(); }
|
||||
|
||||
bool ItemFilter::matchesMask(DFHack::MaterialInfo &mat)
|
||||
{
|
||||
return (mat_mask.whole) ? mat.matches(mat_mask) : true;
|
||||
}
|
||||
|
||||
bool ItemFilter::matches(const df::dfhack_material_category mask) const
|
||||
{
|
||||
return mask.whole & mat_mask.whole;
|
||||
}
|
||||
|
||||
bool ItemFilter::matches(DFHack::MaterialInfo &material) const
|
||||
{
|
||||
for (auto it = materials.begin(); it != materials.end(); ++it)
|
||||
if (material.matches(*it))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ItemFilter::matches(df::item *item)
|
||||
{
|
||||
if (item->getQuality() < min_quality)
|
||||
return false;
|
||||
|
||||
if (decorated_only && !item->hasImprovements())
|
||||
return false;
|
||||
|
||||
auto imattype = item->getActualMaterial();
|
||||
auto imatindex = item->getActualMaterialIndex();
|
||||
auto item_mat = DFHack::MaterialInfo(imattype, imatindex);
|
||||
|
||||
return (materials.size() == 0) ? matchesMask(item_mat) : matches(item_mat);
|
||||
}
|
||||
|
||||
std::vector<std::string> ItemFilter::getMaterialFilterAsVector()
|
||||
{
|
||||
std::vector<std::string> descriptions;
|
||||
|
||||
transform_(materials, descriptions, material_to_string_fn);
|
||||
|
||||
if (descriptions.size() == 0)
|
||||
bitfield_to_string(&descriptions, mat_mask);
|
||||
|
||||
if (descriptions.size() == 0)
|
||||
descriptions.push_back("any");
|
||||
|
||||
return descriptions;
|
||||
}
|
||||
|
||||
std::string ItemFilter::getMaterialFilterAsSerial()
|
||||
{
|
||||
std::string str;
|
||||
|
||||
str.append(bitfield_to_string(mat_mask, ","));
|
||||
str.append("/");
|
||||
if (materials.size() > 0)
|
||||
{
|
||||
for (size_t i = 0; i < materials.size(); i++)
|
||||
str.append(materials[i].getToken() + ",");
|
||||
|
||||
if (str[str.size()-1] == ',')
|
||||
str.resize(str.size () - 1);
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
bool ItemFilter::parseSerializedMaterialTokens(std::string str)
|
||||
{
|
||||
valid = false;
|
||||
std::vector<std::string> tokens;
|
||||
split_string(&tokens, str, "/");
|
||||
|
||||
if (tokens.size() > 0 && !tokens[0].empty())
|
||||
{
|
||||
if (!parseJobMaterialCategory(&mat_mask, tokens[0]))
|
||||
return false;
|
||||
}
|
||||
|
||||
if (tokens.size() > 1 && !tokens[1].empty())
|
||||
{
|
||||
std::vector<std::string> mat_names;
|
||||
split_string(&mat_names, tokens[1], ",");
|
||||
for (auto m = mat_names.begin(); m != mat_names.end(); m++)
|
||||
{
|
||||
DFHack::MaterialInfo material;
|
||||
if (!material.find(*m) || !material.isValid())
|
||||
return false;
|
||||
|
||||
materials.push_back(material);
|
||||
}
|
||||
}
|
||||
|
||||
valid = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string ItemFilter::getMinQuality()
|
||||
{
|
||||
return ENUM_KEY_STR(item_quality, min_quality);
|
||||
}
|
||||
|
||||
bool ItemFilter::isValid()
|
||||
{
|
||||
return valid;
|
||||
}
|
||||
|
||||
void ItemFilter::clear()
|
||||
{
|
||||
mat_mask.whole = 0;
|
||||
materials.clear();
|
||||
}
|
||||
|
||||
static DFHack::MaterialInfo &material_info_identity_fn(DFHack::MaterialInfo &m) { return m; }
|
||||
|
||||
ViewscreenChooseMaterial::ViewscreenChooseMaterial(ItemFilter *filter)
|
||||
{
|
||||
selected_column = 0;
|
||||
masks_column.setTitle("Type");
|
||||
masks_column.multiselect = true;
|
||||
masks_column.allow_search = false;
|
||||
masks_column.left_margin = 2;
|
||||
materials_column.left_margin = MAX_MASK + 3;
|
||||
materials_column.setTitle("Material");
|
||||
materials_column.multiselect = true;
|
||||
this->filter = filter;
|
||||
|
||||
masks_column.changeHighlight(0);
|
||||
|
||||
populateMasks();
|
||||
populateMaterials();
|
||||
|
||||
masks_column.selectDefaultEntry();
|
||||
materials_column.selectDefaultEntry();
|
||||
materials_column.changeHighlight(0);
|
||||
}
|
||||
|
||||
void ViewscreenChooseMaterial::feed(set<df::interface_key> *input)
|
||||
{
|
||||
bool key_processed = false;
|
||||
switch (selected_column)
|
||||
{
|
||||
case 0:
|
||||
key_processed = masks_column.feed(input);
|
||||
if (input->count(interface_key::SELECT))
|
||||
populateMaterials(); // Redo materials lists based on category selection
|
||||
break;
|
||||
case 1:
|
||||
key_processed = materials_column.feed(input);
|
||||
break;
|
||||
}
|
||||
|
||||
if (key_processed)
|
||||
return;
|
||||
|
||||
if (input->count(interface_key::LEAVESCREEN))
|
||||
{
|
||||
input->clear();
|
||||
Screen::dismiss(this);
|
||||
return;
|
||||
}
|
||||
if (input->count(interface_key::CUSTOM_SHIFT_C))
|
||||
{
|
||||
filter->clear();
|
||||
masks_column.clearSelection();
|
||||
materials_column.clearSelection();
|
||||
populateMaterials();
|
||||
}
|
||||
else if (input->count(interface_key::SEC_SELECT))
|
||||
{
|
||||
// Convert list selections to material filters
|
||||
|
||||
|
||||
filter->mat_mask.whole = 0;
|
||||
filter->materials.clear();
|
||||
|
||||
// Category masks
|
||||
auto masks = masks_column.getSelectedElems();
|
||||
for (auto it = masks.begin(); it != masks.end(); ++it)
|
||||
filter->mat_mask.whole |= it->whole;
|
||||
|
||||
// Specific materials
|
||||
auto materials = materials_column.getSelectedElems();
|
||||
transform_(materials, filter->materials, material_info_identity_fn);
|
||||
|
||||
Screen::dismiss(this);
|
||||
}
|
||||
else if (input->count(interface_key::CURSOR_LEFT))
|
||||
{
|
||||
--selected_column;
|
||||
validateColumn();
|
||||
}
|
||||
else if (input->count(interface_key::CURSOR_RIGHT))
|
||||
{
|
||||
selected_column++;
|
||||
validateColumn();
|
||||
}
|
||||
else if (enabler->tracking_on && enabler->mouse_lbut)
|
||||
{
|
||||
if (masks_column.setHighlightByMouse())
|
||||
selected_column = 0;
|
||||
else if (materials_column.setHighlightByMouse())
|
||||
selected_column = 1;
|
||||
|
||||
enabler->mouse_lbut = enabler->mouse_rbut = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void ViewscreenChooseMaterial::render()
|
||||
{
|
||||
if (Screen::isDismissed(this))
|
||||
return;
|
||||
|
||||
dfhack_viewscreen::render();
|
||||
|
||||
Screen::clear();
|
||||
Screen::drawBorder(" Building Material ");
|
||||
|
||||
masks_column.display(selected_column == 0);
|
||||
materials_column.display(selected_column == 1);
|
||||
|
||||
int32_t y = gps->dimy - 3;
|
||||
int32_t x = 2;
|
||||
OutputHotkeyString(x, y, "Toggle", "Enter");
|
||||
x += 3;
|
||||
OutputHotkeyString(x, y, "Save", "Shift-Enter");
|
||||
x += 3;
|
||||
OutputHotkeyString(x, y, "Clear", "C");
|
||||
x += 3;
|
||||
OutputHotkeyString(x, y, "Cancel", "Esc");
|
||||
}
|
||||
|
||||
// START Room Reservation
|
||||
ReservedRoom::ReservedRoom(df::building *building, std::string noble_code)
|
||||
{
|
||||
this->building = building;
|
||||
config = DFHack::World::AddPersistentData("buildingplan/reservedroom");
|
||||
config.val() = noble_code;
|
||||
config.ival(1) = building->id;
|
||||
pos = df::coord(building->centerx, building->centery, building->z);
|
||||
}
|
||||
|
||||
ReservedRoom::ReservedRoom(PersistentDataItem &config, color_ostream &out)
|
||||
{
|
||||
this->config = config;
|
||||
|
||||
building = df::building::find(config.ival(1));
|
||||
if (!building)
|
||||
return;
|
||||
pos = df::coord(building->centerx, building->centery, building->z);
|
||||
}
|
||||
|
||||
bool ReservedRoom::checkRoomAssignment()
|
||||
{
|
||||
if (!isValid())
|
||||
return false;
|
||||
|
||||
auto np = getOwnersNobleCode();
|
||||
bool correctOwner = false;
|
||||
for (auto iter = np.begin(); iter != np.end(); iter++)
|
||||
{
|
||||
if (iter->position->code == getCode())
|
||||
{
|
||||
correctOwner = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (correctOwner)
|
||||
return true;
|
||||
|
||||
for (auto iter = world->units.active.begin(); iter != world->units.active.end(); iter++)
|
||||
{
|
||||
df::unit* unit = *iter;
|
||||
if (!Units::isCitizen(unit))
|
||||
continue;
|
||||
|
||||
if (DFHack::Units::isDead(unit))
|
||||
continue;
|
||||
|
||||
np = getUniqueNoblePositions(unit);
|
||||
for (auto iter = np.begin(); iter != np.end(); iter++)
|
||||
{
|
||||
if (iter->position->code == getCode())
|
||||
{
|
||||
Buildings::setOwner(building, unit);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string RoomMonitor::getReservedNobleCode(int32_t buildingId)
|
||||
{
|
||||
for (auto iter = reservedRooms.begin(); iter != reservedRooms.end(); iter++)
|
||||
{
|
||||
if (buildingId == iter->getId())
|
||||
return iter->getCode();
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
void RoomMonitor::toggleRoomForPosition(int32_t buildingId, std::string noble_code)
|
||||
{
|
||||
bool found = false;
|
||||
for (auto iter = reservedRooms.begin(); iter != reservedRooms.end(); iter++)
|
||||
{
|
||||
if (buildingId != iter->getId())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (noble_code == iter->getCode())
|
||||
{
|
||||
iter->remove();
|
||||
reservedRooms.erase(iter);
|
||||
}
|
||||
else
|
||||
{
|
||||
iter->setCode(noble_code);
|
||||
}
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found)
|
||||
{
|
||||
ReservedRoom room(df::building::find(buildingId), noble_code);
|
||||
reservedRooms.push_back(room);
|
||||
}
|
||||
}
|
||||
|
||||
void RoomMonitor::doCycle()
|
||||
{
|
||||
for (auto iter = reservedRooms.begin(); iter != reservedRooms.end();)
|
||||
{
|
||||
if (iter->checkRoomAssignment())
|
||||
{
|
||||
++iter;
|
||||
}
|
||||
else
|
||||
{
|
||||
iter->remove();
|
||||
iter = reservedRooms.erase(iter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RoomMonitor::reset(color_ostream &out)
|
||||
{
|
||||
reservedRooms.clear();
|
||||
std::vector<PersistentDataItem> items;
|
||||
DFHack::World::GetPersistentData(&items, "buildingplan/reservedroom");
|
||||
|
||||
for (auto i = items.begin(); i != items.end(); i++)
|
||||
{
|
||||
ReservedRoom rr(*i, out);
|
||||
if (rr.isValid())
|
||||
addRoom(rr);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void delete_item_fn(df::job_item *x) { delete x; }
|
||||
|
||||
// START Planning
|
||||
|
||||
PlannedBuilding::PlannedBuilding(df::building *building, ItemFilter *filter)
|
||||
{
|
||||
this->building = building;
|
||||
this->filter = *filter;
|
||||
pos = df::coord(building->centerx, building->centery, building->z);
|
||||
config = DFHack::World::AddPersistentData("buildingplan/constraints");
|
||||
config.val() = filter->getMaterialFilterAsSerial();
|
||||
config.ival(1) = building->id;
|
||||
config.ival(2) = filter->min_quality + 1;
|
||||
config.ival(3) = static_cast<int>(filter->decorated_only) + 1;
|
||||
}
|
||||
|
||||
PlannedBuilding::PlannedBuilding(PersistentDataItem &config, color_ostream &out)
|
||||
{
|
||||
this->config = config;
|
||||
|
||||
if (!filter.parseSerializedMaterialTokens(config.val()))
|
||||
{
|
||||
out.printerr("Buildingplan: Cannot parse filter: %s\nDiscarding.", config.val().c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
building = df::building::find(config.ival(1));
|
||||
if (!building)
|
||||
return;
|
||||
|
||||
pos = df::coord(building->centerx, building->centery, building->z);
|
||||
filter.min_quality = static_cast<df::item_quality>(config.ival(2) - 1);
|
||||
filter.decorated_only = config.ival(3) - 1;
|
||||
}
|
||||
|
||||
bool PlannedBuilding::assignClosestItem(std::vector<df::item *> *items_vector)
|
||||
{
|
||||
decltype(items_vector->begin()) closest_item;
|
||||
int32_t closest_distance = -1;
|
||||
for (auto item_iter = items_vector->begin(); item_iter != items_vector->end(); item_iter++)
|
||||
{
|
||||
auto item = *item_iter;
|
||||
if (!filter.matches(item))
|
||||
continue;
|
||||
|
||||
auto pos = item->pos;
|
||||
auto distance = abs(pos.x - building->centerx) +
|
||||
abs(pos.y - building->centery) +
|
||||
abs(pos.z - building->z) * 50;
|
||||
|
||||
if (closest_distance > -1 && distance >= closest_distance)
|
||||
continue;
|
||||
|
||||
closest_distance = distance;
|
||||
closest_item = item_iter;
|
||||
}
|
||||
|
||||
if (closest_distance > -1 && assignItem(*closest_item))
|
||||
{
|
||||
debug("Item assigned");
|
||||
items_vector->erase(closest_item);
|
||||
remove();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PlannedBuilding::assignItem(df::item *item)
|
||||
{
|
||||
auto ref = df::allocate<df::general_ref_building_holderst>();
|
||||
if (!ref)
|
||||
{
|
||||
Core::printerr("Could not allocate general_ref_building_holderst\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
ref->building_id = building->id;
|
||||
|
||||
if (building->jobs.size() != 1)
|
||||
return false;
|
||||
|
||||
auto job = building->jobs[0];
|
||||
|
||||
for_each_(job->job_items, delete_item_fn);
|
||||
job->job_items.clear();
|
||||
job->flags.bits.suspend = false;
|
||||
|
||||
bool rough = false;
|
||||
Job::attachJobItem(job, item, df::job_item_ref::Hauled);
|
||||
if (item->getType() == item_type::BOULDER)
|
||||
rough = true;
|
||||
building->mat_type = item->getMaterial();
|
||||
building->mat_index = item->getMaterialIndex();
|
||||
|
||||
job->mat_type = building->mat_type;
|
||||
job->mat_index = building->mat_index;
|
||||
|
||||
if (building->needsDesign())
|
||||
{
|
||||
auto act = (df::building_actual *) building;
|
||||
act->design = new df::building_design();
|
||||
act->design->flags.bits.rough = rough;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlannedBuilding::isValid()
|
||||
{
|
||||
bool valid = filter.isValid() &&
|
||||
building && Buildings::findAtTile(pos) == building &&
|
||||
building->getBuildStage() == 0;
|
||||
|
||||
if (!valid)
|
||||
remove();
|
||||
|
||||
return valid;
|
||||
}
|
||||
|
||||
void Planner::reset(color_ostream &out)
|
||||
{
|
||||
planned_buildings.clear();
|
||||
std::vector<PersistentDataItem> items;
|
||||
DFHack::World::GetPersistentData(&items, "buildingplan/constraints");
|
||||
|
||||
for (auto i = items.begin(); i != items.end(); i++)
|
||||
{
|
||||
PlannedBuilding pb(*i, out);
|
||||
if (pb.isValid())
|
||||
planned_buildings.push_back(pb);
|
||||
}
|
||||
}
|
||||
|
||||
void Planner::initialize()
|
||||
{
|
||||
std::vector<std::string> item_names;
|
||||
typedef df::enum_traits<df::item_type> item_types;
|
||||
int size = item_types::last_item_value - item_types::first_item_value+1;
|
||||
for (size_t i = 1; i < size; i++)
|
||||
{
|
||||
is_relevant_item_type[(df::item_type) (i-1)] = false;
|
||||
std::string item_name = toLower(item_types::key_table[i]);
|
||||
std::string item_name_clean;
|
||||
for (auto c = item_name.begin(); c != item_name.end(); c++)
|
||||
{
|
||||
if (*c == '_')
|
||||
continue;
|
||||
item_name_clean += *c;
|
||||
}
|
||||
item_names.push_back(item_name_clean);
|
||||
}
|
||||
|
||||
typedef df::enum_traits<df::building_type> building_types;
|
||||
size = building_types::last_item_value - building_types::first_item_value+1;
|
||||
for (size_t i = 1; i < size; i++)
|
||||
{
|
||||
auto building_type = (df::building_type) (i-1);
|
||||
if (building_type == building_type::Weapon || building_type == building_type::Floodgate)
|
||||
continue;
|
||||
|
||||
std::string building_name = toLower(building_types::key_table[i]);
|
||||
for (size_t j = 0; j < item_names.size(); j++)
|
||||
{
|
||||
if (building_name == item_names[j])
|
||||
{
|
||||
auto btype = (df::building_type) (i-1);
|
||||
auto itype = (df::item_type) j;
|
||||
|
||||
item_for_building_type[btype] = itype;
|
||||
default_item_filters[btype] = ItemFilter();
|
||||
available_item_vectors[itype] = std::vector<df::item *>();
|
||||
is_relevant_item_type[itype] = true;
|
||||
|
||||
if (planmode_enabled.find(btype) == planmode_enabled.end())
|
||||
{
|
||||
planmode_enabled[btype] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Planner::doCycle()
|
||||
{
|
||||
debug("Running Cycle");
|
||||
if (planned_buildings.size() == 0)
|
||||
return;
|
||||
|
||||
debug("Planned count: " + int_to_string(planned_buildings.size()));
|
||||
|
||||
gather_available_items();
|
||||
for (auto building_iter = planned_buildings.begin(); building_iter != planned_buildings.end();)
|
||||
{
|
||||
if (building_iter->isValid())
|
||||
{
|
||||
if (show_debugging)
|
||||
debug(std::string("Trying to allocate ") + enum_item_key_str(building_iter->getType()));
|
||||
|
||||
auto required_item_type = item_for_building_type[building_iter->getType()];
|
||||
auto items_vector = &available_item_vectors[required_item_type];
|
||||
if (items_vector->size() == 0 || !building_iter->assignClosestItem(items_vector))
|
||||
{
|
||||
debug("Unable to allocate an item");
|
||||
++building_iter;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
debug("Removing building plan");
|
||||
building_iter = planned_buildings.erase(building_iter);
|
||||
}
|
||||
}
|
||||
|
||||
bool Planner::allocatePlannedBuilding(df::building_type type)
|
||||
{
|
||||
coord32_t cursor;
|
||||
if (!DFHack::Gui::getCursorCoords(cursor.x, cursor.y, cursor.z))
|
||||
return false;
|
||||
|
||||
auto newinst = Buildings::allocInstance(cursor.get_coord16(), type);
|
||||
if (!newinst)
|
||||
return false;
|
||||
|
||||
df::job_item *filter = new df::job_item();
|
||||
filter->item_type = item_type::NONE;
|
||||
filter->mat_index = 0;
|
||||
filter->flags2.bits.building_material = true;
|
||||
std::vector<df::job_item*> filters;
|
||||
filters.push_back(filter);
|
||||
|
||||
if (!Buildings::constructWithFilters(newinst, filters))
|
||||
{
|
||||
delete newinst;
|
||||
return false;
|
||||
}
|
||||
|
||||
for (auto iter = newinst->jobs.begin(); iter != newinst->jobs.end(); iter++)
|
||||
{
|
||||
(*iter)->flags.bits.suspend = true;
|
||||
}
|
||||
|
||||
if (type == building_type::Door)
|
||||
{
|
||||
auto door = virtual_cast<df::building_doorst>(newinst);
|
||||
if (door)
|
||||
door->door_flags.bits.pet_passable = true;
|
||||
}
|
||||
|
||||
addPlannedBuilding(newinst);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
PlannedBuilding *Planner::getSelectedPlannedBuilding()
|
||||
{
|
||||
for (auto building_iter = planned_buildings.begin(); building_iter != planned_buildings.end(); building_iter++)
|
||||
{
|
||||
if (building_iter->isCurrentlySelectedBuilding())
|
||||
{
|
||||
return &(*building_iter);
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void Planner::cycleDefaultQuality(df::building_type type)
|
||||
{
|
||||
auto quality = &getDefaultItemFilterForType(type)->min_quality;
|
||||
*quality = static_cast<df::item_quality>(*quality + 1);
|
||||
if (*quality == item_quality::Artifact)
|
||||
(*quality) = item_quality::Ordinary;
|
||||
}
|
@ -0,0 +1,495 @@
|
||||
#ifndef BUILDINGPLAN_H
|
||||
#define BUILDINGPLAN_H
|
||||
|
||||
#include "uicommon.h"
|
||||
|
||||
#include <functional>
|
||||
|
||||
// DF data structure definition headers
|
||||
#include "DataDefs.h"
|
||||
#include "Types.h"
|
||||
#include "df/build_req_choice_genst.h"
|
||||
#include "df/build_req_choice_specst.h"
|
||||
#include "df/item.h"
|
||||
#include "df/ui.h"
|
||||
#include "df/ui_build_selector.h"
|
||||
#include "df/viewscreen_dwarfmodest.h"
|
||||
#include "df/items_other_id.h"
|
||||
#include "df/job.h"
|
||||
#include "df/world.h"
|
||||
#include "df/building_constructionst.h"
|
||||
#include "df/building_design.h"
|
||||
#include "df/entity_position.h"
|
||||
|
||||
#include "modules/Buildings.h"
|
||||
#include "modules/Maps.h"
|
||||
#include "modules/Items.h"
|
||||
#include "modules/Units.h"
|
||||
#include "modules/Gui.h"
|
||||
|
||||
#include "TileTypes.h"
|
||||
#include "df/job_item.h"
|
||||
#include "df/dfhack_material_category.h"
|
||||
#include "df/general_ref_building_holderst.h"
|
||||
#include "modules/Job.h"
|
||||
#include "df/building_design.h"
|
||||
#include "df/buildings_other_id.h"
|
||||
#include "modules/World.h"
|
||||
#include "df/building.h"
|
||||
#include "df/building_doorst.h"
|
||||
|
||||
using df::global::ui;
|
||||
using df::global::ui_build_selector;
|
||||
using df::global::world;
|
||||
|
||||
struct MaterialDescriptor
|
||||
{
|
||||
df::item_type item_type;
|
||||
int16_t item_subtype;
|
||||
int16_t type;
|
||||
int32_t index;
|
||||
bool valid;
|
||||
|
||||
bool matches(const MaterialDescriptor &a) const
|
||||
{
|
||||
return a.valid && valid &&
|
||||
a.type == type &&
|
||||
a.index == index &&
|
||||
a.item_type == item_type &&
|
||||
a.item_subtype == item_subtype;
|
||||
}
|
||||
};
|
||||
|
||||
#define MAX_MASK 10
|
||||
#define MAX_MATERIAL 21
|
||||
#define SIDEBAR_WIDTH 30
|
||||
|
||||
static bool canReserveRoom(df::building *building)
|
||||
{
|
||||
if (!building)
|
||||
return false;
|
||||
|
||||
if (building->jobs.size() > 0 && building->jobs[0]->job_type == job_type::DestroyBuilding)
|
||||
return false;
|
||||
|
||||
return building->is_room;
|
||||
}
|
||||
|
||||
static std::vector<Units::NoblePosition> getUniqueNoblePositions(df::unit *unit)
|
||||
{
|
||||
std::vector<Units::NoblePosition> np;
|
||||
Units::getNoblePositions(&np, unit);
|
||||
for (auto iter = np.begin(); iter != np.end(); iter++)
|
||||
{
|
||||
if (iter->position->code == "MILITIA_CAPTAIN")
|
||||
{
|
||||
np.erase(iter);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return np;
|
||||
}
|
||||
|
||||
static void delete_item_fn(df::job_item *x);
|
||||
|
||||
static MaterialInfo &material_info_identity_fn(MaterialInfo &m);
|
||||
|
||||
static map<df::building_type, bool> planmode_enabled, saved_planmodes;
|
||||
|
||||
static void enable_quickfort_fn(pair<const df::building_type, bool>& pair);
|
||||
|
||||
static void debug(const std::string &msg);
|
||||
static std::string material_to_string_fn(MaterialInfo m);
|
||||
|
||||
static bool show_debugging = true;
|
||||
static bool show_help = false;
|
||||
|
||||
struct ItemFilter
|
||||
{
|
||||
df::dfhack_material_category mat_mask;
|
||||
std::vector<DFHack::MaterialInfo> materials;
|
||||
df::item_quality min_quality;
|
||||
bool decorated_only;
|
||||
|
||||
ItemFilter() : min_quality(df::item_quality::Ordinary), decorated_only(false), valid(true)
|
||||
{ }
|
||||
|
||||
bool matchesMask(DFHack::MaterialInfo &mat);
|
||||
|
||||
bool matches(const df::dfhack_material_category mask) const;
|
||||
|
||||
bool matches(DFHack::MaterialInfo &material) const;
|
||||
|
||||
bool matches(df::item *item);
|
||||
|
||||
std::vector<std::string> getMaterialFilterAsVector();
|
||||
|
||||
std::string getMaterialFilterAsSerial();
|
||||
|
||||
bool parseSerializedMaterialTokens(std::string str);
|
||||
|
||||
std::string getMinQuality();
|
||||
|
||||
bool isValid();
|
||||
|
||||
void clear();
|
||||
|
||||
private:
|
||||
bool valid;
|
||||
};
|
||||
|
||||
class ViewscreenChooseMaterial : public dfhack_viewscreen
|
||||
{
|
||||
public:
|
||||
ViewscreenChooseMaterial(ItemFilter *filter);
|
||||
|
||||
void feed(set<df::interface_key> *input);
|
||||
|
||||
void render();
|
||||
|
||||
std::string getFocusString() { return "buildingplan_choosemat"; }
|
||||
|
||||
private:
|
||||
ListColumn<df::dfhack_material_category> masks_column;
|
||||
ListColumn<MaterialInfo> materials_column;
|
||||
int selected_column;
|
||||
ItemFilter *filter;
|
||||
|
||||
df::building_type btype;
|
||||
|
||||
void addMaskEntry(df::dfhack_material_category &mask, const std::string &text)
|
||||
{
|
||||
auto entry = ListEntry<df::dfhack_material_category>(pad_string(text, MAX_MASK, false), mask);
|
||||
if (filter->matches(mask))
|
||||
entry.selected = true;
|
||||
|
||||
masks_column.add(entry);
|
||||
}
|
||||
|
||||
void populateMasks()
|
||||
{
|
||||
masks_column.clear();
|
||||
df::dfhack_material_category mask;
|
||||
|
||||
mask.whole = 0;
|
||||
mask.bits.stone = true;
|
||||
addMaskEntry(mask, "Stone");
|
||||
|
||||
mask.whole = 0;
|
||||
mask.bits.wood = true;
|
||||
addMaskEntry(mask, "Wood");
|
||||
|
||||
mask.whole = 0;
|
||||
mask.bits.metal = true;
|
||||
addMaskEntry(mask, "Metal");
|
||||
|
||||
mask.whole = 0;
|
||||
mask.bits.soap = true;
|
||||
addMaskEntry(mask, "Soap");
|
||||
|
||||
masks_column.filterDisplay();
|
||||
}
|
||||
|
||||
void populateMaterials()
|
||||
{
|
||||
materials_column.clear();
|
||||
df::dfhack_material_category selected_category;
|
||||
std::vector<df::dfhack_material_category> selected_masks = masks_column.getSelectedElems();
|
||||
if (selected_masks.size() == 1)
|
||||
selected_category = selected_masks[0];
|
||||
else if (selected_masks.size() > 1)
|
||||
return;
|
||||
|
||||
df::world_raws &raws = world->raws;
|
||||
for (int i = 1; i < DFHack::MaterialInfo::NUM_BUILTIN; i++)
|
||||
{
|
||||
auto obj = raws.mat_table.builtin[i];
|
||||
if (obj)
|
||||
{
|
||||
MaterialInfo material;
|
||||
material.decode(i, -1);
|
||||
addMaterialEntry(selected_category, material, material.toString());
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < raws.inorganics.size(); i++)
|
||||
{
|
||||
df::inorganic_raw *p = raws.inorganics[i];
|
||||
MaterialInfo material;
|
||||
material.decode(0, i);
|
||||
addMaterialEntry(selected_category, material, material.toString());
|
||||
}
|
||||
|
||||
decltype(selected_category) wood_flag;
|
||||
wood_flag.bits.wood = true;
|
||||
if (!selected_category.whole || selected_category.bits.wood)
|
||||
{
|
||||
for (size_t i = 0; i < raws.plants.all.size(); i++)
|
||||
{
|
||||
df::plant_raw *p = raws.plants.all[i];
|
||||
for (size_t j = 0; p->material.size() > 1 && j < p->material.size(); j++)
|
||||
{
|
||||
auto t = p->material[j];
|
||||
if (p->material[j]->id != "WOOD")
|
||||
continue;
|
||||
|
||||
MaterialInfo material;
|
||||
material.decode(DFHack::MaterialInfo::PLANT_BASE+j, i);
|
||||
auto name = material.toString();
|
||||
ListEntry<MaterialInfo> entry(pad_string(name, MAX_MATERIAL, false), material);
|
||||
if (filter->matches(material))
|
||||
entry.selected = true;
|
||||
|
||||
materials_column.add(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
materials_column.sort();
|
||||
}
|
||||
|
||||
void addMaterialEntry(df::dfhack_material_category &selected_category,
|
||||
MaterialInfo &material, std::string name)
|
||||
{
|
||||
if (!selected_category.whole || material.matches(selected_category))
|
||||
{
|
||||
ListEntry<MaterialInfo> entry(pad_string(name, MAX_MATERIAL, false), material);
|
||||
if (filter->matches(material))
|
||||
entry.selected = true;
|
||||
|
||||
materials_column.add(entry);
|
||||
}
|
||||
}
|
||||
|
||||
void validateColumn()
|
||||
{
|
||||
set_to_limit(selected_column, 1);
|
||||
}
|
||||
|
||||
void resize(int32_t x, int32_t y)
|
||||
{
|
||||
dfhack_viewscreen::resize(x, y);
|
||||
masks_column.resize();
|
||||
materials_column.resize();
|
||||
}
|
||||
};
|
||||
|
||||
class ReservedRoom
|
||||
{
|
||||
public:
|
||||
ReservedRoom(df::building *building, std::string noble_code);
|
||||
|
||||
ReservedRoom(PersistentDataItem &config, color_ostream &out);
|
||||
|
||||
bool checkRoomAssignment();
|
||||
|
||||
void remove() { DFHack::World::DeletePersistentData(config); }
|
||||
|
||||
bool isValid()
|
||||
{
|
||||
if (!building)
|
||||
return false;
|
||||
|
||||
if (Buildings::findAtTile(pos) != building)
|
||||
return false;
|
||||
|
||||
return canReserveRoom(building);
|
||||
}
|
||||
|
||||
int32_t getId()
|
||||
{
|
||||
if (!isValid())
|
||||
return 0;
|
||||
|
||||
return building->id;
|
||||
}
|
||||
|
||||
std::string getCode() { return config.val(); }
|
||||
|
||||
void setCode(const std::string &noble_code) { config.val() = noble_code; }
|
||||
|
||||
private:
|
||||
df::building *building;
|
||||
PersistentDataItem config;
|
||||
df::coord pos;
|
||||
|
||||
std::vector<Units::NoblePosition> getOwnersNobleCode()
|
||||
{
|
||||
if (!building->owner)
|
||||
return std::vector<Units::NoblePosition> ();
|
||||
|
||||
return getUniqueNoblePositions(building->owner);
|
||||
}
|
||||
};
|
||||
|
||||
class RoomMonitor
|
||||
{
|
||||
public:
|
||||
RoomMonitor() { }
|
||||
|
||||
std::string getReservedNobleCode(int32_t buildingId);
|
||||
|
||||
void toggleRoomForPosition(int32_t buildingId, std::string noble_code);
|
||||
|
||||
void doCycle();
|
||||
|
||||
void reset(color_ostream &out);
|
||||
|
||||
private:
|
||||
std::vector<ReservedRoom> reservedRooms;
|
||||
|
||||
void addRoom(ReservedRoom &rr)
|
||||
{
|
||||
for (auto iter = reservedRooms.begin(); iter != reservedRooms.end(); iter++)
|
||||
{
|
||||
if (iter->getId() == rr.getId())
|
||||
return;
|
||||
}
|
||||
|
||||
reservedRooms.push_back(rr);
|
||||
}
|
||||
};
|
||||
|
||||
// START Planning
|
||||
class PlannedBuilding
|
||||
{
|
||||
public:
|
||||
PlannedBuilding(df::building *building, ItemFilter *filter);
|
||||
|
||||
PlannedBuilding(PersistentDataItem &config, color_ostream &out);
|
||||
|
||||
bool assignClosestItem(std::vector<df::item *> *items_vector);
|
||||
|
||||
bool assignItem(df::item *item);
|
||||
|
||||
bool isValid();
|
||||
|
||||
df::building_type getType() { return building->getType(); }
|
||||
|
||||
bool isCurrentlySelectedBuilding() { return isValid() && (building == world->selected_building); }
|
||||
|
||||
ItemFilter *getFilter() { return &filter; }
|
||||
|
||||
void remove() { DFHack::World::DeletePersistentData(config); }
|
||||
|
||||
private:
|
||||
df::building *building;
|
||||
PersistentDataItem config;
|
||||
df::coord pos;
|
||||
ItemFilter filter;
|
||||
};
|
||||
|
||||
class Planner
|
||||
{
|
||||
public:
|
||||
bool in_dummmy_screen;
|
||||
|
||||
Planner() : quickfort_mode(false), in_dummmy_screen(false) { }
|
||||
|
||||
bool isPlanableBuilding(const df::building_type type) const
|
||||
{
|
||||
return item_for_building_type.find(type) != item_for_building_type.end();
|
||||
}
|
||||
|
||||
void reset(color_ostream &out);
|
||||
|
||||
void initialize();
|
||||
|
||||
void addPlannedBuilding(df::building *bld)
|
||||
{
|
||||
PlannedBuilding pb(bld, &default_item_filters[bld->getType()]);
|
||||
planned_buildings.push_back(pb);
|
||||
}
|
||||
|
||||
void doCycle();
|
||||
|
||||
bool allocatePlannedBuilding(df::building_type type);
|
||||
|
||||
PlannedBuilding *getSelectedPlannedBuilding();
|
||||
|
||||
void removeSelectedPlannedBuilding() { getSelectedPlannedBuilding()->remove(); }
|
||||
|
||||
ItemFilter *getDefaultItemFilterForType(df::building_type type) { return &default_item_filters[type]; }
|
||||
|
||||
void cycleDefaultQuality(df::building_type type);
|
||||
|
||||
void enableQuickfortMode()
|
||||
{
|
||||
saved_planmodes = planmode_enabled;
|
||||
for_each_(planmode_enabled, enable_quickfort_fn);
|
||||
|
||||
quickfort_mode = true;
|
||||
}
|
||||
|
||||
void disableQuickfortMode()
|
||||
{
|
||||
planmode_enabled = saved_planmodes;
|
||||
quickfort_mode = false;
|
||||
}
|
||||
|
||||
bool inQuickFortMode() { return quickfort_mode; }
|
||||
|
||||
private:
|
||||
map<df::building_type, df::item_type> item_for_building_type;
|
||||
map<df::building_type, ItemFilter> default_item_filters;
|
||||
map<df::item_type, std::vector<df::item *>> available_item_vectors;
|
||||
map<df::item_type, bool> is_relevant_item_type; //Needed for fast check when looping over all items
|
||||
bool quickfort_mode;
|
||||
|
||||
std::vector<PlannedBuilding> planned_buildings;
|
||||
|
||||
void gather_available_items()
|
||||
{
|
||||
debug("Gather available items");
|
||||
for (auto iter = available_item_vectors.begin(); iter != available_item_vectors.end(); iter++)
|
||||
{
|
||||
iter->second.clear();
|
||||
}
|
||||
|
||||
// Precompute a bitmask with the bad flags
|
||||
df::item_flags bad_flags;
|
||||
bad_flags.whole = 0;
|
||||
|
||||
#define F(x) bad_flags.bits.x = true;
|
||||
F(dump); F(forbid); F(garbage_collect);
|
||||
F(hostile); F(on_fire); F(rotten); F(trader);
|
||||
F(in_building); F(construction); F(artifact);
|
||||
#undef F
|
||||
|
||||
std::vector<df::item*> &items = world->items.other[items_other_id::IN_PLAY];
|
||||
|
||||
for (size_t i = 0; i < items.size(); i++)
|
||||
{
|
||||
df::item *item = items[i];
|
||||
|
||||
if (item->flags.whole & bad_flags.whole)
|
||||
continue;
|
||||
|
||||
df::item_type itype = item->getType();
|
||||
if (!is_relevant_item_type[itype])
|
||||
continue;
|
||||
|
||||
if (itype == item_type::BOX && item->isBag())
|
||||
continue; //Skip bags
|
||||
|
||||
if (item->flags.bits.artifact)
|
||||
continue;
|
||||
|
||||
if (item->flags.bits.in_job ||
|
||||
item->isAssignedToStockpile() ||
|
||||
item->flags.bits.owned ||
|
||||
item->flags.bits.in_chest)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
available_item_vectors[itype].push_back(item);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
static Planner planner;
|
||||
|
||||
static RoomMonitor roomMonitor;
|
||||
|
||||
#endif
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,188 @@
|
||||
#include "buildingplan-lib.h"
|
||||
#include <fstream>
|
||||
#include <vector>
|
||||
|
||||
DFHACK_PLUGIN("fortplan");
|
||||
#define PLUGIN_VERSION 0.10
|
||||
|
||||
command_result fortplan(color_ostream &out, vector<string> & params);
|
||||
|
||||
struct BuildingInfo {
|
||||
std::string code;
|
||||
df::building_type type;
|
||||
std::string name;
|
||||
|
||||
BuildingInfo(std::string theCode, df::building_type theType, std::string theName) {
|
||||
code = theCode;
|
||||
type = theType;
|
||||
name = theName;
|
||||
}
|
||||
};
|
||||
|
||||
class MatchesCode
|
||||
{
|
||||
std::string _code;
|
||||
|
||||
public:
|
||||
MatchesCode(const std::string &code) : _code(code) {}
|
||||
|
||||
bool operator()(const BuildingInfo &check) const
|
||||
{
|
||||
return check.code == _code;
|
||||
}
|
||||
};
|
||||
|
||||
std::vector<BuildingInfo> buildings;
|
||||
|
||||
DFhackCExport command_result plugin_init ( color_ostream &out, vector <PluginCommand> &commands) {
|
||||
commands.push_back(PluginCommand("fortplan","Lay out buildings in your fortress based on a Quickfort-style CSV input file.",fortplan,false,
|
||||
"Lay out buildings in your fortress based on a Quickfort-style CSV input file.\n"
|
||||
"Usage: fortplan [filename]\n"));
|
||||
|
||||
buildings.push_back(BuildingInfo("a",df::building_type::Armorstand,"Armor Stand"));
|
||||
buildings.push_back(BuildingInfo("r",df::building_type::Weaponrack,"Weapon Rack"));
|
||||
buildings.push_back(BuildingInfo("b",df::building_type::Bed,"Bed"));
|
||||
buildings.push_back(BuildingInfo("f",df::building_type::Cabinet,"Cabinet"));
|
||||
buildings.push_back(BuildingInfo("h",df::building_type::Box,"Box"));
|
||||
buildings.push_back(BuildingInfo("d",df::building_type::Door,"Door"));
|
||||
buildings.push_back(BuildingInfo("n",df::building_type::Coffin,"Coffin"));
|
||||
buildings.push_back(BuildingInfo("c",df::building_type::Chair,"Chair"));
|
||||
buildings.push_back(BuildingInfo("t",df::building_type::Table,"Table"));
|
||||
|
||||
out << "Loaded fortplan version " << PLUGIN_VERSION << endl;
|
||||
return CR_OK;
|
||||
}
|
||||
|
||||
#define DAY_TICKS 1200
|
||||
DFhackCExport command_result plugin_onupdate(color_ostream &out)
|
||||
{
|
||||
static decltype(world->frame_counter) last_frame_count = 0;
|
||||
if ((world->frame_counter - last_frame_count) >= DAY_TICKS/2)
|
||||
{
|
||||
last_frame_count = world->frame_counter;
|
||||
planner.doCycle();
|
||||
}
|
||||
|
||||
return CR_OK;
|
||||
}
|
||||
|
||||
DFHACK_PLUGIN_IS_ENABLED(is_enabled);
|
||||
|
||||
DFhackCExport command_result plugin_enable(color_ostream &out, bool enable)
|
||||
{
|
||||
if (!gps)
|
||||
return CR_FAILURE;
|
||||
|
||||
if (enable != is_enabled)
|
||||
{
|
||||
planner.reset(out);
|
||||
|
||||
is_enabled = enable;
|
||||
}
|
||||
|
||||
return CR_OK;
|
||||
}
|
||||
std::string get_working_path()
|
||||
{
|
||||
char temp[MAXPATHLEN];
|
||||
return ( getcwd(temp, MAXPATHLEN) ? std::string( temp ) : std::string("") );
|
||||
}
|
||||
|
||||
command_result fortplan(color_ostream &out, vector<string> & params) {
|
||||
|
||||
auto & con = out;
|
||||
if (params.size()) {
|
||||
coord32_t cursor;
|
||||
coord32_t startCursor;
|
||||
if (!DFHack::Gui::getCursorCoords(cursor.x, cursor.y, cursor.z)) {
|
||||
con.print("You must have an active in-game cursor.\n");
|
||||
return CR_FAILURE;
|
||||
}
|
||||
DFHack::Gui::getCursorCoords(startCursor.x, startCursor.y, startCursor.z);
|
||||
|
||||
std::string cwd = get_working_path();
|
||||
std::string filename = cwd+"/"+params[0];
|
||||
con.print("Loading file '%s'...\n",filename.c_str());
|
||||
std::ifstream infile(filename.c_str());
|
||||
if (!infile.good()) {
|
||||
con.print("Could not open the file.\n");
|
||||
return CR_FAILURE;
|
||||
}
|
||||
std::string line;
|
||||
int lineNum = 0;
|
||||
while (std::getline(infile, line)) {
|
||||
lineNum++;
|
||||
if (lineNum==1) {
|
||||
auto hashBuild = line.find("#build");
|
||||
if (hashBuild >= 0) {
|
||||
auto startLoc = line.find("start(");
|
||||
if (startLoc != line.npos) {
|
||||
startLoc += 6;
|
||||
auto nextDelimiter = line.find(";",startLoc);
|
||||
std::string startXStr = line.substr(startLoc,nextDelimiter-startLoc);
|
||||
int startXOffset = std::stoi(startXStr);
|
||||
startLoc = nextDelimiter+1;
|
||||
nextDelimiter = line.find(";",startLoc);
|
||||
std::string startYStr = line.substr(startLoc,nextDelimiter-startLoc);
|
||||
int startYOffset = std::stoi(startYStr);
|
||||
startCursor.x -= startXOffset;
|
||||
startCursor.y -= startYOffset;
|
||||
DFHack::Gui::setCursorCoords(startCursor.x,startCursor.y,startCursor.z);
|
||||
|
||||
auto startEnd = line.find(")",nextDelimiter);
|
||||
|
||||
con.print("Starting at (%d,%d,%d) which is described as: %s\n",startCursor.x,startCursor.y,startCursor.z,line.substr(nextDelimiter+1,startEnd-nextDelimiter).c_str());
|
||||
std::string desc = line.substr(startEnd+1);
|
||||
if (desc.size()>0) {
|
||||
con.print("Description of this plan: %s\n",desc.c_str());
|
||||
}
|
||||
continue;
|
||||
} else {
|
||||
con.print("No start location found for this block\n");
|
||||
}
|
||||
} else {
|
||||
con.print("Not a build file: %s\n",line.c_str());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int start = 0;
|
||||
auto nextInd = line.find(',');
|
||||
std::string curCell = line.substr(start,nextInd-start);
|
||||
if (strcmp(curCell.substr(0,1).c_str(),"#")==0) {
|
||||
continue;
|
||||
}
|
||||
do {
|
||||
|
||||
if (strcmp(curCell.c_str(),"`")!=0) {
|
||||
con.print("Found a cell with '%s' in it (line %d:%d-%d)\n",curCell.c_str(),lineNum,start,nextInd);
|
||||
auto buildingIndex = std::find_if(buildings.begin(), buildings.end(), MatchesCode(curCell.c_str()));
|
||||
|
||||
// = std::find(validInstructions.begin(), validInstructions.end(), curCell);
|
||||
if(buildingIndex == buildings.end()) {
|
||||
con.print("That is not a valid code.\n");
|
||||
} else {
|
||||
//con.print("I can build that!\n");
|
||||
BuildingInfo buildingInfo = *buildingIndex;
|
||||
con.print("Building a(n) %s.\n",buildingInfo.name.c_str());
|
||||
planner.allocatePlannedBuilding(buildingInfo.type);
|
||||
}
|
||||
}
|
||||
cursor.x++;
|
||||
DFHack::Gui::setCursorCoords(cursor.x, cursor.y, cursor.z);
|
||||
start = nextInd+1;
|
||||
nextInd = line.find(',',start);
|
||||
curCell = line.substr(start,nextInd-start);
|
||||
} while (nextInd != line.npos);
|
||||
|
||||
cursor.y++;
|
||||
cursor.x = startCursor.x;
|
||||
|
||||
}
|
||||
con.print("Done with file.\n");
|
||||
} else {
|
||||
con.print("You must supply a filename to read.\n");
|
||||
}
|
||||
|
||||
return CR_OK;
|
||||
}
|
Loading…
Reference in New Issue