diff --git a/.travis.yml b/.travis.yml index ad17693a9..ede4b132b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,13 +3,17 @@ language: cpp cache: pip: true directories: + - $HOME/DF-travis - $HOME/lua53 addons: apt: packages: &default_packages + - libsdl-image1.2-dev + - libsdl-ttf2.0-dev + - libsdl1.2-dev - libxml-libxml-perl - libxml-libxslt-perl - - zlib1g-dev:i386 + - zlib1g-dev matrix: include: - env: GCC_VERSION=4.8 @@ -19,11 +23,15 @@ matrix: - ubuntu-toolchain-r-test packages: - *default_packages - - gcc-4.8-multilib - - g++-4.8-multilib + - gcc-4.8 + - g++-4.8 before_install: +- export DF_VERSION=$(sh travis/get-df-version.sh) +- export DF_FOLDER="$HOME/DF-travis/$DF_VERSION" - pip install --user "sphinx==1.4" "requests[security]" - sh travis/build-lua.sh +- sh travis/download-df.sh +- echo "export DFHACK_HEADLESS=1" >> "$HOME/.dfhackrc" script: - export PATH="$PATH:$HOME/lua53/bin" - git tag tmp-travis-build @@ -37,8 +45,12 @@ script: - python travis/script-syntax.py --ext=rb --cmd="ruby -c" - mkdir build-travis - cd build-travis -- cmake .. -DCMAKE_C_COMPILER=gcc-$GCC_VERSION -DCMAKE_CXX_COMPILER=g++-$GCC_VERSION -DBUILD_DOCS:BOOL=ON -- make -j3 +- cmake .. -DCMAKE_C_COMPILER=gcc-$GCC_VERSION -DCMAKE_CXX_COMPILER=g++-$GCC_VERSION -DDFHACK_BUILD_ARCH=64 -DBUILD_DOCS:BOOL=ON -DCMAKE_INSTALL_PREFIX="$DF_FOLDER" +- make -j3 install +- mv "$DF_FOLDER"/dfhack.init-example "$DF_FOLDER"/dfhack.init +- cd .. +- cp travis/dfhack_travis.init "$DF_FOLDER"/ +- python travis/run-tests.py "$DF_FOLDER" notifications: email: false irc: diff --git a/NEWS.rst b/NEWS.rst index 478c79e01..ee685f8f9 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -36,6 +36,33 @@ Changelog .. contents:: :depth: 2 +DFHack 0.44.05-r2 +================= + +New Plugins +----------- +- `embark-assistant`: adds more information and features to embark screen + +New Scripts +----------- +- `adv-fix-sleepers`: fixes units in adventure mode who refuse to wake up (:bug:`6798`) +- `hermit`: blocks caravans, migrants, diplomats (for hermit challenge) + +New Features +------------ +- With ``PRINT_MODE:TEXT``, setting the ``DFHACK_HEADLESS`` environment variable + will hide DF's display and allow the console to be used normally. (Note that + this is intended for testing and is not very useful for actual gameplay.) + +Fixes +----- +- `devel/inject-raws`: fixed gloves and shoes (old typo causing errors) +- `view-item-info`: fixed an error with some shields + +Misc Improvements +----------------- +- `autochop`: can now exclude trees with fruit, + DFHack 0.44.05-r1 ================= diff --git a/docs/Authors.rst b/docs/Authors.rst index 8c1b5096e..aecfc519a 100644 --- a/docs/Authors.rst +++ b/docs/Authors.rst @@ -31,6 +31,7 @@ Clément Vuchener cvuchener Dan Amlund danamlund David Corbett dscorbett David Seguin dseguin +David Timm dtimm Deon DoctorVanGogh DoctorVanGogh Donald Ruegsegger hashaash diff --git a/docs/Core.rst b/docs/Core.rst index 15adcc533..b15305fa9 100644 --- a/docs/Core.rst +++ b/docs/Core.rst @@ -430,6 +430,45 @@ Other init files directory, will be run when any world or that save is loaded. +Environment variables +===================== + +DFHack's behavior can be adjusted with some environment variables. For example, +on UNIX-like systems:: + + DFHACK_SOME_VAR=1 ./dfhack + +- ``DFHACK_PORT``: the port to use for the RPC server (used by ``dfhack-run`` + and `remotefortressreader` among others) instead of the default ``5000``. As + with the default, if this port cannot be used, the server is not started. + +- ``DFHACK_DISABLE_CONSOLE``: if set, the DFHack console is not set up. This is + the default behavior if ``PRINT_MODE:TEXT`` is set in ``data/init/init.txt``. + Intended for situations where DFHack cannot run in a terminal window. + +- ``DFHACK_HEADLESS``: if set, and ``PRINT_MODE:TEXT`` is set, DF's display will + be hidden, and the console will be started unless ``DFHACK_DISABLE_CONSOLE`` + is also set. Intended for non-interactive gameplay only. + +- ``DFHACK_NO_GLOBALS``, ``DFHACK_NO_VTABLES``: ignores all global or vtable + addresses in ``symbols.xml``, respectively. Intended for development use - + e.g. to make sure tools do not crash when these addresses are missing. + +- ``DFHACK_NO_DEV_PLUGINS``: if set, any plugins from the plugins/devel folder + that are built and installed will not be loaded on startup. + +- ``DFHACK_LOG_MEM_RANGES`` (macOS only): if set, logs memory ranges to + ``stderr.log``. Note that `devel/lsmem` can also do this. + +Other (non-DFHack-specific) variables that affect DFHack: + +- ``TERM``: if this is set to ``dumb`` or ``cons25`` on \*nix, the console will + not support any escape sequences (arrow keys, etc.). + +- ``LANG``, ``LC_CTYPE``: if either of these contain "UTF8" or "UTF-8" (not case + sensitive), ``DF2CONSOLE()`` will produce UTF-8-encoded text. Note that this + should be the case in most UTF-8-capable \*nix terminal emulators already. + Miscellaneous Notes =================== This section is for odd but important notes that don't fit anywhere else. diff --git a/docs/Plugins.rst b/docs/Plugins.rst index c896b5fd0..da1231184 100644 --- a/docs/Plugins.rst +++ b/docs/Plugins.rst @@ -526,6 +526,18 @@ nopause Disables pausing (both manual and automatic) with the exception of pause forced by `reveal` ``hell``. This is nice for digging under rivers. +.. _embark-assistant: + +embark-assistant +================ + +This plugin provides embark site selection help. It has to be run with the +``embark-assistant`` command while the pre-embark screen is displayed and shows +extended (and correct(?)) resource information for the embark rectangle as well +as normally undisplayed sites in the current embark region. It also has a site +selection tool with more options than DF's vanilla search tool. For detailed +help invoke the in game info screen. Requires 42 lines to display properly. + .. _embark-tools: embark-tools diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index 40c122de9..4d4a4e9f1 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -57,6 +57,7 @@ SET(MAIN_SOURCES Core.cpp ColorText.cpp DataDefs.cpp +Error.cpp VTableInterpose.cpp LuaWrapper.cpp LuaTypes.cpp @@ -319,7 +320,7 @@ ADD_DEPENDENCIES(dfhack-version git-describe) ADD_LIBRARY(dfhack SHARED ${PROJECT_SOURCES}) ADD_DEPENDENCIES(dfhack generate_headers generate_proto_core) -ADD_LIBRARY(dfhack-client SHARED RemoteClient.cpp ColorText.cpp MiscUtils.cpp ${PROJECT_PROTO_SRCS}) +ADD_LIBRARY(dfhack-client SHARED RemoteClient.cpp ColorText.cpp MiscUtils.cpp Error.cpp ${PROJECT_PROTO_SRCS}) ADD_DEPENDENCIES(dfhack-client dfhack) ADD_EXECUTABLE(dfhack-run dfhack-run.cpp) diff --git a/library/Core.cpp b/library/Core.cpp index 5173a3ad9..58b3f85f5 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -81,6 +81,10 @@ using namespace DFHack; #include "SDL_events.h" +#ifdef LINUX_BUILD +#include +#endif + using namespace tthread; using namespace df::enums; using df::global::init; @@ -1638,7 +1642,24 @@ bool Core::Init() cerr << "Initializing Console.\n"; // init the console. bool is_text_mode = (init && init->display.flag.is_set(init_display_flags::TEXT)); - if (is_text_mode || getenv("DFHACK_DISABLE_CONSOLE")) + bool is_headless = bool(getenv("DFHACK_HEADLESS")); + if (is_headless) + { +#ifdef LINUX_BUILD + auto endwin = (int(*)(void))dlsym(RTLD_DEFAULT, "endwin"); + if (endwin) + { + endwin(); + } + else + { + cerr << "endwin(): bind failed" << endl; + } +#else + cerr << "Headless mode not supported on Windows" << endl; +#endif + } + if ((is_text_mode && !is_headless) || getenv("DFHACK_DISABLE_CONSOLE")) { con.init(true); cerr << "Console is not available. Use dfhack-run to send commands.\n"; @@ -1718,7 +1739,7 @@ bool Core::Init() HotkeyMutex = new mutex(); HotkeyCond = new condition_variable(); - if (!is_text_mode) + if (!is_text_mode || is_headless) { cerr << "Starting IO thread.\n"; // create IO thread diff --git a/library/Error.cpp b/library/Error.cpp new file mode 100644 index 000000000..ba1482918 --- /dev/null +++ b/library/Error.cpp @@ -0,0 +1,43 @@ +#include "Error.h" +#include "MiscUtils.h" + +using namespace DFHack::Error; + +inline std::string safe_str(const char *s) +{ + return s ? s : "(NULL)"; +} + +NullPointer::NullPointer(const char *varname) + :All("NULL pointer: " + safe_str(varname)), + varname(varname) +{} + +InvalidArgument::InvalidArgument(const char *expr) + :All("Invalid argument; expected: " + safe_str(expr)), + expr(expr) +{} + +VTableMissing::VTableMissing(const char *name) + :All("Missing vtable address: " + safe_str(name)), + name(name) +{} + +SymbolsXmlParse::SymbolsXmlParse(const char* desc, int id, int row, int col) + :AllSymbols(stl_sprintf("error %d: %s, at row %d col %d", id, desc, row, col)), + desc(safe_str(desc)), id(id), row(row), col(col) +{} + +SymbolsXmlBadAttribute::SymbolsXmlBadAttribute(const char *attr) + :AllSymbols("attribute is either missing or invalid: " + safe_str(attr)), + attr(safe_str(attr)) +{} + +SymbolsXmlNoRoot::SymbolsXmlNoRoot() + :AllSymbols("no root element") +{} + +SymbolsXmlUnderspecifiedEntry::SymbolsXmlUnderspecifiedEntry(const char *where) + :AllSymbols("Underspecified symbol file entry, each entry needs to set both the name attribute and have a value. parent: " + safe_str(where)), + where(safe_str(where)) +{} diff --git a/library/Hooks-linux.cpp b/library/Hooks-linux.cpp index b0bf5a781..8291bf899 100644 --- a/library/Hooks-linux.cpp +++ b/library/Hooks-linux.cpp @@ -88,6 +88,10 @@ DFhackCExport int SDL_PollEvent(SDL::Event* event) struct WINDOW; DFhackCExport int wgetch(WINDOW *win) { + if (getenv("DFHACK_HEADLESS")) + { + return 0; + } static int (*_wgetch)(WINDOW * win) = (int (*)( WINDOW * )) dlsym(RTLD_NEXT, "wgetch"); if(!_wgetch) { diff --git a/library/LuaTypes.cpp b/library/LuaTypes.cpp index f30a1878e..f744f3eba 100644 --- a/library/LuaTypes.cpp +++ b/library/LuaTypes.cpp @@ -1076,20 +1076,8 @@ int LuaWrapper::method_wrapper_core(lua_State *state, function_identity_base *id try { id->invoke(state, 1); } - catch (Error::NullPointer &e) { - const char *vn = e.varname(); - std::string tmp = stl_sprintf("NULL pointer: %s", vn ? vn : "?"); - field_error(state, UPVAL_METHOD_NAME, tmp.c_str(), "invoke"); - } - catch (Error::InvalidArgument &e) { - const char *vn = e.expr(); - std::string tmp = stl_sprintf("Invalid argument; expected: %s", vn ? vn : "?"); - field_error(state, UPVAL_METHOD_NAME, tmp.c_str(), "invoke"); - } - catch (Error::VTableMissing &e) { - const char *cn = e.name(); - std::string tmp = stl_sprintf("Missing vtable address: %s", cn ? cn : "?"); - field_error(state, UPVAL_METHOD_NAME, tmp.c_str(), "invoke"); + catch (Error::All &e) { + field_error(state, UPVAL_METHOD_NAME, e.what(), "invoke"); } catch (std::exception &e) { std::string tmp = stl_sprintf("C++ exception: %s", e.what()); @@ -1107,17 +1095,8 @@ int Lua::CallWithCatch(lua_State *state, int (*fn)(lua_State*), const char *cont try { return fn(state); } - catch (Error::NullPointer &e) { - const char *vn = e.varname(); - return luaL_error(state, "%s: NULL pointer: %s", context, vn ? vn : "?"); - } - catch (Error::InvalidArgument &e) { - const char *vn = e.expr(); - return luaL_error(state, "%s: Invalid argument; expected: %s", context, vn ? vn : "?"); - } - catch (Error::VTableMissing &e) { - const char *cn = e.name(); - return luaL_error(state, "%s: Missing vtable address: %s", context, cn ? cn : "?"); + catch (Error::All &e) { + return luaL_error(state, "%s: %s", context, e.what()); } catch (std::exception &e) { return luaL_error(state, "%s: C++ exception: %s", context, e.what()); diff --git a/library/MiscUtils.cpp b/library/MiscUtils.cpp index bff7f07e1..e350cb9ec 100644 --- a/library/MiscUtils.cpp +++ b/library/MiscUtils.cpp @@ -25,7 +25,6 @@ distribution. #include "Internal.h" #include "Export.h" #include "MiscUtils.h" -#include "Error.h" #ifndef LINUX_BUILD #include @@ -41,18 +40,6 @@ distribution. #include #include -const char *DFHack::Error::NullPointer::what() const throw() { - return "DFHack::Error::NullPointer"; -} - -const char *DFHack::Error::InvalidArgument::what() const throw() { - return "DFHack::Error::InvalidArgument"; -} - -const char *DFHack::Error::VTableMissing::what() const throw() { - return "DFHack::Error::VTableMissing"; -} - std::string stl_sprintf(const char *fmt, ...) { va_list lst; va_start(lst, fmt); diff --git a/library/PluginManager.cpp b/library/PluginManager.cpp index a4927a756..b6d1dcc92 100644 --- a/library/PluginManager.cpp +++ b/library/PluginManager.cpp @@ -797,6 +797,25 @@ PluginManager::~PluginManager() void PluginManager::init() { loadAll(); + + bool any_loaded = false; + for (auto p : all_plugins) + { + if (p.second->getState() == Plugin::PS_LOADED) + { + any_loaded = true; + break; + } + } + if (!any_loaded && !listPlugins().empty()) + { + Core::printerr("\n" +"All plugins present failed to load.\n" +"If you are using Windows XP, this is probably due to a Visual Studio 2015 bug.\n" +"Windows XP is unsupported by Microsoft as of 2014, so we do not support it.\n\n" +"If this was unexpected and you are not using Windows XP, please report this.\n\n" + ); + } } bool PluginManager::addPlugin(string name) diff --git a/library/RemoteServer.cpp b/library/RemoteServer.cpp index bc30daff3..e4204014f 100644 --- a/library/RemoteServer.cpp +++ b/library/RemoteServer.cpp @@ -63,6 +63,8 @@ using namespace DFHack; #include "tinythread.h" using namespace tthread; +#include "jsoncpp.h" + using dfproto::CoreTextNotification; using dfproto::CoreTextFragment; using google::protobuf::MessageLite; @@ -284,7 +286,11 @@ void ServerConnection::threadFn() } else { - if (!fn->in()->ParseFromArray(buf.get(), header.size)) + if (((fn->flags & SF_ALLOW_REMOTE) != SF_ALLOW_REMOTE) && strcmp(socket->GetClientAddr(), "127.0.0.1") != 0) + { + stream.printerr("In call to %s: forbidden host: %s\n", fn->name, socket->GetClientAddr()); + } + else if (!fn->in()->ParseFromArray(buf.get(), header.size)) { stream.printerr("In call to %s: could not decode input args.\n", fn->name); } @@ -375,8 +381,44 @@ bool ServerMain::listen(int port) socket->Initialize(); - if (!socket->Listen("127.0.0.1", port)) - return false; + std::string filename("dfhack-config/remote-server.json"); + + Json::Value configJson; + + std::ifstream inFile(filename, std::ios_base::in); + + bool allow_remote = false; + + if (inFile.is_open()) + { + inFile >> configJson; + inFile.close(); + + allow_remote = configJson.get("allow_remote", "false").asBool(); + port = configJson.get("port", port).asInt(); + } + + configJson["allow_remote"] = allow_remote; + configJson["port"] = port; + + std::ofstream outFile(filename, std::ios_base::trunc); + + if (outFile.is_open()) + { + outFile << configJson; + outFile.close(); + } + + if (allow_remote) + { + if (!socket->Listen(NULL, port)) + return false; + } + else + { + if (!socket->Listen("127.0.0.1", port)) + return false; + } thread = new tthread::thread(threadFn, this); thread->detach(); diff --git a/library/RemoteTools.cpp b/library/RemoteTools.cpp index e29220fb7..d0bdee00f 100644 --- a/library/RemoteTools.cpp +++ b/library/RemoteTools.cpp @@ -654,29 +654,29 @@ CoreService::CoreService() { suspend_depth = 0; // These 2 methods must be first, so that they get id 0 and 1 - addMethod("BindMethod", &CoreService::BindMethod, SF_DONT_SUSPEND); + addMethod("BindMethod", &CoreService::BindMethod, SF_DONT_SUSPEND | SF_ALLOW_REMOTE); addMethod("RunCommand", &CoreService::RunCommand, SF_DONT_SUSPEND); // Add others here: - addMethod("CoreSuspend", &CoreService::CoreSuspend, SF_DONT_SUSPEND); - addMethod("CoreResume", &CoreService::CoreResume, SF_DONT_SUSPEND); + addMethod("CoreSuspend", &CoreService::CoreSuspend, SF_DONT_SUSPEND | SF_ALLOW_REMOTE); + addMethod("CoreResume", &CoreService::CoreResume, SF_DONT_SUSPEND | SF_ALLOW_REMOTE); addMethod("RunLua", &CoreService::RunLua); // Functions: - addFunction("GetVersion", GetVersion, SF_DONT_SUSPEND); - addFunction("GetDFVersion", GetDFVersion, SF_DONT_SUSPEND); + addFunction("GetVersion", GetVersion, SF_DONT_SUSPEND | SF_ALLOW_REMOTE); + addFunction("GetDFVersion", GetDFVersion, SF_DONT_SUSPEND | SF_ALLOW_REMOTE); - addFunction("GetWorldInfo", GetWorldInfo); + addFunction("GetWorldInfo", GetWorldInfo, SF_ALLOW_REMOTE); - addFunction("ListEnums", ListEnums, SF_CALLED_ONCE | SF_DONT_SUSPEND); - addFunction("ListJobSkills", ListJobSkills, SF_CALLED_ONCE | SF_DONT_SUSPEND); + addFunction("ListEnums", ListEnums, SF_CALLED_ONCE | SF_DONT_SUSPEND | SF_ALLOW_REMOTE); + addFunction("ListJobSkills", ListJobSkills, SF_CALLED_ONCE | SF_DONT_SUSPEND | SF_ALLOW_REMOTE); - addFunction("ListMaterials", ListMaterials, SF_CALLED_ONCE); - addFunction("ListUnits", ListUnits); - addFunction("ListSquads", ListSquads); + addFunction("ListMaterials", ListMaterials, SF_CALLED_ONCE | SF_ALLOW_REMOTE); + addFunction("ListUnits", ListUnits, SF_ALLOW_REMOTE); + addFunction("ListSquads", ListSquads, SF_ALLOW_REMOTE); - addFunction("SetUnitLabors", SetUnitLabors); + addFunction("SetUnitLabors", SetUnitLabors, SF_ALLOW_REMOTE); } CoreService::~CoreService() diff --git a/library/include/Error.h b/library/include/Error.h index 2d0baadfc..d3d208b1f 100644 --- a/library/include/Error.h +++ b/library/include/Error.h @@ -41,118 +41,91 @@ namespace DFHack #ifdef _MSC_VER #pragma push /** - * C4275 is - The warning officially is non dll-interface class 'std::exception' used as base for + * C4275 - The warning officially is non dll-interface class 'std::exception' used as base for * dll-interface class * - * Basically, its saying that you might have an ABI problem if you mismatch compilers. We don't + * Basically, it's saying that you might have an ABI problem if you mismatch compilers. We don't * care since we build all of DFhack at once against whatever Toady is using */ #pragma warning(disable: 4275) #endif - class DFHACK_EXPORT All : public std::exception{}; + class DFHACK_EXPORT All : public std::exception + { + public: + const std::string full; + All(const std::string &full) + :full(full) + {} + virtual const char *what() const noexcept + { + return full.c_str(); + } + virtual ~All() noexcept {} + }; #ifdef _MSC_VER #pragma pop #endif class DFHACK_EXPORT NullPointer : public All { - const char *varname_; public: - NullPointer(const char *varname_ = NULL) : varname_(varname_) {} - const char *varname() const { return varname_; } - virtual const char *what() const throw(); + const char *const varname; + NullPointer(const char *varname = NULL); }; #define CHECK_NULL_POINTER(var) \ { if (var == NULL) throw DFHack::Error::NullPointer(#var); } class DFHACK_EXPORT InvalidArgument : public All { - const char *expr_; public: - InvalidArgument(const char *expr_ = NULL) : expr_(expr_) {} - const char *expr() const { return expr_; } - virtual const char *what() const throw(); + const char *const expr; + InvalidArgument(const char *expr = NULL); }; #define CHECK_INVALID_ARGUMENT(expr) \ { if (!(expr)) throw DFHack::Error::InvalidArgument(#expr); } class DFHACK_EXPORT VTableMissing : public All { - const char *name_; public: - VTableMissing(const char *name_ = NULL) : name_(name_) {} - const char *name() const { return name_; } - virtual const char *what() const throw(); + const char *const name; + VTableMissing(const char *name = NULL); }; - class DFHACK_EXPORT AllSymbols : public All{}; + class DFHACK_EXPORT AllSymbols : public All + { + public: + AllSymbols(const std::string &full) + :All(full) + {} + }; // Syntax errors and whatnot, the xml can't be read class DFHACK_EXPORT SymbolsXmlParse : public AllSymbols { public: - SymbolsXmlParse(const char* _desc, int _id, int _row, int _col) - :desc(_desc), id(_id), row(_row), col(_col) - { - std::stringstream s; - s << "error " << id << ": " << desc << ", at row " << row << " col " << col; - full = s.str(); - } - std::string full; + SymbolsXmlParse(const char* desc, int id, int row, int col); const std::string desc; const int id; const int row; const int col; - virtual ~SymbolsXmlParse() throw(){}; - virtual const char* what() const throw() - { - return full.c_str(); - } }; - class DFHACK_EXPORT SymbolsXmlBadAttribute : public All + class DFHACK_EXPORT SymbolsXmlBadAttribute : public AllSymbols { public: - SymbolsXmlBadAttribute(const char* _attr) : attr(_attr) - { - std::stringstream s; - s << "attribute is either missing or invalid: " << attr; - full = s.str(); - } - std::string full; + SymbolsXmlBadAttribute(const char* attr); std::string attr; - virtual ~SymbolsXmlBadAttribute() throw(){}; - virtual const char* what() const throw() - { - return full.c_str(); - } }; - class DFHACK_EXPORT SymbolsXmlNoRoot : public All + class DFHACK_EXPORT SymbolsXmlNoRoot : public AllSymbols { public: - SymbolsXmlNoRoot() {} - virtual ~SymbolsXmlNoRoot() throw(){}; - virtual const char* what() const throw() - { - return "Symbol file is missing root element."; - } + SymbolsXmlNoRoot(); }; - class DFHACK_EXPORT SymbolsXmlUnderspecifiedEntry : public All + class DFHACK_EXPORT SymbolsXmlUnderspecifiedEntry : public AllSymbols { public: - SymbolsXmlUnderspecifiedEntry(const char * _where) : where(_where) - { - std::stringstream s; - s << "Underspecified symbol file entry, each entry needs to set both the name attribute and have a value. parent: " << where; - full = s.str(); - } - virtual ~SymbolsXmlUnderspecifiedEntry() throw(){}; + SymbolsXmlUnderspecifiedEntry(const char *where); std::string where; - std::string full; - virtual const char* what() const throw() - { - return full.c_str(); - } }; } } diff --git a/library/include/RemoteServer.h b/library/include/RemoteServer.h index df2f42712..514f91250 100644 --- a/library/include/RemoteServer.h +++ b/library/include/RemoteServer.h @@ -46,7 +46,10 @@ namespace DFHack SF_CALLED_ONCE = 1, // Don't automatically suspend the core around the call. // The function is supposed to manage locking itself. - SF_DONT_SUSPEND = 2 + SF_DONT_SUSPEND = 2, + // The function is considered safe to call from a remote computer. + // All other functions cannot be allowed for security reasons. + SF_ALLOW_REMOTE = 4 }; class DFHACK_EXPORT ServerFunctionBase : public RPCFunctionBase { diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index b6e05b749..e85e0aaf4 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -111,6 +111,7 @@ if (BUILD_SUPPORTED) add_subdirectory(diggingInvaders) DFHACK_PLUGIN(dwarfvet dwarfvet.cpp) DFHACK_PLUGIN(dwarfmonitor dwarfmonitor.cpp LINK_LIBRARIES lua) + add_subdirectory(embark-assistant) DFHACK_PLUGIN(embark-tools embark-tools.cpp) DFHACK_PLUGIN(eventful eventful.cpp LINK_LIBRARIES lua) DFHACK_PLUGIN(fastdwarf fastdwarf.cpp) diff --git a/plugins/autochop.cpp b/plugins/autochop.cpp index dbc79af42..a2a863540 100644 --- a/plugins/autochop.cpp +++ b/plugins/autochop.cpp @@ -16,6 +16,7 @@ #include "df/items_other_id.h" #include "df/job.h" #include "df/map_block.h" +#include "df/material.h" #include "df/plant.h" #include "df/plant_raw.h" #include "df/tile_dig_designation.h" @@ -48,6 +49,26 @@ static bool autochop_enabled = false; static int min_logs, max_logs; static const int LOG_CAP_MAX = 99999; static bool wait_for_threshold; +struct Skip { + bool fruit_trees; + bool food_trees; + bool cook_trees; + operator int() { + return (fruit_trees ? 1 : 0) | + (food_trees ? 2 : 0) | + (cook_trees ? 4 : 0); + } + Skip &operator= (int in) { + // set all fields to false if they haven't been set in this save yet + if (in < 0) + in = 0; + fruit_trees = (in & 1); + food_trees = (in & 2); + cook_trees = (in & 4); + return *this; + } +}; +static Skip skip; static PersistentDataItem config_autochop; @@ -179,6 +200,7 @@ static void save_config() config_autochop.ival(1) = min_logs; config_autochop.ival(2) = max_logs; config_autochop.ival(3) = wait_for_threshold; + config_autochop.ival(4) = skip; } static void initialize() @@ -188,6 +210,7 @@ static void initialize() min_logs = 80; max_logs = 100; wait_for_threshold = false; + skip = 0; config_autochop = World::GetPersistentData("autochop/config"); if (config_autochop.isValid()) @@ -197,6 +220,7 @@ static void initialize() min_logs = config_autochop.ival(1); max_logs = config_autochop.ival(2); wait_for_threshold = config_autochop.ival(3); + skip = config_autochop.ival(4); } else { @@ -206,26 +230,86 @@ static void initialize() } } -static int do_chop_designation(bool chop, bool count_only) +static bool skip_plant(const df::plant * plant, bool *restricted) +{ + if (restricted) + *restricted = false; + + // Skip all non-trees immediately. + if (plant->flags.bits.is_shrub) + return true; + + // Skip plants with invalid tile. + df::map_block *cur = Maps::getTileBlock(plant->pos); + if (!cur) + return true; + + int x = plant->pos.x % 16; + int y = plant->pos.y % 16; + + // Skip all unrevealed plants. + if (cur->designation[x][y].bits.hidden) + return true; + + df::tiletype_material material = tileMaterial(cur->tiletype[x][y]); + if (material != tiletype_material::TREE) + return true; + + const df::plant_raw *plant_raw = df::plant_raw::find(plant->material); + + // Skip fruit trees if set. + if (skip.fruit_trees && plant_raw->material_defs.type_drink != -1) + { + if (restricted) + *restricted = true; + return true; + } + + if (skip.food_trees || skip.cook_trees) + { + df::material * mat; + for (int idx = 0; idx < plant_raw->material.size(); idx++) + { + mat = plant_raw->material[idx]; + if (skip.food_trees && mat->flags.is_set(material_flags::EDIBLE_RAW)) + { + if (restricted) + *restricted = true; + return true; + } + + if (skip.cook_trees && mat->flags.is_set(material_flags::EDIBLE_COOKED)) + { + if (restricted) + *restricted = true; + return true; + } + } + } + + return false; +} + +static int do_chop_designation(bool chop, bool count_only, int *skipped = nullptr) { int count = 0; + if (skipped) + { + *skipped = 0; + } for (size_t i = 0; i < world->plants.all.size(); i++) { const df::plant *plant = world->plants.all[i]; - df::map_block *cur = Maps::getTileBlock(plant->pos); - if (!cur) - continue; - int x = plant->pos.x % 16; - int y = plant->pos.y % 16; - if (plant->flags.bits.is_shrub) - continue; - if (cur->designation[x][y].bits.hidden) - continue; - - df::tiletype_material material = tileMaterial(cur->tiletype[x][y]); - if (material != tiletype_material::TREE) + bool restricted = false; + if (skip_plant(plant, &restricted)) + { + if (restricted && skipped) + { + ++*skipped; + } continue; + } if (!count_only && !watchedBurrows.isValidPos(plant->pos)) continue; @@ -367,7 +451,11 @@ static void do_autochop() class ViewscreenAutochop : public dfhack_viewscreen { public: - ViewscreenAutochop() + ViewscreenAutochop(): + selected_column(0), + current_log_count(0), + marked_tree_count(0), + skipped_tree_count(0) { edit_mode = EDIT_NONE; burrows_column.multiselect = true; @@ -401,7 +489,7 @@ public: burrows_column.filterDisplay(); current_log_count = get_log_count(); - marked_tree_count = do_chop_designation(false, true); + marked_tree_count = do_chop_designation(false, true, &skipped_tree_count); } void change_min_logs(int delta) @@ -501,13 +589,21 @@ public: { int count = do_chop_designation(true, false); message = "Trees marked for chop: " + int_to_string(count); - marked_tree_count = do_chop_designation(false, true); + marked_tree_count = do_chop_designation(false, true, &skipped_tree_count); + if (skipped_tree_count) + { + message += ", skipped: " + int_to_string(skipped_tree_count); + } } else if (input->count(interface_key::CUSTOM_U)) { int count = do_chop_designation(false, false); message = "Trees unmarked: " + int_to_string(count); - marked_tree_count = do_chop_designation(false, true); + marked_tree_count = do_chop_designation(false, true, &skipped_tree_count); + if (skipped_tree_count) + { + message += ", skipped: " + int_to_string(skipped_tree_count); + } } else if (input->count(interface_key::CUSTOM_N)) { @@ -554,6 +650,18 @@ public: { change_max_logs(10); } + else if (input->count(interface_key::CUSTOM_F)) + { + skip.fruit_trees = !skip.fruit_trees; + } + else if (input->count(interface_key::CUSTOM_E)) + { + skip.food_trees = !skip.food_trees; + } + else if (input->count(interface_key::CUSTOM_C)) + { + skip.cook_trees = !skip.cook_trees; + } else if (enabler->tracking_on && enabler->mouse_lbut) { if (burrows_column.setHighlightByMouse()) @@ -598,13 +706,14 @@ public: } ++y; - OutputToggleString(x, y, "Autochop", "a", autochop_enabled, true, left_margin); - OutputHotkeyString(x, y, "Designate Now", "d", true, left_margin); - OutputHotkeyString(x, y, "Undesignate Now", "u", true, left_margin); + + using namespace df::enums::interface_key; + OutputToggleString(x, y, "Autochop", CUSTOM_A, autochop_enabled, true, left_margin); + OutputHotkeyString(x, y, "Designate Now", CUSTOM_D, true, left_margin); + OutputHotkeyString(x, y, "Undesignate Now", CUSTOM_U, true, left_margin); OutputHotkeyString(x, y, "Toggle Burrow", "Enter", true, left_margin); if (autochop_enabled) { - using namespace df::enums::interface_key; const struct { const char *caption; int count; @@ -615,7 +724,7 @@ public: {"Min Logs: ", min_logs, edit_mode == EDIT_MIN, CUSTOM_N, {CUSTOM_H, CUSTOM_J, CUSTOM_SHIFT_H, CUSTOM_SHIFT_J}}, {"Max Logs: ", max_logs, edit_mode == EDIT_MAX, CUSTOM_M, {CUSTOM_K, CUSTOM_L, CUSTOM_SHIFT_K, CUSTOM_SHIFT_L}} }; - for (size_t i = 0; i < sizeof(rows)/sizeof(rows[0]); ++i) + for (size_t i = 0; i < sizeof(rows) / sizeof(rows[0]); ++i) { auto row = rows[i]; OutputHotkeyString(x, y, row.caption, row.key); @@ -629,13 +738,16 @@ public: if (edit_mode == EDIT_NONE) { x = std::max(x, prev_x + 10); - for (size_t j = 0; j < sizeof(row.skeys)/sizeof(row.skeys[0]); ++j) + for (size_t j = 0; j < sizeof(row.skeys) / sizeof(row.skeys[0]); ++j) OutputString(COLOR_LIGHTGREEN, x, y, DFHack::Screen::getKeyDisplay(row.skeys[j])); OutputString(COLOR_WHITE, x, y, ": Step"); } OutputString(COLOR_WHITE, x, y, "", true, left_margin); } OutputHotkeyString(x, y, "No limit", CUSTOM_SHIFT_N, true, left_margin); + OutputToggleString(x, y, "Skip Fruit Trees", CUSTOM_F, skip.fruit_trees, true, left_margin); + OutputToggleString(x, y, "Skip Edible Product Trees", CUSTOM_E, skip.food_trees, true, left_margin); + OutputToggleString(x, y, "Skip Cookable Product Trees", CUSTOM_C, skip.cook_trees, true, left_margin); } ++y; @@ -660,6 +772,7 @@ private: int selected_column; int current_log_count; int marked_tree_count; + int skipped_tree_count; MapExtras::MapCache mcache; string message; enum { EDIT_NONE, EDIT_MIN, EDIT_MAX } edit_mode; diff --git a/plugins/dwarfmonitor.cpp b/plugins/dwarfmonitor.cpp index 7aed7188d..bf7c02efc 100644 --- a/plugins/dwarfmonitor.cpp +++ b/plugins/dwarfmonitor.cpp @@ -1870,15 +1870,11 @@ struct dwarf_monitor_hook : public df::viewscreen_dwarfmodest { typedef df::viewscreen_dwarfmodest interpose_base; - DEFINE_VMETHOD_INTERPOSE(void, feed, (set *input)) - { - INTERPOSE_NEXT(feed)(input); - } - DEFINE_VMETHOD_INTERPOSE(void, render, ()) { INTERPOSE_NEXT(render)(); + CoreSuspendClaimer suspend; if (Maps::IsValid()) { dm_lua::call("render_all"); @@ -1886,7 +1882,6 @@ struct dwarf_monitor_hook : public df::viewscreen_dwarfmodest } }; -IMPLEMENT_VMETHOD_INTERPOSE(dwarf_monitor_hook, feed); IMPLEMENT_VMETHOD_INTERPOSE(dwarf_monitor_hook, render); static bool set_monitoring_mode(const string &mode, const bool &state) @@ -1933,8 +1928,7 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) load_config(); if (is_enabled != enable) { - if (!INTERPOSE_HOOK(dwarf_monitor_hook, feed).apply(enable) || - !INTERPOSE_HOOK(dwarf_monitor_hook, render).apply(enable)) + if (!INTERPOSE_HOOK(dwarf_monitor_hook, render).apply(enable)) return CR_FAILURE; reset(); diff --git a/plugins/embark-assistant/CMakeLists.txt b/plugins/embark-assistant/CMakeLists.txt new file mode 100644 index 000000000..e57561ec1 --- /dev/null +++ b/plugins/embark-assistant/CMakeLists.txt @@ -0,0 +1,48 @@ +PROJECT (embark-assistant) +# A list of source files +SET(PROJECT_SRCS + biome_type.cpp + embark-assistant.cpp + finder_ui.cpp + help_ui.cpp + matcher.cpp + overlay.cpp + screen.cpp + survey.cpp +) +# A list of headers +SET(PROJECT_HDRS + biome_type.h + defs.h + embark-assistant.h + finder_ui.h + help_ui.h + matcher.h + overlay.h + screen.h + survey.h +) +SET_SOURCE_FILES_PROPERTIES( ${PROJECT_HDRS} PROPERTIES HEADER_FILE_ONLY TRUE) + +# mash them together (headers are marked as headers and nothing will try to compile them) +LIST(APPEND PROJECT_SRCS ${PROJECT_HDRS}) + +# option to use a thread for no particular reason +#OPTION(SKELETON_THREAD "Use threads in the skeleton plugin." ON) +#linux +IF(UNIX) + add_definitions(-DLINUX_BUILD) + SET(PROJECT_LIBS + # add any extra linux libs here + ${PROJECT_LIBS} + ) +# windows +ELSE(UNIX) + SET(PROJECT_LIBS + # add any extra windows libs here + ${PROJECT_LIBS} + $(NOINHERIT) + ) +ENDIF(UNIX) +# this makes sure all the stuff is put in proper places and linked to dfhack +DFHACK_PLUGIN(embark-assistant ${PROJECT_SRCS} LINK_LIBRARIES ${PROJECT_LIBS}) diff --git a/plugins/embark-assistant/biome_type.cpp b/plugins/embark-assistant/biome_type.cpp new file mode 100644 index 000000000..01bc0044a --- /dev/null +++ b/plugins/embark-assistant/biome_type.cpp @@ -0,0 +1,754 @@ +/* The code is copied from Ragundo's repo referenced below. +The changes are: +- The addition of a .h file reference. +- The simplification of the code using ofsub to remove the use of (and + .h reference to) that function (analysis of the code showed the + simplified code is the result, as the ofsub expressions will never be + true given the range of the values it can be passed in these functions). +- The change of the main function to take a separate y coordinate for + use in the tropicality determination to allow proper determination of + the tropicality of mid level tiles ("region tiles") referencing a + neighboring world tile's biome. +*/ +/* +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: +1. The origin of this software must not be misrepresented; you must not +claim that you wrote the original software. If you use this software +in a product, an acknowledgment in the product documentation would be +appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be +misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +*/ + +// You can always find the latest version of this plugin in Github +// https://github.com/ragundo/exportmaps + +#include + +#include "DataDefs.h" +#include +#include +#include +#include + +#include "biome_type.h" + +/***************************************************************************** +Local functions forward declaration +*****************************************************************************/ +std::pair check_tropicality(df::region_map_entry& region, + int a1 +); + +int get_lake_biome(df::region_map_entry& region, + bool is_possible_tropical_area_by_latitude +); + +int get_ocean_biome(df::region_map_entry& region, + bool is_tropical_area_by_latitude +); + +int get_desert_biome(df::region_map_entry& region); + +int get_biome_grassland(bool is_possible_tropical_area_by_latitude, + bool is_tropical_area_by_latitude, + int y, + int x +); + +int get_biome_savanna(bool is_possible_tropical_area_by_latitude, + bool is_tropical_area_by_latitude, + int y, + int x +); + +int get_biome_shrubland(bool is_possible_tropical_area_by_latitude, + bool is_tropical_area_by_latitude, + int y, + int x +); + +int get_biome_marsh(df::region_map_entry& region, + bool is_possible_tropical_area_by_latitude, + bool is_tropical_area_by_latitude, + int y, + int x +); + +int get_biome_forest(df::region_map_entry& region, + bool is_possible_tropical_area_by_latitude, + bool is_tropical_area_by_latitude, + int y, + int x +); + +int get_biome_swamp(df::region_map_entry& region, + bool is_possible_tropical_area_by_latitude, + bool is_tropical_area_by_latitude, + int y, + int x +); + +int get_biome_desert_or_grassland_or_savanna(df::region_map_entry& region, + bool is_possible_tropical_area_by_latitude, + bool is_tropical_area_by_latitude, + int y, + int x +); + +int get_biome_shrubland_or_marsh(df::region_map_entry& region, + bool is_possible_tropical_area_by_latitude, + bool is_tropical_area_by_latitude, + int y, + int x +); + +/***************************************************************************** +Module main function. +Return the biome type, given a position coordinate expressed in world_tiles +The world ref coordinates are used for tropicality determination and may refer +to a tile neighboring the "official" one. +*****************************************************************************/ +int get_biome_type(int world_coord_x, + int world_coord_y, + int world_ref_coord_y +) +{ + // Biome is per region, so get the region where this biome exists + df::region_map_entry& region = df::global::world->world_data->region_map[world_coord_x][world_coord_y]; + + // Check if the y reference position coordinate belongs to a tropical area + std::pair p = check_tropicality(region, + world_ref_coord_y + ); + bool is_possible_tropical_area_by_latitude = p.first; + bool is_tropical_area_by_latitude = p.second; + + // Begin the discrimination + if (region.flags.is_set(df::region_map_entry_flags::is_lake)) // is it a lake? + return get_lake_biome(region, + is_possible_tropical_area_by_latitude + ); + + // Not a lake. Check elevation + // Elevation greater then 149 means a mountain biome + // Elevation below 100 means a ocean biome + // Elevation between 100 and 149 are land biomes + + if (region.elevation >= 150) // is it a mountain? + return df::enums::biome_type::biome_type::MOUNTAIN; // 0 + + if (region.elevation < 100) // is it a ocean? + return get_ocean_biome(region, + is_possible_tropical_area_by_latitude + ); + + // land biome. Elevation between 100 and 149 + if (region.temperature <= -5) + { + if (region.drainage < 75) + return df::enums::biome_type::biome_type::TUNDRA; // 2 + else + return df::enums::biome_type::biome_type::GLACIER; // 1 + } + + // Not a lake, mountain, ocean, glacier or tundra + // Vegetation determines the biome type + if (region.vegetation < 66) + { + if (region.vegetation < 33) + return get_biome_desert_or_grassland_or_savanna(region, + is_possible_tropical_area_by_latitude, + is_tropical_area_by_latitude, + world_coord_y, + world_coord_x + ); + else // vegetation between 33 and 65 + return get_biome_shrubland_or_marsh(region, + is_possible_tropical_area_by_latitude, + is_tropical_area_by_latitude, + world_coord_y, + world_coord_x + ); + } + + // Not a lake, mountain, ocean, glacier, tundra, desert, grassland or savanna + // vegetation >= 66 + if (region.drainage >= 33) + return get_biome_forest(region, + is_possible_tropical_area_by_latitude, + is_tropical_area_by_latitude, + world_coord_y, + world_coord_x + ); + + // Not a lake, mountain, ocean, glacier, tundra, desert, grassland, savanna or forest + // vegetation >= 66, drainage < 33 + return get_biome_swamp(region, + is_possible_tropical_area_by_latitude, + is_tropical_area_by_latitude, + world_coord_y, + world_coord_x); +} + + + +//----------------------------------------------------------------------------// +// Utility function +// +//----------------------------------------------------------------------------// +std::pair check_tropicality_no_poles_world(df::region_map_entry& region, + int y_pos +) +{ + bool is_possible_tropical_area_by_latitude = false; + bool is_tropical_area_by_latitude = false; + + // If there're no poles, tropical area is determined by temperature + if (region.temperature >= 75) + is_possible_tropical_area_by_latitude = true; + is_tropical_area_by_latitude = region.temperature >= 85; + + return std::pair(is_possible_tropical_area_by_latitude, + is_tropical_area_by_latitude + ); +} + +//----------------------------------------------------------------------------// +// Utility function +// +//----------------------------------------------------------------------------// +std::pair check_tropicality_north_pole_only_world(df::region_map_entry& region, + int y_pos +) +{ + int v6; + bool is_possible_tropical_area_by_latitude = false; + bool is_tropical_area_by_latitude = false; + df::world_data* wdata = df::global::world->world_data; + + // Scale the smaller worlds to the big one + if (wdata->world_height == 17) + v6 = 16 * y_pos; + else if (wdata->world_height == 33) + v6 = 8 * y_pos; + else if (wdata->world_height == 65) + v6 = 4 * y_pos; + else if (wdata->world_height == 129) + v6 = 2 * y_pos; + else + v6 = y_pos; + + is_possible_tropical_area_by_latitude = v6 > 170; + is_tropical_area_by_latitude = v6 >= 200; + + return std::pair(is_possible_tropical_area_by_latitude, + is_tropical_area_by_latitude + ); +} + +//----------------------------------------------------------------------------// +// Utility function +// +//----------------------------------------------------------------------------// +std::pair check_tropicality_south_pole_only_world(df::region_map_entry& region, + int y_pos +) +{ + int v6 = df::global::world->world_data->world_height - y_pos - 1; + bool is_possible_tropical_area_by_latitude = false; + bool is_tropical_area_by_latitude = false; + df::world_data* wdata = df::global::world->world_data; + + if (wdata->world_height == 17) + v6 *= 16; + else if (wdata->world_height == 33) + v6 *= 8; + else if (wdata->world_height == 65) + v6 *= 4; + else if (wdata->world_height == 129) + v6 *= 2; + else + v6 *= 1; + + is_possible_tropical_area_by_latitude = v6 > 170; + is_tropical_area_by_latitude = v6 >= 200; + + return std::pair(is_possible_tropical_area_by_latitude, + is_tropical_area_by_latitude + ); +} + +//----------------------------------------------------------------------------// +// Utility function +// +//----------------------------------------------------------------------------// +std::pair check_tropicality_both_poles_world(df::region_map_entry& region, + int y_pos +) +{ + int v6; + bool is_possible_tropical_area_by_latitude = false; + bool is_tropical_area_by_latitude = false; + df::world_data* wdata = df::global::world->world_data; + + if (y_pos < wdata->world_height / 2) + v6 = 2 * y_pos; + else + { + v6 = wdata->world_height + 2 * (wdata->world_height / 2 - y_pos) - 1; + if (v6 < 0) + v6 = 0; + if (v6 >= wdata->world_height) + v6 = wdata->world_height - 1; + } + + if (wdata->world_height == 17) + v6 *= 16; + else if (wdata->world_height == 33) + v6 *= 8; + else if (wdata->world_height == 65) + v6 *= 4; + else if (wdata->world_height == 129) + v6 *= 2; + else + v6 *= 1; + + is_possible_tropical_area_by_latitude = v6 > 170; + is_tropical_area_by_latitude = v6 >= 200; + + return std::pair(is_possible_tropical_area_by_latitude, + is_tropical_area_by_latitude + ); +} + +//----------------------------------------------------------------------------// +// Utility function +// +//----------------------------------------------------------------------------// +std::pair check_tropicality(df::region_map_entry& region, + int y_pos +) +{ + int flip_latitude = df::global::world->world_data->flip_latitude; + + if (flip_latitude == -1) // NO POLES + return check_tropicality_no_poles_world(region, + y_pos + ); + + else if (flip_latitude == 0) // NORTH POLE ONLY + return check_tropicality_north_pole_only_world(region, + y_pos + ); + + else if (flip_latitude == 1) // SOUTH_POLE ONLY + return check_tropicality_south_pole_only_world(region, + y_pos + ); + + else if (flip_latitude == 2) // BOTH POLES + return check_tropicality_both_poles_world(region, + y_pos + ); + + return std::pair(false, false); +} + +//----------------------------------------------------------------------------// +// Utility function +// +//----------------------------------------------------------------------------// +int get_parameter_percentage(int flip_latitude, + int y_pos, + int rainfall, + int world_height +) +{ + int result; + int ypos = y_pos; + + if (flip_latitude == -1) // NO POLES + return 100; + + else if (flip_latitude == 1) // SOUTH POLE + ypos = world_height - y_pos - 1; + else if (flip_latitude == 2) // NORTH & SOUTH POLE + { + if (ypos < world_height / 2) + ypos *= 2; + else + { + ypos = world_height + 2 * (world_height / 2 - ypos) - 1; + if (ypos < 0) + ypos = 0; + if (ypos >= world_height) + ypos = world_height - 1; + } + } + + int latitude; // 0 - 256 (size of a large world) + switch (world_height) + { + case 17: // Pocket world + latitude = 16 * ypos; + break; + case 33: // Smaller world + latitude = 8 * ypos; + break; + case 65: // Small world + latitude = 4 * ypos; + break; + case 129: // Medium world + latitude = 2 * ypos; + break; + default: // Large world + latitude = ypos; + break; + } + + // latitude > 220 + if ((latitude - 171) > 49) + return 100; + + + // Latitude between 191 and 200 + if ((latitude > 190) && (latitude < 201)) + return 0; + + // Latitude between 201 and 220 + if ((latitude > 190) && (latitude >= 201)) + result = rainfall + 16 * (latitude - 207); + else + // Latitude between 0 and 190 + result = (16 * (184 - latitude) - rainfall); + + if (result < 0) + return 0; + + if (result > 100) + return 100; + + return result; +} + +//----------------------------------------------------------------------------// +// Utility function +// +// return some unknow parameter as a percentage +//----------------------------------------------------------------------------// +int get_region_parameter(int y, + int x, + char a4 +) +{ + int result = 100; + + if ((df::global::cur_season && *df::global::cur_season != 1) || !a4) + { + int world_height = df::global::world->world_data->world_height; + if (world_height > 65) // Medium and large worlds + { + // access to region 2D array + df::region_map_entry& region = df::global::world->world_data->region_map[x][y]; + return get_parameter_percentage(df::global::world->world_data->flip_latitude, + y, + region.rainfall, + world_height + ); + } + } + return result; +} + +//----------------------------------------------------------------------------// +// Utility function +// +//----------------------------------------------------------------------------// +int get_lake_biome(df::region_map_entry& region, + bool is_possible_tropical_area_by_latitude +) +{ + // salinity values tell us the lake type + // greater than 66 is a salt water lake + // between 33 and 65 is a brackish water lake + // less than 33 is a fresh water lake + if (region.salinity < 66) + { + if (region.salinity < 33) + if (is_possible_tropical_area_by_latitude) + return df::enums::biome_type::biome_type::LAKE_TROPICAL_FRESHWATER; // 39 + else + return df::enums::biome_type::biome_type::LAKE_TEMPERATE_FRESHWATER; // 36 + else // salinity >= 33 + if (is_possible_tropical_area_by_latitude) + return df::enums::biome_type::biome_type::LAKE_TROPICAL_BRACKISHWATER; // 40 + else + return df::enums::biome_type::biome_type::LAKE_TEMPERATE_BRACKISHWATER; // 37 + } + else // salinity >= 66 + { + if (is_possible_tropical_area_by_latitude) + return df::enums::biome_type::biome_type::LAKE_TROPICAL_SALTWATER;// 41 + else + return df::enums::biome_type::biome_type::LAKE_TEMPERATE_SALTWATER; // 38 + } +} + +//----------------------------------------------------------------------------// +// Utility function +// +//----------------------------------------------------------------------------// +int get_ocean_biome(df::region_map_entry& region, + bool is_tropical_area_by_latitude +) +{ + if (is_tropical_area_by_latitude) + return df::enums::biome_type::biome_type::OCEAN_TROPICAL; // 27 + else + if (region.temperature <= -5) + return df::enums::biome_type::biome_type::OCEAN_ARCTIC; // 29 + else + return df::enums::biome_type::biome_type::OCEAN_TEMPERATE; // 28 +} + +//----------------------------------------------------------------------------// +// Utility function +// +//----------------------------------------------------------------------------// +int get_desert_biome(df::region_map_entry& region) +{ + if (region.drainage < 66) + { + if (region.drainage < 33) + return df::enums::biome_type::biome_type::DESERT_SAND; // 26 + else // drainage between 33 and 65 + return df::enums::biome_type::biome_type::DESERT_ROCK; // 25 + } + // drainage >= 66 + return df::enums::biome_type::biome_type::DESERT_BADLAND; // 24 +} + +//----------------------------------------------------------------------------// +// Utility function +// +//----------------------------------------------------------------------------// +int get_biome_grassland(bool is_possible_tropical_area_by_latitude, + bool is_tropical_area_by_latitude, + int y, + int x +) +{ + if ((is_possible_tropical_area_by_latitude && (get_region_parameter(y, x, 0) < 66)) || is_tropical_area_by_latitude) + return df::enums::biome_type::biome_type::GRASSLAND_TROPICAL; // 21 + else + return df::enums::biome_type::biome_type::GRASSLAND_TEMPERATE; //18; +} + +//----------------------------------------------------------------------------// +// Utility function +// +//----------------------------------------------------------------------------// +int get_biome_savanna(bool is_possible_tropical_area_by_latitude, + bool is_tropical_area_by_latitude, + int y, + int x +) +{ + if ((is_possible_tropical_area_by_latitude && (get_region_parameter(y, x, 0) <= 6)) || is_tropical_area_by_latitude) + return df::enums::biome_type::biome_type::SAVANNA_TROPICAL; // 22 + else + return df::enums::biome_type::biome_type::SAVANNA_TEMPERATE; //19; + +} + +//----------------------------------------------------------------------------// +// Utility function +// +//----------------------------------------------------------------------------// +int get_biome_desert_or_grassland_or_savanna(df::region_map_entry& region, + bool is_possible_tropical_area_by_latitude, + bool is_tropical_area_by_latitude, + int y, + int x +) +{ + if (region.vegetation < 20) + { + if (region.vegetation < 10) + return get_desert_biome(region); + else // vegetation between 10 and 19 + return get_biome_grassland(is_possible_tropical_area_by_latitude, is_tropical_area_by_latitude, y, x); + } + // vegetation between 20 and 32 + return get_biome_savanna(is_possible_tropical_area_by_latitude, is_tropical_area_by_latitude, y, x); +} + +//----------------------------------------------------------------------------// +// Utility function +// +//----------------------------------------------------------------------------// +int get_biome_shrubland(bool is_possible_tropical_area_by_latitude, + bool is_tropical_area_by_latitude, + int y, + int x +) +{ + if (is_possible_tropical_area_by_latitude && (get_region_parameter(y, x, 0) < 66 || is_tropical_area_by_latitude)) + return df::enums::biome_type::biome_type::SHRUBLAND_TROPICAL; // 23 + else + return df::enums::biome_type::biome_type::SHRUBLAND_TEMPERATE; // 20 +} + +//----------------------------------------------------------------------------// +// Utility function +// +//----------------------------------------------------------------------------// +int get_biome_marsh(df::region_map_entry& region, + bool is_possible_tropical_area_by_latitude, + bool is_tropical_area_by_latitude, + int y, + int x +) +{ + if (region.salinity < 66) + { + if ((is_possible_tropical_area_by_latitude && (get_region_parameter(y, x, 0) < 66)) || is_tropical_area_by_latitude) + return df::enums::biome_type::biome_type::MARSH_TROPICAL_FRESHWATER; // 10 + else + return df::enums::biome_type::biome_type::MARSH_TEMPERATE_FRESHWATER; // 5 + } + else // drainage < 33, salinity >= 66 + { + if ((is_possible_tropical_area_by_latitude && (get_region_parameter(y, x, 0) < 66)) || is_tropical_area_by_latitude) + return df::enums::biome_type::biome_type::MARSH_TROPICAL_SALTWATER; // 11 + else + return df::enums::biome_type::biome_type::MARSH_TEMPERATE_SALTWATER; // 6 + } +} + +//----------------------------------------------------------------------------// +// Utility function +// +//----------------------------------------------------------------------------// +int get_biome_shrubland_or_marsh(df::region_map_entry& region, + bool is_possible_tropical_area_by_latitude, + bool is_tropical_area_by_latitude, + int y, + int x +) +{ + if (region.drainage >= 33) + return get_biome_shrubland(is_possible_tropical_area_by_latitude, + is_tropical_area_by_latitude, + y, + x + ); + // drainage < 33 + return get_biome_marsh(region, + is_possible_tropical_area_by_latitude, + is_tropical_area_by_latitude, + y, + x + ); +} + + +//----------------------------------------------------------------------------// +// Utility function +// +//----------------------------------------------------------------------------// +int get_biome_forest(df::region_map_entry& region, + bool is_possible_tropical_area_by_latitude, + bool is_tropical_area_by_latitude, + int y, + int x +) +{ + int parameter = get_region_parameter(y, x, 0); + + // drainage >= 33, not tropical area + if (!is_possible_tropical_area_by_latitude) + { + if ((region.rainfall < 75) || (region.temperature < 65)) + { + if (region.temperature >= 10) + return df::enums::biome_type::biome_type::FOREST_TEMPERATE_CONIFER; // 13 + else + return df::enums::biome_type::biome_type::FOREST_TAIGA; // 12 + } + else + return df::enums::biome_type::biome_type::FOREST_TEMPERATE_BROADLEAF; // 14 + } + else // drainage >= 33, tropical area + { + if (((parameter < 66) || is_tropical_area_by_latitude) && (region.rainfall < 75)) + return df::enums::biome_type::biome_type::FOREST_TROPICAL_CONIFER; // 15 + if (parameter < 66) + return df::enums::biome_type::biome_type::FOREST_TROPICAL_DRY_BROADLEAF; // 16 + if (is_tropical_area_by_latitude) + return df::enums::biome_type::biome_type::FOREST_TROPICAL_MOIST_BROADLEAF; // 17 + else + { + if ((region.rainfall < 75) || (region.temperature < 65)) + { + if (region.temperature >= 10) + return df::enums::biome_type::biome_type::FOREST_TEMPERATE_CONIFER; // 13 + else + return df::enums::biome_type::biome_type::FOREST_TAIGA; // 12 + } + else + return df::enums::biome_type::biome_type::FOREST_TEMPERATE_BROADLEAF; // 14 + } + } +} + +//----------------------------------------------------------------------------// +// Utility function +// +//----------------------------------------------------------------------------// +int get_biome_swamp(df::region_map_entry& region, + bool is_possible_tropical_area_by_latitude, + bool is_tropical_area_by_latitude, + int y, + int x +) +{ + int parameter = get_region_parameter(y, x, 0); + + if (is_possible_tropical_area_by_latitude) + { + if (region.salinity < 66) + { + if ((parameter < 66) || is_tropical_area_by_latitude) + return df::enums::biome_type::biome_type::SWAMP_TROPICAL_FRESHWATER; // 7 + else + return df::enums::biome_type::biome_type::SWAMP_TEMPERATE_FRESHWATER;// 3 + } + else // elevation between 100 and 149, vegetation >= 66, drainage < 33, salinity >= 66 + { + if ((parameter < 66) || is_tropical_area_by_latitude) + { + if (region.drainage < 10) + return df::enums::biome_type::biome_type::SWAMP_MANGROVE; //9 + else // drainage >= 10 + return df::enums::biome_type::biome_type::SWAMP_TROPICAL_SALTWATER; // 8 + } + else + return df::enums::biome_type::biome_type::SWAMP_TEMPERATE_SALTWATER; // 4 + } + } + else // elevation between 100 and 149, vegetation >= 66, drainage < 33, not tropical area + { + if (region.salinity >= 66) + return df::enums::biome_type::biome_type::SWAMP_TEMPERATE_SALTWATER; // 4 + else + return df::enums::biome_type::biome_type::SWAMP_TEMPERATE_FRESHWATER; // 3 + } +} \ No newline at end of file diff --git a/plugins/embark-assistant/biome_type.h b/plugins/embark-assistant/biome_type.h new file mode 100644 index 000000000..9ea562749 --- /dev/null +++ b/plugins/embark-assistant/biome_type.h @@ -0,0 +1,7 @@ +// world_coord_x/y is the location of the tile "owning" the biome, while world_ref_coord_y is the +// location of the tile the biome appears on. They differ when a mid level tile ("region tile") +// refers to a neighboring tile for the biome parameters. The difference can affect the tropicality +// determination. Since Tropicality is determined by latitude, the x coordinate of the reference is +// omitted. +// +int get_biome_type(int world_coord_x, int world_coord_y, int world_ref_coord_y); diff --git a/plugins/embark-assistant/defs.h b/plugins/embark-assistant/defs.h new file mode 100644 index 000000000..79ad5dd5b --- /dev/null +++ b/plugins/embark-assistant/defs.h @@ -0,0 +1,256 @@ +#pragma once + +#include +#include +#include + +using namespace std; +using std::array; +using std::ostringstream; +using std::string; +using std::vector; + +namespace embark_assist { + namespace defs { + // Survey types + // + enum class river_sizes { + None, + Brook, + Stream, + Minor, + Medium, + Major + }; + + struct mid_level_tile { + bool aquifer = false; + bool clay = false; + bool sand = false; + bool flux = false; + int8_t soil_depth; + int8_t offset; + int16_t elevation; + bool river_present = false; + int16_t river_elevation = 100; + int8_t biome_offset; + uint8_t savagery_level; // 0 - 2 + uint8_t evilness_level; // 0 - 2 + std::vector metals; + std::vector economics; + std::vector minerals; + }; + + typedef std::array, 16> mid_level_tiles; +// typedef mid_level_tile mid_level_tiles[16][16]; + + struct region_tile_datum { + bool surveyed = false; + uint16_t aquifer_count = 0; + uint16_t clay_count = 0; + uint16_t sand_count = 0; + uint16_t flux_count = 0; + uint8_t min_region_soil = 10; + uint8_t max_region_soil = 0; + bool waterfall = false; + + river_sizes river_size; + int16_t biome_index[10]; // Indexed through biome_offset; -1 = null, Index of region, [0] not used + int16_t biome[10]; // Indexed through biome_offset; -1 = null, df::biome_type, [0] not used + uint8_t biome_count; + bool evil_weather[10]; + bool evil_weather_possible; + bool evil_weather_full; + bool reanimating[10]; + bool reanimating_possible; + bool reanimating_full; + bool thralling[10]; + bool thralling_possible; + bool thralling_full; + uint16_t savagery_count[3]; + uint16_t evilness_count[3]; + std::vector metals; + std::vector economics; + std::vector minerals; + }; + + struct geo_datum { + uint8_t soil_size = 0; + bool top_soil_only = true; + bool top_soil_aquifer_only = true; + bool aquifer_absent = true; + bool clay_absent = true; + bool sand_absent = true; + bool flux_absent = true; + std::vector possible_metals; + std::vector possible_economics; + std::vector possible_minerals; + }; + + typedef std::vector geo_data; + + struct sites { + uint8_t x; + uint8_t y; + char type; + }; + + struct site_infos { + bool aquifer; + bool aquifer_full; + uint8_t min_soil; + uint8_t max_soil; + bool flat; + bool waterfall; + bool clay; + bool sand; + bool flux; + std::vector metals; + std::vector economics; + std::vector minerals; + // Could add savagery, evilness, and biomes, but DF provides those easily. + }; + + typedef std::vector site_lists; + + typedef std::vector> world_tile_data; + + typedef bool mlt_matches[16][16]; + // An embark region match is indicated by marking the top left corner + // tile as a match. Thus, the bottom and right side won't show matches + // unless the appropriate dimension has a width of 1. + + struct matches { + bool preliminary_match; + bool contains_match; + mlt_matches mlt_match; + }; + + typedef std::vector> match_results; + + // matcher types + // + enum class evil_savagery_values : int8_t { + NA = -1, + All, + Present, + Absent + }; + + enum class evil_savagery_ranges : int8_t { + Low, + Medium, + High + }; + + enum class aquifer_ranges : int8_t { + NA = -1, + All, + Present, + Partial, + Not_All, + Absent + }; + + enum class river_ranges : int8_t { + NA = -1, + None, + Brook, + Stream, + Minor, + Medium, + Major + }; + + enum class yes_no_ranges : int8_t { + NA = -1, + Yes, + No + }; + + enum class all_present_ranges : int8_t { + All, + Present + }; + enum class present_absent_ranges : int8_t { + NA = -1, + Present, + Absent + }; + + enum class soil_ranges : int8_t { + NA = -1, + None, + Very_Shallow, + Shallow, + Deep, + Very_Deep + }; + + /* // Future possible enhancement + enum class freezing_ranges : int8_t { + NA = -1, + Permanent, + At_Least_Partial, + Partial, + At_Most_Partial, + Never + }; + */ + + struct finders { + uint16_t x_dim; + uint16_t y_dim; + evil_savagery_values savagery[static_cast(evil_savagery_ranges::High) + 1]; + evil_savagery_values evilness[static_cast(evil_savagery_ranges::High) + 1]; + aquifer_ranges aquifer; + river_ranges min_river; + river_ranges max_river; + yes_no_ranges waterfall; + yes_no_ranges flat; + present_absent_ranges clay; + present_absent_ranges sand; + present_absent_ranges flux; + soil_ranges soil_min; + all_present_ranges soil_min_everywhere; + soil_ranges soil_max; + /*freezing_ranges freezing;*/ + yes_no_ranges evil_weather; // Will probably blow up with the magic release arcs... + yes_no_ranges reanimation; + yes_no_ranges thralling; + int8_t biome_count_min; // N/A(-1), 1-9 + int8_t biome_count_max; // N/A(-1), 1-9 + int8_t region_type_1; // N/A(-1), df::world_region_type + int8_t region_type_2; // N/A(-1), df::world_region_type + int8_t region_type_3; // N/A(-1), df::world_region_type + int8_t biome_1; // N/A(-1), df::biome_type + int8_t biome_2; // N/A(-1), df::biome_type + int8_t biome_3; // N/A(-1), df::biome_type + int16_t metal_1; // N/A(-1), 0-max_inorganic; + int16_t metal_2; // N/A(-1), 0-max_inorganic; + int16_t metal_3; // N/A(-1), 0-max_inorganic; + int16_t economic_1; // N/A(-1), 0-max_inorganic; + int16_t economic_2; // N/A(-1), 0-max_inorganic; + int16_t economic_3; // N/A(-1), 0-max_inorganic; + int16_t mineral_1; // N/A(-1), 0-max_inorganic; + int16_t mineral_2; // N/A(-1), 0-max_inorganic; + int16_t mineral_3; // N/A(-1), 0-max_inorganic; + }; + + struct match_iterators { + bool active; + uint16_t x; // x position of focus when iteration started so we can return it. + uint16_t y; // y + uint16_t i; + uint16_t k; + bool x_right; + bool y_down; + bool inhibit_x_turn; + bool inhibit_y_turn; + uint16_t count; + finders finder; + }; + + typedef void(*find_callbacks) (embark_assist::defs::finders finder); + } +} \ No newline at end of file diff --git a/plugins/embark-assistant/embark-assistant.cpp b/plugins/embark-assistant/embark-assistant.cpp new file mode 100644 index 000000000..7e0ea21cc --- /dev/null +++ b/plugins/embark-assistant/embark-assistant.cpp @@ -0,0 +1,296 @@ +#include "Core.h" +#include +#include +#include + +#include +#include +#include + +#include "DataDefs.h" +#include "df/coord2d.h" +#include "df/inorganic_flags.h" +#include "df/inorganic_raw.h" +#include "df/interfacest.h" +#include "df/viewscreen.h" +#include "df/viewscreen_choose_start_sitest.h" +#include "df/world.h" +#include "df/world_data.h" +#include "df/world_geo_biome.h" +#include "df/world_raws.h" + +#include "defs.h" +#include "embark-assistant.h" +#include "finder_ui.h" +#include "matcher.h" +#include "overlay.h" +#include "survey.h" + +DFHACK_PLUGIN("embark-assistant"); + +using namespace DFHack; +using namespace df::enums; +using namespace Gui; + +REQUIRE_GLOBAL(world); + +namespace embark_assist { + namespace main { + struct states { + embark_assist::defs::geo_data geo_summary; + embark_assist::defs::world_tile_data survey_results; + embark_assist::defs::site_lists region_sites; + embark_assist::defs::site_infos site_info; + embark_assist::defs::match_results match_results; + embark_assist::defs::match_iterators match_iterator; + uint16_t max_inorganic; + }; + + static states *state = nullptr; + + void embark_update (); + void shutdown(); + + //=============================================================================== + + void embark_update() { + auto screen = Gui::getViewscreenByType(0); + embark_assist::defs::mid_level_tiles mlt; + embark_assist::survey::initiate(&mlt); + + embark_assist::survey::survey_mid_level_tile(&state->geo_summary, + &state->survey_results, + &mlt); + + embark_assist::survey::survey_embark(&mlt, &state->site_info, false); + embark_assist::overlay::set_embark(&state->site_info); + + embark_assist::survey::survey_region_sites(&state->region_sites); + embark_assist::overlay::set_sites(&state->region_sites); + + embark_assist::overlay::set_mid_level_tile_match(state->match_results.at(screen->location.region_pos.x).at(screen->location.region_pos.y).mlt_match); + } + + //=============================================================================== + + void match() { +// color_ostream_proxy out(Core::getInstance().getConsole()); + + uint16_t count = embark_assist::matcher::find(&state->match_iterator, + &state->geo_summary, + &state->survey_results, + &state->match_results); + + embark_assist::overlay::match_progress(count, &state->match_results, !state->match_iterator.active); + + if (!state->match_iterator.active) { + auto screen = Gui::getViewscreenByType(0); + embark_assist::overlay::set_mid_level_tile_match(state->match_results.at(screen->location.region_pos.x).at(screen->location.region_pos.y).mlt_match); + } + } + + //=============================================================================== + + void clear_match() { +// color_ostream_proxy out(Core::getInstance().getConsole()); + if (state->match_iterator.active) { + embark_assist::matcher::move_cursor(state->match_iterator.x, state->match_iterator.y); + } + embark_assist::survey::clear_results(&state->match_results); + embark_assist::overlay::clear_match_results(); + embark_assist::main::state->match_iterator.active = false; + } + + //=============================================================================== + + void find(embark_assist::defs::finders finder) { +// color_ostream_proxy out(Core::getInstance().getConsole()); + + state->match_iterator.x = embark_assist::survey::get_last_pos().x; + state->match_iterator.y = embark_assist::survey::get_last_pos().y; + state->match_iterator.finder = finder; + embark_assist::overlay::initiate_match(); + } + + //=============================================================================== + + void shutdown() { +// color_ostream_proxy out(Core::getInstance().getConsole()); + embark_assist::survey::shutdown(); + embark_assist::finder_ui::shutdown(); + embark_assist::overlay::shutdown(); + delete state; + state = nullptr; + } + } +} + +//======================================================================================= + +command_result embark_assistant (color_ostream &out, std::vector & parameters); + +//======================================================================================= + +DFhackCExport command_result plugin_init (color_ostream &out, std::vector &commands) +{ + commands.push_back(PluginCommand( + "embark-assistant", "Embark site selection support.", + embark_assistant, true, /* true means that the command can't be used from non-interactive user interface */ + // Extended help string. Used by CR_WRONG_USAGE and the help command: + " This command starts the embark-assist plugin that provides embark site\n" + " selection help. It has to be called while the pre-embark screen is\n" + " displayed and shows extended (and correct(?)) resource information for\n" + " the embark rectangle as well as normally undisplayed sites in the\n" + " current embark region. It also has a site selection tool with more\n" + " options than DF's vanilla search tool. For detailed help invoke the\n" + " in game info screen. Requires 42 lines to display properly.\n" + )); + return CR_OK; +} + +//======================================================================================= + +DFhackCExport command_result plugin_shutdown (color_ostream &out) +{ + return CR_OK; +} + +//======================================================================================= + +DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) +{ + switch (event) { + case DFHack::SC_UNKNOWN: + break; + + case DFHack::SC_WORLD_LOADED: + break; + + case DFHack::SC_WORLD_UNLOADED: + case DFHack::SC_MAP_LOADED: + if (embark_assist::main::state) { + embark_assist::main::shutdown(); + } + break; + + case DFHack::SC_MAP_UNLOADED: + break; + + case DFHack::SC_VIEWSCREEN_CHANGED: + break; + + case DFHack::SC_CORE_INITIALIZED: + break; + + case DFHack::SC_BEGIN_UNLOAD: + break; + + case DFHack::SC_PAUSED: + break; + + case DFHack::SC_UNPAUSED: + break; + } + return CR_OK; +} + + +//======================================================================================= + +command_result embark_assistant(color_ostream &out, std::vector & parameters) +{ + if (!parameters.empty()) + return CR_WRONG_USAGE; + + CoreSuspender suspend; + + auto screen = Gui::getViewscreenByType(0); + if (!screen) { + out.printerr("This plugin works only in the embark site selection phase.\n"); + return CR_WRONG_USAGE; + } + + df::world_data *world_data = world->world_data; + + if (embark_assist::main::state) { + out.printerr("You can't invoke the embark assistant while it's already active.\n"); + return CR_WRONG_USAGE; + } + + embark_assist::main::state = new embark_assist::main::states; + + embark_assist::main::state->match_iterator.active = false; + + // Find the end of the normal inorganic definitions. + embark_assist::main::state->max_inorganic = 0; + for (uint16_t i = 0; i < world->raws.inorganics.size(); i++) { + if (world->raws.inorganics[i]->flags.is_set(df::inorganic_flags::GENERATED)) embark_assist::main::state->max_inorganic = i; + } + embark_assist::main::state->max_inorganic++; // To allow it to be used as size() replacement + + if (!embark_assist::overlay::setup(plugin_self, + embark_assist::main::embark_update, + embark_assist::main::match, + embark_assist::main::clear_match, + embark_assist::main::find, + embark_assist::main::shutdown, + embark_assist::main::state->max_inorganic)) { + return CR_FAILURE; + } + + embark_assist::survey::setup(embark_assist::main::state->max_inorganic); + embark_assist::main::state->geo_summary.resize(world_data->geo_biomes.size()); + embark_assist::main::state->survey_results.resize(world->worldgen.worldgen_parms.dim_x); + + for (uint16_t i = 0; i < world->worldgen.worldgen_parms.dim_x; i++) { + embark_assist::main::state->survey_results[i].resize(world->worldgen.worldgen_parms.dim_y); + + for (uint16_t k = 0; k < world->worldgen.worldgen_parms.dim_y; k++) { + embark_assist::main::state->survey_results[i][k].surveyed = false; + embark_assist::main::state->survey_results[i][k].aquifer_count = 0; + embark_assist::main::state->survey_results[i][k].clay_count = 0; + embark_assist::main::state->survey_results[i][k].sand_count = 0; + embark_assist::main::state->survey_results[i][k].flux_count = 0; + embark_assist::main::state->survey_results[i][k].min_region_soil = 10; + embark_assist::main::state->survey_results[i][k].max_region_soil = 0; + embark_assist::main::state->survey_results[i][k].waterfall = false; + embark_assist::main::state->survey_results[i][k].river_size = embark_assist::defs::river_sizes::None; + + for (uint8_t l = 1; l < 10; l++) { + embark_assist::main::state->survey_results[i][k].biome_index[l] = -1; + embark_assist::main::state->survey_results[i][k].biome[l] = -1; + embark_assist::main::state->survey_results[i][k].evil_weather[l] = false; + embark_assist::main::state->survey_results[i][k].reanimating[l] = false; + embark_assist::main::state->survey_results[i][k].thralling[l] = false; + } + + for (uint8_t l = 0; l < 2; l++) { + embark_assist::main::state->survey_results[i][k].savagery_count[l] = 0; + embark_assist::main::state->survey_results[i][k].evilness_count[l] = 0; + } + embark_assist::main::state->survey_results[i][k].metals.resize(embark_assist::main::state->max_inorganic); + embark_assist::main::state->survey_results[i][k].economics.resize(embark_assist::main::state->max_inorganic); + embark_assist::main::state->survey_results[i][k].minerals.resize(embark_assist::main::state->max_inorganic); + } + } + + embark_assist::survey::high_level_world_survey(&embark_assist::main::state->geo_summary, + &embark_assist::main::state->survey_results); + + embark_assist::main::state->match_results.resize(world->worldgen.worldgen_parms.dim_x); + + for (uint16_t i = 0; i < world->worldgen.worldgen_parms.dim_x; i++) { + embark_assist::main::state->match_results[i].resize(world->worldgen.worldgen_parms.dim_y); + } + + embark_assist::survey::clear_results(&embark_assist::main::state->match_results); + embark_assist::survey::survey_region_sites(&embark_assist::main::state->region_sites); + embark_assist::overlay::set_sites(&embark_assist::main::state->region_sites); + + embark_assist::defs::mid_level_tiles mlt; + embark_assist::survey::survey_mid_level_tile(&embark_assist::main::state->geo_summary, &embark_assist::main::state->survey_results, &mlt); + embark_assist::survey::survey_embark(&mlt, &embark_assist::main::state->site_info, false); + embark_assist::overlay::set_embark(&embark_assist::main::state->site_info); + + return CR_OK; +} diff --git a/plugins/embark-assistant/embark-assistant.h b/plugins/embark-assistant/embark-assistant.h new file mode 100644 index 000000000..6f70f09be --- /dev/null +++ b/plugins/embark-assistant/embark-assistant.h @@ -0,0 +1 @@ +#pragma once diff --git a/plugins/embark-assistant/finder_ui.cpp b/plugins/embark-assistant/finder_ui.cpp new file mode 100644 index 000000000..feea89199 --- /dev/null +++ b/plugins/embark-assistant/finder_ui.cpp @@ -0,0 +1,1145 @@ +#include "Core.h" +#include + +#include + +#include "Types.h" + +#include "df/biome_type.h" +#include "df/inorganic_raw.h" +#include "df/material_flags.h" +#include "df/viewscreen_choose_start_sitest.h" +#include "df/world.h" +#include "df/world_region_type.h" +#include "df/world_raws.h" + +#include "embark-assistant.h" +#include "finder_ui.h" +#include "screen.h" + +using df::global::world; + +namespace embark_assist { + namespace finder_ui { + + enum class fields : int8_t { + x_dim, + y_dim, + savagery_calm, + savagery_medium, + savagery_savage, + good, + neutral, + evil, + aquifer, + min_river, + max_river, + waterfall, + flat, + clay, + sand, + flux, + soil_min, + soil_min_everywhere, + soil_max, + evil_weather, + reanimation, + thralling, + biome_count_min, + biome_count_max, + region_type_1, + region_type_2, + region_type_3, + biome_1, + biome_2, + biome_3, + metal_1, + metal_2, + metal_3, + economic_1, + economic_2, + economic_3, + mineral_1, + mineral_2, + mineral_3 + }; + fields first_fields = fields::x_dim; + fields last_fields = fields::mineral_3; + + struct display_map_elements { + std::string text; + int16_t key; + }; + + typedef std::vector display_maps; + typedef std::vector name_lists; + typedef std::list< display_map_elements> sort_lists; + + struct ui_lists { + uint16_t current_display_value; // Not the value itself, but a reference to its index. + int16_t current_value; // The integer representation of the value (if an enum). + uint16_t current_index; // What's selected + uint16_t focus; // The value under the (possibly inactive) cursor + display_maps list; // The strings to be displayed together with keys + // to allow location of the actual elements (e.g. a raws.inorganics mat_index + // or underlying enum value). + }; + + typedef std::vector uis; + + const DFHack::Screen::Pen active_pen(' ', COLOR_YELLOW); + const DFHack::Screen::Pen passive_pen(' ', COLOR_DARKGREY); + const DFHack::Screen::Pen normal_pen(' ', COLOR_GREY); + const DFHack::Screen::Pen white_pen(' ', COLOR_WHITE); + const DFHack::Screen::Pen lr_pen(' ', COLOR_LIGHTRED); + + //========================================================================================================== + + struct states { + embark_assist::defs::find_callbacks find_callback; + uis ui; + display_maps finder_list; // Don't need the element key, but it's easier to use the same type. + uint16_t finder_list_focus; + bool finder_list_active; + uint16_t max_inorganic; + }; + + static states *state = 0; + + //========================================================================================================== + + bool compare(const display_map_elements& first, const display_map_elements& second) { + uint16_t i = 0; + while (i < first.text.length() && i < second.text.length()) { + if (first.text[i] < second.text[i]) { + return true; + } + else if (first.text[i] > second.text[i]) { + return false; + } + ++i; + } + return first.text.length() < second.text.length(); + } + + //========================================================================================================== + + void append(sort_lists *sort_list, display_map_elements element) { + sort_lists::iterator iterator; + for (iterator = sort_list->begin(); iterator != sort_list->end(); ++iterator) { + if (iterator->key == element.key) { + return; + } + } + sort_list->push_back(element); + } + + //========================================================================================================== + + void ui_setup(embark_assist::defs::find_callbacks find_callback, uint16_t max_inorganic) { +// color_ostream_proxy out(Core::getInstance().getConsole()); + if (!embark_assist::finder_ui::state) { + state = new(states); + state->finder_list_focus = 0; + state->finder_list_active = true; + state->find_callback = find_callback; + state->max_inorganic = max_inorganic; + } + + fields i = first_fields; + ui_lists *element; + + while (true) { + element = new ui_lists; + element->current_display_value = 0; + element->current_index = 0; + element->focus = 0; + + switch (i) { + case fields::x_dim: + for (int16_t k = 1; k < 16; k++) { + element->list.push_back({ std::to_string(k), k }); + } + + break; + + case fields::y_dim: + for (int16_t k = 1; k < 16; k++) { + element->list.push_back({ std::to_string(k), k }); + } + + break; + + case fields::savagery_calm: + case fields::savagery_medium: + case fields::savagery_savage: + case fields::good: + case fields::neutral: + case fields::evil: + { + embark_assist::defs::evil_savagery_values k = embark_assist::defs::evil_savagery_values::NA; + while (true) { + switch (k) { + case embark_assist::defs::evil_savagery_values::NA: + element->list.push_back({ "N/A", static_cast(k) }); + break; + + case embark_assist::defs::evil_savagery_values::All: + element->list.push_back({ "All", static_cast(k) }); + break; + + case embark_assist::defs::evil_savagery_values::Present: + element->list.push_back({ "Present", static_cast(k) }); + break; + + case embark_assist::defs::evil_savagery_values::Absent: + element->list.push_back({ "Absent", static_cast(k) }); + break; + } + + if (k == embark_assist::defs::evil_savagery_values::Absent) { + break; + } + + k = static_cast (static_cast(k) + 1); + } + } + + break; + + case fields::aquifer: + { + embark_assist::defs::aquifer_ranges k = embark_assist::defs::aquifer_ranges::NA; + while (true) { + switch (k) { + case embark_assist::defs::aquifer_ranges::NA: + element->list.push_back({ "N/A", static_cast(k) }); + break; + + case embark_assist::defs::aquifer_ranges::All: + element->list.push_back({ "All", static_cast(k) }); + break; + + case embark_assist::defs::aquifer_ranges::Present: + element->list.push_back({ "Present", static_cast(k) }); + break; + + case embark_assist::defs::aquifer_ranges::Partial: + element->list.push_back({ "Partial", static_cast(k) }); + break; + + case embark_assist::defs::aquifer_ranges::Not_All: + element->list.push_back({ "Not All", static_cast(k) }); + break; + + case embark_assist::defs::aquifer_ranges::Absent: + element->list.push_back({ "Absent", static_cast(k) }); + break; + } + + if (k == embark_assist::defs::aquifer_ranges::Absent) { + break; + } + + k = static_cast (static_cast(k) + 1); + } + } + + break; + + case fields::min_river: + case fields::max_river: + { + embark_assist::defs::river_ranges k = embark_assist::defs::river_ranges::NA; + while (true) { + switch (k) { + case embark_assist::defs::river_ranges::NA: + element->list.push_back({ "N/A", static_cast(k) }); + break; + + case embark_assist::defs::river_ranges::None: + element->list.push_back({ "None", static_cast(k) }); + break; + + case embark_assist::defs::river_ranges::Brook: + element->list.push_back({ "Brook", static_cast(k) }); + break; + + case embark_assist::defs::river_ranges::Stream: + element->list.push_back({ "Stream", static_cast(k) }); + break; + + case embark_assist::defs::river_ranges::Minor: + element->list.push_back({ "Minor", static_cast(k) }); + break; + + case embark_assist::defs::river_ranges::Medium: + element->list.push_back({ "Medium", static_cast(k) }); + break; + + case embark_assist::defs::river_ranges::Major: + element->list.push_back({ "Major", static_cast(k) }); + break; + } + + if (k == embark_assist::defs::river_ranges::Major) { + break; + } + + k = static_cast (static_cast(k) + 1); + } + } + + break; + + case fields::waterfall: + case fields::flat: + case fields::evil_weather: + case fields::reanimation: + case fields::thralling: + { + embark_assist::defs::yes_no_ranges k = embark_assist::defs::yes_no_ranges::NA; + while (true) { + switch (k) { + case embark_assist::defs::yes_no_ranges::NA: + element->list.push_back({ "N/A", static_cast(k) }); + break; + + case embark_assist::defs::yes_no_ranges::Yes: + element->list.push_back({ "Yes", static_cast(k) }); + break; + + case embark_assist::defs::yes_no_ranges::No: + element->list.push_back({ "No", static_cast(k) }); + break; + } + + if (k == embark_assist::defs::yes_no_ranges::No) { + break; + } + + k = static_cast (static_cast(k) + 1); + } + } + + break; + + case fields::soil_min_everywhere: + { + embark_assist::defs::all_present_ranges k = embark_assist::defs::all_present_ranges::All; + while (true) { + switch (k) { + case embark_assist::defs::all_present_ranges::All: + element->list.push_back({ "All", static_cast(k) }); + break; + + case embark_assist::defs::all_present_ranges::Present: + element->list.push_back({ "Present", static_cast(k) }); + break; + } + + if (k == embark_assist::defs::all_present_ranges::Present) { + break; + } + + k = static_cast (static_cast(k) + 1); + } + } + + break; + + case fields::clay: + case fields::sand: + case fields::flux: + { + embark_assist::defs::present_absent_ranges k = embark_assist::defs::present_absent_ranges::NA; + while (true) { + switch (k) { + case embark_assist::defs::present_absent_ranges::NA: + element->list.push_back({ "N/A", static_cast(k) }); + break; + + case embark_assist::defs::present_absent_ranges::Present: + element->list.push_back({ "Present", static_cast(k) }); + break; + + case embark_assist::defs::present_absent_ranges::Absent: + element->list.push_back({ "Absent", static_cast(k) }); + break; + } + + if (k == embark_assist::defs::present_absent_ranges::Absent) { + break; + } + + k = static_cast (static_cast(k) + 1); + } + } + + break; + + case fields::soil_min: + case fields::soil_max: + { + embark_assist::defs::soil_ranges k = embark_assist::defs::soil_ranges::NA; + while (true) { + switch (k) { + case embark_assist::defs::soil_ranges::NA: + element->list.push_back({ "N/A", static_cast(k) }); + break; + + case embark_assist::defs::soil_ranges::None: + element->list.push_back({ "None", static_cast(k) }); + break; + + case embark_assist::defs::soil_ranges::Very_Shallow: + element->list.push_back({ "Very Shallow", static_cast(k) }); + break; + + case embark_assist::defs::soil_ranges::Shallow: + element->list.push_back({ "Shallow", static_cast(k) }); + break; + + case embark_assist::defs::soil_ranges::Deep: + element->list.push_back({ "Deep", static_cast(k) }); + break; + + case embark_assist::defs::soil_ranges::Very_Deep: + element->list.push_back({ "Very Deep", static_cast(k) }); + break; + } + + if (k == embark_assist::defs::soil_ranges::Very_Deep) { + break; + } + + k = static_cast (static_cast(k) + 1); + } + } + + break; + + case fields::biome_count_min: + case fields::biome_count_max: + for (int16_t k = 0; k < 10; k++) { + if (k == 0) { + element->list.push_back({ "N/A", -1 }); + } + else { + element->list.push_back({ std::to_string(k), k }); + } + } + + break; + + case fields::region_type_1: + case fields::region_type_2: + case fields::region_type_3: + { + std::list name; + std::list::iterator iterator; + + FOR_ENUM_ITEMS(world_region_type, iter) { + name.push_back({ ENUM_KEY_STR(world_region_type, iter), static_cast(iter) }); + } + name.sort(compare); + + element->list.push_back({ "N/A", -1 }); + + for (iterator = name.begin(); iterator != name.end(); ++iterator) { + element->list.push_back({ iterator->text, iterator->key }); + } + + name.clear(); + } + break; + + case fields::biome_1: + case fields::biome_2: + case fields::biome_3: + { + sort_lists name; + sort_lists::iterator iterator; + + FOR_ENUM_ITEMS(biome_type, iter) { + std::string s = ENUM_KEY_STR(biome_type, iter); + + if (s.substr(0, 4) != "POOL" && + s.substr(0, 5) != "RIVER" && + s.substr(0, 3) != "SUB") { + name.push_back({ s, static_cast(iter) }); + } + } + name.sort(compare); + + element->list.push_back({ "N/A", -1 }); + + for (iterator = name.begin(); iterator != name.end(); ++iterator) { + element->list.push_back({ iterator->text, iterator->key }); + } + + name.clear(); + } + break; + + case fields::metal_1: + case fields::metal_2: + case fields::metal_3: + { + sort_lists name; + sort_lists::iterator iterator; + + for (uint16_t k = 0; k < embark_assist::finder_ui::state->max_inorganic; k++) { + for (uint16_t l = 0; l < world->raws.inorganics[k]->metal_ore.mat_index.size(); l++) { + append(&name, { world->raws.inorganics[world->raws.inorganics[k]->metal_ore.mat_index[l]]->id, + world->raws.inorganics[k]->metal_ore.mat_index[l] }); + } + } + + name.sort(compare); + + element->list.push_back({ "N/A", -1 }); + + for (iterator = name.begin(); iterator != name.end(); ++iterator) { + element->list.push_back({ iterator->text, iterator->key }); + } + + name.clear(); + } + break; + + case fields::economic_1: + case fields::economic_2: + case fields::economic_3: + { + sort_lists name; + sort_lists::iterator iterator; + + for (int16_t k = 0; k < embark_assist::finder_ui::state->max_inorganic; k++) { + if (world->raws.inorganics[k]->economic_uses.size() != 0 && + !world->raws.inorganics[k]->material.flags.is_set(df::material_flags::IS_METAL)) { + append(&name, { world->raws.inorganics[k]->id, k }); + } + } + + name.sort(compare); + + element->list.push_back({ "N/A", -1 }); + + for (iterator = name.begin(); iterator != name.end(); ++iterator) { + element->list.push_back({ iterator->text, iterator->key }); + } + + name.clear(); + } + break; + + case fields::mineral_1: + case fields::mineral_2: + case fields::mineral_3: + { + sort_lists name; + sort_lists::iterator iterator; + + for (int16_t k = 0; k < embark_assist::finder_ui::state->max_inorganic; k++) { + if (world->raws.inorganics[k]->environment.location.size() != 0 || + world->raws.inorganics[k]->environment_spec.mat_index.size() != 0 || + world->raws.inorganics[k]->flags.is_set(df::inorganic_flags::SEDIMENTARY) || + world->raws.inorganics[k]->flags.is_set(df::inorganic_flags::IGNEOUS_EXTRUSIVE) || + world->raws.inorganics[k]->flags.is_set(df::inorganic_flags::IGNEOUS_INTRUSIVE) || + world->raws.inorganics[k]->flags.is_set(df::inorganic_flags::METAMORPHIC) || + world->raws.inorganics[k]->flags.is_set(df::inorganic_flags::SOIL)) { + append(&name, { world->raws.inorganics[k]->id, k }); + } + } + + name.sort(compare); + + element->list.push_back({ "N/A", -1 }); + + for (iterator = name.begin(); iterator != name.end(); ++iterator) { + element->list.push_back({ iterator->text, iterator->key }); + } + + name.clear(); + } + break; + } + + element->current_value = element->list[0].key; + + state->ui.push_back(element); + + switch (i) { + case fields::x_dim: + state->finder_list.push_back({ "X Dimension", static_cast(i) }); + break; + + case fields::y_dim: + state->finder_list.push_back({ "Y Dimension", static_cast(i) }); + break; + + case fields::savagery_calm: + state->finder_list.push_back({ "Low Savagery", static_cast(i) }); + break; + + case fields::savagery_medium: + state->finder_list.push_back({ "Medium Savagery", static_cast(i) }); + break; + + case fields::savagery_savage: + state->finder_list.push_back({ "High Savagery", static_cast(i) }); + break; + + case fields::good: + state->finder_list.push_back({ "Good", static_cast(i) }); + break; + + case fields::neutral: + state->finder_list.push_back({ "Neutral", static_cast(i) }); + break; + + case fields::evil: + state->finder_list.push_back({ "Evil", static_cast(i) }); + break; + + case fields::aquifer: + state->finder_list.push_back({ "Aquifer", static_cast(i) }); + break; + + case fields::min_river: + state->finder_list.push_back({ "Min River", static_cast(i) }); + break; + + case fields::max_river: + state->finder_list.push_back({ "Max River", static_cast(i) }); + break; + + case fields::waterfall: + state->finder_list.push_back({ "Waterfall", static_cast(i) }); + break; + + case fields::flat: + state->finder_list.push_back({ "Flat", static_cast(i) }); + break; + + case fields::soil_min_everywhere: + state->finder_list.push_back({ "Min Soil Everywhere", static_cast(i) }); + break; + + case fields::evil_weather: + state->finder_list.push_back({ "Evil Weather", static_cast(i) }); + break; + + case fields::reanimation: + state->finder_list.push_back({ "Reanimation", static_cast(i) }); + break; + + case fields::thralling: + state->finder_list.push_back({ "Thralling", static_cast(i) }); + break; + + case fields::clay: + state->finder_list.push_back({ "Clay", static_cast(i) }); + break; + + case fields::sand: + state->finder_list.push_back({ "Sand", static_cast(i) }); + break; + + case fields::flux: + state->finder_list.push_back({ "Flux", static_cast(i) }); + break; + + case fields::soil_min: + state->finder_list.push_back({ "Min Soil", static_cast(i) }); + break; + + case fields::soil_max: + state->finder_list.push_back({ "Max Soil", static_cast(i) }); + break; + + case fields::biome_count_min: + state->finder_list.push_back({ "Min Biome Count", static_cast(i) }); + break; + + case fields::biome_count_max: + state->finder_list.push_back({ "Max Biome Count", static_cast(i) }); + break; + + case fields::region_type_1: + state->finder_list.push_back({ "Region Type 1", static_cast(i) }); + break; + + case fields::region_type_2: + state->finder_list.push_back({ "Region Type 2", static_cast(i) }); + break; + + case fields::region_type_3: + state->finder_list.push_back({ "Region Type 3", static_cast(i) }); + break; + + case fields::biome_1: + state->finder_list.push_back({ "Biome 1", static_cast(i) }); + break; + + case fields::biome_2: + state->finder_list.push_back({ "Biome 2", static_cast(i) }); + break; + + case fields::biome_3: + state->finder_list.push_back({ "Biome 3", static_cast(i) }); + break; + + case fields::metal_1: + state->finder_list.push_back({ "Metal 1", static_cast(i) }); + break; + + case fields::metal_2: + state->finder_list.push_back({ "Metal 2", static_cast(i) }); + break; + + case fields::metal_3: + state->finder_list.push_back({ "Metal 3", static_cast(i) }); + break; + + case fields::economic_1: + state->finder_list.push_back({ "Economic 1", static_cast(i) }); + break; + + case fields::economic_2: + state->finder_list.push_back({ "Economic 2", static_cast(i) }); + break; + + case fields::economic_3: + state->finder_list.push_back({ "Economic 3", static_cast(i) }); + break; + + case fields::mineral_1: + state->finder_list.push_back({ "Mineral 1", static_cast(i) }); + break; + + case fields::mineral_2: + state->finder_list.push_back({ "Mineral 2", static_cast(i) }); + break; + + case fields::mineral_3: + state->finder_list.push_back({ "Mineral 3", static_cast(i) }); + break; + } + + if (i == last_fields) { + break; // done + } + + i = static_cast (static_cast(i) + 1); + } + + // Default embark area size to that of the current selection. The "size" calculation is actually one + // off to compensate for the list starting with 1 at index 0. + // + auto screen = Gui::getViewscreenByType(0); + int16_t x = screen->location.region_pos.x; + int16_t y = screen->location.region_pos.y; + state->ui[static_cast(fields::x_dim)]->current_display_value = + Gui::getViewscreenByType(0)->location.embark_pos_max.x - + Gui::getViewscreenByType(0)->location.embark_pos_min.x; + state->ui[static_cast(fields::x_dim)]->current_index = + state->ui[static_cast(fields::x_dim)]->current_display_value; + state->ui[static_cast(fields::x_dim)]->current_value = + state->ui[static_cast(fields::x_dim)]->current_display_value + 1; + + state->ui[static_cast(fields::y_dim)]->current_display_value = + Gui::getViewscreenByType(0)->location.embark_pos_max.y - + Gui::getViewscreenByType(0)->location.embark_pos_min.y; + state->ui[static_cast(fields::y_dim)]->current_index = + state->ui[static_cast(fields::y_dim)]->current_display_value; + state->ui[static_cast(fields::y_dim)]->current_value = + state->ui[static_cast(fields::y_dim)]->current_display_value + 1; + } + + + //========================================================================================================== + + void find() { +// color_ostream_proxy out(Core::getInstance().getConsole()); + embark_assist::defs::finders finder; + fields i = first_fields; + + while (true) { + switch (i) { + case fields::x_dim: + finder.x_dim = state->ui[static_cast(i)]->current_value; + break; + + case fields::y_dim: + finder.y_dim = state->ui[static_cast(i)]->current_value; + break; + + case fields::savagery_calm: + finder.savagery[0] = + static_cast(state->ui[static_cast(i)]->current_value); + break; + + case fields::savagery_medium: + finder.savagery[1] = + static_cast(state->ui[static_cast(i)]->current_value); + break; + case fields::savagery_savage: + finder.savagery[2] = + static_cast(state->ui[static_cast(i)]->current_value); + break; + + case fields::good: + finder.evilness[0] = + static_cast(state->ui[static_cast(i)]->current_value); + break; + + case fields::neutral: + finder.evilness[1] = + static_cast(state->ui[static_cast(i)]->current_value); + break; + + case fields::evil: + finder.evilness[2] = + static_cast(state->ui[static_cast(i)]->current_value); + break; + + case fields::aquifer: + finder.aquifer = + static_cast(state->ui[static_cast(i)]->current_value); + break; + + case fields::min_river: + finder.min_river = + static_cast(state->ui[static_cast(i)]->current_value); + break; + + case fields::max_river: + finder.max_river = + static_cast(state->ui[static_cast(i)]->current_value); + break; + + case fields::waterfall: + finder.waterfall = + static_cast(state->ui[static_cast(i)]->current_value); + break; + + case fields::flat: + finder.flat = + static_cast(state->ui[static_cast(i)]->current_value); + break; + + case fields::soil_min_everywhere: + finder.soil_min_everywhere = + static_cast(state->ui[static_cast(i)]->current_value); + break; + + case fields::evil_weather: + finder.evil_weather = + static_cast(state->ui[static_cast(i)]->current_value); + break; + + case fields::reanimation: + finder.reanimation = + static_cast(state->ui[static_cast(i)]->current_value); + break; + + case fields::thralling: + finder.thralling = + static_cast(state->ui[static_cast(i)]->current_value); + break; + + case fields::clay: + finder.clay = + static_cast(state->ui[static_cast(i)]->current_value); + break; + + case fields::sand: + finder.sand = + static_cast(state->ui[static_cast(i)]->current_value); + break; + + case fields::flux: + finder.flux = + static_cast(state->ui[static_cast(i)]->current_value); + break; + + case fields::soil_min: + finder.soil_min = + static_cast(state->ui[static_cast(i)]->current_value); + break; + + case fields::soil_max: + finder.soil_max = + static_cast(state->ui[static_cast(i)]->current_value); + break; + + case fields::biome_count_min: + finder.biome_count_min = state->ui[static_cast(i)]->current_value; + break; + + case fields::biome_count_max: + finder.biome_count_max = state->ui[static_cast(i)]->current_value; + break; + + case fields::region_type_1: + finder.region_type_1 = state->ui[static_cast(i)]->current_value; + break; + + case fields::region_type_2: + finder.region_type_2 = state->ui[static_cast(i)]->current_value; + break; + + case fields::region_type_3: + finder.region_type_3 = state->ui[static_cast(i)]->current_value; + break; + + case fields::biome_1: + finder.biome_1 = state->ui[static_cast(i)]->current_value; + break; + + case fields::biome_2: + finder.biome_2 = state->ui[static_cast(i)]->current_value; + break; + + case fields::biome_3: + finder.biome_3 = state->ui[static_cast(i)]->current_value; + break; + + case fields::metal_1: + finder.metal_1 = state->ui[static_cast(i)]->current_value; + break; + + case fields::metal_2: + finder.metal_2 = state->ui[static_cast(i)]->current_value; + break; + + case fields::metal_3: + finder.metal_3 = state->ui[static_cast(i)]->current_value; + break; + + case fields::economic_1: + finder.economic_1 = state->ui[static_cast(i)]->current_value; + break; + + case fields::economic_2: + finder.economic_2 = state->ui[static_cast(i)]->current_value; + break; + + case fields::economic_3: + finder.economic_3 = state->ui[static_cast(i)]->current_value; + break; + + case fields::mineral_1: + finder.mineral_1 = state->ui[static_cast(i)]->current_value; + break; + + case fields::mineral_2: + finder.mineral_2 = state->ui[static_cast(i)]->current_value; + break; + + case fields::mineral_3: + finder.mineral_3 = state->ui[static_cast(i)]->current_value; + break; + } + + if (i == last_fields) { + break; // done + } + + i = static_cast (static_cast(i) + 1); + } + + state->find_callback(finder); + } + + //========================================================================================================== + + class ViewscreenFindUi : public dfhack_viewscreen + { + public: + ViewscreenFindUi(); + + void feed(std::set *input); + + void render(); + + std::string getFocusString() { return "Finder UI"; } + + private: + }; + + //=============================================================================== + + void ViewscreenFindUi::feed(std::set *input) { + if (input->count(df::interface_key::LEAVESCREEN)) + { + input->clear(); + Screen::dismiss(this); + return; + + } else if (input->count(df::interface_key::CURSOR_LEFT) || + input->count(df::interface_key::CURSOR_RIGHT)) { + state->finder_list_active = !state->finder_list_active; + + } else if (input->count(df::interface_key::CURSOR_UP)) { + if (state->finder_list_active) { + if (state->finder_list_focus > 0) { + state->finder_list_focus--; + } + else { + state->finder_list_focus = static_cast(last_fields); + } + } + else { + if (state->ui[state->finder_list_focus]->current_index > 0) { + state->ui[state->finder_list_focus]->current_index--; + } else { + state->ui[state->finder_list_focus]->current_index = static_cast(state->ui[state->finder_list_focus]->list.size()) - 1; + } + } + + } else if (input->count(df::interface_key::CURSOR_DOWN)) { + if (state->finder_list_active) { + if (state->finder_list_focus < static_cast(last_fields)) { + state->finder_list_focus++; + } else { + state->finder_list_focus = 0; + } + } + else { + if (state->ui[state->finder_list_focus]->current_index < state->ui[state->finder_list_focus]->list.size() - 1) { + state->ui[state->finder_list_focus]->current_index++; + } else { + state->ui[state->finder_list_focus]->current_index = 0; + } + } + } else if (input->count(df::interface_key::SELECT)) { + if (!state->finder_list_active) { + state->ui[state->finder_list_focus]->current_display_value = state->ui[state->finder_list_focus]->current_index; + state->ui[state->finder_list_focus]->current_value = state->ui[state->finder_list_focus]->list[state->ui[state->finder_list_focus]->current_index].key; + state->finder_list_active = true; + } + } else if (input->count(df::interface_key::CUSTOM_F)) { + input->clear(); + Screen::dismiss(this); + find(); + return; + } + } + + //=============================================================================== + + void ViewscreenFindUi::render() { +// color_ostream_proxy out(Core::getInstance().getConsole()); + auto screen_size = DFHack::Screen::getWindowSize(); + const int list_column = 53; + uint16_t offset = 0; + + Screen::clear(); + Screen::drawBorder("Embark Assistant Site Finder"); + + embark_assist::screen::paintString(lr_pen, 1, 1, "4/6"); + embark_assist::screen::paintString(white_pen, 4, 1, ":Shift list"); + embark_assist::screen::paintString(lr_pen, 16, 1, "8/2"); + embark_assist::screen::paintString(white_pen, 19, 1, ":Up/down"); + embark_assist::screen::paintString(lr_pen, 28, 1, "ENTER"); + embark_assist::screen::paintString(white_pen, 33, 1, ":Select item"); + embark_assist::screen::paintString(lr_pen, 46, 1, "f"); + embark_assist::screen::paintString(white_pen, 47, 1, ":Find"); + embark_assist::screen::paintString(lr_pen, 53, 1, "ESC"); + embark_assist::screen::paintString(white_pen, 56, 1, ":Abort"); + + for (uint16_t i = 0; i < state->finder_list.size(); i++) { + if (i == state->finder_list_focus) { + if (state->finder_list_active) { + embark_assist::screen::paintString(active_pen, 1, 2 + i, state->finder_list[i].text); + } + else { + embark_assist::screen::paintString(passive_pen, 1, 2 + i, state->finder_list[i].text); + } + + embark_assist::screen::paintString(active_pen, + 21, + 2 + i, + state->ui[i]->list[state->ui[i]->current_display_value].text); + } + else { + embark_assist::screen::paintString(normal_pen, 1, 2 + i, state->finder_list[i].text); + + embark_assist::screen::paintString(white_pen, + 21, + 2 + i, + state->ui[i]->list[state->ui[i]->current_display_value].text); + } + + } + + // Implement scrolling lists if they don't fit on the screen. + if (state->ui[state->finder_list_focus]->list.size() > screen_size.y - 3) { + offset = (screen_size.y - 3) / 2; + if (state->ui[state->finder_list_focus]->current_index < offset) { + offset = 0; + } + else { + offset = state->ui[state->finder_list_focus]->current_index - offset; + } + + if (state->ui[state->finder_list_focus]->list.size() - offset < screen_size.y - 3) { + offset = static_cast(state->ui[state->finder_list_focus]->list.size()) - (screen_size.y - 3); + } + } + + for (uint16_t i = 0; i < state->ui[state->finder_list_focus]->list.size(); i++) { + if (i == state->ui[state->finder_list_focus]->current_index) { + if (!state->finder_list_active) { // Negated expression to get the display lines in the same order as above. + embark_assist::screen::paintString(active_pen, list_column, 2 + i - offset, state->ui[state->finder_list_focus]->list[i].text); + } + else { + embark_assist::screen::paintString(passive_pen, list_column, 2 + i - offset, state->ui[state->finder_list_focus]->list[i].text); + } + } + else { + embark_assist::screen::paintString(normal_pen, list_column, 2 + i - offset, state->ui[state->finder_list_focus]->list[i].text); + } + } + + dfhack_viewscreen::render(); + } + + //=============================================================================== + + ViewscreenFindUi::ViewscreenFindUi() { + } + } +} + +//=============================================================================== +// Exported operations +//=============================================================================== + +void embark_assist::finder_ui::init(DFHack::Plugin *plugin_self, embark_assist::defs::find_callbacks find_callback, uint16_t max_inorganic) { + if (!embark_assist::finder_ui::state) { // First call. Have to do the setup + embark_assist::finder_ui::ui_setup(find_callback, max_inorganic); + } + Screen::show(new ViewscreenFindUi(), plugin_self); +} + +//=============================================================================== + +void embark_assist::finder_ui::activate() { +} + +//=============================================================================== + +void embark_assist::finder_ui::shutdown() { + if (embark_assist::finder_ui::state) { + for (uint16_t i = 0; i < embark_assist::finder_ui::state->ui.size(); i++) { + delete embark_assist::finder_ui::state->ui[i]; + } + + delete embark_assist::finder_ui::state; + embark_assist::finder_ui::state = nullptr; + } +} diff --git a/plugins/embark-assistant/finder_ui.h b/plugins/embark-assistant/finder_ui.h new file mode 100644 index 000000000..70bf4ce42 --- /dev/null +++ b/plugins/embark-assistant/finder_ui.h @@ -0,0 +1,17 @@ +#pragma once + +#include "PluginManager.h" + +#include "DataDefs.h" + +#include "defs.h" + +using namespace DFHack; + +namespace embark_assist { + namespace finder_ui { + void init(DFHack::Plugin *plugin_self, embark_assist::defs::find_callbacks find_callback, uint16_t max_inorganic); + void activate(); + void shutdown(); + } +} \ No newline at end of file diff --git a/plugins/embark-assistant/help_ui.cpp b/plugins/embark-assistant/help_ui.cpp new file mode 100644 index 000000000..d75d00ce2 --- /dev/null +++ b/plugins/embark-assistant/help_ui.cpp @@ -0,0 +1,314 @@ +#include "Core.h" +#include + +#include +#include +#include + +#include "Types.h" + +#include "help_ui.h" +#include "screen.h" + +using std::vector; + +namespace embark_assist{ + namespace help_ui { + enum class pages { + Intro, + General, + Finder, + Caveats + }; + + class ViewscreenHelpUi : public dfhack_viewscreen + { + public: + ViewscreenHelpUi(); + + void feed(std::set *input); + + void render(); + + std::string getFocusString() { return "Help UI"; } + + private: + pages current_page = pages::Intro; + }; + + //=============================================================================== + + void ViewscreenHelpUi::feed(std::set *input) { + if (input->count(df::interface_key::LEAVESCREEN)) + { + input->clear(); + Screen::dismiss(this); + return; + } + else if (input->count(df::interface_key::CHANGETAB)) { + switch (current_page) { + case pages::Intro: + current_page = pages::General; + break; + + case pages::General: + current_page = pages::Finder; + break; + + case pages::Finder: + current_page = pages::Caveats; + break; + + case pages::Caveats: + current_page = pages::Intro; + break; + } + } + else if (input->count(df::interface_key::SEC_CHANGETAB)) { + switch (current_page) { + case pages::Intro: + current_page = pages::Caveats; + break; + + case pages::General: + current_page = pages::Intro; + break; + + case pages::Finder: + current_page = pages::General; + break; + + case pages::Caveats: + current_page = pages::Intro; + break; + } + } + } + + //=============================================================================== + + void ViewscreenHelpUi::render() { + color_ostream_proxy out(Core::getInstance().getConsole()); + auto screen_size = DFHack::Screen::getWindowSize(); + Screen::Pen pen(' ', COLOR_WHITE); + Screen::Pen site_pen = Screen::Pen(' ', COLOR_YELLOW, COLOR_BLACK, false); + Screen::Pen pen_lr(' ', COLOR_LIGHTRED); + + std::vector help_text; + + Screen::clear(); + + switch (current_page) { + case pages::Intro: + Screen::drawBorder("Embark Assistant Help/Info Introduction Page"); + + help_text.push_back("Embark Assistant is used on the embark selection screen to provide information"); + help_text.push_back("to help selecting a suitable embark site. It provides three services:"); + help_text.push_back("- Display of normally invisible sites overlayed on the region map."); + help_text.push_back("- Embark rectangle resources. More detailed and correct than vanilla DF."); + help_text.push_back("- Site find search. Richer set of selection criteria than the vanilla"); + help_text.push_back(" DF Find that Embark Assistant suppresses (by using the same key)."); + help_text.push_back(""); + help_text.push_back("The functionality requires a screen height of at least 42 lines to display"); + help_text.push_back("correctly (that's the height of the Finder screen), as fitting everything"); + help_text.push_back("onto a standard 80*25 screen would be too challenging. The help is adjusted"); + help_text.push_back("to fit into onto an 80*42 screen."); + help_text.push_back("This help/info is split over several screens, and you can move between them"); + help_text.push_back("using the TAB/Shift-TAB keys, and leave the help from any screen using ESC."); + help_text.push_back(""); + help_text.push_back("When the Embark Assistant is started it provides site information (if any)"); + help_text.push_back("as an overlay over the region map. Beneath that you will find a summary of"); + help_text.push_back("the resources in the current embark rectangle (explained in more detail on"); + help_text.push_back("the the next screen)."); + help_text.push_back("On the right side the command keys the Embark Assistant uses are listed"); + help_text.push_back("(this partially overwrites the DFHack Embark Tools information until the"); + help_text.push_back("screen is resized). It can also be mentioned that the DF 'f'ind key help"); + help_text.push_back("at the bottom of the screen is masked as the functionality is overridden by"); + help_text.push_back("the Embark Assistant."); + help_text.push_back("Main screen control keys used by the Embark Assistant:"); + help_text.push_back("i: Info/Help. Brings up this display."); + help_text.push_back("f: Brings up the Find Embark screen. See the Find page for more information."); + help_text.push_back("c: Clears the results of a Find operation, and also cancels an operation if"); + help_text.push_back(" one is under way."); + help_text.push_back("q: Quits the Embark Assistant and brings you back to the vanilla DF interface."); + help_text.push_back(" It can be noted that the Embark Assistant automatically cancels itself"); + help_text.push_back(" when DF leaves the embark screen either through Abort Game or by"); + help_text.push_back(" embarking."); + help_text.push_back("Below this a Matching World Tiles count is displayed. It shows the number"); + help_text.push_back("of World Tiles that have at least one embark matching the Find criteria."); + + break; + + case pages::General: + Screen::drawBorder("Embark Assistant Help/Info General Page"); + + help_text.push_back("The Embark Assistant overlays the region map with characters indicating sites"); + help_text.push_back("normally not displayed by DF. The following key is used:"); + help_text.push_back("C: Camp"); + help_text.push_back("c: Cave. Only displayed if the DF worldgen parameter does not display caves."); + help_text.push_back("i: Important Location. The author doesn't actually know what those are."); + help_text.push_back("l: Lair"); + help_text.push_back("L: Labyrinth"); + help_text.push_back("M: Monument. The author is unsure how/if this is broken down further."); + help_text.push_back("S: Shrine"); + help_text.push_back("V: Vault"); + help_text.push_back("The Embark info below the region map differs from the vanilla DF display in a"); + help_text.push_back("few respects. Firstly, it shows resources in the embark rectangle, rather than"); + help_text.push_back("DF's display of resources in the region DF currently displays. Secondly, the"); + help_text.push_back("DF display doesn't take elevation based soil erosion or the magma sea depth"); + help_text.push_back("into consideration, so it can display resources that actually are cut away."); + help_text.push_back("(It can be noted that the DFHack Sand indicator does take these elements into"); + help_text.push_back("account)."); + help_text.push_back("The info the Embark Assistant displays is:"); + help_text.push_back("Sand, if present"); + help_text.push_back("Clay, if present"); + help_text.push_back("Min and Max soil depth in the embark rectangle."); + help_text.push_back("Flat indicator if all the tiles in the embark have the same elevation."); + help_text.push_back("Aquifer indicator, color coded as blue if all tiles have an aquifer and light"); + help_text.push_back("blue if some, but not all, tiles have one."); + help_text.push_back("Waterfall, if the embark has river elevation differences."); + help_text.push_back("Flux, if present"); + help_text.push_back("A list of all metals present in the embark."); + help_text.push_back("A list of all economic minerals present in the embark. Both clays and flux"); + help_text.push_back("stones are economic, so they show up here as well."); + help_text.push_back("In addition to the above, the Find functionality can also produce blinking"); + help_text.push_back("overlays over the region map and the middle world map to indicate where"); + help_text.push_back("matching embarks are found. The region display marks the top left corner of"); + help_text.push_back("a matching embark rectangle as a matching tile."); + + break; + + case pages::Finder: + Screen::drawBorder("Embark Assistant Help/Info Find Page"); + + help_text.push_back("The Embark Assist Finder page is brought up with the f command key."); + help_text.push_back("The top of the Finder page lists the command keys available on the page:"); + help_text.push_back("4/6 or horizontal arrow keys to move between the element and element values."); + help_text.push_back("8/2 or vertical arrow keys to move up/down in the current list."); + help_text.push_back("ENTER to select a value in the value list, entering it among the selections."); + help_text.push_back("f to activate the Find functionality using the values in the middle column."); + help_text.push_back("ESC to leave the screen without activating a Find operation."); + help_text.push_back("The X and Y dimensions are those of the embark to search for. Unlike DF"); + help_text.push_back("itself these parameters are initiated to match the actual embark rectangle"); + help_text.push_back("when a new search is initiated (prior results are cleared."); + help_text.push_back("The 6 Savagery and Evilness parameters takes some getting used to. They"); + help_text.push_back("allow for searching for embarks with e.g. has both Good and Evil tiles."); + help_text.push_back("All as a parameter means every embark tile has to have this value."); + help_text.push_back("Present means at least one embark tile has to have this value."); + help_text.push_back("Absent means the feature mustn't exist in any of the embark tiles."); + help_text.push_back("N/A means no restrictions are applied."); + help_text.push_back("The Aquifer criterion introduces some new parameters:"); + help_text.push_back("Partial means at least one tile has to have an aquifer, but it also has"); + help_text.push_back("to be absent from at least one tile. Not All means an aquifer is tolerated"); + help_text.push_back("as long as at least one tile doesn't have one, but it doesn't have to have"); + help_text.push_back("any at all."); + help_text.push_back("Min/Max rivers should be self explanatory. The Yes and No values of"); + help_text.push_back("Waterfall, Flat, etc. means one has to be Present and Absent respectivey."); + help_text.push_back("Min/Max soil uses the same terminology as DF for 1-4. The Min Soil"); + help_text.push_back("Everywhere toggles the Min Soil parameter between acting as All and"); + help_text.push_back("and Present."); + help_text.push_back("The parameters for biomes, regions, etc. all require that the required"); + help_text.push_back("feature is Present in the embark, and entering the same value multiple"); + help_text.push_back("times does nothing (the first match ticks all requirements off). It can be"); + help_text.push_back("noted that all the Economic materials are found in the much longer Mineral"); + help_text.push_back("list. Note that Find is a fairly time consuming task (is it is in vanilla)."); + break; + + case pages::Caveats: + Screen::drawBorder("Embark Assistant Help/Info Caveats Page"); + + help_text.push_back("Find searching first does a sanity check (e.g. max < min) and then a rough"); + help_text.push_back("world tile match to find tiles that may have matching embarks. This results"); + help_text.push_back("in an overlay of inverted yellow X on top of the middle world map. Then"); + help_text.push_back("those tiles are scanned in detail, one feature shell (16*16 world tile"); + help_text.push_back("block) at a time, and the results are displayed as green inverted X on"); + help_text.push_back("the same map (replacing or erasing the yellow ones). region map overlay"); + help_text.push_back("data is generated as well."); + help_text.push_back(""); + help_text.push_back("Caveats & technical stuff:"); + help_text.push_back("- The Find searching uses simulated cursor movement input to DF to get it"); + help_text.push_back(" to load feature shells and detailed region data, and this costs the"); + help_text.push_back(" least when done one feature shell at a time."); + help_text.push_back("- The search strategy causes detailed region data to update surveyed"); + help_text.push_back(" world info, and this can cause a subsequent search to generate a smaller"); + help_text.push_back(" set of preliminary matches (yellow tiles) than a previous search."); + help_text.push_back(" However, this is a bug only if it causes the search to fail to find"); + help_text.push_back(" actual existing matches."); + help_text.push_back("- The site info is deduced by the author, so there may be errors and"); + help_text.push_back(" there are probably site types that end up not being identified."); + help_text.push_back("- Aquifer indications are based on the author's belief that they occur"); + help_text.push_back(" whenever an aquifer supporting layer is present at a depth of 3 or"); + help_text.push_back(" more."); + help_text.push_back("- The biome determination logic comes from code provided by Ragundo,"); + help_text.push_back(" with only marginal changes by the author. References can be found in"); + help_text.push_back(" the source file."); + help_text.push_back("- Thralling is determined by weather material interactions causing"); + help_text.push_back(" blinking, which the author believes is one of 4 thralling changes."); + help_text.push_back("- The geo information is gathered by code which is essentially a"); + help_text.push_back(" copy of parts of prospector's code adapted for this plugin."); + help_text.push_back("- Clay determination is made by finding the reaction MAKE_CLAY_BRICKS."); + help_text.push_back(" Flux determination is made by finding the reaction PIG_IRON_MAKING."); + help_text.push_back("- Right world map overlay not implemented as author has failed to"); + help_text.push_back(" emulate the sizing logic exactly."); + help_text.push_back("Version 0.1 2017-08-30"); + + break; + } + + // Add control keys to first line. + embark_assist::screen::paintString(pen_lr, 1, 1, "TAB/Shift-TAB"); + embark_assist::screen::paintString(pen, 14, 1, ":Next/Previous Page"); + embark_assist::screen::paintString(pen_lr, 34, 1, "ESC"); + embark_assist::screen::paintString(pen, 37, 1, ":Leave Info/Help"); + + for (uint16_t i = 0; i < help_text.size(); i++) { + embark_assist::screen::paintString(pen, 1, 2 + i, help_text[i]); + } + + switch (current_page) { + case pages::Intro: + embark_assist::screen::paintString(pen_lr, 1, 26, "i"); + embark_assist::screen::paintString(pen_lr, 1, 27, "f"); + embark_assist::screen::paintString(pen_lr, 1, 28, "c"); + embark_assist::screen::paintString(pen_lr, 1, 30, "q"); + break; + + case pages::General: + embark_assist::screen::paintString(site_pen, 1, 4, "C"); + embark_assist::screen::paintString(site_pen, 1, 5, "c"); + embark_assist::screen::paintString(site_pen, 1, 6, "i"); + embark_assist::screen::paintString(site_pen, 1, 7, "l"); + embark_assist::screen::paintString(site_pen, 1, 8, "L"); + embark_assist::screen::paintString(site_pen, 1, 9, "M"); + embark_assist::screen::paintString(site_pen, 1, 10, "S"); + embark_assist::screen::paintString(site_pen, 1, 11, "V"); + break; + + case pages::Finder: + embark_assist::screen::paintString(pen_lr, 1, 4, "4/6"); + embark_assist::screen::paintString(pen_lr, 1, 5, "8/2"); + embark_assist::screen::paintString(pen_lr, 1, 6, "ENTER"); + embark_assist::screen::paintString(pen_lr, 1, 7, "f"); + embark_assist::screen::paintString(pen_lr, 1, 8, "ESC"); + break; + + case pages::Caveats: + break; + } + dfhack_viewscreen::render(); + } + + //=============================================================================== + + ViewscreenHelpUi::ViewscreenHelpUi() { + } + } +} + +//=============================================================================== +// Exported operations +//=============================================================================== + +void embark_assist::help_ui::init(DFHack::Plugin *plugin_self) { + Screen::show(new embark_assist::help_ui::ViewscreenHelpUi(), plugin_self); +} diff --git a/plugins/embark-assistant/help_ui.h b/plugins/embark-assistant/help_ui.h new file mode 100644 index 000000000..65a2e4f1d --- /dev/null +++ b/plugins/embark-assistant/help_ui.h @@ -0,0 +1,15 @@ +#pragma once + +#include "PluginManager.h" + +#include "DataDefs.h" + +#include "defs.h" + +using namespace DFHack; + +namespace embark_assist { + namespace help_ui { + void init(DFHack::Plugin *plugin_self); + } +} \ No newline at end of file diff --git a/plugins/embark-assistant/matcher.cpp b/plugins/embark-assistant/matcher.cpp new file mode 100644 index 000000000..484ee3cf8 --- /dev/null +++ b/plugins/embark-assistant/matcher.cpp @@ -0,0 +1,1445 @@ +#include + +#include + +#include "DataDefs.h" +#include "df/biome_type.h" +#include "df/inorganic_raw.h" +#include "df/region_map_entry.h" +#include "df/viewscreen.h" +#include "df/viewscreen_choose_start_sitest.h" +#include "df/world.h" +#include "df/world_data.h" +#include "df/world_raws.h" +#include "df/world_region.h" +#include "df/world_region_type.h" + +#include "matcher.h" +#include "survey.h" + +using df::global::world; + +namespace embark_assist { + namespace matcher { + + //======================================================================================= + + //======================================================================================= + + bool embark_match(embark_assist::defs::world_tile_data *survey_results, + embark_assist::defs::mid_level_tiles *mlt, + uint16_t x, + uint16_t y, + uint16_t start_x, + uint16_t start_y, + embark_assist::defs::finders *finder) { + +// color_ostream_proxy out(Core::getInstance().getConsole()); + df::world_data *world_data = world->world_data; + bool savagery_found[3] = { false, false, false }; + bool evilness_found[3] = { false, false, false }; + uint16_t aquifer_count = 0; + bool river_found = false; + bool waterfall_found = false; + uint16_t river_elevation; + uint16_t elevation = mlt->at(start_x).at(start_y).elevation; + bool clay_found = false; + bool sand_found = false; + bool flux_found = false; + uint8_t max_soil = 0; + bool uneven = false; + bool evil_weather_found = false; + bool reanimation_found = false; + bool thralling_found = false; + bool biomes[ENUM_LAST_ITEM(biome_type) + 1]; + bool region_types[ENUM_LAST_ITEM(world_region_type) + 1]; + uint8_t biome_count; + bool metal_1 = finder->metal_1 == -1; + bool metal_2 = finder->metal_2 == -1; + bool metal_3 = finder->metal_3 == -1; + bool economic_1 = finder->economic_1 == -1; + bool economic_2 = finder->economic_2 == -1; + bool economic_3 = finder->economic_3 == -1; + bool mineral_1 = finder->mineral_1 == -1; + bool mineral_2 = finder->mineral_2 == -1; + bool mineral_3 = finder->mineral_3 == -1; + + const uint16_t embark_size = finder->x_dim * finder->y_dim; + + if (finder->biome_count_min != -1 || + finder->biome_count_max != -1 || + finder->biome_1 != -1 || + finder->biome_2 != -1 || + finder->biome_3 != -1) { + for (uint8_t i = 0; i <= ENUM_LAST_ITEM(biome_type); i++) biomes[i] = false; + } + + for (uint8_t i = 0; i <= ENUM_LAST_ITEM(world_region_type); i++) region_types[i] = false; + + for (uint16_t i = start_x; i < start_x + finder->x_dim; i++) { + for (uint16_t k = start_y; k < start_y + finder->y_dim; k++) { + + // Savagery & Evilness + { + savagery_found[mlt->at(i).at(k).savagery_level] = true; + evilness_found[mlt->at(i).at(k).evilness_level] = true; + + embark_assist::defs::evil_savagery_ranges l = embark_assist::defs::evil_savagery_ranges::Low; + while (true) { + if (mlt->at(i).at(k).savagery_level == static_cast(l)) { + if (finder->savagery[static_cast (l)] == + embark_assist::defs::evil_savagery_values::Absent) return false; + } + else { + if (finder->savagery[static_cast (l)] == + embark_assist::defs::evil_savagery_values::All) return false; + } + + if (mlt->at(i).at(k).evilness_level == static_cast(l)) { + if (finder->evilness[static_cast (l)] == + embark_assist::defs::evil_savagery_values::Absent) return false; + } + else { + if (finder->evilness[static_cast (l)] == + embark_assist::defs::evil_savagery_values::All) return false; + } + + if (l == embark_assist::defs::evil_savagery_ranges::High) break; + l = static_cast (static_cast(l) + 1); + } + } + + // Aquifer + switch (finder->aquifer) { + case embark_assist::defs::aquifer_ranges::NA: + break; + + case embark_assist::defs::aquifer_ranges::All: + if (!mlt->at(i).at(k).aquifer) return false; + aquifer_count++; + break; + + case embark_assist::defs::aquifer_ranges::Present: + case embark_assist::defs::aquifer_ranges::Partial: + case embark_assist::defs::aquifer_ranges::Not_All: + if (mlt->at(i).at(k).aquifer) aquifer_count++; + break; + + case embark_assist::defs::aquifer_ranges::Absent: + if (mlt->at(i).at(k).aquifer) return false; + break; + } + + // River & Waterfall + if (mlt->at(i).at(k).river_present) { + // Actual size values were checked on the world tile level for min rivers + if (finder->max_river != embark_assist::defs::river_ranges::NA && + finder->max_river < static_cast(survey_results->at(x).at(y).river_size)) return false; + + if (river_found && river_elevation != mlt->at(i).at(k).river_elevation) { + if (finder->waterfall == embark_assist::defs::yes_no_ranges::No) return false; + waterfall_found = true; + } + river_found = true; + river_elevation = mlt->at(i).at(k).river_elevation; + } + + // Flat + if (finder->flat == embark_assist::defs::yes_no_ranges::Yes && + elevation != mlt->at(i).at(k).elevation) return false; + + if (elevation != mlt->at(i).at(k).elevation) uneven = true; + + // Clay + if (mlt->at(i).at(k).clay) { + if (finder->clay == embark_assist::defs::present_absent_ranges::Absent) return false; + clay_found = true; + } + + // Sand + if (mlt->at(i).at(k).sand) { + if (finder->sand == embark_assist::defs::present_absent_ranges::Absent) return false; + sand_found = true; + } + + // Flux + if (mlt->at(i).at(k).flux) { + if (finder->flux == embark_assist::defs::present_absent_ranges::Absent) return false; + flux_found = true; + } + + // Min Soil + if (finder->soil_min != embark_assist::defs::soil_ranges::NA && + mlt->at(i).at(k).soil_depth < static_cast(finder->soil_min) && + finder->soil_min_everywhere == embark_assist::defs::all_present_ranges::All) return false; + + if (max_soil < mlt->at(i).at(k).soil_depth) { + max_soil = mlt->at(i).at(k).soil_depth; + } + + // Max Soil + if (finder->soil_max != embark_assist::defs::soil_ranges::NA && + mlt->at(i).at(k).soil_depth > static_cast(finder->soil_max)) return false; + + // Evil Weather + if (survey_results->at(x).at(y).evil_weather[mlt->at(i).at(k).biome_offset]) { + if (finder->evil_weather == embark_assist::defs::yes_no_ranges::No) return false; + evil_weather_found = true; + } + + // Reanmation + if (survey_results->at(x).at(y).reanimating[mlt->at(i).at(k).biome_offset]) { + if (finder->reanimation == embark_assist::defs::yes_no_ranges::No) return false; + reanimation_found = true; + } + + // Thralling + if (survey_results->at(x).at(y).thralling[mlt->at(i).at(k).biome_offset]) { + if (finder->thralling == embark_assist::defs::yes_no_ranges::No) return false; + thralling_found = true; + } + + // Biomes + biomes[survey_results->at(x).at(y).biome[mlt->at(i).at(k).biome_offset]] = true; + + // Region Type + region_types[world_data->regions[survey_results->at(x).at(y).biome_index[mlt->at(i).at(k).biome_offset]]->type] = true; + + // Metals + metal_1 = metal_1 || mlt->at(i).at(k).metals[finder->metal_1]; + metal_2 = metal_2 || mlt->at(i).at(k).metals[finder->metal_2]; + metal_3 = metal_3 || mlt->at(i).at(k).metals[finder->metal_3]; + + // Economics + economic_1 = economic_1 || mlt->at(i).at(k).economics[finder->economic_1]; + economic_2 = economic_2 || mlt->at(i).at(k).economics[finder->economic_2]; + economic_3 = economic_3 || mlt->at(i).at(k).economics[finder->economic_3]; + + // Minerals + mineral_1 = mineral_1 || mlt->at(i).at(k).minerals[finder->mineral_1]; + mineral_2 = mineral_2 || mlt->at(i).at(k).minerals[finder->mineral_2]; + mineral_3 = mineral_3 || mlt->at(i).at(k).minerals[finder->mineral_3]; + } + } + + // Summary section, for all the stuff that require the complete picture + // + // Savagery & Evilness + { + embark_assist::defs::evil_savagery_ranges l = embark_assist::defs::evil_savagery_ranges::Low; + + while (true) { + if (finder->savagery[static_cast (l)] == + embark_assist::defs::evil_savagery_values::Present && + !savagery_found[static_cast(l)]) return false; + + if (finder->evilness[static_cast (l)] == + embark_assist::defs::evil_savagery_values::Present && + !evilness_found[static_cast(l)]) return false; + + if (l == embark_assist::defs::evil_savagery_ranges::High) break; + l = static_cast (static_cast(l) + 1); + } + } + + // Aquifer + switch (finder->aquifer) { + case embark_assist::defs::aquifer_ranges::NA: + case embark_assist::defs::aquifer_ranges::All: // Checked above + case embark_assist::defs::aquifer_ranges::Absent: // Ditto + break; + + case embark_assist::defs::aquifer_ranges::Present: + if (aquifer_count == 0) return false; + break; + + case embark_assist::defs::aquifer_ranges::Partial: + if (aquifer_count == 0 || aquifer_count == embark_size) return false; + break; + + case embark_assist::defs::aquifer_ranges::Not_All: + if (aquifer_count == embark_size) return false; + break; + } + + // River & Waterfall + if (!river_found && finder->min_river > embark_assist::defs::river_ranges::None) return false; + if (finder->waterfall == embark_assist::defs::yes_no_ranges::Yes && !waterfall_found) return false; + + // Flat + if (!uneven && finder->flat == embark_assist::defs::yes_no_ranges::No) return false; + + // Clay + if (finder->clay == embark_assist::defs::present_absent_ranges::Present && !clay_found) return false; + + // Sand + if (finder->sand == embark_assist::defs::present_absent_ranges::Present && !sand_found) return false; + + // Flux + if (finder->flux == embark_assist::defs::present_absent_ranges::Present && !flux_found) return false; + + // Min Soil + if (finder->soil_min != embark_assist::defs::soil_ranges::NA && + finder->soil_min_everywhere == embark_assist::defs::all_present_ranges::Present && + max_soil < static_cast(finder->soil_min)) return false; + + // Evil Weather + if (finder->evil_weather == embark_assist::defs::yes_no_ranges::Yes && !evil_weather_found) return false; + + // Reanimation + if (finder->reanimation == embark_assist::defs::yes_no_ranges::Yes && !reanimation_found) return false; + + // Thralling + if (finder->thralling == embark_assist::defs::yes_no_ranges::Yes && !thralling_found) return false; + + // Biomes + if (finder->biome_count_min != -1 || + finder->biome_count_max != -1) { + biome_count = 0; + for (uint8_t i = 0; i <= ENUM_LAST_ITEM(biome_type); i++) { + if (biomes[i]) biome_count++; + } + + if (biome_count < finder->biome_count_min || + (finder->biome_count_max != -1 && + finder->biome_count_max < biome_count)) return false; + } + + if (finder->biome_1 != -1 && !biomes[finder->biome_1]) return false; + if (finder->biome_2 != -1 && !biomes[finder->biome_2]) return false; + if (finder->biome_3 != -1 && !biomes[finder->biome_3]) return false; + + // Region Type + if (finder->region_type_1 != -1 && !region_types[finder->region_type_1]) return false; + if (finder->region_type_2 != -1 && !region_types[finder->region_type_2]) return false; + if (finder->region_type_3 != -1 && !region_types[finder->region_type_3]) return false; + + // Metals, Economics, and Minerals + if (!metal_1 || + !metal_2 || + !metal_3 || + !economic_1 || + !economic_2 || + !economic_3 || + !mineral_1 || + !mineral_2 || + !mineral_3) return false; + + return true; + } + + //======================================================================================= + + void mid_level_tile_match(embark_assist::defs::world_tile_data *survey_results, + embark_assist::defs::mid_level_tiles *mlt, + uint16_t x, + uint16_t y, + embark_assist::defs::finders *finder, + embark_assist::defs::match_results *match_results) { + +// color_ostream_proxy out(Core::getInstance().getConsole()); + bool match = false; + + for (uint16_t i = 0; i < 16; i++) { + for (uint16_t k = 0; k < 16; k++) { + if (i < 16 - finder->x_dim + 1 && k < 16 - finder->y_dim + 1) { + match_results->at(x).at(y).mlt_match[i][k] = embark_match(survey_results, mlt, x, y, i, k, finder); + match = match || match_results->at(x).at(y).mlt_match[i][k]; + } + else { + match_results->at(x).at(y).mlt_match[i][k] = false; + } + } + } + match_results->at(x).at(y).contains_match = match; + match_results->at(x).at(y).preliminary_match = false; + } + + //======================================================================================= + + bool world_tile_match(embark_assist::defs::world_tile_data *survey_results, + uint16_t x, + uint16_t y, + embark_assist::defs::finders *finder) { + +// color_ostream_proxy out(Core::getInstance().getConsole()); + df::world_data *world_data = world->world_data; + embark_assist::defs::region_tile_datum *tile = &survey_results->at(x).at(y); + const uint16_t embark_size = finder->x_dim * finder->y_dim; + uint16_t count; + bool found; + + if (tile->surveyed) { + // Savagery + for (uint8_t i = 0; i < 3; i++) + { + switch (finder->savagery[i]) { + case embark_assist::defs::evil_savagery_values::NA: + break; // No restriction + + case embark_assist::defs::evil_savagery_values::All: + if (tile->savagery_count[i] < embark_size) return false; + break; + + case embark_assist::defs::evil_savagery_values::Present: + if (tile->savagery_count[i] == 0) return false; + break; + + case embark_assist::defs::evil_savagery_values::Absent: + if (tile->savagery_count[i] > 256 - embark_size) return false; + break; + } + } + + // Evilness + for (uint8_t i = 0; i < 3; i++) + { + switch (finder->evilness[i]) { + case embark_assist::defs::evil_savagery_values::NA: + break; // No restriction + + case embark_assist::defs::evil_savagery_values::All: + if (tile->evilness_count[i] < embark_size) return false; + break; + + case embark_assist::defs::evil_savagery_values::Present: + if (tile->evilness_count[i] == 0) return false; + break; + + case embark_assist::defs::evil_savagery_values::Absent: + if (tile->evilness_count[i] > 256 - embark_size) return false; + break; + } + } + + // Aquifer + switch (finder->aquifer) { + case embark_assist::defs::aquifer_ranges::NA: + break; // No restriction + + case embark_assist::defs::aquifer_ranges::All: + if (tile->aquifer_count < 256 - embark_size) return false; + break; + + case embark_assist::defs::aquifer_ranges::Present: + if (tile->aquifer_count == 0) return false; + break; + + case embark_assist::defs::aquifer_ranges::Partial: + if (tile->aquifer_count == 0 || + tile->aquifer_count == 256) return false; + break; + + case embark_assist::defs::aquifer_ranges::Not_All: + if (tile->aquifer_count == 256) return false; + break; + + case embark_assist::defs::aquifer_ranges::Absent: + if (tile->aquifer_count > 256 - embark_size) return false; + break; + } + + // River size. Every tile has riverless tiles, so max rivers has to be checked on the detailed level. + switch (tile->river_size) { + case embark_assist::defs::river_sizes::None: + if (finder->min_river > embark_assist::defs::river_ranges::None) return false; + break; + + case embark_assist::defs::river_sizes::Brook: + if (finder->min_river > embark_assist::defs::river_ranges::Brook) return false; + break; + + case embark_assist::defs::river_sizes::Stream: + if (finder->min_river > embark_assist::defs::river_ranges::Stream) return false; + break; + + case embark_assist::defs::river_sizes::Minor: + if (finder->min_river > embark_assist::defs::river_ranges::Minor) return false; + break; + + case embark_assist::defs::river_sizes::Medium: + if (finder->min_river > embark_assist::defs::river_ranges::Medium) return false; + break; + + case embark_assist::defs::river_sizes::Major: + if (finder->max_river != embark_assist::defs::river_ranges::NA) return false; + break; + } + + // Waterfall + switch (finder->waterfall) { + case embark_assist::defs::yes_no_ranges::NA: + break; // No restriction + + case embark_assist::defs::yes_no_ranges::Yes: + if (!tile->waterfall) return false; + break; + + case embark_assist::defs::yes_no_ranges::No: + if (tile->waterfall && + embark_size == 256) return false; + break; + } + + // Flat. No world tile checks. Need to look at the details + + // Clay + switch (finder->clay) { + case embark_assist::defs::present_absent_ranges::NA: + break; // No restriction + + case embark_assist::defs::present_absent_ranges::Present: + if (tile->clay_count == 0) return false; + break; + case embark_assist::defs::present_absent_ranges::Absent: + if (tile->clay_count > 256 - embark_size) return false; + break; + } + + // Sand + switch (finder->sand) { + case embark_assist::defs::present_absent_ranges::NA: + break; // No restriction + + case embark_assist::defs::present_absent_ranges::Present: + if (tile->sand_count == 0) return false; + break; + case embark_assist::defs::present_absent_ranges::Absent: + if (tile->sand_count > 256 - embark_size) return false; + break; + } + + // Flux + switch (finder->flux) { + case embark_assist::defs::present_absent_ranges::NA: + break; // No restriction + + case embark_assist::defs::present_absent_ranges::Present: + if (tile->flux_count == 0) return false; + break; + case embark_assist::defs::present_absent_ranges::Absent: + if (tile->flux_count > 256 - embark_size) return false; + break; + } + + // Soil Min + switch (finder->soil_min) { + case embark_assist::defs::soil_ranges::NA: + case embark_assist::defs::soil_ranges::None: + break; // No restriction + + case embark_assist::defs::soil_ranges::Very_Shallow: + if (tile->max_region_soil < 1) return false; + break; + + case embark_assist::defs::soil_ranges::Shallow: + if (tile->max_region_soil < 2) return false; + break; + + case embark_assist::defs::soil_ranges::Deep: + if (tile->max_region_soil < 3) return false; + break; + + case embark_assist::defs::soil_ranges::Very_Deep: + if (tile->max_region_soil < 4) return false; + break; + } + + // soil_min_everywhere only applies on the detailed level + + // Soil Max + switch (finder->soil_max) { + case embark_assist::defs::soil_ranges::NA: + case embark_assist::defs::soil_ranges::Very_Deep: + break; // No restriction + + case embark_assist::defs::soil_ranges::None: + if (tile->min_region_soil > 0) return false; + break; + + case embark_assist::defs::soil_ranges::Very_Shallow: + if (tile->min_region_soil > 1) return false; + break; + + case embark_assist::defs::soil_ranges::Shallow: + if (tile->min_region_soil > 2) return false; + break; + + case embark_assist::defs::soil_ranges::Deep: + if (tile->min_region_soil > 3) return false; + break; + } + + // Evil Weather + switch (finder->evil_weather) { + case embark_assist::defs::yes_no_ranges::NA: + break; // No restriction + + case embark_assist::defs::yes_no_ranges::Yes: + if (!tile->evil_weather_possible) return false; + break; + + case embark_assist::defs::yes_no_ranges::No: + if (tile->evil_weather_full) return false; + break; + } + + // Reanimating + switch (finder->reanimation) { + case embark_assist::defs::yes_no_ranges::NA: + break; // No restriction + + case embark_assist::defs::yes_no_ranges::Yes: + if (!tile->reanimating_possible) return false; + break; + + case embark_assist::defs::yes_no_ranges::No: + if (tile->reanimating_full) return false; + break; + } + + // Thralling + switch (finder->thralling) { + case embark_assist::defs::yes_no_ranges::NA: + break; // No restriction + + case embark_assist::defs::yes_no_ranges::Yes: + if (!tile->thralling_possible) return false; + break; + + case embark_assist::defs::yes_no_ranges::No: + if (tile->thralling_full) return false; + break; + } + + // Biome Count Min (Can't do anything with Max at this level) + if (finder->biome_count_min > tile->biome_count) return false; + + // Region Type 1 + if (finder->region_type_1 != -1) { + found = false; + + for (uint8_t k = 1; k < 10; k++) { + if (tile->biome_index[k] != -1) { + if (world_data->regions[tile->biome_index[k]]->type == finder->region_type_1) { + found = true; + break; + } + } + + if (found) break; + } + + if (!found) return false; + } + + // Region Type 2 + if (finder->region_type_2 != -1) { + found = false; + + for (uint8_t k = 1; k < 10; k++) { + if (tile->biome_index[k] != -1) { + if (world_data->regions[tile->biome_index[k]]->type == finder->region_type_2) { + found = true; + break; + } + } + + if (found) break; + } + + if (!found) return false; + } + + // Region Type 3 + if (finder->region_type_3 != -1) { + found = false; + + for (uint8_t k = 1; k < 10; k++) { + if (tile->biome_index[k] != -1) { + if (world_data->regions[tile->biome_index[k]]->type == finder->region_type_3) { + found = true; + break; + } + } + + if (found) break; + } + + if (!found) return false; + } + + // Biome 1 + if (finder->biome_1 != -1) { + found = false; + + for (uint8_t i = 1; i < 10; i++) { + if (tile->biome[i] == finder->biome_1) { + found = true; + break; + } + } + + if (!found) return false; + } + + // Biome 2 + if (finder->biome_2 != -1) { + found = false; + + for (uint8_t i = 1; i < 10; i++) { + if (tile->biome[i] == finder->biome_2) { + found = true; + break; + } + } + + if (!found) return false; + } + + // Biome 3 + if (finder->biome_3 != -1) { + found = false; + + for (uint8_t i = 1; i < 10; i++) { + if (tile->biome[i] == finder->biome_3) { + found = true; + break; + } + } + + if (!found) return false; + } + + if (finder->metal_1 != -1 || + finder->metal_2 != -1 || + finder->metal_3 != -1 || + finder->economic_1 != -1 || + finder->economic_2 != -1 || + finder->economic_3 != -1 || + finder->mineral_1 != -1 || + finder->mineral_2 != -1 || + finder->mineral_3 != -1) { + count = 0; + bool metal_1 = finder->metal_1 == -1; + bool metal_2 = finder->metal_2 == -1; + bool metal_3 = finder->metal_3 == -1; + bool economic_1 = finder->economic_1 == -1; + bool economic_2 = finder->economic_2 == -1; + bool economic_3 = finder->economic_3 == -1; + bool mineral_1 = finder->mineral_1 == -1; + bool mineral_2 = finder->mineral_2 == -1; + bool mineral_3 = finder->mineral_3 == -1; + + metal_1 = metal_1 || tile->metals[finder->metal_1]; + metal_2 = metal_2 || tile->metals[finder->metal_2]; + metal_3 = metal_3 || tile->metals[finder->metal_3]; + economic_1 = economic_1 || tile->economics[finder->economic_1]; + economic_2 = economic_2 || tile->economics[finder->economic_2]; + economic_3 = economic_3 || tile->economics[finder->economic_3]; + mineral_1 = mineral_1 || tile->minerals[finder->mineral_1]; + mineral_2 = mineral_2 || tile->minerals[finder->mineral_2]; + mineral_3 = mineral_3 || tile->minerals[finder->mineral_3]; + + if (!metal_1 || + !metal_2 || + !metal_3 || + !economic_1 || + !economic_2 || + !economic_3 || + !mineral_1 || + !mineral_2 || + !mineral_3) return false; + } + } + else { // Not surveyed + // Savagery + for (uint8_t i = 0; i < 3; i++) + { + switch (finder->savagery[i]) { + case embark_assist::defs::evil_savagery_values::NA: + break; // No restriction + + case embark_assist::defs::evil_savagery_values::All: + if (tile->savagery_count[i] == 0) return false; + break; + + case embark_assist::defs::evil_savagery_values::Present: + if (tile->savagery_count[i] == 0) return false; + break; + + case embark_assist::defs::evil_savagery_values::Absent: + if (tile->savagery_count[i] == 256) return false; + break; + } + } + + // Evilness + for (uint8_t i = 0; i < 3; i++) + { + switch (finder->evilness[i]) { + case embark_assist::defs::evil_savagery_values::NA: + break; // No restriction + + case embark_assist::defs::evil_savagery_values::All: + if (tile->evilness_count[i] == 0) return false; + break; + + case embark_assist::defs::evil_savagery_values::Present: + if (tile->evilness_count[i] == 0) return false; + break; + + case embark_assist::defs::evil_savagery_values::Absent: + if (tile->evilness_count[i] == 256) return false; + break; + } + } + + // Aquifer + switch (finder->aquifer) { + case embark_assist::defs::aquifer_ranges::NA: + break; // No restriction + + case embark_assist::defs::aquifer_ranges::All: + if (tile->aquifer_count == 0) return false; + break; + + case embark_assist::defs::aquifer_ranges::Present: + if (tile->aquifer_count == 0) return false; + break; + + case embark_assist::defs::aquifer_ranges::Partial: + if (tile->aquifer_count == 0 || + tile->aquifer_count == 256) return false; + break; + + case embark_assist::defs::aquifer_ranges::Not_All: + if (tile->aquifer_count == 256) return false; + break; + + case embark_assist::defs::aquifer_ranges::Absent: + if (tile->aquifer_count == 256) return false; + break; + } + + // River size + switch (tile->river_size) { + case embark_assist::defs::river_sizes::None: + if (finder->min_river > embark_assist::defs::river_ranges::None) return false; + break; + + case embark_assist::defs::river_sizes::Brook: + if (finder->min_river > embark_assist::defs::river_ranges::Brook) return false; + break; + + case embark_assist::defs::river_sizes::Stream: + if (finder->min_river > embark_assist::defs::river_ranges::Stream) return false; + break; + + case embark_assist::defs::river_sizes::Minor: + if (finder->min_river > embark_assist::defs::river_ranges::Minor) return false; + break; + + case embark_assist::defs::river_sizes::Medium: + if (finder->min_river > embark_assist::defs::river_ranges::Medium) return false; + break; + + case embark_assist::defs::river_sizes::Major: + if (finder->max_river != embark_assist::defs::river_ranges::NA) return false; + break; + } + + // Waterfall + if (finder->waterfall == embark_assist::defs::yes_no_ranges::Yes && + tile->river_size == embark_assist::defs::river_sizes::None) return false; + + // Flat. No world tile checks. Need to look at the details + + // Clay + switch (finder->clay) { + case embark_assist::defs::present_absent_ranges::NA: + break; // No restriction + + case embark_assist::defs::present_absent_ranges::Present: + if (tile->clay_count == 0) return false; + break; + case embark_assist::defs::present_absent_ranges::Absent: + if (tile->clay_count == 256) return false; + break; + } + + // Sand + switch (finder->sand) { + case embark_assist::defs::present_absent_ranges::NA: + break; // No restriction + + case embark_assist::defs::present_absent_ranges::Present: + if (tile->sand_count == 0) return false; + break; + case embark_assist::defs::present_absent_ranges::Absent: + if (tile->sand_count == 256) return false; + break; + } + + // Flux + switch (finder->flux) { + case embark_assist::defs::present_absent_ranges::NA: + break; // No restriction + + case embark_assist::defs::present_absent_ranges::Present: + if (tile->flux_count == 0) return false; + break; + case embark_assist::defs::present_absent_ranges::Absent: + if (tile->flux_count == 256) return false; + break; + } + + // Soil Min + switch (finder->soil_min) { + case embark_assist::defs::soil_ranges::NA: + case embark_assist::defs::soil_ranges::None: + break; // No restriction + + case embark_assist::defs::soil_ranges::Very_Shallow: + if (tile->max_region_soil < 1) return false; + break; + + case embark_assist::defs::soil_ranges::Shallow: + if (tile->max_region_soil < 2) return false; + break; + + case embark_assist::defs::soil_ranges::Deep: + if (tile->max_region_soil < 3) return false; + break; + + case embark_assist::defs::soil_ranges::Very_Deep: + if (tile->max_region_soil < 4) return false; + break; + } + + // soil_min_everywhere only applies on the detailed level + + // Soil Max + // Can't say anything as the preliminary data isn't reliable + + // Evil Weather + switch (finder->evil_weather) { + case embark_assist::defs::yes_no_ranges::NA: + break; // No restriction + + case embark_assist::defs::yes_no_ranges::Yes: + if (!tile->evil_weather_possible) return false; + break; + + case embark_assist::defs::yes_no_ranges::No: + if (tile->evil_weather_full) return false; + break; + } + + // Reanimating + switch (finder->reanimation) { + case embark_assist::defs::yes_no_ranges::NA: + break; // No restriction + + case embark_assist::defs::yes_no_ranges::Yes: + if (!tile->reanimating_possible) return false; + break; + + case embark_assist::defs::yes_no_ranges::No: + if (tile->reanimating_full) return false; + break; + } + + // Thralling + switch (finder->thralling) { + case embark_assist::defs::yes_no_ranges::NA: + break; // No restriction + + case embark_assist::defs::yes_no_ranges::Yes: + if (!tile->thralling_possible) return false; + break; + + case embark_assist::defs::yes_no_ranges::No: + if (tile->thralling_full) return false; + break; + } + + // Biome Count Min (Can't do anything with Max at this level) + if (finder->biome_count_min > tile->biome_count) return false; + + // Region Type 1 + if (finder->region_type_1 != -1) { + found = false; + + for (uint8_t k = 1; k < 10; k++) { + if (tile->biome_index[k] != -1) { + if (world_data->regions[tile->biome_index[k]]->type == finder->region_type_1) { + found = true; + break; + } + } + + if (found) break; + } + + if (!found) return false; + } + + // Region Type 2 + if (finder->region_type_2 != -1) { + found = false; + + for (uint8_t k = 1; k < 10; k++) { + if (tile->biome_index[k] != -1) { + if (world_data->regions[tile->biome_index[k]]->type == finder->region_type_2) { + found = true; + break; + } + } + + if (found) break; + } + + if (!found) return false; + } + + // Region Type 3 + if (finder->region_type_3 != -1) { + found = false; + + for (uint8_t k = 1; k < 10; k++) { + if (tile->biome_index[k] != -1) { + if (world_data->regions[tile->biome_index[k]]->type == finder->region_type_3) { + found = true; + break; + } + } + + if (found) break; + } + + if (!found) return false; + } + + // Biome 1 + if (finder->biome_1 != -1) { + found = false; + + for (uint8_t i = 1; i < 10; i++) { + if (tile->biome[i] == finder->biome_1) { + found = true; + break; + } + } + + if (!found) return false; + } + + // Biome 2 + if (finder->biome_2 != -1) { + found = false; + + for (uint8_t i = 1; i < 10; i++) { + if (tile->biome[i] == finder->biome_2) { + found = true; + break; + } + } + + if (!found) return false; + } + + // Biome 3 + if (finder->biome_3 != -1) { + found = false; + + for (uint8_t i = 1; i < 10; i++) { + if (tile->biome[i] == finder->biome_3) { + found = true; + break; + } + } + + if (!found) return false; + } + + if (finder->metal_1 != -1 || + finder->metal_2 != -1 || + finder->metal_3 != -1 || + finder->economic_1 != -1 || + finder->economic_2 != -1 || + finder->economic_3 != -1 || + finder->mineral_1 != -1 || + finder->mineral_2 != -1 || + finder->mineral_3 != -1) { + count = 0; + bool metal_1 = finder->metal_1 == -1; + bool metal_2 = finder->metal_2 == -1; + bool metal_3 = finder->metal_3 == -1; + bool economic_1 = finder->economic_1 == -1; + bool economic_2 = finder->economic_2 == -1; + bool economic_3 = finder->economic_3 == -1; + bool mineral_1 = finder->mineral_1 == -1; + bool mineral_2 = finder->mineral_2 == -1; + bool mineral_3 = finder->mineral_3 == -1; + + metal_1 = metal_1 || tile->metals[finder->metal_1]; + metal_2 = metal_2 || tile->metals[finder->metal_2]; + metal_3 = metal_3 || tile->metals[finder->metal_3]; + economic_1 = economic_1 || tile->economics[finder->economic_1]; + economic_2 = economic_2 || tile->economics[finder->economic_2]; + economic_3 = economic_3 || tile->economics[finder->economic_3]; + mineral_1 = mineral_1 || tile->minerals[finder->mineral_1]; + mineral_2 = mineral_2 || tile->minerals[finder->mineral_2]; + mineral_3 = mineral_3 || tile->minerals[finder->mineral_3]; + + if (!metal_1 || + !metal_2 || + !metal_3 || + !economic_1 || + !economic_2 || + !economic_3 || + !mineral_1 || + !mineral_2 || + !mineral_3) return false; + } + } + return true; + } + + //======================================================================================= + + uint32_t preliminary_world_match(embark_assist::defs::world_tile_data *survey_results, + embark_assist::defs::finders *finder, + embark_assist::defs::match_results *match_results) { + // color_ostream_proxy out(Core::getInstance().getConsole()); + uint32_t count = 0; + for (uint16_t i = 0; i < world->worldgen.worldgen_parms.dim_x; i++) { + for (uint16_t k = 0; k < world->worldgen.worldgen_parms.dim_y; k++) { + match_results->at(i).at(k).preliminary_match = + world_tile_match(survey_results, i, k, finder); + if (match_results->at(i).at(k).preliminary_match) count++; + match_results->at(i).at(k).contains_match = false; + } + } + + return count; + } + + //======================================================================================= + + void match_world_tile(embark_assist::defs::geo_data *geo_summary, + embark_assist::defs::world_tile_data *survey_results, + embark_assist::defs::finders *finder, + embark_assist::defs::match_results *match_results, + uint16_t x, + uint16_t y) { + +// color_ostream_proxy out(Core::getInstance().getConsole()); + embark_assist::defs::mid_level_tiles mlt; + + embark_assist::survey::survey_mid_level_tile(geo_summary, + survey_results, + &mlt); + + mid_level_tile_match(survey_results, + &mlt, + x, + y, + finder, + match_results); + } + } +} + +//======================================================================================= +// Visible operations +//======================================================================================= + +void embark_assist::matcher::move_cursor(uint16_t x, uint16_t y) { + // color_ostream_proxy out(Core::getInstance().getConsole()); + auto screen = Gui::getViewscreenByType(0); + uint16_t original_x = screen->location.region_pos.x; + uint16_t original_y = screen->location.region_pos.y; + + uint16_t large_x = std::abs(original_x - x) / 10; + uint16_t small_x = std::abs(original_x - x) % 10; + uint16_t large_y = std::abs(original_y - y) / 10; + uint16_t small_y = std::abs(original_y - y) % 10; + + while (large_x > 0 || large_y > 0) { + if (large_x > 0 && large_y > 0) { + if (original_x - x > 0 && original_y - y > 0) { + screen->feed_key(df::interface_key::CURSOR_UPLEFT_FAST); + } + else if (original_x - x > 0 && original_y - y < 0) { + screen->feed_key(df::interface_key::CURSOR_DOWNLEFT_FAST); + } + else if (original_y - y > 0) { + screen->feed_key(df::interface_key::CURSOR_UPRIGHT_FAST); + } + else { + screen->feed_key(df::interface_key::CURSOR_DOWNRIGHT_FAST); + } + large_x--; + large_y--; + } + else if (large_x > 0) { + if (original_x - x > 0) { + screen->feed_key(df::interface_key::CURSOR_LEFT_FAST); + } + else { + screen->feed_key(df::interface_key::CURSOR_RIGHT_FAST); + } + large_x--; + } + else { + if (original_y - y > 0) { + screen->feed_key(df::interface_key::CURSOR_UP_FAST); + } + else { + screen->feed_key(df::interface_key::CURSOR_DOWN_FAST); + } + large_y--; + } + } + + while (small_x > 0 || small_y > 0) { + if (small_x > 0 && small_y > 0) { + if (original_x - x > 0 && original_y - y > 0) { + screen->feed_key(df::interface_key::CURSOR_UPLEFT); + } + else if (original_x - x > 0 && original_y - y < 0) { + screen->feed_key(df::interface_key::CURSOR_DOWNLEFT); + } + else if (original_y - y > 0) { + screen->feed_key(df::interface_key::CURSOR_UPRIGHT); + } + else { + screen->feed_key(df::interface_key::CURSOR_DOWNRIGHT); + } + small_x--; + small_y--; + } + else if (small_x > 0) { + if (original_x - x > 0) { + screen->feed_key(df::interface_key::CURSOR_LEFT); + } + else { + screen->feed_key(df::interface_key::CURSOR_RIGHT); + } + small_x--; + } + else { + if (original_y - y > 0) { + screen->feed_key(df::interface_key::CURSOR_UP); + } + else { + screen->feed_key(df::interface_key::CURSOR_DOWN); + } + small_y--; + } + } +} + +//======================================================================================= + +uint16_t embark_assist::matcher::find(embark_assist::defs::match_iterators *iterator, + embark_assist::defs::geo_data *geo_summary, + embark_assist::defs::world_tile_data *survey_results, + embark_assist::defs::match_results *match_results) { + + color_ostream_proxy out(Core::getInstance().getConsole()); + auto screen = Gui::getViewscreenByType(0); + uint16_t x_end; + uint16_t y_end; + bool turn; + uint16_t count; + uint16_t preliminary_matches; + + if (!iterator->active) { + embark_assist::survey::clear_results(match_results); + + // Static check for impossible requirements + // + count = 0; + for (uint8_t i = 0; i < 3; i++) { + if (iterator->finder.evilness[i] == embark_assist::defs::evil_savagery_values::All) { + count++; + } + } + + if (count > 1) { + out.printerr("matcher::find: Will never find any due to multiple All evilness requirements\n"); + return 0; + } + + count = 0; + for (uint8_t i = 0; i < 3; i++) { + if (iterator->finder.savagery[i] == embark_assist::defs::evil_savagery_values::All) { + count++; + } + } + + if (count > 1) { + out.printerr("matcher::find: Will never find any due to multiple All savagery requirements\n"); + return 0; + } + + if (iterator->finder.max_river < iterator->finder.min_river && + iterator->finder.max_river != embark_assist::defs::river_ranges::NA) { + out.printerr("matcher::find: Will never find any due to max river < min river\n"); + return 0; + } + + if (iterator->finder.waterfall == embark_assist::defs::yes_no_ranges::Yes && + iterator->finder.max_river == embark_assist::defs::river_ranges::None) { + out.printerr("matcher::find: Will never find any waterfalls with None as max river\n"); + return 0; + } + + if (iterator->finder.soil_max < iterator->finder.soil_min && + iterator->finder.soil_max != embark_assist::defs::soil_ranges::NA) { + out.printerr("matcher::find: Will never find any matches with max soil < min soil\n"); + return 0; + } + + if (iterator->finder.biome_count_max < iterator->finder.biome_count_min && + iterator->finder.biome_count_max != -1) { + out.printerr("matcher::find: Will never find any matches with max biomes < min biomes\n"); + return 0; + } + + preliminary_matches = preliminary_world_match(survey_results, &iterator->finder, match_results); + + if (preliminary_matches == 0) { + out.printerr("matcher::find: Preliminarily matching world tiles: %i\n", preliminary_matches); + return 0; + } + else { + out.print("matcher::find: Preliminarily matching world tiles: %i\n", preliminary_matches); + } + + while (screen->location.region_pos.x != 0 || screen->location.region_pos.y != 0) { + screen->feed_key(df::interface_key::CURSOR_UPLEFT_FAST); + } + iterator->active = true; + iterator->i = 0; + iterator->k = 0; + iterator->x_right = true; + iterator->y_down = true; + iterator->inhibit_x_turn = false; + iterator->inhibit_y_turn = false; + iterator->count = 0; + } + + if ((iterator->k == world->worldgen.worldgen_parms.dim_x / 16 && iterator->x_right) || + (iterator->k == 0 && !iterator->x_right)) { + x_end = 0; + } + else { + x_end = 15; + } + + if (iterator->i == world->worldgen.worldgen_parms.dim_y / 16) { + y_end = 0; + } + else { + y_end = 15; + } + + for (uint16_t l = 0; l <= x_end; l++) { + for (uint16_t m = 0; m <= y_end; m++) { + // This is where the payload goes + if (match_results->at(screen->location.region_pos.x).at(screen->location.region_pos.y).preliminary_match) { + match_world_tile(geo_summary, + survey_results, + &iterator->finder, + match_results, + screen->location.region_pos.x, + screen->location.region_pos.y); + if (match_results->at(screen->location.region_pos.x).at(screen->location.region_pos.y).contains_match) { + iterator->count++; + } + } + else { + for (uint16_t n = 0; n < 16; n++) { + for (uint16_t p = 0; p < 16; p++) { + match_results->at(screen->location.region_pos.x).at(screen->location.region_pos.y).mlt_match[n][p] = false; + } + } + } + // End of payload section + + if (m != y_end) { + if (iterator->y_down) { + screen->feed_key(df::interface_key::CURSOR_DOWN); + } + else { + screen->feed_key(df::interface_key::CURSOR_UP); + } + } + else { + if (screen->location.region_pos.x != 0 && + screen->location.region_pos.x != world->worldgen.worldgen_parms.dim_x - 1) { + turn = true; + } + else { + iterator->inhibit_y_turn = !iterator->inhibit_y_turn; + turn = iterator->inhibit_y_turn; + } + + if (turn) { + iterator->y_down = !iterator->y_down; + } + else { + if (iterator->y_down) { + screen->feed_key(df::interface_key::CURSOR_DOWN); + } + else { + screen->feed_key(df::interface_key::CURSOR_UP); + } + } + } + } + + if (iterator->x_right) { // Won't do anything at the edge, so we don't bother filter those cases. + screen->feed_key(df::interface_key::CURSOR_RIGHT); + } + else { + screen->feed_key(df::interface_key::CURSOR_LEFT); + } + + if (!iterator->x_right && + screen->location.region_pos.x == 0) { + turn = !turn; + + if (turn) { + iterator->x_right = true; + } + } + else if (iterator->x_right && + screen->location.region_pos.x == world->worldgen.worldgen_parms.dim_x - 1) { + turn = !turn; + + if (turn) { + iterator->x_right = false; + } + } + } + // } + + iterator->k++; + if (iterator->k > world->worldgen.worldgen_parms.dim_x / 16) + { + iterator->k = 0; + iterator->i++; + iterator->active = !(iterator->i > world->worldgen.worldgen_parms.dim_y / 16); + + if (!iterator->active) { + embark_assist::matcher::move_cursor(iterator->x, iterator->y); + } + } + + return iterator->count; +} diff --git a/plugins/embark-assistant/matcher.h b/plugins/embark-assistant/matcher.h new file mode 100644 index 000000000..9887ffbd0 --- /dev/null +++ b/plugins/embark-assistant/matcher.h @@ -0,0 +1,21 @@ +#pragma once + +#include "DataDefs.h" + +#include "defs.h" + +using namespace DFHack; + +namespace embark_assist { + namespace matcher { + void move_cursor(uint16_t x, uint16_t y); + + // Used to iterate over the whole world to generate a map of world tiles + // that contain matching embarks. + // + uint16_t find(embark_assist::defs::match_iterators *iterator, + embark_assist::defs::geo_data *geo_summary, + embark_assist::defs::world_tile_data *survey_results, + embark_assist::defs::match_results *match_results); + } +} \ No newline at end of file diff --git a/plugins/embark-assistant/overlay.cpp b/plugins/embark-assistant/overlay.cpp new file mode 100644 index 000000000..6b63fa70d --- /dev/null +++ b/plugins/embark-assistant/overlay.cpp @@ -0,0 +1,439 @@ +#include + +#include "df/coord2d.h" +#include "df/inorganic_raw.h" +#include "df/dfhack_material_category.h" +#include "df/interface_key.h" +#include "df/viewscreen.h" +#include "df/viewscreen_choose_start_sitest.h" +#include "df/world.h" +#include "df/world_raws.h" + +#include "finder_ui.h" +#include "help_ui.h" +#include "overlay.h" +#include "screen.h" + +using df::global::world; + +namespace embark_assist { + namespace overlay { + DFHack::Plugin *plugin_self; + const Screen::Pen empty_pen = Screen::Pen('\0', COLOR_YELLOW, COLOR_BLACK, false); + const Screen::Pen yellow_x_pen = Screen::Pen('X', COLOR_BLACK, COLOR_YELLOW, false); + const Screen::Pen green_x_pen = Screen::Pen('X', COLOR_BLACK, COLOR_GREEN, false); + + struct display_strings { + Screen::Pen pen; + std::string text; + }; + + typedef Screen::Pen *pen_column; + + struct states { + int blink_count = 0; + bool show = true; + + bool matching = false; + bool match_active = false; + + embark_update_callbacks embark_update; + match_callbacks match_callback; + clear_match_callbacks clear_match_callback; + embark_assist::defs::find_callbacks find_callback; + shutdown_callbacks shutdown_callback; + + Screen::Pen site_grid[16][16]; + uint8_t current_site_grid = 0; + + std::vector embark_info; + + Screen::Pen region_match_grid[16][16]; + + pen_column *world_match_grid = nullptr; + uint16_t match_count = 0; + + uint16_t max_inorganic; + }; + + static states *state = nullptr; + + //==================================================================== + +/* // Attempt to replicate the DF logic for sizing the right world map. This + // code seems to compute the values correctly, but the author hasn't been + // able to apply them at the same time as DF does to 100%. + // DF seems to round down on 0.5 values. + df::coord2d world_dimension_size(uint16_t available_screen, uint16_t map_size) { + uint16_t result; + + for (uint16_t factor = 1; factor < 17; factor++) { + result = map_size / factor; + if ((map_size - result * factor) * 2 != factor) { + result = (map_size + factor / 2) / factor; + } + + if (result <= available_screen) { + return {result, factor}; + } + } + return{16, 16}; // Should never get here. + } +*/ + //==================================================================== + + class ViewscreenOverlay : public df::viewscreen_choose_start_sitest + { + public: + typedef df::viewscreen_choose_start_sitest interpose_base; + + void send_key(const df::interface_key &key) + { + std::set< df::interface_key > keys; + keys.insert(key); + this->feed(&keys); + } + + DEFINE_VMETHOD_INTERPOSE(void, feed, (std::set *input)) + { +// color_ostream_proxy out(Core::getInstance().getConsole()); + if (input->count(df::interface_key::CUSTOM_Q)) { + state->shutdown_callback(); + return; + + } + else if (input->count(df::interface_key::SETUP_LOCAL_X_MUP) || + input->count(df::interface_key::SETUP_LOCAL_X_MDOWN) || + input->count(df::interface_key::SETUP_LOCAL_Y_MUP) || + input->count(df::interface_key::SETUP_LOCAL_Y_MDOWN) || + input->count(df::interface_key::SETUP_LOCAL_X_UP) || + input->count(df::interface_key::SETUP_LOCAL_X_DOWN) || + input->count(df::interface_key::SETUP_LOCAL_Y_UP) || + input->count(df::interface_key::SETUP_LOCAL_Y_DOWN)) { + INTERPOSE_NEXT(feed)(input); + state->embark_update(); + } + else if (input->count(df::interface_key::CUSTOM_C)) { + state->match_active = false; + state->matching = false; + state->clear_match_callback(); + } + else if (input->count(df::interface_key::CUSTOM_F)) { + if (!state->match_active && !state->matching) { + embark_assist::finder_ui::init(embark_assist::overlay::plugin_self, state->find_callback, state->max_inorganic); + } + } + else if (input->count(df::interface_key::CUSTOM_I)) { + embark_assist::help_ui::init(embark_assist::overlay::plugin_self); + } + else { + INTERPOSE_NEXT(feed)(input); + } + } + + //==================================================================== + + DEFINE_VMETHOD_INTERPOSE(void, render, ()) + { + INTERPOSE_NEXT(render)(); +// color_ostream_proxy out(Core::getInstance().getConsole()); + auto current_screen = Gui::getViewscreenByType(0); + int16_t x = current_screen->location.region_pos.x; + int16_t y = current_screen->location.region_pos.y; + auto width = Screen::getWindowSize().x; + auto height = Screen::getWindowSize().y; + + state->blink_count++; + if (state->blink_count == 35) { + state->blink_count = 0; + state->show = !state->show; + } + + if (state->matching) state->show = true; + + Screen::drawBorder("Embark Assistant"); + + Screen::Pen pen_lr(' ', COLOR_LIGHTRED); + Screen::Pen pen_w(' ', COLOR_WHITE); + + Screen::paintString(pen_lr, width - 28, 20, "i", false); + Screen::paintString(pen_w, width - 27, 20, ":Embark Assistant Info", false); + Screen::paintString(pen_lr, width - 28, 21, "f", false); + Screen::paintString(pen_w, width - 27, 21, ":Find Embark ", false); + Screen::paintString(pen_lr, width - 28, 22, "c", false); + Screen::paintString(pen_w, width - 27, 22, ":Cancel/Clear Find", false); + Screen::paintString(pen_lr, width - 28, 23, "q", false); + Screen::paintString(pen_w, width - 27, 23, ":Quit Embark Assistant", false); + Screen::paintString(pen_w, width - 28, 25, "Matching World Tiles", false); + Screen::paintString(empty_pen, width - 7, 25, to_string(state->match_count), false); + + if (height > 25) { // Mask the vanilla DF find help as it's overridden. + Screen::paintString(pen_w, 50, height - 2, " ", false); + } + + for (uint8_t i = 0; i < 16; i++) { + for (uint8_t k = 0; k < 16; k++) { + if (state->site_grid[i][k].ch) { + Screen::paintTile(state->site_grid[i][k], i + 1, k + 2); + } + } + } + + for (auto i = 0; i < state->embark_info.size(); i++) { + embark_assist::screen::paintString(state->embark_info[i].pen, 1, i + 19, state->embark_info[i].text, false); + } + + if (state->show) { + int16_t left_x = x - (width / 2 - 7 - 18 + 1) / 2; + int16_t right_x; + int16_t top_y = y - (height - 8 - 2 + 1) / 2; + int16_t bottom_y; + + if (left_x < 0) { left_x = 0; } + + if (top_y < 0) { top_y = 0; } + + right_x = left_x + width / 2 - 7 - 18; + bottom_y = top_y + height - 8 - 2; + + if (right_x >= world->worldgen.worldgen_parms.dim_x) { + right_x = world->worldgen.worldgen_parms.dim_x - 1; + left_x = right_x - (width / 2 - 7 - 18); + } + + if (bottom_y >= world->worldgen.worldgen_parms.dim_y) { + bottom_y = world->worldgen.worldgen_parms.dim_y - 1; + top_y = bottom_y - (height - 8 - 2); + } + + if (left_x < 0) { left_x = 0; } + + if (top_y < 0) { top_y = 0; } + + + for (uint16_t i = left_x; i <= right_x; i++) { + for (uint16_t k = top_y; k <= bottom_y; k++) { + if (state->world_match_grid[i][k].ch) { + Screen::paintTile(state->world_match_grid[i][k], i - left_x + 18, k - top_y + 2); + } + } + } + + for (uint8_t i = 0; i < 16; i++) { + for (uint8_t k = 0; k < 16; k++) { + if (state->region_match_grid[i][k].ch) { + Screen::paintTile(state->region_match_grid[i][k], i + 1, k + 2); + } + } + } + +/* // Stuff for trying to replicate the DF right world map sizing logic. Close, but not there. + Screen::Pen pen(' ', COLOR_YELLOW); + // Boundaries of the top level world map + Screen::paintString(pen, width / 2 - 5, 2, "X", false); // Marks UL corner of right world map. Constant +// Screen::paintString(pen, width - 30, 2, "X", false); // Marks UR corner of right world map area. +// Screen::paintString(pen, width / 2 - 5, height - 8, "X", false); // BL corner of right world map area. +// Screen::paintString(pen, width - 30, height - 8, "X", false); // BR corner of right world map area. + + uint16_t l_width = width - 30 - (width / 2 - 5) + 1; // Horizontal space available for right world map. + uint16_t l_height = height - 8 - 2 + 1; // Vertical space available for right world map. + df::coord2d size_factor_x = world_dimension_size(l_width, world->worldgen.worldgen_parms.dim_x); + df::coord2d size_factor_y = world_dimension_size(l_height, world->worldgen.worldgen_parms.dim_y); + + Screen::paintString(pen, width / 2 - 5 + size_factor_x.x - 1, 2, "X", false); + Screen::paintString(pen, width / 2 - 5, 2 + size_factor_y.x - 1, "X", false); + Screen::paintString(pen, width / 2 - 5 + size_factor_x.x - 1, 2 + size_factor_y.x - 1, "X", false); + */ + } + + if (state->matching) { + embark_assist::overlay::state->match_callback(); + } + } + }; + + IMPLEMENT_VMETHOD_INTERPOSE(embark_assist::overlay::ViewscreenOverlay, feed); + IMPLEMENT_VMETHOD_INTERPOSE(embark_assist::overlay::ViewscreenOverlay, render); + } +} + +//==================================================================== + +bool embark_assist::overlay::setup(DFHack::Plugin *plugin_self, + embark_update_callbacks embark_update_callback, + match_callbacks match_callback, + clear_match_callbacks clear_match_callback, + embark_assist::defs::find_callbacks find_callback, + shutdown_callbacks shutdown_callback, + uint16_t max_inorganic) +{ +// color_ostream_proxy out(Core::getInstance().getConsole()); + state = new(states); + + embark_assist::overlay::plugin_self = plugin_self; + embark_assist::overlay::state->embark_update = embark_update_callback; + embark_assist::overlay::state->match_callback = match_callback; + embark_assist::overlay::state->clear_match_callback = clear_match_callback; + embark_assist::overlay::state->find_callback = find_callback; + embark_assist::overlay::state->shutdown_callback = shutdown_callback; + embark_assist::overlay::state->max_inorganic = max_inorganic; + embark_assist::overlay::state->match_active = false; + + state->world_match_grid = new pen_column[world->worldgen.worldgen_parms.dim_x]; + if (!state->world_match_grid) { + return false; // Out of memory + } + + for (uint16_t i = 0; i < world->worldgen.worldgen_parms.dim_x; i++) { + state->world_match_grid[i] = new Screen::Pen[world->worldgen.worldgen_parms.dim_y]; + if (!state->world_match_grid[i]) { // Out of memory. + return false; + } + } + + clear_match_results(); + + return INTERPOSE_HOOK(embark_assist::overlay::ViewscreenOverlay, feed).apply(true) && + INTERPOSE_HOOK(embark_assist::overlay::ViewscreenOverlay, render).apply(true); +} + +//==================================================================== + +void embark_assist::overlay::set_sites(embark_assist::defs::site_lists *site_list) { + for (uint8_t i = 0; i < 16; i++) { + for (uint8_t k = 0; k < 16; k++) { + state->site_grid[i][k] = empty_pen; + } + } + + for (uint16_t i = 0; i < site_list->size(); i++) { + state->site_grid[site_list->at(i).x][site_list->at(i).y].ch = site_list->at(i).type; + } +} + +//==================================================================== + +void embark_assist::overlay::initiate_match() { + embark_assist::overlay::state->matching = true; +} + +//==================================================================== + +void embark_assist::overlay::match_progress(uint16_t count, embark_assist::defs::match_results *match_results, bool done) { +// color_ostream_proxy out(Core::getInstance().getConsole()); + state->matching = !done; + state->match_count = count; + for (uint16_t i = 0; i < world->worldgen.worldgen_parms.dim_x; i++) { + for (uint16_t k = 0; k < world->worldgen.worldgen_parms.dim_y; k++) { + if (match_results->at(i).at(k).preliminary_match) { + state->world_match_grid[i][k] = yellow_x_pen; + + } else if (match_results->at(i).at(k).contains_match) { + state->world_match_grid[i][k] = green_x_pen; + } + else { + state->world_match_grid[i][k] = empty_pen; + } + } + } +} + +//==================================================================== + +void embark_assist::overlay::set_embark(embark_assist::defs::site_infos *site_info) { + state->embark_info.clear(); + + if (site_info->sand) { + state->embark_info.push_back({ Screen::Pen(' ', COLOR_YELLOW), "Sand" }); + } + + if (site_info->clay) { + state->embark_info.push_back({ Screen::Pen(' ', COLOR_RED), "Clay" }); + } + + state->embark_info.push_back({ Screen::Pen(' ', COLOR_BROWN), "Soil " + std::to_string(site_info->min_soil) + " - " + std::to_string(site_info->max_soil) }); + + if (site_info->flat) { + state->embark_info.push_back({ Screen::Pen(' ', COLOR_BROWN), "Flat" }); + } + + if (site_info->aquifer) { + if (site_info->aquifer_full) { + state->embark_info.push_back({ Screen::Pen(' ', COLOR_BLUE), "Aquifer" }); + + } + else { + state->embark_info.push_back({ Screen::Pen(' ', COLOR_LIGHTBLUE), "Aquifer" }); + } + } + + if (site_info->waterfall) { + state->embark_info.push_back({ Screen::Pen(' ', COLOR_BLUE), "Waterfall" }); + } + + if (site_info->flux) { + state->embark_info.push_back({ Screen::Pen(' ', COLOR_WHITE), "Flux" }); + } + + for (auto const& i : site_info->metals) { + state->embark_info.push_back({ Screen::Pen(' ', COLOR_GREY), world->raws.inorganics[i]->id }); + } + + for (auto const& i : site_info->economics) { + state->embark_info.push_back({ Screen::Pen(' ', COLOR_WHITE), world->raws.inorganics[i]->id }); + } +} + +//==================================================================== + +void embark_assist::overlay::set_mid_level_tile_match(embark_assist::defs::mlt_matches mlt_matches) { + for (uint8_t i = 0; i < 16; i++) { + for (uint8_t k = 0; k < 16; k++) { + if (mlt_matches[i][k]) { + state->region_match_grid[i][k] = green_x_pen; + + } + else { + state->region_match_grid[i][k] = empty_pen; + } + } + } +} + +//==================================================================== + +void embark_assist::overlay::clear_match_results() { + for (uint16_t i = 0; i < world->worldgen.worldgen_parms.dim_x; i++) { + for (uint16_t k = 0; k < world->worldgen.worldgen_parms.dim_y; k++) { + state->world_match_grid[i][k] = empty_pen; + } + } + + for (uint8_t i = 0; i < 16; i++) { + for (uint8_t k = 0; k < 16; k++) { + state->region_match_grid[i][k] = empty_pen; + } + } +} + +//==================================================================== + +void embark_assist::overlay::shutdown() { + if (state && + state->world_match_grid) { + INTERPOSE_HOOK(ViewscreenOverlay, render).remove(); + INTERPOSE_HOOK(ViewscreenOverlay, feed).remove(); + + for (uint16_t i = 0; i < world->worldgen.worldgen_parms.dim_x; i++) { + delete[] state->world_match_grid[i]; + } + + delete[] state->world_match_grid; + } + + if (state) { + state->embark_info.clear(); + delete state; + state = nullptr; + } +} diff --git a/plugins/embark-assistant/overlay.h b/plugins/embark-assistant/overlay.h new file mode 100644 index 000000000..d66d6d7fd --- /dev/null +++ b/plugins/embark-assistant/overlay.h @@ -0,0 +1,37 @@ +#pragma once + +#include +#include "PluginManager.h" + +#include "DataDefs.h" +#include "df/viewscreen_choose_start_sitest.h" + +#include "defs.h" + +using df::global::enabler; +using df::global::gps; + +namespace embark_assist { + namespace overlay { + typedef void(*embark_update_callbacks)(); + typedef void(*match_callbacks)(); + typedef void(*clear_match_callbacks)(); + typedef void(*shutdown_callbacks)(); + + bool setup(DFHack::Plugin *plugin_self, + embark_update_callbacks embark_update_callback, + match_callbacks match_callback, + clear_match_callbacks clear_match_callback, + embark_assist::defs::find_callbacks find_callback, + shutdown_callbacks shutdown_callback, + uint16_t max_inorganic); + + void set_sites(embark_assist::defs::site_lists *site_list); + void initiate_match(); + void match_progress(uint16_t count, embark_assist::defs::match_results *match_results, bool done); + void set_embark(embark_assist::defs::site_infos *site_info); + void set_mid_level_tile_match(embark_assist::defs::mlt_matches mlt_matches); + void clear_match_results(); + void shutdown(); + } +} \ No newline at end of file diff --git a/plugins/embark-assistant/screen.cpp b/plugins/embark-assistant/screen.cpp new file mode 100644 index 000000000..477e5c63c --- /dev/null +++ b/plugins/embark-assistant/screen.cpp @@ -0,0 +1,27 @@ +#include "screen.h" + +namespace embark_assist { + namespace screen { + } +} + +bool embark_assist::screen::paintString(const DFHack::Screen::Pen &pen, int x, int y, const std::string &text, bool map) { + auto screen_size = DFHack::Screen::getWindowSize(); + + if (y < 1 || y + 1 >= screen_size.y || x < 1) + { + return false; // Won't paint outside of the screen or on the frame + } + + if (x + text.length() - 1 < screen_size.x - 2) { + DFHack::Screen::paintString(pen, x, y, text, map); + } + else if (x < screen_size.x - 2) { + DFHack::Screen::paintString(pen, x, y, text.substr(0, screen_size.x - 2 - x + 1), map); + } + else { + return false; + } + + return true; +} diff --git a/plugins/embark-assistant/screen.h b/plugins/embark-assistant/screen.h new file mode 100644 index 000000000..06b723f24 --- /dev/null +++ b/plugins/embark-assistant/screen.h @@ -0,0 +1,7 @@ +#include "modules/Screen.h" + +namespace embark_assist { + namespace screen { + bool paintString(const DFHack::Screen::Pen &pen, int x, int y, const std::string &text, bool map = false); + } +} \ No newline at end of file diff --git a/plugins/embark-assistant/survey.cpp b/plugins/embark-assistant/survey.cpp new file mode 100644 index 000000000..6583d2479 --- /dev/null +++ b/plugins/embark-assistant/survey.cpp @@ -0,0 +1,1081 @@ +#include + +#include "Core.h" +#include +#include +#include + +#include +#include "modules/Materials.h" + +#include "DataDefs.h" +#include "df/coord2d.h" +#include "df/creature_interaction_effect.h" +#include "df/creature_interaction_effect_display_symbolst.h" +#include "df/creature_interaction_effect_type.h" +#include "df/feature_init.h" +#include "df/inorganic_flags.h" +#include "df/inorganic_raw.h" +#include "df/interaction.h" +#include "df/interaction_instance.h" +#include "df/interaction_source.h" +#include "df/interaction_source_regionst.h" +#include "df/interaction_source_type.h" +#include "df/interaction_target.h" +#include "df/interaction_target_corpsest.h" +#include "df/interaction_target_materialst.h" +#include "df/material_common.h" +#include "df/reaction.h" +#include "df/region_map_entry.h" +#include "df/syndrome.h" +#include "df/viewscreen.h" +#include "df/viewscreen_choose_start_sitest.h" +#include "df/world.h" +#include "df/world_data.h" +#include "df/world_geo_biome.h" +#include "df/world_geo_layer.h" +#include "df/world_raws.h" +#include "df/world_region.h" +#include "df/world_region_details.h" +#include "df/world_region_feature.h" +#include "df/world_river.h" +#include "df/world_site.h" +#include "df/world_site_type.h" +#include "df/world_underground_region.h" + +#include "biome_type.h" +#include "defs.h" +#include "survey.h" + +using namespace DFHack; +using namespace df::enums; +using namespace Gui; + +using df::global::world; + +namespace embark_assist { + namespace survey { + struct states { + uint16_t clay_reaction = -1; + uint16_t flux_reaction = -1; + uint16_t x; + uint16_t y; + uint8_t local_min_x; + uint8_t local_min_y; + uint8_t local_max_x; + uint8_t local_max_y; + uint16_t max_inorganic; + }; + + static states *state; + + //======================================================================================= + + bool geo_survey(embark_assist::defs::geo_data *geo_summary) { + color_ostream_proxy out(Core::getInstance().getConsole()); + df::world_data *world_data = world->world_data; + auto reactions = world->raws.reactions; + bool non_soil_found; + uint16_t size; + + for (uint16_t i = 0; i < reactions.size(); i++) { + if (reactions[i]->code == "MAKE_CLAY_BRICKS") { + state->clay_reaction = i; + } + + if (reactions[i]->code == "PIG_IRON_MAKING") { + state->flux_reaction = i; + } + } + + if (state->clay_reaction == -1) { + out.printerr("The reaction 'MAKE_CLAY_BRICKS' was not found, so clay can't be identified.\n"); + } + + if (state->flux_reaction == -1) { + out.printerr("The reaction 'PIG_IRON_MAKING' was not found, so flux can't be identified.\n"); + } + + for (uint16_t i = 0; i < world_data->geo_biomes.size(); i++) { + geo_summary->at(i).possible_metals.resize(state->max_inorganic); + geo_summary->at(i).possible_economics.resize(state->max_inorganic); + geo_summary->at(i).possible_minerals.resize(state->max_inorganic); + + non_soil_found = true; + df::world_geo_biome *geo = world_data->geo_biomes[i]; + + for (uint16_t k = 0; k < geo->layers.size() && k < 16; k++) { + df::world_geo_layer *layer = geo->layers[k]; + + if (layer->type == df::geo_layer_type::SOIL || + layer->type == df::geo_layer_type::SOIL_SAND) { + geo_summary->at(i).soil_size += layer->top_height - layer->bottom_height + 1; + + if (world->raws.inorganics[layer->mat_index]->flags.is_set(df::inorganic_flags::SOIL_SAND)) { + geo_summary->at(i).sand_absent = false; + } + + if (non_soil_found) { + geo_summary->at(i).top_soil_only = false; + } + } + else { + non_soil_found = true; + } + + geo_summary->at(i).possible_minerals[layer->mat_index] = true; + + size = (uint16_t)world->raws.inorganics[layer->mat_index]->metal_ore.mat_index.size(); + + for (uint16_t l = 0; l < size; l++) { + geo_summary->at(i).possible_metals.at(world->raws.inorganics[layer->mat_index]->metal_ore.mat_index[l]) = true; + } + + size = (uint16_t)world->raws.inorganics[layer->mat_index]->economic_uses.size(); + if (size != 0) { + geo_summary->at(i).possible_economics[layer->mat_index] = true; + + for (uint16_t l = 0; l < size; l++) { + if (world->raws.inorganics[layer->mat_index]->economic_uses[l] == state->clay_reaction) { + geo_summary->at(i).clay_absent = false; + } + + if (world->raws.inorganics[layer->mat_index]->economic_uses[l] == state->flux_reaction) { + geo_summary->at(i).flux_absent = false; + } + } + } + + size = (uint16_t)layer->vein_mat.size(); + + for (uint16_t l = 0; l < size; l++) { + auto vein = layer->vein_mat[l]; + geo_summary->at(i).possible_minerals[vein] = true; + + for (uint16_t m = 0; m < world->raws.inorganics[vein]->metal_ore.mat_index.size(); m++) { + geo_summary->at(i).possible_metals.at(world->raws.inorganics[vein]->metal_ore.mat_index[m]) = true; + } + + if (world->raws.inorganics[vein]->economic_uses.size() != 0) { + geo_summary->at(i).possible_economics[vein] = true; + + for (uint16_t m = 0; m < world->raws.inorganics[vein]->economic_uses.size(); m++) { + if (world->raws.inorganics[vein]->economic_uses[m] == state->clay_reaction) { + geo_summary->at(i).clay_absent = false; + } + + if (world->raws.inorganics[vein]->economic_uses[m] == state->flux_reaction) { + geo_summary->at(i).flux_absent = false; + } + } + } + } + + if (layer->bottom_height <= -3 && + world->raws.inorganics[layer->mat_index]->flags.is_set(df::inorganic_flags::AQUIFER)) { + geo_summary->at(i).aquifer_absent = false; + } + + if (non_soil_found == true) { + geo_summary->at(i).top_soil_aquifer_only = false; + } + } + } + return true; + } + + + //================================================================================= + + void survey_rivers(embark_assist::defs::world_tile_data *survey_results) { +// color_ostream_proxy out(Core::getInstance().getConsole()); + df::world_data *world_data = world->world_data; + int16_t x; + int16_t y; + + for (uint16_t i = 0; i < world_data->rivers.size(); i++) { + for (uint16_t k = 0; k < world_data->rivers[i]->path.x.size(); k++) { + x = world_data->rivers[i]->path.x[k]; + y = world_data->rivers[i]->path.y[k]; + + if (world_data->rivers[i]->flow[k] < 5000) { + if (world_data->region_map[x][y].flags.is_set(df::region_map_entry_flags::is_brook)) { + survey_results->at(x).at(y).river_size = embark_assist::defs::river_sizes::Brook; + } + else { + survey_results->at(x).at(y).river_size = embark_assist::defs::river_sizes::Stream; + } + } + else if (world_data->rivers[i]->flow[k] < 10000) { + survey_results->at(x).at(y).river_size = embark_assist::defs::river_sizes::Minor; + } + else if (world_data->rivers[i]->flow[k] < 20000) { + survey_results->at(x).at(y).river_size = embark_assist::defs::river_sizes::Medium; + } + else { + survey_results->at(x).at(y).river_size = embark_assist::defs::river_sizes::Major; + } + } + + x = world_data->rivers[i]->end_pos.x; + y = world_data->rivers[i]->end_pos.y; + + // Make the guess the river size for the end is the same as the tile next to the end. Note that DF + // doesn't actually recognize this tile as part of the river in the pre embark river name display. + // We also assume the is_river/is_brook flags are actually set properly for the end tile. + // + if (x >= 0 && y >= 0 && x < world->worldgen.worldgen_parms.dim_x && y < world->worldgen.worldgen_parms.dim_y) { + if (survey_results->at(x).at(y).river_size == embark_assist::defs::river_sizes::None) { + if (world_data->rivers[i]->path.x.size() && + world_data->rivers[i]->flow[world_data->rivers[i]->path.x.size() - 1] < 5000) { + if (world_data->region_map[x][y].flags.is_set(df::region_map_entry_flags::is_brook)) { + survey_results->at(x).at(y).river_size = embark_assist::defs::river_sizes::Brook; + } + else { + survey_results->at(x).at(y).river_size = embark_assist::defs::river_sizes::Stream; + } + } + else if (world_data->rivers[i]->flow[world_data->rivers[i]->path.x.size() - 1] < 10000) { + survey_results->at(x).at(y).river_size = embark_assist::defs::river_sizes::Minor; + } + else if (world_data->rivers[i]->flow[world_data->rivers[i]->path.x.size() - 1] < 20000) { + survey_results->at(x).at(y).river_size = embark_assist::defs::river_sizes::Medium; + } + else { + survey_results->at(x).at(y).river_size = embark_assist::defs::river_sizes::Major; + } + } + } + } + } + + //================================================================================= + + void survey_evil_weather(embark_assist::defs::world_tile_data *survey_results) { +// color_ostream_proxy out(Core::getInstance().getConsole()); + df::world_data *world_data = world->world_data; + + for (uint16_t i = 0; i < world->interaction_instances.all.size(); i++) { + auto interaction = world->raws.interactions[world->interaction_instances.all[i]->interaction_id]; + uint16_t region_index = world->interaction_instances.all[i]->region_index; + bool thralling = false; + bool reanimating = false; + + if (interaction->sources.size() && + interaction->sources[0]->getType() == df::interaction_source_type::REGION) { + for (uint16_t k = 0; k < interaction->targets.size(); k++) { + if (interaction->targets[k]->getType() == 0) { // Returns wrong type. Should be df::interaction_target_type::CORPSE + reanimating = true; + } + else if (interaction->targets[k]->getType() == 2) {// Returns wrong type.. Should be df::interaction_target_type::MATERIAL + df::interaction_target_materialst* material = virtual_cast(interaction->targets[k]); + if (material && DFHack::MaterialInfo(material->mat_type, material->mat_index).isInorganic()) { + for (uint16_t l = 0; l < world->raws.inorganics[material->mat_index]->material.syndrome.size(); l++) { + for (uint16_t m = 0; m < world->raws.inorganics[material->mat_index]->material.syndrome[l]->ce.size(); m++) { + if (world->raws.inorganics[material->mat_index]->material.syndrome[l]->ce[m]->getType() == df::creature_interaction_effect_type::FLASH_TILE) { + // Using this as a proxy. There seems to be a group of 4 effects for thralls: + // display symbol, flash symbol, phys att change and one more. + thralling = true; + } + } + } + } + } + } + } + + for (uint16_t k = 0; k < world_data->regions[region_index]->region_coords.size(); k++) { + survey_results->at(world_data->regions[region_index]->region_coords[k].x).at(world_data->regions[region_index]->region_coords[k].y).evil_weather[5] = true; + survey_results->at(world_data->regions[region_index]->region_coords[k].x).at(world_data->regions[region_index]->region_coords[k].y).reanimating[5] = reanimating; + survey_results->at(world_data->regions[region_index]->region_coords[k].x).at(world_data->regions[region_index]->region_coords[k].y).thralling[5] = thralling; + } + } + + for (uint16_t i = 0; i < world->worldgen.worldgen_parms.dim_x; i++) { + for (uint16_t k = 0; k < world->worldgen.worldgen_parms.dim_y; k++) { + survey_results->at(i).at(k).evil_weather_possible = false; + survey_results->at(i).at(k).reanimating_possible = false; + survey_results->at(i).at(k).thralling_possible = false; + survey_results->at(i).at(k).evil_weather_full = true; + survey_results->at(i).at(k).reanimating_full = true; + survey_results->at(i).at(k).thralling_full = true; + + for (uint8_t l = 1; l < 10; l++) { + if (survey_results->at(i).at(k).biome_index[l] != -1) { + df::coord2d adjusted = apply_offset(i, k, l); + survey_results->at(i).at(k).evil_weather[l] = survey_results->at(adjusted.x).at(adjusted.y).evil_weather[5]; + survey_results->at(i).at(k).reanimating[l] = survey_results->at(adjusted.x).at(adjusted.y).reanimating[5]; + survey_results->at(i).at(k).thralling[l] = survey_results->at(adjusted.x).at(adjusted.y).thralling[5]; + + if (survey_results->at(i).at(k).evil_weather[l]) { + survey_results->at(i).at(k).evil_weather_possible = true; + } + else { + survey_results->at(i).at(k).evil_weather_full = false; + } + + if (survey_results->at(i).at(k).reanimating[l]) { + survey_results->at(i).at(k).reanimating_possible = true; + } + else { + survey_results->at(i).at(k).reanimating_full = false; + } + + if (survey_results->at(i).at(k).thralling[l]) { + survey_results->at(i).at(k).thralling_possible = true; + } + else { + survey_results->at(i).at(k).thralling_full = false; + } + } + } + } + } + } + } +} + +//================================================================================= +// Exported operations +//================================================================================= + +void embark_assist::survey::setup(uint16_t max_inorganic) { + embark_assist::survey::state = new(embark_assist::survey::states); + embark_assist::survey::state->max_inorganic = max_inorganic; +} + +//================================================================================= + +df::coord2d embark_assist::survey::get_last_pos() { + return{ embark_assist::survey::state->x, embark_assist::survey::state->y }; +} + +//================================================================================= + +void embark_assist::survey::initiate(embark_assist::defs::mid_level_tiles *mlt) { + for (uint8_t i = 0; i < 16; i++) { + for (uint8_t k = 0; k < 16; k++) { + mlt->at(i).at(k).metals.resize(state->max_inorganic); + mlt->at(i).at(k).economics.resize(state->max_inorganic); + mlt->at(i).at(k).minerals.resize(state->max_inorganic); + } + } +} + +//================================================================================= + +void embark_assist::survey::clear_results(embark_assist::defs::match_results *match_results) { + for (uint16_t i = 0; i < world->worldgen.worldgen_parms.dim_x; i++) { + for (uint16_t k = 0; k < world->worldgen.worldgen_parms.dim_y; k++) { + match_results->at(i).at(k).preliminary_match = false; + match_results->at(i).at(k).contains_match = false; + + for (uint16_t l = 0; l < 16; l++) { + for (uint16_t m = 0; m < 16; m++) { + match_results->at(i).at(k).mlt_match[l][m] = false; + } + } + } + } +} + +//================================================================================= + +void embark_assist::survey::high_level_world_survey(embark_assist::defs::geo_data *geo_summary, + embark_assist::defs::world_tile_data *survey_results) { + // color_ostream_proxy out(Core::getInstance().getConsole()); + + embark_assist::survey::geo_survey(geo_summary); + for (uint16_t i = 0; i < world->worldgen.worldgen_parms.dim_x; i++) { + for (uint16_t k = 0; k < world->worldgen.worldgen_parms.dim_y; k++) { + df::coord2d adjusted; + df::world_data *world_data = world->world_data; + uint16_t geo_index; + uint16_t sav_ev; + uint8_t offset_count = 0; + survey_results->at(i).at(k).surveyed = false; + survey_results->at(i).at(k).aquifer_count = 0; + survey_results->at(i).at(k).clay_count = 0; + survey_results->at(i).at(k).sand_count = 0; + survey_results->at(i).at(k).flux_count = 0; + survey_results->at(i).at(k).min_region_soil = 10; + survey_results->at(i).at(k).max_region_soil = 0; + survey_results->at(i).at(k).waterfall = false; + survey_results->at(i).at(k).savagery_count[0] = 0; + survey_results->at(i).at(k).savagery_count[1] = 0; + survey_results->at(i).at(k).savagery_count[2] = 0; + survey_results->at(i).at(k).evilness_count[0] = 0; + survey_results->at(i).at(k).evilness_count[1] = 0; + survey_results->at(i).at(k).evilness_count[2] = 0; + survey_results->at(i).at(k).metals.resize(state->max_inorganic); + survey_results->at(i).at(k).economics.resize(state->max_inorganic); + survey_results->at(i).at(k).minerals.resize(state->max_inorganic); + // Evil weather and rivers are handled in later operations. Should probably be merged into one. + + for (uint8_t l = 1; l < 10; l++) + { + adjusted = apply_offset(i, k, l); + if (adjusted.x != i || adjusted.y != k || l == 5) { + offset_count++; + + survey_results->at(i).at(k).biome_index[l] = world_data->region_map[adjusted.x][adjusted.y].region_id; + survey_results->at(i).at(k).biome[l] = get_biome_type(adjusted.x, adjusted.y, k); + geo_index = world_data->region_map[adjusted.x][adjusted.y].geo_index; + + if (!geo_summary->at(geo_index).aquifer_absent) survey_results->at(i).at(k).aquifer_count++; + if (!geo_summary->at(geo_index).clay_absent) survey_results->at(i).at(k).clay_count++; + if (!geo_summary->at(geo_index).sand_absent) survey_results->at(i).at(k).sand_count++; + if (!geo_summary->at(geo_index).flux_absent) survey_results->at(i).at(k).flux_count++; + + if (geo_summary->at(geo_index).soil_size < survey_results->at(i).at(k).min_region_soil) + survey_results->at(i).at(k).min_region_soil = geo_summary->at(geo_index).soil_size; + + if (geo_summary->at(geo_index).soil_size > survey_results->at(i).at(k).max_region_soil) + survey_results->at(i).at(k).max_region_soil = geo_summary->at(geo_index).soil_size; + + sav_ev = world_data->region_map[adjusted.x][adjusted.y].savagery / 33; + if (sav_ev == 3) sav_ev = 2; + survey_results->at(i).at(k).savagery_count[sav_ev]++; + + sav_ev = world_data->region_map[adjusted.x][adjusted.y].evilness / 33; + if (sav_ev == 3) sav_ev = 2; + survey_results->at(i).at(k).evilness_count[sav_ev]++; + + for (uint16_t m = 0; m < state->max_inorganic; m++) { + if (geo_summary->at(geo_index).possible_metals[m]) survey_results->at(i).at(k).metals[m] = true; + if (geo_summary->at(geo_index).possible_economics[m]) survey_results->at(i).at(k).economics[m] = true; + if (geo_summary->at(geo_index).possible_minerals[m]) survey_results->at(i).at(k).minerals[m] = true; + } + } + else { + survey_results->at(i).at(k).biome_index[l] = -1; + survey_results->at(i).at(k).biome[l] = -1; + } + } + + survey_results->at(i).at(k).biome_count = 0; + for (uint8_t l = 1; l < 10; l++) { + if (survey_results->at(i).at(k).biome[l] != -1) survey_results->at(i).at(k).biome_count++; + } + + if (survey_results->at(i).at(k).aquifer_count == offset_count) survey_results->at(i).at(k).aquifer_count = 256; + if (survey_results->at(i).at(k).clay_count == offset_count) survey_results->at(i).at(k).clay_count = 256; + if (survey_results->at(i).at(k).sand_count == offset_count) survey_results->at(i).at(k).sand_count = 256; + if (survey_results->at(i).at(k).flux_count == offset_count) survey_results->at(i).at(k).flux_count = 256; + for (uint8_t l = 0; l < 3; l++) { + if (survey_results->at(i).at(k).savagery_count[l] == offset_count) survey_results->at(i).at(k).savagery_count[l] = 256; + if (survey_results->at(i).at(k).evilness_count[l] == offset_count) survey_results->at(i).at(k).evilness_count[l] = 256; + } + } + } + + embark_assist::survey::survey_rivers(survey_results); + embark_assist::survey::survey_evil_weather(survey_results); +} + +//================================================================================= + +void embark_assist::survey::survey_mid_level_tile(embark_assist::defs::geo_data *geo_summary, + embark_assist::defs::world_tile_data *survey_results, + embark_assist::defs::mid_level_tiles *mlt) { + // color_ostream_proxy out(Core::getInstance().getConsole()); + auto screen = Gui::getViewscreenByType(0); + int16_t x = screen->location.region_pos.x; + int16_t y = screen->location.region_pos.y; + embark_assist::defs::region_tile_datum *tile = &survey_results->at(x).at(y); + int8_t max_soil_depth; + int8_t offset; + int16_t elevation; + int16_t last_bottom; + int16_t top_z; + int16_t base_z; + int16_t min_z = 0; // Initialized to silence warning about potential usage of uninitialized data. + int16_t bottom_z; + df::coord2d adjusted; + df::world_data *world_data = world->world_data; + df::world_region_details *details = world_data->region_details[0]; + df::region_map_entry *world_tile = &world_data->region_map[x][y]; + std::vector features; + uint8_t soil_erosion; + uint16_t end_check_l; + uint16_t end_check_m; + uint16_t end_check_n; + + for (uint16_t i = 0; i < state->max_inorganic; i++) { + tile->metals[i] = 0; + tile->economics[i] = 0; + tile->minerals[i] = 0; + } + + for (uint8_t i = 0; i < 16; i++) { + for (uint8_t k = 0; k < 16; k++) { + mlt->at(i).at(k).metals.resize(state->max_inorganic); + mlt->at(i).at(k).economics.resize(state->max_inorganic); + mlt->at(i).at(k).minerals.resize(state->max_inorganic); + } + } + + for (uint8_t i = 1; i < 10; i++) survey_results->at(x).at(y).biome_index[i] = -1; + + for (uint8_t i = 0; i < 16; i++) { + for (uint8_t k = 0; k < 16; k++) { + max_soil_depth = -1; + + offset = details->biome[i][k]; + adjusted = apply_offset(x, y, offset); + + if (adjusted.x != x || adjusted.y != y) + { + mlt->at(i).at(k).biome_offset = offset; + } + else + { + mlt->at(i).at(k).biome_offset = 5; + }; + + survey_results->at(x).at(y).biome_index[mlt->at(i).at(k).biome_offset] = + world_data->region_map[adjusted.x][adjusted.y].region_id; + + mlt->at(i).at(k).savagery_level = world_data->region_map[adjusted.x][adjusted.y].savagery / 33; + if (mlt->at(i).at(k).savagery_level == 3) { + mlt->at(i).at(k).savagery_level = 2; + } + mlt->at(i).at(k).evilness_level = world_data->region_map[adjusted.x][adjusted.y].evilness / 33; + if (mlt->at(i).at(k).evilness_level == 3) { + mlt->at(i).at(k).evilness_level = 2; + } + + elevation = details->elevation[i][k]; + + // Special biome adjustments + if (!world_data->region_map[adjusted.x][adjusted.y].flags.is_set(region_map_entry_flags::is_lake)) { + if (world_data->region_map[adjusted.x][adjusted.y].elevation >= 150) { // Mountain + max_soil_depth = 0; + + } + else if (world_data->region_map[adjusted.x][adjusted.y].elevation < 100) { // Ocean + if (elevation == 99) { + elevation = 98; + } + + if ((world_data->geo_biomes[world_data->region_map[x][y].geo_index]->unk1 == 4 || + world_data->geo_biomes[world_data->region_map[x][y].geo_index]->unk1 == 5) && + details->unk12e8 < 500) { + max_soil_depth = 0; + } + } + } + + base_z = elevation - 1; + features = details->features[i][k]; + std::map layer_bottom, layer_top; + + end_check_l = static_cast(features.size()); + for (size_t l = 0; l < end_check_l; l++) { + auto feature = features[l]; + + if (feature->layer != -1 && + feature->min_z != -30000) { + auto layer = world_data->underground_regions[feature->layer]; + + layer_bottom[layer->layer_depth] = feature->min_z; + layer_top[layer->layer_depth] = feature->max_z; + base_z = std::min((int)base_z, (int)feature->min_z); + + if (layer->type == df::world_underground_region::MagmaSea) { + min_z = feature->min_z; // The features are individual per region tile + break; + } + } + } + + // Compute shifts for layers in the stack. + + if (max_soil_depth == -1) { // Not set to zero by the biome + max_soil_depth = std::max((154 - elevation) / 5, 1); + } + + soil_erosion = geo_summary->at(world_data->region_map[adjusted.x][adjusted.y].geo_index).soil_size - + std::min((int)geo_summary->at(world_data->region_map[adjusted.x][adjusted.y].geo_index).soil_size, (int)max_soil_depth); + int16_t layer_shift[16]; + int16_t cur_shift = elevation + soil_erosion - 1; + + mlt->at(i).at(k).aquifer = false; + mlt->at(i).at(k).clay = false; + mlt->at(i).at(k).sand = false; + mlt->at(i).at(k).flux = false; + if (max_soil_depth == 0) { + mlt->at(i).at(k).soil_depth = 0; + } + else { + mlt->at(i).at(k).soil_depth = geo_summary->at(world_data->region_map[adjusted.x][adjusted.y].geo_index).soil_size - soil_erosion; + } + mlt->at(i).at(k).offset = offset; + mlt->at(i).at(k).elevation = details->elevation[i][k]; + mlt->at(i).at(k).river_present = false; + mlt->at(i).at(k).river_elevation = 100; + + if (details->rivers_vertical.active[i][k] == 1) { + mlt->at(i).at(k).river_present = true; + mlt->at(i).at(k).river_elevation = details->rivers_vertical.elevation[i][k]; + } + else if (details->rivers_horizontal.active[i][k] == 1) { + mlt->at(i).at(k).river_present = true; + mlt->at(i).at(k).river_elevation = details->rivers_horizontal.elevation[i][k]; + } + + if (tile->min_region_soil > mlt->at(i).at(k).soil_depth) { + tile->min_region_soil = mlt->at(i).at(k).soil_depth; + } + + if (tile->max_region_soil < mlt->at(i).at(k).soil_depth) { + tile->max_region_soil = mlt->at(i).at(k).soil_depth; + } + + end_check_l = static_cast(world_data->geo_biomes[world_data->region_map[adjusted.x][adjusted.y].geo_index]->layers.size()); + if (end_check_l > 16) end_check_l = 16; + + for (uint16_t l = 0; l < end_check_l; l++) { + auto layer = world_data->geo_biomes[world_data->region_map[adjusted.x][adjusted.y].geo_index]->layers[l]; + layer_shift[l] = cur_shift; + + if (layer->type == df::geo_layer_type::SOIL || + layer->type == df::geo_layer_type::SOIL_SAND) { + int16_t size = layer->top_height - layer->bottom_height - 1; + // Comment copied from prospector.cpp(like the logic)... + // This is to replicate the behavior of a probable bug in the + // map generation code : if a layer is partially eroded, the + // removed levels are in fact transferred to the layer below, + // because unlike the case of removing the whole layer, the code + // does not execute a loop to shift the lower part of the stack up. + if (size > soil_erosion) { + cur_shift = cur_shift - soil_erosion; + } + + soil_erosion -= std::min((int)soil_erosion, (int)size); + } + } + + last_bottom = elevation; + // Don't have to set up the end_check as we can reuse the one above. + + for (uint16_t l = 0; l < end_check_l; l++) { + auto layer = world_data->geo_biomes[world_data->region_map[adjusted.x][adjusted.y].geo_index]->layers[l]; + top_z = last_bottom - 1; + bottom_z = std::max((int)layer->bottom_height + layer_shift[l], (int)min_z); + + if (l == 15) { + bottom_z = min_z; // stretch layer if needed + } + + if (top_z >= bottom_z) { + mlt->at(i).at(k).minerals[layer->mat_index] = true; + + end_check_m = static_cast(world->raws.inorganics[layer->mat_index]->metal_ore.mat_index.size()); + + for (uint16_t m = 0; m < end_check_m; m++) { + mlt->at(i).at(k).metals[world->raws.inorganics[layer->mat_index]->metal_ore.mat_index[m]] = true; + } + + if (layer->type == df::geo_layer_type::SOIL || + layer->type == df::geo_layer_type::SOIL_SAND) { + if (world->raws.inorganics[layer->mat_index]->flags.is_set(df::inorganic_flags::SOIL_SAND)) { + mlt->at(i).at(k).sand = true; + } + } + + if (world->raws.inorganics[layer->mat_index]->economic_uses.size() > 0) { + mlt->at(i).at(k).economics[layer->mat_index] = true; + + end_check_m = static_cast(world->raws.inorganics[layer->mat_index]->economic_uses.size()); + for (uint16_t m = 0; m < end_check_m; m++) { + if (world->raws.inorganics[layer->mat_index]->economic_uses[m] == state->clay_reaction) { + mlt->at(i).at(k).clay = true; + } + + else if (world->raws.inorganics[layer->mat_index]->economic_uses[m] == state->flux_reaction) { + mlt->at(i).at(k).flux = true; + } + } + } + + end_check_m = static_cast(layer->vein_mat.size()); + + for (uint16_t m = 0; m < end_check_m; m++) { + mlt->at(i).at(k).minerals[layer->vein_mat[m]] = true; + + end_check_n = static_cast(world->raws.inorganics[layer->vein_mat[m]]->metal_ore.mat_index.size()); + + for (uint16_t n = 0; n < end_check_n; n++) { + mlt->at(i).at(k).metals[world->raws.inorganics[layer->vein_mat[m]]->metal_ore.mat_index[n]] = true; + } + + if (world->raws.inorganics[layer->vein_mat[m]]->economic_uses.size() > 0) { + mlt->at(i).at(k).economics[layer->vein_mat[m]] = true; + + end_check_n = static_cast(world->raws.inorganics[layer->vein_mat[m]]->economic_uses.size()); + for (uint16_t n = 0; n < end_check_n; n++) { + if (world->raws.inorganics[layer->vein_mat[m]]->economic_uses[n] == state->clay_reaction) { + mlt->at(i).at(k).clay = true; + } + + else if (world->raws.inorganics[layer->vein_mat[m]]->economic_uses[n] == state->flux_reaction) { + mlt->at(i).at(k).flux = true; + } + } + } + } + + if (bottom_z <= elevation - 3 && + world->raws.inorganics[layer->mat_index]->flags.is_set(df::inorganic_flags::AQUIFER)) { + mlt->at(i).at(k).aquifer = true; + } + } + } + } + } + + survey_results->at(x).at(y).aquifer_count = 0; + survey_results->at(x).at(y).clay_count = 0; + survey_results->at(x).at(y).sand_count = 0; + survey_results->at(x).at(y).flux_count = 0; + survey_results->at(x).at(y).min_region_soil = 10; + survey_results->at(x).at(y).max_region_soil = 0; + survey_results->at(x).at(y).savagery_count[0] = 0; + survey_results->at(x).at(y).savagery_count[1] = 0; + survey_results->at(x).at(y).savagery_count[2] = 0; + survey_results->at(x).at(y).evilness_count[0] = 0; + survey_results->at(x).at(y).evilness_count[1] = 0; + survey_results->at(x).at(y).evilness_count[2] = 0; + + bool river_elevation_found = false; + int16_t river_elevation; + + for (uint8_t i = 0; i < 16; i++) { + for (uint8_t k = 0; k < 16; k++) { + if (mlt->at(i).at(k).aquifer) { survey_results->at(x).at(y).aquifer_count++; } + if (mlt->at(i).at(k).clay) { survey_results->at(x).at(y).clay_count++; } + if (mlt->at(i).at(k).sand) { survey_results->at(x).at(y).sand_count++; } + if (mlt->at(i).at(k).flux) { survey_results->at(x).at(y).flux_count++; } + + if (mlt->at(i).at(k).soil_depth < survey_results->at(x).at(y).min_region_soil) { + survey_results->at(x).at(y).min_region_soil = mlt->at(i).at(k).soil_depth; + } + + if (mlt->at(i).at(k).soil_depth > survey_results->at(x).at(y).max_region_soil) { + survey_results->at(x).at(y).max_region_soil = mlt->at(i).at(k).soil_depth; + } + + if (mlt->at(i).at(k).river_present) { + if (river_elevation_found) { + if (mlt->at(i).at(k).river_elevation != river_elevation) + { + survey_results->at(x).at(y).waterfall = true; + } + } + else { + river_elevation_found = true; + river_elevation = mlt->at(i).at(k).river_elevation; + } + } + + // River size surveyed separately + // biome_index handled above + // biome handled below + // evil weather handled separately + // reanimating handled separately + // thralling handled separately + + survey_results->at(x).at(y).savagery_count[mlt->at(i).at(k).savagery_level]++; + survey_results->at(x).at(y).evilness_count[mlt->at(i).at(k).evilness_level]++; + + for (uint16_t l = 0; l < state->max_inorganic; l++) { + if (mlt->at(i).at(k).metals[l]) { survey_results->at(x).at(y).metals[l] = true; } + if (mlt->at(i).at(k).economics[l]) { survey_results->at(x).at(y).economics[l] = true; } + if (mlt->at(i).at(k).minerals[l]) { survey_results->at(x).at(y).minerals[l] = true; } + } + } + } + + for (uint8_t i = 1; i < 10; i++) { + if (survey_results->at(x).at(y).biome_index[i] == -1) { + survey_results->at(x).at(y).biome[i] = -1; + } + } + + bool biomes[ENUM_LAST_ITEM(biome_type) + 1]; + for (uint8_t i = 0; i <= ENUM_LAST_ITEM(biome_type); i++) { + biomes[i] = false; + } + + for (uint8_t i = 1; i < 10; i++) + { + if (survey_results->at(x).at(y).biome[i] != -1) { + biomes[survey_results->at(x).at(y).biome[i]] = true; + } + } + int count = 0; + for (uint8_t i = 0; i <= ENUM_LAST_ITEM(biome_type); i++) { + if (biomes[i]) count++; + } + + tile->biome_count = count; + tile->surveyed = true; +} +//================================================================================= + +df::coord2d embark_assist::survey::apply_offset(uint16_t x, uint16_t y, int8_t offset) { + df::coord2d result; + result.x = x; + result.y = y; + + switch (offset) { + case 1: + result.x--; + result.y++; + break; + + case 2: + result.y++; + break; + + case 3: + result.x++; + result.y++; + break; + + case 4: + result.x--; + break; + + case 5: + break; // Center. No change + + case 6: + result.x++; + break; + + case 7: + result.x--; + result.y--; + break; + + case 8: + result.y--; + break; + + case 9: + result.x++; + result.y--; + break; + + default: + // Bug. Just act as if it's the center... + break; + } + + if (result.x < 0) { + result.x = 0; + } + else if (result.x >= world->worldgen.worldgen_parms.dim_x) { + result.x = world->worldgen.worldgen_parms.dim_x - 1; + } + + if (result.y < 0) { + result.y = 0; + } + else if (result.y >= world->worldgen.worldgen_parms.dim_y) { + result.y = world->worldgen.worldgen_parms.dim_y - 1; + } + + return result; +} + +//================================================================================= + +void embark_assist::survey::survey_region_sites(embark_assist::defs::site_lists *site_list) { + // color_ostream_proxy out(Core::getInstance().getConsole()); + auto screen = Gui::getViewscreenByType(0); + df::world_data *world_data = world->world_data; + int8_t index = 0; + + site_list->clear(); + + for (uint32_t i = 0; i < world_data->region_map[screen->location.region_pos.x][screen->location.region_pos.y].sites.size(); i++) { + auto site = world_data->region_map[screen->location.region_pos.x][screen->location.region_pos.y].sites[i]; + switch (site->type) { + case df::world_site_type::PlayerFortress: + case df::world_site_type::DarkFortress: + case df::world_site_type::MountainHalls: + case df::world_site_type::ForestRetreat: + case df::world_site_type::Town: + case df::world_site_type::Fortress: + break; // Already visible + + case df::world_site_type::Cave: + if (!world->worldgen.worldgen_parms.all_caves_visible) { + site_list->push_back({ (uint8_t)site->rgn_min_x , (uint8_t)site->rgn_min_y, 'c' }); // Cave + } + break; + + case df::world_site_type::Monument: + if (site->subtype_info->lair_type != -1 || + site->subtype_info->is_monument == 0) { // Not Tomb, which is visible already + } + else if (site->subtype_info->lair_type == -1) { + site_list->push_back({ (uint8_t)site->rgn_min_x , (uint8_t)site->rgn_min_y, 'V' }); // Vault + } + else { + site_list->push_back({ (uint8_t)site->rgn_min_x , (uint8_t)site->rgn_min_y, 'M' }); // Any other Monument type. Pyramid? + } + break; + + case df::world_site_type::ImportantLocation: + site_list->push_back({ (uint8_t)site->rgn_min_x , (uint8_t)site->rgn_min_y, 'i' }); // Don't really know what that is... + break; + + case df::world_site_type::LairShrine: + if (site->subtype_info->lair_type == 0 || + site->subtype_info->lair_type == 1 || + site->subtype_info->lair_type == 4) { // Only Rocs seen. Mountain lair? + site_list->push_back({ (uint8_t)site->rgn_min_x , (uint8_t)site->rgn_min_y, 'l' }); // Lair + } + else if (site->subtype_info->lair_type == 2) { + site_list->push_back({ (uint8_t)site->rgn_min_x , (uint8_t)site->rgn_min_y, 'L' }); // Labyrinth + } + else if (site->subtype_info->lair_type == 3) { + site_list->push_back({ (uint8_t)site->rgn_min_x , (uint8_t)site->rgn_min_y, 'S' }); // Shrine + } + else { + site_list->push_back({ (uint8_t)site->rgn_min_x , (uint8_t)site->rgn_min_y, '?' }); // Can these exist? + } + break; + + case df::world_site_type::Camp: + site_list->push_back({ (uint8_t)site->rgn_min_x , (uint8_t)site->rgn_min_y, 'C' }); // Camp + break; + + default: + site_list->push_back({ (uint8_t)site->rgn_min_x , (uint8_t)site->rgn_min_y, '!' }); // Not even in the enum... + break; + } + } +} + +//================================================================================= + +void embark_assist::survey::survey_embark(embark_assist::defs::mid_level_tiles *mlt, + embark_assist::defs::site_infos *site_info, + bool use_cache) { + + // color_ostream_proxy out(Core::getInstance().getConsole()); + auto screen = Gui::getViewscreenByType(0); + int16_t elevation; + uint16_t x = screen->location.region_pos.x; + uint16_t y = screen->location.region_pos.y; + bool river_found = false; + int16_t river_elevation; + std::vector metals(state->max_inorganic); + std::vector economics(state->max_inorganic); + std::vector minerals(state->max_inorganic); + + if (!use_cache) { // For some reason DF scrambles these values on world tile movements (at least in Lua...). + state->local_min_x = screen->location.embark_pos_min.x; + state->local_min_y = screen->location.embark_pos_min.y; + state->local_max_x = screen->location.embark_pos_max.x; + state->local_max_y = screen->location.embark_pos_max.y; + } + + state->x = x; + state->y = y; + + site_info->aquifer = false; + site_info->aquifer_full = true; + site_info->min_soil = 10; + site_info->max_soil = 0; + site_info->flat = true; + site_info->waterfall = false; + site_info->clay = false; + site_info->sand = false; + site_info->flux = false; + site_info->metals.clear(); + site_info->economics.clear(); + site_info->metals.clear(); + + for (uint8_t i = state->local_min_x; i <= state->local_max_x; i++) { + for (uint8_t k = state->local_min_y; k <= state->local_max_y; k++) { + if (mlt->at(i).at(k).aquifer) { + site_info->aquifer = true; + } + else { + site_info->aquifer_full = false; + } + + if (mlt->at(i).at(k).soil_depth < site_info->min_soil) { + site_info->min_soil = mlt->at(i).at(k).soil_depth; + } + + if (mlt->at(i).at(k).soil_depth > site_info->max_soil) { + site_info->max_soil = mlt->at(i).at(k).soil_depth; + } + + if (i == state->local_min_x && k == state->local_min_y) { + elevation = mlt->at(i).at(k).elevation; + + } + else if (elevation != mlt->at(i).at(k).elevation) { + site_info->flat = false; + } + + if (mlt->at(i).at(k).river_present) { + if (river_found) { + if (river_elevation != mlt->at(i).at(k).river_elevation) { + site_info->waterfall = true; + } + } + else { + river_elevation = mlt->at(i).at(k).river_elevation; + river_found = true; + } + } + + if (mlt->at(i).at(k).clay) { + site_info->clay = true; + } + + if (mlt->at(i).at(k).sand) { + site_info->sand = true; + } + + if (mlt->at(i).at(k).flux) { + site_info->flux = true; + } + + for (uint16_t l = 0; l < state->max_inorganic; l++) { + metals[l] = metals[l] || mlt->at(i).at(k).metals[l]; + economics[l] = economics[l] || mlt->at(i).at(k).economics[l]; + minerals[l] = minerals[l] || mlt->at(i).at(k).minerals[l]; + } + } + } + for (uint16_t l = 0; l < state->max_inorganic; l++) { + if (metals[l]) { + site_info->metals.push_back(l); + } + + if (economics[l]) { + site_info->economics.push_back(l); + } + + if (minerals[l]) { + site_info->minerals.push_back(l); + } + } +} + +//================================================================================= + +void embark_assist::survey::shutdown() { + delete state; +} + diff --git a/plugins/embark-assistant/survey.h b/plugins/embark-assistant/survey.h new file mode 100644 index 000000000..03d2d9a03 --- /dev/null +++ b/plugins/embark-assistant/survey.h @@ -0,0 +1,38 @@ +#pragma once +#include + +#include "DataDefs.h" +#include "df/coord2d.h" + +#include "defs.h" + +using namespace DFHack; + +namespace embark_assist { + namespace survey { + void setup(uint16_t max_inorganic); + + df::coord2d get_last_pos(); + + void initiate(embark_assist::defs::mid_level_tiles *mlt); + + void clear_results(embark_assist::defs::match_results *match_results); + + void high_level_world_survey(embark_assist::defs::geo_data *geo_summary, + embark_assist::defs::world_tile_data *survey_results); + + void survey_mid_level_tile(embark_assist::defs::geo_data *geo_summary, + embark_assist::defs::world_tile_data *survey_results, + embark_assist::defs::mid_level_tiles *mlt); + + df::coord2d apply_offset(uint16_t x, uint16_t y, int8_t offset); + + void survey_region_sites(embark_assist::defs::site_lists *site_list); + + void survey_embark(embark_assist::defs::mid_level_tiles *mlt, + embark_assist::defs::site_infos *site_info, + bool use_cache); + + void shutdown(); + } +} \ No newline at end of file diff --git a/plugins/tweak/tweak.cpp b/plugins/tweak/tweak.cpp index 252f4c738..6b5b1f687 100644 --- a/plugins/tweak/tweak.cpp +++ b/plugins/tweak/tweak.cpp @@ -98,6 +98,7 @@ #include "tweaks/kitchen-prefs-empty.h" #include "tweaks/max-wheelbarrow.h" #include "tweaks/military-assign.h" +#include "tweaks/pausing-fps-counter.h" #include "tweaks/nestbox-color.h" #include "tweaks/shift-8-scroll.h" #include "tweaks/stable-cursor.h" @@ -232,6 +233,9 @@ DFhackCExport command_result plugin_init (color_ostream &out, std::vector display_frames == 1); + } + return init_have_fps_yes; + } + + DEFINE_VMETHOD_INTERPOSE(void, render, ()) + { + INTERPOSE_NEXT(render)(); + + if (!df::global::pause_state || !df::global::enabler || !df::global::world + || !df::global::gps || !df::global::pause_state) + return; + + // if init.txt does not have [FPS:YES] then dont show this FPS counter + if (!dwarfmode_pausing_fps_counter_hook::init_have_fps_yes()) + return; + + static bool prev_paused = true; + static uint32_t prev_clock = 0; + static int32_t prev_frames = 0; + static uint32_t elapsed_clock = 0; + static uint32_t elapsed_frames = 0; + static double history[history_length]; + + if (prev_clock == 0) + { + // init + for (int i = 0; i < history_length; i++) + history[i] = 0.0; + } + + // disable default FPS counter because it is rendered on top of this FPS counter. + if (df::global::gps->display_frames == 1) + df::global::gps->display_frames = 0; + + if (*df::global::pause_state) + prev_paused = true; + else + { + uint32_t clock = df::global::enabler->clock; + int32_t frames = df::global::world->frame_counter; + + if (!prev_paused && prev_clock != 0 + && clock >= prev_clock && frames >= prev_frames) + { + // if we were previously paused, then dont add clock/frames, + // but wait for the next time render is called. + elapsed_clock += clock - prev_clock; + elapsed_frames += frames - prev_frames; + } + + prev_paused = false; + prev_clock = clock; + prev_frames = frames; + + // add FPS to history every second or after at least one frame. + if (elapsed_clock >= 1000 && elapsed_frames >= 1) + { + double fps = elapsed_frames / (elapsed_clock / 1000.0); + for (int i = history_length - 1; i >= 1; i--) + history[i] = history[i - 1]; + history[0] = fps; + + elapsed_clock = 0; + elapsed_frames = 0; + } + } + + // average fps over a few seconds to stabilize the counter. + double fps_sum = 0.0; + int fps_count = 0; + for (int i = 0; i < history_length; i++) + { + if (history[i] > 0.0) + { + fps_sum += history[i]; + fps_count++; + } + } + + double fps = fps_count == 0 ? 1.0 : fps_sum / fps_count; + double gfps = df::global::enabler->calculated_gfps; + + std::stringstream fps_counter; + fps_counter << "FPS:" + << setw(4) << fixed << setprecision(fps >= 1.0 ? 0 : 2) << fps + << " (" << gfps << ")"; + + // show this FPS counter same as the default counter. + int x = 10; + int y = 0; + OutputString(COLOR_WHITE, x, y, fps_counter.str(), + false, 0, COLOR_CYAN, false); + } +}; + +struct title_pausing_fps_counter_hook : df::viewscreen_titlest { + typedef df::viewscreen_titlest interpose_base; + + DEFINE_VMETHOD_INTERPOSE(void, render, ()) + { + INTERPOSE_NEXT(render)(); + + // if init.txt have FPS:YES then enable default FPS counter when exiting dwarf mode. + // So it is enabled if starting adventure mode without exiting dwarf fortress. + if (dwarfmode_pausing_fps_counter_hook::init_have_fps_yes() + && df::global::gps && df::global::gps->display_frames == 0) + df::global::gps->display_frames = 1; + } +}; + + +IMPLEMENT_VMETHOD_INTERPOSE(dwarfmode_pausing_fps_counter_hook, render); + +IMPLEMENT_VMETHOD_INTERPOSE(title_pausing_fps_counter_hook, render); diff --git a/scripts b/scripts index 8d079a591..28a1ccc98 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 8d079a59122d9ba72ce9c0f7687402a343d09bc7 +Subproject commit 28a1ccc9883d85106fa3b0ed59ddd57fcfc3f5c7 diff --git a/test/main.lua b/test/main.lua new file mode 100644 index 000000000..0085a9711 --- /dev/null +++ b/test/main.lua @@ -0,0 +1,10 @@ +function set_test_stage(stage) + local f = io.open('test_stage.txt', 'w') + f:write(stage) + f:close() +end + +print('running tests') + +set_test_stage('done') +dfhack.run_command('die') diff --git a/travis/dfhack_travis.init b/travis/dfhack_travis.init new file mode 100644 index 000000000..d9bc3e5ba --- /dev/null +++ b/travis/dfhack_travis.init @@ -0,0 +1,2 @@ +:lua dfhack.internal.addScriptPath(os.getenv('TRAVIS_BUILD_DIR')) +test/main diff --git a/travis/download-df.sh b/travis/download-df.sh new file mode 100755 index 000000000..20dc3dbd4 --- /dev/null +++ b/travis/download-df.sh @@ -0,0 +1,40 @@ +#!/bin/sh + +tardest="df.tar.bz2" + +which md5sum && alias md5=md5sum +selfmd5=$(openssl md5 < "$0") +echo $selfmd5 + +cd "$(dirname "$0")" +echo "DF_VERSION: $DF_VERSION" +echo "DF_FOLDER: $DF_FOLDER" +mkdir -p "$DF_FOLDER" +cd "$DF_FOLDER" + +if [ -f receipt ]; then + if [ "$selfmd5" != "$(cat receipt)" ]; then + echo "download-df.sh changed; removing DF" + else + echo "Already downloaded $DF_VERSION" + exit 0 + fi +fi + +rm -rif "$tardest" df_linux + +minor=$(echo "$DF_VERSION" | cut -d. -f2) +patch=$(echo "$DF_VERSION" | cut -d. -f3) +url="http://www.bay12games.com/dwarves/df_${minor}_${patch}_linux.tar.bz2" + +echo Downloading +wget "$url" -O "$tardest" +echo Extracting +tar xf "$tardest" --strip-components=1 +echo Changing settings +echo '' >> "$DF_FOLDER/data/init/init.txt" +echo '[PRINT_MODE:TEXT]' >> "$DF_FOLDER/data/init/init.txt" +echo '[SOUND:NO]' >> "$DF_FOLDER/data/init/init.txt" +echo Done + +echo "$selfmd5" > receipt diff --git a/travis/get-df-version.sh b/travis/get-df-version.sh new file mode 100755 index 000000000..13d317d2e --- /dev/null +++ b/travis/get-df-version.sh @@ -0,0 +1,4 @@ +#!/bin/sh +cd "$(dirname "$0")" +cd .. +grep DF_VERSION CMakeLists.txt | perl -ne 'print "$&\n" if /[\d\.]+/' diff --git a/travis/run-tests.py b/travis/run-tests.py new file mode 100644 index 000000000..a8265088c --- /dev/null +++ b/travis/run-tests.py @@ -0,0 +1,30 @@ +import os, subprocess, sys + +MAX_TRIES = 5 + +dfhack = 'Dwarf Fortress.exe' if sys.platform == 'win32' else './dfhack' +test_stage = 'test_stage.txt' + +def get_test_stage(): + if os.path.isfile(test_stage): + return open(test_stage).read().strip() + return '0' + +os.chdir(sys.argv[1]) +if os.path.exists(test_stage): + os.remove(test_stage) + +tries = 0 +while True: + tries += 1 + stage = get_test_stage() + print('Run #%i: stage=%s' % (tries, get_test_stage())) + if stage == 'done': + print('Done!') + os.remove(test_stage) + sys.exit(0) + if tries > MAX_TRIES: + print('Too many tries - aborting') + sys.exit(1) + + os.system(dfhack)