Merge branch 'master' of git://github.com/quietust/dfhack
commit
5b0f37276f
@ -0,0 +1,249 @@
|
||||
/*
|
||||
https://github.com/peterix/dfhack
|
||||
Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com)
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
#include "Internal.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
|
||||
#include "MemAccess.h"
|
||||
#include "Core.h"
|
||||
#include "VersionInfo.h"
|
||||
#include "VTableInterpose.h"
|
||||
|
||||
#include "MiscUtils.h"
|
||||
|
||||
using namespace DFHack;
|
||||
|
||||
/*
|
||||
* Code for accessing method pointers directly. Very compiler-specific.
|
||||
*/
|
||||
|
||||
#if defined(_MSC_VER)
|
||||
|
||||
struct MSVC_MPTR {
|
||||
void *method;
|
||||
intptr_t this_shift;
|
||||
};
|
||||
|
||||
bool DFHack::is_vmethod_pointer_(void *pptr)
|
||||
{
|
||||
auto pobj = (MSVC_MPTR*)pptr;
|
||||
if (!pobj->method) return false;
|
||||
|
||||
// MSVC implements pointers to vmethods via thunks.
|
||||
// This expects that they all follow a very specific pattern.
|
||||
auto pval = (unsigned*)pobj->method;
|
||||
switch (pval[0]) {
|
||||
case 0x20FF018BU: // mov eax, [ecx]; jmp [eax]
|
||||
case 0x60FF018BU: // mov eax, [ecx]; jmp [eax+0x??]
|
||||
case 0xA0FF018BU: // mov eax, [ecx]; jmp [eax+0x????????]
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
int DFHack::vmethod_pointer_to_idx_(void *pptr)
|
||||
{
|
||||
auto pobj = (MSVC_MPTR*)pptr;
|
||||
if (!pobj->method || pobj->this_shift != 0) return -1;
|
||||
|
||||
auto pval = (unsigned*)pobj->method;
|
||||
switch (pval[0]) {
|
||||
case 0x20FF018BU: // mov eax, [ecx]; jmp [eax]
|
||||
return 0;
|
||||
case 0x60FF018BU: // mov eax, [ecx]; jmp [eax+0x??]
|
||||
return ((int8_t)pval[1])/sizeof(void*);
|
||||
case 0xA0FF018BU: // mov eax, [ecx]; jmp [eax+0x????????]
|
||||
return ((int32_t)pval[1])/sizeof(void*);
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
void* DFHack::method_pointer_to_addr_(void *pptr)
|
||||
{
|
||||
if (is_vmethod_pointer_(pptr)) return NULL;
|
||||
auto pobj = (MSVC_MPTR*)pptr;
|
||||
return pobj->method;
|
||||
}
|
||||
|
||||
void DFHack::addr_to_method_pointer_(void *pptr, void *addr)
|
||||
{
|
||||
auto pobj = (MSVC_MPTR*)pptr;
|
||||
pobj->method = addr;
|
||||
pobj->this_shift = 0;
|
||||
}
|
||||
|
||||
#elif defined(__GXX_ABI_VERSION)
|
||||
|
||||
struct GCC_MPTR {
|
||||
intptr_t method;
|
||||
intptr_t this_shift;
|
||||
};
|
||||
|
||||
bool DFHack::is_vmethod_pointer_(void *pptr)
|
||||
{
|
||||
auto pobj = (GCC_MPTR*)pptr;
|
||||
return (pobj->method & 1) != 0;
|
||||
}
|
||||
|
||||
int DFHack::vmethod_pointer_to_idx_(void *pptr)
|
||||
{
|
||||
auto pobj = (GCC_MPTR*)pptr;
|
||||
if ((pobj->method & 1) == 0 || pobj->this_shift != 0)
|
||||
return -1;
|
||||
return (pobj->method-1)/sizeof(void*);
|
||||
}
|
||||
|
||||
void* DFHack::method_pointer_to_addr_(void *pptr)
|
||||
{
|
||||
auto pobj = (GCC_MPTR*)pptr;
|
||||
if ((pobj->method & 1) != 0 || pobj->this_shift != 0)
|
||||
return NULL;
|
||||
return (void*)pobj->method;
|
||||
}
|
||||
|
||||
void DFHack::addr_to_method_pointer_(void *pptr, void *addr)
|
||||
{
|
||||
auto pobj = (GCC_MPTR*)pptr;
|
||||
pobj->method = (intptr_t)addr;
|
||||
pobj->this_shift = 0;
|
||||
}
|
||||
|
||||
#else
|
||||
#error Unknown compiler type
|
||||
#endif
|
||||
|
||||
void *virtual_identity::get_vmethod_ptr(int idx)
|
||||
{
|
||||
assert(idx >= 0);
|
||||
void **vtable = (void**)vtable_ptr;
|
||||
if (!vtable) return NULL;
|
||||
return vtable[idx];
|
||||
}
|
||||
|
||||
bool virtual_identity::set_vmethod_ptr(int idx, void *ptr)
|
||||
{
|
||||
assert(idx >= 0);
|
||||
void **vtable = (void**)vtable_ptr;
|
||||
if (!vtable) return NULL;
|
||||
return Core::getInstance().p->patchMemory(&vtable[idx], &ptr, sizeof(void*));
|
||||
}
|
||||
|
||||
void VMethodInterposeLinkBase::set_chain(void *chain)
|
||||
{
|
||||
saved_chain = chain;
|
||||
addr_to_method_pointer_(chain_mptr, chain);
|
||||
}
|
||||
|
||||
VMethodInterposeLinkBase::VMethodInterposeLinkBase(virtual_identity *host, int vmethod_idx, void *interpose_method, void *chain_mptr)
|
||||
: host(host), vmethod_idx(vmethod_idx), interpose_method(interpose_method), chain_mptr(chain_mptr),
|
||||
saved_chain(NULL), next(NULL), prev(NULL)
|
||||
{
|
||||
if (vmethod_idx < 0 || interpose_method == NULL)
|
||||
{
|
||||
fprintf(stderr, "Bad VMethodInterposeLinkBase arguments: %d %08x\n",
|
||||
vmethod_idx, unsigned(interpose_method));
|
||||
fflush(stderr);
|
||||
abort();
|
||||
}
|
||||
}
|
||||
|
||||
VMethodInterposeLinkBase::~VMethodInterposeLinkBase()
|
||||
{
|
||||
if (is_applied())
|
||||
remove();
|
||||
}
|
||||
|
||||
bool VMethodInterposeLinkBase::apply()
|
||||
{
|
||||
if (is_applied())
|
||||
return true;
|
||||
if (!host->vtable_ptr)
|
||||
return false;
|
||||
|
||||
// Retrieve the current vtable entry
|
||||
void *old_ptr = host->get_vmethod_ptr(vmethod_idx);
|
||||
assert(old_ptr != NULL);
|
||||
|
||||
// Check if there are other interpose entries for the same slot
|
||||
VMethodInterposeLinkBase *old_link = NULL;
|
||||
|
||||
for (int i = host->interpose_list.size()-1; i >= 0; i--)
|
||||
{
|
||||
if (host->interpose_list[i]->vmethod_idx != vmethod_idx)
|
||||
continue;
|
||||
|
||||
old_link = host->interpose_list[i];
|
||||
assert(old_link->next == NULL && old_ptr == old_link->interpose_method);
|
||||
break;
|
||||
}
|
||||
|
||||
// Apply the new method ptr
|
||||
if (!host->set_vmethod_ptr(vmethod_idx, interpose_method))
|
||||
return false;
|
||||
|
||||
set_chain(old_ptr);
|
||||
host->interpose_list.push_back(this);
|
||||
|
||||
// Link into the chain if any
|
||||
if (old_link)
|
||||
{
|
||||
old_link->next = this;
|
||||
prev = old_link;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void VMethodInterposeLinkBase::remove()
|
||||
{
|
||||
if (!is_applied())
|
||||
return;
|
||||
|
||||
// Remove from the list in the identity
|
||||
for (int i = host->interpose_list.size()-1; i >= 0; i--)
|
||||
if (host->interpose_list[i] == this)
|
||||
vector_erase_at(host->interpose_list, i);
|
||||
|
||||
// Remove from the chain
|
||||
if (prev)
|
||||
prev->next = next;
|
||||
|
||||
if (next)
|
||||
{
|
||||
next->set_chain(saved_chain);
|
||||
next->prev = prev;
|
||||
}
|
||||
else
|
||||
{
|
||||
host->set_vmethod_ptr(vmethod_idx, saved_chain);
|
||||
}
|
||||
|
||||
prev = next = NULL;
|
||||
set_chain(NULL);
|
||||
}
|
@ -0,0 +1,173 @@
|
||||
/*
|
||||
https://github.com/peterix/dfhack
|
||||
Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com)
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "DataFuncs.h"
|
||||
|
||||
namespace DFHack
|
||||
{
|
||||
template<bool> struct StaticAssert;
|
||||
template<> struct StaticAssert<true> {};
|
||||
|
||||
#define STATIC_ASSERT(condition) { StaticAssert<(condition)>(); }
|
||||
|
||||
/* Wrapping around compiler-specific representation of pointers to methods. */
|
||||
|
||||
#if defined(_MSC_VER)
|
||||
#define METHOD_POINTER_SIZE (sizeof(void*)*2)
|
||||
#elif defined(__GXX_ABI_VERSION)
|
||||
#define METHOD_POINTER_SIZE (sizeof(void*)*2)
|
||||
#else
|
||||
#error Unknown compiler type
|
||||
#endif
|
||||
|
||||
#define ASSERT_METHOD_POINTER(type) \
|
||||
STATIC_ASSERT(df::return_type<type>::is_method && sizeof(type)==METHOD_POINTER_SIZE);
|
||||
|
||||
DFHACK_EXPORT bool is_vmethod_pointer_(void*);
|
||||
DFHACK_EXPORT int vmethod_pointer_to_idx_(void*);
|
||||
DFHACK_EXPORT void* method_pointer_to_addr_(void*);
|
||||
DFHACK_EXPORT void addr_to_method_pointer_(void*,void*);
|
||||
|
||||
template<class T> bool is_vmethod_pointer(T ptr) {
|
||||
ASSERT_METHOD_POINTER(T);
|
||||
return is_vmethod_pointer_(&ptr);
|
||||
}
|
||||
template<class T> int vmethod_pointer_to_idx(T ptr) {
|
||||
ASSERT_METHOD_POINTER(T);
|
||||
return vmethod_pointer_to_idx_(&ptr);
|
||||
}
|
||||
template<class T> void *method_pointer_to_addr(T ptr) {
|
||||
ASSERT_METHOD_POINTER(T);
|
||||
return method_pointer_to_addr_(&ptr);
|
||||
}
|
||||
template<class T> T addr_to_method_pointer(void *addr) {
|
||||
ASSERT_METHOD_POINTER(T);
|
||||
T rv;
|
||||
addr_to_method_pointer_(&rv, addr);
|
||||
return rv;
|
||||
}
|
||||
|
||||
/* Access to vmethod pointers from the vtable. */
|
||||
|
||||
template<class P>
|
||||
P virtual_identity::get_vmethod_ptr(P selector)
|
||||
{
|
||||
typedef typename df::return_type<P>::class_type host_class;
|
||||
virtual_identity &identity = host_class::_identity;
|
||||
int idx = vmethod_pointer_to_idx(selector);
|
||||
return addr_to_method_pointer<P>(identity.get_vmethod_ptr(idx));
|
||||
}
|
||||
|
||||
/* VMethod interpose API.
|
||||
|
||||
This API allows replacing an entry in the original vtable
|
||||
with code defined by DFHack, while retaining ability to
|
||||
call the original code. The API can be safely used from
|
||||
plugins, and multiple hooks for the same vmethod are
|
||||
automatically chained in undefined order.
|
||||
|
||||
Usage:
|
||||
|
||||
struct my_hack : df::someclass {
|
||||
typedef df::someclass interpose_base;
|
||||
|
||||
DEFINE_VMETHOD_INTERPOSE(void, foo, (int arg)) {
|
||||
// If needed by the code, claim the suspend lock.
|
||||
// DO NOT USE THE USUAL CoreSuspender, OR IT WILL DEADLOCK!
|
||||
// CoreSuspendClaimer suspend;
|
||||
...
|
||||
INTERPOSE_NEXT(foo)(arg) // call the original
|
||||
...
|
||||
}
|
||||
};
|
||||
|
||||
IMPLEMENT_VMETHOD_INTERPOSE(my_hack, foo);
|
||||
|
||||
void init() {
|
||||
if (!INTERPOSE_HOOK(my_hack, foo).apply())
|
||||
error();
|
||||
}
|
||||
|
||||
void shutdown() {
|
||||
INTERPOSE_HOOK(my_hack, foo).remove();
|
||||
}
|
||||
*/
|
||||
|
||||
#define DEFINE_VMETHOD_INTERPOSE(rtype, name, args) \
|
||||
typedef rtype (interpose_base::*interpose_ptr_##name)args; \
|
||||
static DFHack::VMethodInterposeLink<interpose_base,interpose_ptr_##name> interpose_##name; \
|
||||
rtype interpose_fn_##name args
|
||||
|
||||
#define IMPLEMENT_VMETHOD_INTERPOSE(class,name) \
|
||||
DFHack::VMethodInterposeLink<class::interpose_base,class::interpose_ptr_##name> \
|
||||
class::interpose_##name(&class::interpose_base::name, &class::interpose_fn_##name);
|
||||
|
||||
#define INTERPOSE_NEXT(name) (this->*interpose_##name.chain)
|
||||
#define INTERPOSE_HOOK(class, name) (class::interpose_##name)
|
||||
|
||||
class DFHACK_EXPORT VMethodInterposeLinkBase {
|
||||
/*
|
||||
These link objects try to:
|
||||
1) Allow multiple hooks into the same vmethod
|
||||
2) Auto-remove hooks when a plugin is unloaded.
|
||||
*/
|
||||
|
||||
virtual_identity *host; // Class with the vtable
|
||||
int vmethod_idx;
|
||||
void *interpose_method; // Pointer to the code of the interposing method
|
||||
void *chain_mptr; // Pointer to the chain field below
|
||||
|
||||
void *saved_chain; // Previous pointer to the code
|
||||
VMethodInterposeLinkBase *next, *prev; // Other hooks for the same method
|
||||
|
||||
void set_chain(void *chain);
|
||||
public:
|
||||
VMethodInterposeLinkBase(virtual_identity *host, int vmethod_idx, void *interpose_method, void *chain_mptr);
|
||||
~VMethodInterposeLinkBase();
|
||||
|
||||
bool is_applied() { return saved_chain != NULL; }
|
||||
bool apply();
|
||||
void remove();
|
||||
};
|
||||
|
||||
template<class Base, class Ptr>
|
||||
class VMethodInterposeLink : public VMethodInterposeLinkBase {
|
||||
public:
|
||||
Ptr chain;
|
||||
|
||||
operator Ptr () { return chain; }
|
||||
|
||||
template<class Ptr2>
|
||||
VMethodInterposeLink(Ptr target, Ptr2 src)
|
||||
: VMethodInterposeLinkBase(
|
||||
&Base::_identity,
|
||||
vmethod_pointer_to_idx(target),
|
||||
method_pointer_to_addr(src),
|
||||
&chain
|
||||
)
|
||||
{ src = target; /* check compatibility */ }
|
||||
};
|
||||
}
|
@ -0,0 +1,170 @@
|
||||
/*
|
||||
https://github.com/peterix/dfhack
|
||||
Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com)
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "Export.h"
|
||||
#include "Module.h"
|
||||
#include "BitArray.h"
|
||||
#include "ColorText.h"
|
||||
#include <string>
|
||||
|
||||
#include "DataDefs.h"
|
||||
#include "df/graphic.h"
|
||||
#include "df/viewscreen.h"
|
||||
|
||||
/**
|
||||
* \defgroup grp_screen utilities for painting to the screen
|
||||
* @ingroup grp_screen
|
||||
*/
|
||||
|
||||
namespace DFHack
|
||||
{
|
||||
class Core;
|
||||
|
||||
/**
|
||||
* The Screen module
|
||||
* \ingroup grp_modules
|
||||
* \ingroup grp_screen
|
||||
*/
|
||||
namespace Screen
|
||||
{
|
||||
/// Data structure describing all properties a screen tile can have
|
||||
struct Pen {
|
||||
// Ordinary text symbol
|
||||
char ch;
|
||||
int8_t fg, bg;
|
||||
bool bold;
|
||||
|
||||
// Graphics tile
|
||||
int tile;
|
||||
enum TileMode {
|
||||
AsIs, // Tile colors used without modification
|
||||
CharColor, // The fg/bg pair is used
|
||||
TileColor // The fields below are used
|
||||
} tile_mode;
|
||||
int8_t tile_fg, tile_bg;
|
||||
|
||||
Pen(char ch = 0, int8_t fg = 7, int8_t bg = 0, int tile = 0, bool color_tile = false)
|
||||
: ch(ch), fg(fg&7), bg(bg), bold(!!(fg&8)),
|
||||
tile(tile), tile_mode(color_tile ? CharColor : AsIs), tile_fg(0), tile_bg(0)
|
||||
{}
|
||||
Pen(char ch, int8_t fg, int8_t bg, bool bold, int tile = 0, bool color_tile = false)
|
||||
: ch(ch), fg(fg), bg(bg), bold(bold),
|
||||
tile(tile), tile_mode(color_tile ? CharColor : AsIs), tile_fg(0), tile_bg(0)
|
||||
{}
|
||||
Pen(char ch, int8_t fg, int8_t bg, int tile, int8_t tile_fg, int8_t tile_bg)
|
||||
: ch(ch), fg(fg&7), bg(bg), bold(!!(fg&8)),
|
||||
tile(tile), tile_mode(TileColor), tile_fg(tile_fg), tile_bg(tile_bg)
|
||||
{}
|
||||
Pen(char ch, int8_t fg, int8_t bg, bool bold, int tile, int8_t tile_fg, int8_t tile_bg)
|
||||
: ch(ch), fg(fg), bg(bg), bold(bold),
|
||||
tile(tile), tile_mode(TileColor), tile_fg(tile_fg), tile_bg(tile_bg)
|
||||
{}
|
||||
};
|
||||
|
||||
DFHACK_EXPORT df::coord2d getMousePos();
|
||||
DFHACK_EXPORT df::coord2d getWindowSize();
|
||||
|
||||
/// Returns the state of [GRAPHICS:YES/NO]
|
||||
DFHACK_EXPORT bool inGraphicsMode();
|
||||
|
||||
/// Paint one screen tile with the given pen
|
||||
DFHACK_EXPORT bool paintTile(const Pen &pen, int x, int y);
|
||||
|
||||
/// Paint a string onto the screen. Ignores ch and tile of pen.
|
||||
DFHACK_EXPORT bool paintString(const Pen &pen, int x, int y, const std::string &text);
|
||||
|
||||
/// Fills a rectangle with one pen. Possibly more efficient than a loop over paintTile.
|
||||
DFHACK_EXPORT bool fillRect(const Pen &pen, int x1, int y1, int x2, int y2);
|
||||
|
||||
/// Draws a standard dark gray window border with a title string
|
||||
DFHACK_EXPORT bool drawBorder(const std::string &title);
|
||||
|
||||
/// Wipes the screen to full black
|
||||
DFHACK_EXPORT bool clear();
|
||||
|
||||
/// Requests repaint
|
||||
DFHACK_EXPORT bool invalidate();
|
||||
|
||||
/// Find a loaded graphics tile from graphics raws.
|
||||
DFHACK_EXPORT bool findGraphicsTile(const std::string &page, int x, int y, int *ptile, int *pgs = NULL);
|
||||
|
||||
// Push and remove viewscreens
|
||||
DFHACK_EXPORT bool show(df::viewscreen *screen, df::viewscreen *before = NULL);
|
||||
DFHACK_EXPORT void dismiss(df::viewscreen *screen);
|
||||
DFHACK_EXPORT bool isDismissed(df::viewscreen *screen);
|
||||
}
|
||||
|
||||
class DFHACK_EXPORT dfhack_viewscreen : public df::viewscreen {
|
||||
df::coord2d last_size;
|
||||
void check_resize();
|
||||
|
||||
protected:
|
||||
bool text_input_mode;
|
||||
|
||||
public:
|
||||
dfhack_viewscreen();
|
||||
virtual ~dfhack_viewscreen();
|
||||
|
||||
static bool is_instance(df::viewscreen *screen);
|
||||
|
||||
virtual void logic();
|
||||
virtual void render();
|
||||
|
||||
virtual int8_t movies_okay() { return 1; }
|
||||
virtual bool key_conflict(df::interface_key key);
|
||||
|
||||
virtual bool is_lua_screen() { return false; }
|
||||
virtual std::string getFocusString() = 0;
|
||||
};
|
||||
|
||||
class DFHACK_EXPORT dfhack_lua_viewscreen : public dfhack_viewscreen {
|
||||
std::string focus;
|
||||
|
||||
void update_focus(lua_State *L, int idx);
|
||||
|
||||
bool safe_call_lua(int (*pf)(lua_State *), int args, int rvs);
|
||||
static dfhack_lua_viewscreen *get_self(lua_State *L);
|
||||
|
||||
static int do_destroy(lua_State *L);
|
||||
static int do_render(lua_State *L);
|
||||
static int do_notify(lua_State *L);
|
||||
static int do_input(lua_State *L);
|
||||
|
||||
public:
|
||||
dfhack_lua_viewscreen(lua_State *L, int table_idx);
|
||||
virtual ~dfhack_lua_viewscreen();
|
||||
|
||||
static df::viewscreen *get_pointer(lua_State *L, int idx, bool make);
|
||||
|
||||
virtual bool is_lua_screen() { return true; }
|
||||
virtual std::string getFocusString() { return focus; }
|
||||
|
||||
virtual void render();
|
||||
virtual void logic();
|
||||
virtual void help();
|
||||
virtual void resize(int w, int h);
|
||||
virtual void feed(std::set<df::interface_key> *keys);
|
||||
};
|
||||
}
|
@ -0,0 +1,354 @@
|
||||
-- Viewscreen implementation utility collection.
|
||||
|
||||
local _ENV = mkmodule('gui')
|
||||
|
||||
local dscreen = dfhack.screen
|
||||
|
||||
CLEAR_PEN = {ch=32,fg=0,bg=0}
|
||||
|
||||
function simulateInput(screen,...)
|
||||
local keys = {}
|
||||
local function push_key(arg)
|
||||
local kv = arg
|
||||
if type(arg) == 'string' then
|
||||
kv = df.interface_key[arg]
|
||||
if kv == nil then
|
||||
error('Invalid keycode: '..arg)
|
||||
end
|
||||
end
|
||||
if type(arg) == 'number' then
|
||||
keys[#keys+1] = kv
|
||||
end
|
||||
end
|
||||
for i = 1,select('#',...) do
|
||||
local arg = select(i,...)
|
||||
if arg ~= nil then
|
||||
local t = type(arg)
|
||||
if type(arg) == 'table' then
|
||||
for k,v in pairs(arg) do
|
||||
if v == true then
|
||||
push_key(k)
|
||||
else
|
||||
push_key(v)
|
||||
end
|
||||
end
|
||||
else
|
||||
push_key(arg)
|
||||
end
|
||||
end
|
||||
end
|
||||
dscreen._doSimulateInput(screen, keys)
|
||||
end
|
||||
|
||||
function mkdims_xy(x1,y1,x2,y2)
|
||||
return { x1=x1, y1=y1, x2=x2, y2=y2, width=x2-x1+1, height=y2-y1+1 }
|
||||
end
|
||||
function mkdims_wh(x1,y1,w,h)
|
||||
return { x1=x1, y1=y1, x2=x1+w-1, y2=y1+h-1, width=w, height=h }
|
||||
end
|
||||
function inset(rect,dx1,dy1,dx2,dy2)
|
||||
return mkdims_xy(
|
||||
rect.x1+dx1, rect.y1+dy1,
|
||||
rect.x2-(dx2 or dx1), rect.y2-(dy2 or dy1)
|
||||
)
|
||||
end
|
||||
|
||||
local function to_pen(default, pen, bg, bold)
|
||||
if pen == nil then
|
||||
return default or {}
|
||||
elseif type(pen) ~= 'table' then
|
||||
return {fg=pen,bg=bg,bold=bold}
|
||||
else
|
||||
return pen
|
||||
end
|
||||
end
|
||||
|
||||
----------------------------
|
||||
-- Clipped painter object --
|
||||
----------------------------
|
||||
|
||||
Painter = defclass(Painter, nil)
|
||||
|
||||
function Painter.new(rect, pen)
|
||||
rect = rect or mkdims_wh(0,0,dscreen.getWindowSize())
|
||||
local self = {
|
||||
x1 = rect.x1, clip_x1 = rect.x1,
|
||||
y1 = rect.y1, clip_y1 = rect.y1,
|
||||
x2 = rect.x2, clip_x2 = rect.x2,
|
||||
y2 = rect.y2, clip_y2 = rect.y2,
|
||||
width = rect.x2-rect.x1+1,
|
||||
height = rect.y2-rect.y1+1,
|
||||
cur_pen = to_pen(nil, pen or COLOR_GREY)
|
||||
}
|
||||
return mkinstance(Painter, self):seek(0,0)
|
||||
end
|
||||
|
||||
function Painter:isValidPos()
|
||||
return self.x >= self.clip_x1 and self.x <= self.clip_x2
|
||||
and self.y >= self.clip_y1 and self.y <= self.clip_y2
|
||||
end
|
||||
|
||||
function Painter:viewport(x,y,w,h)
|
||||
local x1,y1 = self.x1+x, self.y1+y
|
||||
local x2,y2 = x1+w-1, y1+h-1
|
||||
local vp = {
|
||||
-- Logical viewport
|
||||
x1 = x1, y1 = y1, x2 = x2, y2 = y2,
|
||||
width = w, height = h,
|
||||
-- Actual clipping rect
|
||||
clip_x1 = math.max(self.clip_x1, x1),
|
||||
clip_y1 = math.max(self.clip_y1, y1),
|
||||
clip_x2 = math.min(self.clip_x2, x2),
|
||||
clip_y2 = math.min(self.clip_y2, y2),
|
||||
-- Pen
|
||||
cur_pen = self.cur_pen
|
||||
}
|
||||
return mkinstance(Painter, vp):seek(0,0)
|
||||
end
|
||||
|
||||
function Painter:localX()
|
||||
return self.x - self.x1
|
||||
end
|
||||
|
||||
function Painter:localY()
|
||||
return self.y - self.y1
|
||||
end
|
||||
|
||||
function Painter:seek(x,y)
|
||||
if x then self.x = self.x1 + x end
|
||||
if y then self.y = self.y1 + y end
|
||||
return self
|
||||
end
|
||||
|
||||
function Painter:advance(dx,dy)
|
||||
if dx then self.x = self.x + dx end
|
||||
if dy then self.y = self.y + dy end
|
||||
return self
|
||||
end
|
||||
|
||||
function Painter:newline(dx)
|
||||
self.y = self.y + 1
|
||||
self.x = self.x1 + (dx or 0)
|
||||
return self
|
||||
end
|
||||
|
||||
function Painter:pen(pen,...)
|
||||
self.cur_pen = to_pen(self.cur_pen, pen, ...)
|
||||
return self
|
||||
end
|
||||
|
||||
function Painter:color(fg,bold,bg)
|
||||
self.cur_pen = copyall(self.cur_pen)
|
||||
self.cur_pen.fg = fg
|
||||
self.cur_pen.bold = bold
|
||||
if bg then self.cur_pen.bg = bg end
|
||||
return self
|
||||
end
|
||||
|
||||
function Painter:clear()
|
||||
dscreen.fillRect(CLEAR_PEN, self.clip_x1, self.clip_y1, self.clip_x2, self.clip_y2)
|
||||
return self
|
||||
end
|
||||
|
||||
function Painter:fill(x1,y1,x2,y2,pen,bg,bold)
|
||||
if type(x1) == 'table' then
|
||||
x1, y1, x2, y2, pen, bg, bold = x1.x1, x1.y1, x1.x2, x1.y2, y1, x2, y2
|
||||
end
|
||||
x1 = math.max(x1,self.clip_x1)
|
||||
y1 = math.max(y1,self.clip_y1)
|
||||
x2 = math.min(x2,self.clip_x2)
|
||||
y2 = math.min(y2,self.clip_y2)
|
||||
dscreen.fillRect(to_pen(self.cur_pen,pen,bg,bold),x1,y1,x2,y2)
|
||||
return self
|
||||
end
|
||||
|
||||
function Painter:char(char,pen,...)
|
||||
if self:isValidPos() then
|
||||
dscreen.paintTile(to_pen(self.cur_pen, pen, ...), self.x, self.y, char)
|
||||
end
|
||||
return self:advance(1, nil)
|
||||
end
|
||||
|
||||
function Painter:tile(char,tile,pen,...)
|
||||
if self:isValidPos() then
|
||||
dscreen.paintTile(to_pen(self.cur_pen, pen, ...), self.x, self.y, char, tile)
|
||||
end
|
||||
return self:advance(1, nil)
|
||||
end
|
||||
|
||||
function Painter:string(text,pen,...)
|
||||
if self.y >= self.clip_y1 and self.y <= self.clip_y2 then
|
||||
local dx = 0
|
||||
if self.x < self.clip_x1 then
|
||||
dx = self.clip_x1 - self.x
|
||||
end
|
||||
local len = #text
|
||||
if self.x + len - 1 > self.clip_x2 then
|
||||
len = self.clip_x2 - self.x + 1
|
||||
end
|
||||
if len > dx then
|
||||
dscreen.paintString(
|
||||
to_pen(self.cur_pen, pen, ...),
|
||||
self.x+dx, self.y,
|
||||
string.sub(text,dx+1,len)
|
||||
)
|
||||
end
|
||||
end
|
||||
return self:advance(#text, nil)
|
||||
end
|
||||
|
||||
------------------------
|
||||
-- Base screen object --
|
||||
------------------------
|
||||
|
||||
Screen = defclass(Screen, dfhack.screen)
|
||||
|
||||
Screen.text_input_mode = false
|
||||
|
||||
function Screen:isShown()
|
||||
return self._native ~= nil
|
||||
end
|
||||
|
||||
function Screen:isActive()
|
||||
return self:isShown() and not self:isDismissed()
|
||||
end
|
||||
|
||||
function Screen:renderParent()
|
||||
if self._native and self._native.parent then
|
||||
self._native.parent:render()
|
||||
else
|
||||
dscreen.clear()
|
||||
end
|
||||
end
|
||||
|
||||
function Screen:sendInputToParent(...)
|
||||
if self._native and self._native.parent then
|
||||
simulateInput(self._native.parent, ...)
|
||||
end
|
||||
end
|
||||
|
||||
function Screen:show(below)
|
||||
if self._native then
|
||||
error("This screen is already on display")
|
||||
end
|
||||
self:onAboutToShow(below)
|
||||
if dscreen.show(self, below) then
|
||||
self:onShown()
|
||||
else
|
||||
error('Could not show screen')
|
||||
end
|
||||
end
|
||||
|
||||
function Screen:onAboutToShow()
|
||||
end
|
||||
|
||||
function Screen:onShown()
|
||||
end
|
||||
|
||||
function Screen:dismiss()
|
||||
if self._native and not dscreen.isDismissed(self) then
|
||||
dscreen.dismiss(self)
|
||||
self:onDismissed()
|
||||
end
|
||||
end
|
||||
|
||||
function Screen:onDismissed()
|
||||
end
|
||||
|
||||
------------------------
|
||||
-- Framed screen object --
|
||||
------------------------
|
||||
|
||||
-- Plain grey-colored frame.
|
||||
GREY_FRAME = {
|
||||
frame_pen = { ch = ' ', fg = COLOR_BLACK, bg = COLOR_GREY },
|
||||
title_pen = { fg = COLOR_BLACK, bg = COLOR_WHITE },
|
||||
signature_pen = { fg = COLOR_BLACK, bg = COLOR_GREY },
|
||||
}
|
||||
|
||||
-- The usual boundary used by the DF screens. Often has fancy pattern in tilesets.
|
||||
BOUNDARY_FRAME = {
|
||||
frame_pen = { ch = 0xDB, fg = COLOR_DARKGREY, bg = COLOR_BLACK },
|
||||
title_pen = { fg = COLOR_BLACK, bg = COLOR_GREY },
|
||||
signature_pen = { fg = COLOR_BLACK, bg = COLOR_DARKGREY },
|
||||
}
|
||||
|
||||
GREY_LINE_FRAME = {
|
||||
frame_pen = { ch = 206, fg = COLOR_GREY, bg = COLOR_BLACK },
|
||||
h_frame_pen = { ch = 205, fg = COLOR_GREY, bg = COLOR_BLACK },
|
||||
v_frame_pen = { ch = 186, fg = COLOR_GREY, bg = COLOR_BLACK },
|
||||
lt_frame_pen = { ch = 201, fg = COLOR_GREY, bg = COLOR_BLACK },
|
||||
lb_frame_pen = { ch = 200, fg = COLOR_GREY, bg = COLOR_BLACK },
|
||||
rt_frame_pen = { ch = 187, fg = COLOR_GREY, bg = COLOR_BLACK },
|
||||
rb_frame_pen = { ch = 188, fg = COLOR_GREY, bg = COLOR_BLACK },
|
||||
title_pen = { fg = COLOR_BLACK, bg = COLOR_GREY },
|
||||
signature_pen = { fg = COLOR_DARKGREY, bg = COLOR_BLACK },
|
||||
}
|
||||
|
||||
function paint_frame(x1,y1,x2,y2,style,title)
|
||||
local pen = style.frame_pen
|
||||
dscreen.paintTile(style.lt_frame_pen or pen, x1, y1)
|
||||
dscreen.paintTile(style.rt_frame_pen or pen, x2, y1)
|
||||
dscreen.paintTile(style.lb_frame_pen or pen, x1, y2)
|
||||
dscreen.paintTile(style.rb_frame_pen or pen, x2, y2)
|
||||
dscreen.fillRect(style.t_frame_pen or style.h_frame_pen or pen,x1+1,y1,x2-1,y1)
|
||||
dscreen.fillRect(style.b_frame_pen or style.h_frame_pen or pen,x1+1,y2,x2-1,y2)
|
||||
dscreen.fillRect(style.l_frame_pen or style.v_frame_pen or pen,x1,y1+1,x1,y2-1)
|
||||
dscreen.fillRect(style.r_frame_pen or style.v_frame_pen or pen,x2,y1+1,x2,y2-1)
|
||||
dscreen.paintString(style.signature_pen or style.title_pen or pen,x2-7,y2,"DFHack")
|
||||
|
||||
if title then
|
||||
local x = math.max(0,math.floor((x2-x1-3-#title)/2)) + x1
|
||||
local tstr = ' '..title..' '
|
||||
if #tstr > x2-x1-1 then
|
||||
tstr = string.sub(tstr,1,x2-x1-1)
|
||||
end
|
||||
dscreen.paintString(style.title_pen or pen, x, y1, tstr)
|
||||
end
|
||||
end
|
||||
|
||||
FramedScreen = defclass(FramedScreen, Screen)
|
||||
|
||||
FramedScreen.frame_style = BOUNDARY_FRAME
|
||||
|
||||
local function hint_coord(gap,hint)
|
||||
if hint and hint > 0 then
|
||||
return math.min(hint,gap)
|
||||
elseif hint and hint < 0 then
|
||||
return math.max(0,gap-hint)
|
||||
else
|
||||
return math.floor(gap/2)
|
||||
end
|
||||
end
|
||||
|
||||
function FramedScreen:updateFrameSize(sw,sh)
|
||||
local iw, ih = sw-2, sh-2
|
||||
local width = math.min(self.frame_width or iw, iw)
|
||||
local height = math.min(self.frame_height or ih, ih)
|
||||
local gw, gh = iw-width, ih-height
|
||||
local x1, y1 = hint_coord(gw,self.frame_xhint), hint_coord(gh,self.frame_yhint)
|
||||
self.frame_rect = mkdims_wh(x1+1,y1+1,width,height)
|
||||
self.frame_opaque = (gw == 0 and gh == 0)
|
||||
end
|
||||
|
||||
function FramedScreen:onResize(w,h)
|
||||
self:updateFrameSize(w,h)
|
||||
end
|
||||
|
||||
function FramedScreen:onRender()
|
||||
local rect = self.frame_rect
|
||||
local x1,y1,x2,y2 = rect.x1-1, rect.y1-1, rect.x2+1, rect.y2+1
|
||||
|
||||
if self.frame_opaque then
|
||||
dscreen.clear()
|
||||
else
|
||||
self:renderParent()
|
||||
dscreen.fillRect(CLEAR_PEN,x1,y1,x2,y2)
|
||||
end
|
||||
|
||||
paint_frame(x1,y1,x2,y2,self.frame_style,self.frame_title)
|
||||
|
||||
self:onRenderBody(Painter.new(rect))
|
||||
end
|
||||
|
||||
return _ENV
|
@ -0,0 +1,266 @@
|
||||
-- Support for messing with the dwarfmode screen
|
||||
|
||||
local _ENV = mkmodule('gui.dwarfmode')
|
||||
|
||||
local gui = require('gui')
|
||||
local dscreen = dfhack.screen
|
||||
local world_map = df.global.world.map
|
||||
|
||||
AREA_MAP_WIDTH = 23
|
||||
MENU_WIDTH = 30
|
||||
|
||||
function getPanelLayout()
|
||||
local sw, sh = dscreen.getWindowSize()
|
||||
local view_height = sh-2
|
||||
local view_rb = sw-1
|
||||
local area_x2 = sw-AREA_MAP_WIDTH-2
|
||||
local menu_x2 = sw-MENU_WIDTH-2
|
||||
local menu_x1 = area_x2-MENU_WIDTH-1
|
||||
local area_pos = df.global.ui_area_map_width
|
||||
local menu_pos = df.global.ui_menu_width
|
||||
local rv = {}
|
||||
|
||||
if area_pos < 3 then
|
||||
rv.area_map = gui.mkdims_xy(area_x2+1,1,view_rb-1,view_height)
|
||||
view_rb = area_x2
|
||||
end
|
||||
if menu_pos < area_pos or df.global.ui.main.mode ~= 0 then
|
||||
if menu_pos >= area_pos then
|
||||
rv.menu_forced = true
|
||||
menu_pos = area_pos-1
|
||||
end
|
||||
local menu_x = menu_x2
|
||||
if menu_pos < 2 then menu_x = menu_x1 end
|
||||
rv.menu = gui.mkdims_xy(menu_x+1,1,view_rb-1,view_height)
|
||||
view_rb = menu_x
|
||||
end
|
||||
rv.area_pos = area_pos
|
||||
rv.menu_pos = menu_pos
|
||||
rv.map = gui.mkdims_xy(1,1,view_rb-1,view_height)
|
||||
return rv
|
||||
end
|
||||
|
||||
function getCursorPos()
|
||||
if df.global.cursor.x ~= -30000 then
|
||||
return copyall(df.global.cursor)
|
||||
end
|
||||
end
|
||||
|
||||
function setCursorPos(cursor)
|
||||
df.global.cursor = cursor
|
||||
end
|
||||
|
||||
function clearCursorPos()
|
||||
df.global.cursor = xyz2pos(nil)
|
||||
end
|
||||
|
||||
Viewport = defclass(Viewport)
|
||||
|
||||
function Viewport.make(map,x,y,z)
|
||||
local self = gui.mkdims_wh(x,y,map.width,map.height)
|
||||
self.z = z
|
||||
return mkinstance(Viewport, self)
|
||||
end
|
||||
|
||||
function Viewport.get(layout)
|
||||
return Viewport.make(
|
||||
(layout or getPanelLayout()).map,
|
||||
df.global.window_x,
|
||||
df.global.window_y,
|
||||
df.global.window_z
|
||||
)
|
||||
end
|
||||
|
||||
function Viewport:resize(layout)
|
||||
return Viewport.make(
|
||||
(layout or getPanelLayout()).map,
|
||||
self.x1, self.y1, self.z
|
||||
)
|
||||
end
|
||||
|
||||
function Viewport:set()
|
||||
local vp = self:clip()
|
||||
df.global.window_x = vp.x1
|
||||
df.global.window_y = vp.y1
|
||||
df.global.window_z = vp.z
|
||||
return vp
|
||||
end
|
||||
|
||||
function Viewport:clip(x,y,z)
|
||||
return self:make(
|
||||
math.max(0, math.min(x or self.x1, world_map.x_count-self.width)),
|
||||
math.max(0, math.min(y or self.y1, world_map.y_count-self.height)),
|
||||
math.max(0, math.min(z or self.z, world_map.z_count-1))
|
||||
)
|
||||
end
|
||||
|
||||
function Viewport:isVisibleXY(target,gap)
|
||||
gap = gap or 0
|
||||
|
||||
return math.max(target.x-gap,0) >= self.x1
|
||||
and math.min(target.x+gap,world_map.x_count-1) <= self.x2
|
||||
and math.max(target.y-gap,0) >= self.y1
|
||||
and math.min(target.y+gap,world_map.y_count-1) <= self.y2
|
||||
end
|
||||
|
||||
function Viewport:isVisible(target,gap)
|
||||
gap = gap or 0
|
||||
|
||||
return self:isVisibleXY(target,gap) and target.z == self.z
|
||||
end
|
||||
|
||||
function Viewport:centerOn(target)
|
||||
return self:clip(
|
||||
target.x - math.floor(self.width/2),
|
||||
target.y - math.floor(self.height/2),
|
||||
target.z
|
||||
)
|
||||
end
|
||||
|
||||
function Viewport:scrollTo(target,gap)
|
||||
gap = math.max(0, gap or 5)
|
||||
if gap*2 >= math.min(self.width, self.height) then
|
||||
gap = math.floor(math.min(self.width, self.height)/2)
|
||||
end
|
||||
local x = math.min(self.x1, target.x-gap)
|
||||
x = math.max(x, target.x+gap+1-self.width)
|
||||
local y = math.min(self.y1, target.y-gap)
|
||||
y = math.max(y, target.y+gap+1-self.height)
|
||||
return self:clip(x, y, target.z)
|
||||
end
|
||||
|
||||
function Viewport:reveal(target,gap,max_scroll,scroll_gap,scroll_z)
|
||||
gap = math.max(0, gap or 5)
|
||||
if self:isVisible(target, gap) then
|
||||
return self
|
||||
end
|
||||
|
||||
max_scroll = math.max(0, max_scroll or 5)
|
||||
if self:isVisibleXY(target, -max_scroll)
|
||||
and (scroll_z or target.z == self.z) then
|
||||
return self:scrollTo(target, scroll_gap or gap)
|
||||
else
|
||||
return self:centerOn(target)
|
||||
end
|
||||
end
|
||||
|
||||
MOVEMENT_KEYS = {
|
||||
CURSOR_UP = { 0, -1, 0 }, CURSOR_DOWN = { 0, 1, 0 },
|
||||
CURSOR_LEFT = { -1, 0, 0 }, CURSOR_RIGHT = { 1, 0, 0 },
|
||||
CURSOR_UPLEFT = { -1, -1, 0 }, CURSOR_UPRIGHT = { 1, -1, 0 },
|
||||
CURSOR_DOWNLEFT = { -1, 1, 0 }, CURSOR_DOWNRIGHT = { 1, 1, 0 },
|
||||
CURSOR_UP_FAST = { 0, -1, 0, true }, CURSOR_DOWN_FAST = { 0, 1, 0, true },
|
||||
CURSOR_LEFT_FAST = { -1, 0, 0, true }, CURSOR_RIGHT_FAST = { 1, 0, 0, true },
|
||||
CURSOR_UPLEFT_FAST = { -1, -1, 0, true }, CURSOR_UPRIGHT_FAST = { 1, -1, 0, true },
|
||||
CURSOR_DOWNLEFT_FAST = { -1, 1, 0, true }, CURSOR_DOWNRIGHT_FAST = { 1, 1, 0, true },
|
||||
CURSOR_UP_Z = { 0, 0, 1 }, CURSOR_DOWN_Z = { 0, 0, -1 },
|
||||
CURSOR_UP_Z_AUX = { 0, 0, 1 }, CURSOR_DOWN_Z_AUX = { 0, 0, -1 },
|
||||
}
|
||||
|
||||
function Viewport:scrollByKey(key)
|
||||
local info = MOVEMENT_KEYS[key]
|
||||
if info then
|
||||
local delta = 10
|
||||
if info[4] then delta = 20 end
|
||||
|
||||
return self:clip(
|
||||
self.x1 + delta*info[1],
|
||||
self.y1 + delta*info[2],
|
||||
self.z + info[3]
|
||||
)
|
||||
else
|
||||
return self
|
||||
end
|
||||
end
|
||||
|
||||
DwarfOverlay = defclass(DwarfOverlay, gui.Screen)
|
||||
|
||||
function DwarfOverlay:updateLayout()
|
||||
self.df_layout = getPanelLayout()
|
||||
end
|
||||
|
||||
function DwarfOverlay:onShown()
|
||||
self:updateLayout()
|
||||
end
|
||||
|
||||
function DwarfOverlay:onResize(w,h)
|
||||
self:updateLayout()
|
||||
end
|
||||
|
||||
function DwarfOverlay:getViewport(old_vp)
|
||||
if old_vp then
|
||||
return old_vp:resize(self.df_layout)
|
||||
else
|
||||
return Viewport.get(self.df_layout)
|
||||
end
|
||||
end
|
||||
|
||||
function DwarfOverlay:propagateMoveKeys(keys)
|
||||
for code,_ in pairs(MOVEMENT_KEYS) do
|
||||
if keys[code] then
|
||||
self:sendInputToParent(code)
|
||||
return code
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function DwarfOverlay:simulateViewScroll(keys, anchor, no_clip_cursor)
|
||||
local layout = self.df_layout
|
||||
local cursor = getCursorPos()
|
||||
|
||||
anchor = anchor or cursor
|
||||
|
||||
if anchor and keys.A_MOVE_SAME_SQUARE then
|
||||
self:getViewport():centerOn(anchor):set()
|
||||
return 'A_MOVE_SAME_SQUARE'
|
||||
end
|
||||
|
||||
for code,_ in pairs(MOVEMENT_KEYS) do
|
||||
if keys[code] then
|
||||
local vp = self:getViewport():scrollByKey(code)
|
||||
if (cursor and not no_clip_cursor) or no_clip_cursor == false then
|
||||
vp = vp:reveal(anchor,4,20,4,true)
|
||||
end
|
||||
vp:set()
|
||||
|
||||
return code
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function DwarfOverlay:onAboutToShow(below)
|
||||
local screen = dfhack.gui.getCurViewscreen()
|
||||
if below then screen = below.parent end
|
||||
if not df.viewscreen_dwarfmodest:is_instance(screen) then
|
||||
error("This screen requires the main dwarfmode view")
|
||||
end
|
||||
end
|
||||
|
||||
MenuOverlay = defclass(MenuOverlay, DwarfOverlay)
|
||||
|
||||
function MenuOverlay:onAboutToShow(below)
|
||||
DwarfOverlay.onAboutToShow(self,below)
|
||||
|
||||
self:updateLayout()
|
||||
if not self.df_layout.menu then
|
||||
error("The menu panel of dwarfmode is not visible")
|
||||
end
|
||||
end
|
||||
|
||||
function MenuOverlay:onRender()
|
||||
self:renderParent()
|
||||
self:updateLayout()
|
||||
|
||||
local menu = self.df_layout.menu
|
||||
if menu then
|
||||
-- Paint signature on the frame.
|
||||
dscreen.paintString(
|
||||
{fg=COLOR_BLACK,bg=COLOR_DARKGREY},
|
||||
menu.x1+1, menu.y2+1, "DFHack"
|
||||
)
|
||||
|
||||
self:onRenderBody(gui.Painter.new(menu))
|
||||
end
|
||||
end
|
||||
|
||||
return _ENV
|
@ -0,0 +1,578 @@
|
||||
/*
|
||||
https://github.com/peterix/dfhack
|
||||
Copyright (c) 2009-2011 Petr Mrázek (peterix@gmail.com)
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
|
||||
#include "Internal.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
using namespace std;
|
||||
|
||||
#include "modules/Screen.h"
|
||||
#include "MemAccess.h"
|
||||
#include "VersionInfo.h"
|
||||
#include "Types.h"
|
||||
#include "Error.h"
|
||||
#include "ModuleFactory.h"
|
||||
#include "Core.h"
|
||||
#include "PluginManager.h"
|
||||
#include "LuaTools.h"
|
||||
|
||||
#include "MiscUtils.h"
|
||||
|
||||
using namespace DFHack;
|
||||
|
||||
#include "DataDefs.h"
|
||||
#include "df/init.h"
|
||||
#include "df/texture_handler.h"
|
||||
#include "df/tile_page.h"
|
||||
#include "df/interfacest.h"
|
||||
#include "df/enabler.h"
|
||||
|
||||
using namespace df::enums;
|
||||
using df::global::init;
|
||||
using df::global::gps;
|
||||
using df::global::texture;
|
||||
using df::global::gview;
|
||||
using df::global::enabler;
|
||||
|
||||
using Screen::Pen;
|
||||
|
||||
/*
|
||||
* Screen painting API.
|
||||
*/
|
||||
|
||||
df::coord2d Screen::getMousePos()
|
||||
{
|
||||
if (!gps || (enabler && !enabler->tracking_on))
|
||||
return df::coord2d(-1, -1);
|
||||
|
||||
return df::coord2d(gps->mouse_x, gps->mouse_y);
|
||||
}
|
||||
|
||||
df::coord2d Screen::getWindowSize()
|
||||
{
|
||||
if (!gps) return df::coord2d(80, 25);
|
||||
|
||||
return df::coord2d(gps->dimx, gps->dimy);
|
||||
}
|
||||
|
||||
bool Screen::inGraphicsMode()
|
||||
{
|
||||
return init && init->display.flag.is_set(init_display_flags::USE_GRAPHICS);
|
||||
}
|
||||
|
||||
static void doSetTile(const Pen &pen, int index)
|
||||
{
|
||||
auto screen = gps->screen + index*4;
|
||||
screen[0] = uint8_t(pen.ch);
|
||||
screen[1] = uint8_t(pen.fg) & 15;
|
||||
screen[2] = uint8_t(pen.bg) & 15;
|
||||
screen[3] = uint8_t(pen.bold) & 1;
|
||||
gps->screentexpos[index] = pen.tile;
|
||||
gps->screentexpos_addcolor[index] = (pen.tile_mode == Screen::Pen::CharColor);
|
||||
gps->screentexpos_grayscale[index] = (pen.tile_mode == Screen::Pen::TileColor);
|
||||
gps->screentexpos_cf[index] = pen.tile_fg;
|
||||
gps->screentexpos_cbr[index] = pen.tile_bg;
|
||||
}
|
||||
|
||||
bool Screen::paintTile(const Pen &pen, int x, int y)
|
||||
{
|
||||
if (!gps) return false;
|
||||
|
||||
int dimx = gps->dimx, dimy = gps->dimy;
|
||||
if (x < 0 || x >= dimx || y < 0 || y >= dimy) return false;
|
||||
|
||||
doSetTile(pen, x*dimy + y);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Screen::paintString(const Pen &pen, int x, int y, const std::string &text)
|
||||
{
|
||||
if (!gps || y < 0 || y >= gps->dimy) return false;
|
||||
|
||||
Pen tmp(pen);
|
||||
bool ok = false;
|
||||
|
||||
for (size_t i = -std::min(0,x); i < text.size(); i++)
|
||||
{
|
||||
if (x + i >= size_t(gps->dimx))
|
||||
break;
|
||||
|
||||
tmp.ch = text[i];
|
||||
tmp.tile = (pen.tile ? pen.tile + uint8_t(text[i]) : 0);
|
||||
paintTile(tmp, x+i, y);
|
||||
ok = true;
|
||||
}
|
||||
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool Screen::fillRect(const Pen &pen, int x1, int y1, int x2, int y2)
|
||||
{
|
||||
if (!gps) return false;
|
||||
|
||||
if (x1 < 0) x1 = 0;
|
||||
if (y1 < 0) y1 = 0;
|
||||
if (x2 >= gps->dimx) x2 = gps->dimx-1;
|
||||
if (y2 >= gps->dimy) y2 = gps->dimy-1;
|
||||
if (x1 > x2 || y1 > y2) return false;
|
||||
|
||||
for (int x = x1; x <= x2; x++)
|
||||
{
|
||||
int index = x*gps->dimy;
|
||||
|
||||
for (int y = y1; y <= y2; y++)
|
||||
doSetTile(pen, index+y);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Screen::drawBorder(const std::string &title)
|
||||
{
|
||||
if (!gps) return false;
|
||||
|
||||
int dimx = gps->dimx, dimy = gps->dimy;
|
||||
Pen border(0xDB, 8);
|
||||
Pen text(0, 0, 7);
|
||||
Pen signature(0, 0, 8);
|
||||
|
||||
for (int x = 0; x < dimx; x++)
|
||||
{
|
||||
doSetTile(border, x * dimy + 0);
|
||||
doSetTile(border, x * dimy + dimy - 1);
|
||||
}
|
||||
for (int y = 0; y < dimy; y++)
|
||||
{
|
||||
doSetTile(border, 0 * dimy + y);
|
||||
doSetTile(border, (dimx - 1) * dimy + y);
|
||||
}
|
||||
|
||||
paintString(signature, dimx-8, dimy-1, "DFHack");
|
||||
|
||||
return paintString(text, (dimx - title.length()) / 2, 0, title);
|
||||
}
|
||||
|
||||
bool Screen::clear()
|
||||
{
|
||||
if (!gps) return false;
|
||||
|
||||
return fillRect(Pen(' ',0,0,false), 0, 0, gps->dimx-1, gps->dimy-1);
|
||||
}
|
||||
|
||||
bool Screen::invalidate()
|
||||
{
|
||||
if (!enabler) return false;
|
||||
|
||||
enabler->flag.bits.render = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Screen::findGraphicsTile(const std::string &pagename, int x, int y, int *ptile, int *pgs)
|
||||
{
|
||||
if (!gps || !texture || x < 0 || y < 0) return false;
|
||||
|
||||
for (size_t i = 0; i < texture->page.size(); i++)
|
||||
{
|
||||
auto page = texture->page[i];
|
||||
if (!page->loaded || page->token != pagename) continue;
|
||||
|
||||
if (x >= page->page_dim_x || y >= page->page_dim_y)
|
||||
break;
|
||||
int idx = y*page->page_dim_x + x;
|
||||
if (size_t(idx) >= page->texpos.size())
|
||||
break;
|
||||
|
||||
if (ptile) *ptile = page->texpos[idx];
|
||||
if (pgs) *pgs = page->texpos_gs[idx];
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Screen::show(df::viewscreen *screen, df::viewscreen *before)
|
||||
{
|
||||
CHECK_NULL_POINTER(screen);
|
||||
CHECK_INVALID_ARGUMENT(!screen->parent && !screen->child);
|
||||
|
||||
if (!gps || !gview) return false;
|
||||
|
||||
df::viewscreen *parent = &gview->view;
|
||||
while (parent && parent->child != before)
|
||||
parent = parent->child;
|
||||
|
||||
if (!parent) return false;
|
||||
|
||||
gps->force_full_display_count += 2;
|
||||
|
||||
screen->child = parent->child;
|
||||
screen->parent = parent;
|
||||
parent->child = screen;
|
||||
if (screen->child)
|
||||
screen->child->parent = screen;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void Screen::dismiss(df::viewscreen *screen)
|
||||
{
|
||||
CHECK_NULL_POINTER(screen);
|
||||
|
||||
screen->breakdown_level = interface_breakdown_types::STOPSCREEN;
|
||||
}
|
||||
|
||||
bool Screen::isDismissed(df::viewscreen *screen)
|
||||
{
|
||||
CHECK_NULL_POINTER(screen);
|
||||
|
||||
return screen->breakdown_level != interface_breakdown_types::NONE;
|
||||
}
|
||||
|
||||
/*
|
||||
* Base DFHack viewscreen.
|
||||
*/
|
||||
|
||||
static std::set<df::viewscreen*> dfhack_screens;
|
||||
|
||||
dfhack_viewscreen::dfhack_viewscreen() : text_input_mode(false)
|
||||
{
|
||||
dfhack_screens.insert(this);
|
||||
}
|
||||
|
||||
dfhack_viewscreen::~dfhack_viewscreen()
|
||||
{
|
||||
dfhack_screens.erase(this);
|
||||
}
|
||||
|
||||
bool dfhack_viewscreen::is_instance(df::viewscreen *screen)
|
||||
{
|
||||
return dfhack_screens.count(screen) != 0;
|
||||
}
|
||||
|
||||
void dfhack_viewscreen::check_resize()
|
||||
{
|
||||
auto size = Screen::getWindowSize();
|
||||
|
||||
if (size != last_size)
|
||||
{
|
||||
last_size = size;
|
||||
resize(size.x, size.y);
|
||||
}
|
||||
}
|
||||
|
||||
void dfhack_viewscreen::logic()
|
||||
{
|
||||
check_resize();
|
||||
|
||||
// Various stuff works poorly unless always repainting
|
||||
Screen::invalidate();
|
||||
}
|
||||
|
||||
void dfhack_viewscreen::render()
|
||||
{
|
||||
check_resize();
|
||||
}
|
||||
|
||||
bool dfhack_viewscreen::key_conflict(df::interface_key key)
|
||||
{
|
||||
if (key == interface_key::OPTIONS)
|
||||
return true;
|
||||
|
||||
if (text_input_mode)
|
||||
{
|
||||
if (key == interface_key::HELP || key == interface_key::MOVIES)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* Lua-backed viewscreen.
|
||||
*/
|
||||
|
||||
static int DFHACK_LUA_VS_TOKEN = 0;
|
||||
|
||||
df::viewscreen *dfhack_lua_viewscreen::get_pointer(lua_State *L, int idx, bool make)
|
||||
{
|
||||
df::viewscreen *screen;
|
||||
|
||||
if (lua_istable(L, idx))
|
||||
{
|
||||
if (!Lua::IsCoreContext(L))
|
||||
luaL_error(L, "only the core context can create lua screens");
|
||||
|
||||
lua_rawgetp(L, idx, &DFHACK_LUA_VS_TOKEN);
|
||||
|
||||
if (!lua_isnil(L, -1))
|
||||
{
|
||||
if (make)
|
||||
luaL_error(L, "this screen is already on display");
|
||||
|
||||
screen = (df::viewscreen*)lua_touserdata(L, -1);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!make)
|
||||
luaL_error(L, "this screen is not on display");
|
||||
|
||||
screen = new dfhack_lua_viewscreen(L, idx);
|
||||
}
|
||||
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
else
|
||||
screen = Lua::CheckDFObject<df::viewscreen>(L, idx);
|
||||
|
||||
return screen;
|
||||
}
|
||||
|
||||
bool dfhack_lua_viewscreen::safe_call_lua(int (*pf)(lua_State *), int args, int rvs)
|
||||
{
|
||||
CoreSuspendClaimer suspend;
|
||||
color_ostream_proxy out(Core::getInstance().getConsole());
|
||||
|
||||
auto L = Lua::Core::State;
|
||||
lua_pushcfunction(L, pf);
|
||||
if (args > 0) lua_insert(L, -args-1);
|
||||
lua_pushlightuserdata(L, this);
|
||||
if (args > 0) lua_insert(L, -args-1);
|
||||
|
||||
return Lua::Core::SafeCall(out, args+1, rvs);
|
||||
}
|
||||
|
||||
dfhack_lua_viewscreen *dfhack_lua_viewscreen::get_self(lua_State *L)
|
||||
{
|
||||
auto self = (dfhack_lua_viewscreen*)lua_touserdata(L, 1);
|
||||
lua_rawgetp(L, LUA_REGISTRYINDEX, self);
|
||||
if (!lua_istable(L, -1)) return NULL;
|
||||
return self;
|
||||
}
|
||||
|
||||
int dfhack_lua_viewscreen::do_destroy(lua_State *L)
|
||||
{
|
||||
auto self = get_self(L);
|
||||
if (!self) return 0;
|
||||
|
||||
lua_pushnil(L);
|
||||
lua_rawsetp(L, LUA_REGISTRYINDEX, self);
|
||||
|
||||
lua_pushnil(L);
|
||||
lua_rawsetp(L, -2, &DFHACK_LUA_VS_TOKEN);
|
||||
lua_pushnil(L);
|
||||
lua_setfield(L, -2, "_native");
|
||||
|
||||
lua_getfield(L, -1, "onDestroy");
|
||||
if (lua_isnil(L, -1))
|
||||
return 0;
|
||||
|
||||
lua_pushvalue(L, -2);
|
||||
lua_call(L, 1, 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void dfhack_lua_viewscreen::update_focus(lua_State *L, int idx)
|
||||
{
|
||||
lua_getfield(L, idx, "text_input_mode");
|
||||
text_input_mode = lua_toboolean(L, -1);
|
||||
lua_pop(L, 1);
|
||||
|
||||
lua_getfield(L, idx, "focus_path");
|
||||
auto str = lua_tostring(L, -1);
|
||||
if (!str) str = "";
|
||||
focus = str;
|
||||
lua_pop(L, 1);
|
||||
|
||||
if (focus.empty())
|
||||
focus = "lua";
|
||||
else
|
||||
focus = "lua/"+focus;
|
||||
}
|
||||
|
||||
int dfhack_lua_viewscreen::do_render(lua_State *L)
|
||||
{
|
||||
auto self = get_self(L);
|
||||
if (!self) return 0;
|
||||
|
||||
lua_getfield(L, -1, "onRender");
|
||||
|
||||
if (lua_isnil(L, -1))
|
||||
{
|
||||
Screen::clear();
|
||||
return 0;
|
||||
}
|
||||
|
||||
lua_pushvalue(L, -2);
|
||||
lua_call(L, 1, 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int dfhack_lua_viewscreen::do_notify(lua_State *L)
|
||||
{
|
||||
int args = lua_gettop(L);
|
||||
|
||||
auto self = get_self(L);
|
||||
if (!self) return 0;
|
||||
|
||||
lua_pushvalue(L, 2);
|
||||
lua_gettable(L, -2);
|
||||
if (lua_isnil(L, -1))
|
||||
return 0;
|
||||
|
||||
// self field args table fn -> table fn table args
|
||||
lua_replace(L, 1);
|
||||
lua_copy(L, -1, 2);
|
||||
lua_insert(L, 1);
|
||||
lua_call(L, args-1, 1);
|
||||
|
||||
self->update_focus(L, 1);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int dfhack_lua_viewscreen::do_input(lua_State *L)
|
||||
{
|
||||
auto self = get_self(L);
|
||||
if (!self) return 0;
|
||||
|
||||
auto keys = (std::set<df::interface_key>*)lua_touserdata(L, 2);
|
||||
|
||||
lua_getfield(L, -1, "onInput");
|
||||
|
||||
if (lua_isnil(L, -1))
|
||||
{
|
||||
if (keys->count(interface_key::LEAVESCREEN))
|
||||
Screen::dismiss(self);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
lua_pushvalue(L, -2);
|
||||
|
||||
lua_createtable(L, 0, keys->size()+3);
|
||||
|
||||
for (auto it = keys->begin(); it != keys->end(); ++it)
|
||||
{
|
||||
auto key = *it;
|
||||
|
||||
if (auto name = enum_item_raw_key(key))
|
||||
lua_pushstring(L, name);
|
||||
else
|
||||
lua_pushinteger(L, key);
|
||||
|
||||
lua_pushboolean(L, true);
|
||||
lua_rawset(L, -3);
|
||||
|
||||
if (key >= interface_key::STRING_A000 &&
|
||||
key <= interface_key::STRING_A255)
|
||||
{
|
||||
lua_pushinteger(L, key - interface_key::STRING_A000);
|
||||
lua_setfield(L, -2, "_STRING");
|
||||
}
|
||||
}
|
||||
|
||||
if (enabler && enabler->tracking_on)
|
||||
{
|
||||
if (enabler->mouse_lbut) {
|
||||
lua_pushboolean(L, true);
|
||||
lua_setfield(L, -2, "_MOUSE_L");
|
||||
}
|
||||
if (enabler->mouse_rbut) {
|
||||
lua_pushboolean(L, true);
|
||||
lua_setfield(L, -2, "_MOUSE_R");
|
||||
}
|
||||
}
|
||||
|
||||
lua_call(L, 2, 0);
|
||||
self->update_focus(L, -1);
|
||||
return 0;
|
||||
}
|
||||
|
||||
dfhack_lua_viewscreen::dfhack_lua_viewscreen(lua_State *L, int table_idx)
|
||||
{
|
||||
assert(Lua::IsCoreContext(L));
|
||||
|
||||
Lua::PushDFObject(L, (df::viewscreen*)this);
|
||||
lua_setfield(L, table_idx, "_native");
|
||||
lua_pushlightuserdata(L, this);
|
||||
lua_rawsetp(L, table_idx, &DFHACK_LUA_VS_TOKEN);
|
||||
|
||||
lua_pushvalue(L, table_idx);
|
||||
lua_rawsetp(L, LUA_REGISTRYINDEX, this);
|
||||
|
||||
update_focus(L, table_idx);
|
||||
}
|
||||
|
||||
dfhack_lua_viewscreen::~dfhack_lua_viewscreen()
|
||||
{
|
||||
safe_call_lua(do_destroy, 0, 0);
|
||||
}
|
||||
|
||||
void dfhack_lua_viewscreen::render()
|
||||
{
|
||||
if (Screen::isDismissed(this)) return;
|
||||
|
||||
dfhack_viewscreen::render();
|
||||
|
||||
safe_call_lua(do_render, 0, 0);
|
||||
}
|
||||
|
||||
void dfhack_lua_viewscreen::logic()
|
||||
{
|
||||
if (Screen::isDismissed(this)) return;
|
||||
|
||||
dfhack_viewscreen::logic();
|
||||
|
||||
lua_pushstring(Lua::Core::State, "onIdle");
|
||||
safe_call_lua(do_notify, 1, 0);
|
||||
}
|
||||
|
||||
void dfhack_lua_viewscreen::help()
|
||||
{
|
||||
if (Screen::isDismissed(this)) return;
|
||||
|
||||
lua_pushstring(Lua::Core::State, "onHelp");
|
||||
safe_call_lua(do_notify, 1, 0);
|
||||
}
|
||||
|
||||
void dfhack_lua_viewscreen::resize(int w, int h)
|
||||
{
|
||||
if (Screen::isDismissed(this)) return;
|
||||
|
||||
auto L = Lua::Core::State;
|
||||
lua_pushstring(L, "onResize");
|
||||
lua_pushinteger(L, w);
|
||||
lua_pushinteger(L, h);
|
||||
safe_call_lua(do_notify, 3, 0);
|
||||
}
|
||||
|
||||
void dfhack_lua_viewscreen::feed(std::set<df::interface_key> *keys)
|
||||
{
|
||||
if (Screen::isDismissed(this)) return;
|
||||
|
||||
lua_pushlightuserdata(Lua::Core::State, keys);
|
||||
safe_call_lua(do_input, 1, 0);
|
||||
}
|
@ -1 +1 @@
|
||||
Subproject commit 9f91e74767b4d583b580d46e16143216ba62ae66
|
||||
Subproject commit 1eeaa08360c39a9a2d811544c2443309adc1a8f1
|
@ -0,0 +1,55 @@
|
||||
#include "Core.h"
|
||||
#include <Console.h>
|
||||
#include <Export.h>
|
||||
#include <PluginManager.h>
|
||||
#include <modules/Gui.h>
|
||||
#include <modules/Screen.h>
|
||||
#include <vector>
|
||||
#include <cstdio>
|
||||
#include <stack>
|
||||
#include <string>
|
||||
#include <cmath>
|
||||
|
||||
#include <VTableInterpose.h>
|
||||
#include "df/graphic.h"
|
||||
#include "df/viewscreen_titlest.h"
|
||||
|
||||
using std::vector;
|
||||
using std::string;
|
||||
using std::stack;
|
||||
using namespace DFHack;
|
||||
|
||||
using df::global::gps;
|
||||
|
||||
DFHACK_PLUGIN("vshook");
|
||||
|
||||
struct title_hook : df::viewscreen_titlest {
|
||||
typedef df::viewscreen_titlest interpose_base;
|
||||
|
||||
DEFINE_VMETHOD_INTERPOSE(void, render, ())
|
||||
{
|
||||
INTERPOSE_NEXT(render)();
|
||||
|
||||
Screen::Pen pen(' ',COLOR_WHITE,COLOR_BLACK);
|
||||
Screen::paintString(pen,0,0,"DFHack " DFHACK_VERSION);
|
||||
}
|
||||
};
|
||||
|
||||
IMPLEMENT_VMETHOD_INTERPOSE(title_hook, render);
|
||||
|
||||
DFhackCExport command_result plugin_init ( color_ostream &out, std::vector <PluginCommand> &commands)
|
||||
{
|
||||
if (gps)
|
||||
{
|
||||
if (!INTERPOSE_HOOK(title_hook, render).apply())
|
||||
out.printerr("Could not interpose viewscreen_titlest::render\n");
|
||||
}
|
||||
|
||||
return CR_OK;
|
||||
}
|
||||
|
||||
DFhackCExport command_result plugin_shutdown ( color_ostream &out )
|
||||
{
|
||||
INTERPOSE_HOOK(title_hook, render).remove();
|
||||
return CR_OK;
|
||||
}
|
@ -0,0 +1,661 @@
|
||||
// Dwarf Manipulator - a Therapist-style labor editor
|
||||
|
||||
#include "Core.h"
|
||||
#include <Console.h>
|
||||
#include <Export.h>
|
||||
#include <PluginManager.h>
|
||||
#include <MiscUtils.h>
|
||||
#include <modules/Screen.h>
|
||||
#include <modules/Translation.h>
|
||||
#include <modules/Units.h>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <set>
|
||||
|
||||
#include <VTableInterpose.h>
|
||||
#include "df/world.h"
|
||||
#include "df/ui.h"
|
||||
#include "df/graphic.h"
|
||||
#include "df/enabler.h"
|
||||
#include "df/viewscreen_unitlistst.h"
|
||||
#include "df/interface_key.h"
|
||||
#include "df/unit.h"
|
||||
#include "df/unit_soul.h"
|
||||
#include "df/unit_skill.h"
|
||||
#include "df/creature_raw.h"
|
||||
#include "df/caste_raw.h"
|
||||
|
||||
using std::set;
|
||||
using std::vector;
|
||||
using std::string;
|
||||
|
||||
using namespace DFHack;
|
||||
using namespace df::enums;
|
||||
|
||||
using df::global::world;
|
||||
using df::global::ui;
|
||||
using df::global::gps;
|
||||
using df::global::enabler;
|
||||
|
||||
DFHACK_PLUGIN("manipulator");
|
||||
|
||||
struct SkillLevel
|
||||
{
|
||||
const char *name;
|
||||
int points;
|
||||
char abbrev;
|
||||
};
|
||||
|
||||
#define NUM_SKILL_LEVELS (sizeof(skill_levels) / sizeof(SkillLevel))
|
||||
|
||||
// The various skill rankings. Zero skill is hardcoded to "Not" and '-'.
|
||||
const SkillLevel skill_levels[] = {
|
||||
{"Dabbling", 500, '0'},
|
||||
{"Novice", 600, '1'},
|
||||
{"Adequate", 700, '2'},
|
||||
{"Competent", 800, '3'},
|
||||
{"Skilled", 900, '4'},
|
||||
{"Proficient", 1000, '5'},
|
||||
{"Talented", 1100, '6'},
|
||||
{"Adept", 1200, '7'},
|
||||
{"Expert", 1300, '8'},
|
||||
{"Professional",1400, '9'},
|
||||
{"Accomplished",1500, 'A'},
|
||||
{"Great", 1600, 'B'},
|
||||
{"Master", 1700, 'C'},
|
||||
{"High Master", 1800, 'D'},
|
||||
{"Grand Master",1900, 'E'},
|
||||
{"Legendary", 2000, 'U'},
|
||||
{"Legendary+1", 2100, 'V'},
|
||||
{"Legendary+2", 2200, 'W'},
|
||||
{"Legendary+3", 2300, 'X'},
|
||||
{"Legendary+4", 2400, 'Y'},
|
||||
{"Legendary+5", 0, 'Z'}
|
||||
};
|
||||
|
||||
struct SkillColumn
|
||||
{
|
||||
df::profession profession;
|
||||
df::unit_labor labor;
|
||||
df::job_skill skill;
|
||||
char label[3];
|
||||
bool special; // specified labor is mutually exclusive with all other special labors
|
||||
};
|
||||
|
||||
#define NUM_COLUMNS (sizeof(columns) / sizeof(SkillColumn))
|
||||
|
||||
// All of the skill/labor columns we want to display. Includes profession (for color), labor, skill, and 2 character label
|
||||
const SkillColumn columns[] = {
|
||||
// Mining
|
||||
{profession::MINER, unit_labor::MINE, job_skill::MINING, "Mi", true},
|
||||
// Woodworking
|
||||
{profession::WOODWORKER, unit_labor::CARPENTER, job_skill::CARPENTRY, "Ca"},
|
||||
{profession::WOODWORKER, unit_labor::BOWYER, job_skill::BOWYER, "Bw"},
|
||||
{profession::WOODWORKER, unit_labor::CUTWOOD, job_skill::WOODCUTTING, "WC", true},
|
||||
// Stoneworking
|
||||
{profession::STONEWORKER, unit_labor::MASON, job_skill::MASONRY, "Ma"},
|
||||
{profession::STONEWORKER, unit_labor::DETAIL, job_skill::DETAILSTONE, "En"},
|
||||
// Hunting/Related
|
||||
{profession::RANGER, unit_labor::ANIMALTRAIN, job_skill::ANIMALTRAIN, "Tr"},
|
||||
{profession::RANGER, unit_labor::ANIMALCARE, job_skill::ANIMALCARE, "Ca"},
|
||||
{profession::RANGER, unit_labor::HUNT, job_skill::SNEAK, "Hu", true},
|
||||
{profession::RANGER, unit_labor::TRAPPER, job_skill::TRAPPING, "Tr"},
|
||||
{profession::RANGER, unit_labor::DISSECT_VERMIN, job_skill::DISSECT_VERMIN, "Di"},
|
||||
// Healthcare
|
||||
{profession::DOCTOR, unit_labor::DIAGNOSE, job_skill::DIAGNOSE, "Di"},
|
||||
{profession::DOCTOR, unit_labor::SURGERY, job_skill::SURGERY, "Su"},
|
||||
{profession::DOCTOR, unit_labor::BONE_SETTING, job_skill::SET_BONE, "Bo"},
|
||||
{profession::DOCTOR, unit_labor::SUTURING, job_skill::SUTURE, "St"},
|
||||
{profession::DOCTOR, unit_labor::DRESSING_WOUNDS, job_skill::DRESS_WOUNDS, "Dr"},
|
||||
{profession::DOCTOR, unit_labor::FEED_WATER_CIVILIANS, job_skill::NONE, "Fd"},
|
||||
{profession::DOCTOR, unit_labor::RECOVER_WOUNDED, job_skill::NONE, "Re"},
|
||||
// Farming/Related
|
||||
{profession::FARMER, unit_labor::BUTCHER, job_skill::BUTCHER, "Bu"},
|
||||
{profession::FARMER, unit_labor::TANNER, job_skill::TANNER, "Ta"},
|
||||
{profession::FARMER, unit_labor::PLANT, job_skill::PLANT, "Gr"},
|
||||
{profession::FARMER, unit_labor::DYER, job_skill::DYER, "Dy"},
|
||||
{profession::FARMER, unit_labor::SOAP_MAKER, job_skill::SOAP_MAKING, "So"},
|
||||
{profession::FARMER, unit_labor::BURN_WOOD, job_skill::WOOD_BURNING, "WB"},
|
||||
{profession::FARMER, unit_labor::POTASH_MAKING, job_skill::POTASH_MAKING, "Po"},
|
||||
{profession::FARMER, unit_labor::LYE_MAKING, job_skill::LYE_MAKING, "Ly"},
|
||||
{profession::FARMER, unit_labor::MILLER, job_skill::MILLING, "Ml"},
|
||||
{profession::FARMER, unit_labor::BREWER, job_skill::BREWING, "Br"},
|
||||
{profession::FARMER, unit_labor::HERBALIST, job_skill::HERBALISM, "He"},
|
||||
{profession::FARMER, unit_labor::PROCESS_PLANT, job_skill::PROCESSPLANTS, "Th"},
|
||||
{profession::FARMER, unit_labor::MAKE_CHEESE, job_skill::CHEESEMAKING, "Ch"},
|
||||
{profession::FARMER, unit_labor::MILK, job_skill::MILK, "Mk"},
|
||||
{profession::FARMER, unit_labor::SHEARER, job_skill::SHEARING, "Sh"},
|
||||
{profession::FARMER, unit_labor::SPINNER, job_skill::SPINNING, "Sp"},
|
||||
{profession::FARMER, unit_labor::COOK, job_skill::COOK, "Co"},
|
||||
{profession::FARMER, unit_labor::PRESSING, job_skill::PRESSING, "Pr"},
|
||||
{profession::FARMER, unit_labor::BEEKEEPING, job_skill::BEEKEEPING, "Be"},
|
||||
// Fishing/Related
|
||||
{profession::FISHERMAN, unit_labor::FISH, job_skill::FISH, "Fi"},
|
||||
{profession::FISHERMAN, unit_labor::CLEAN_FISH, job_skill::PROCESSFISH, "Cl"},
|
||||
{profession::FISHERMAN, unit_labor::DISSECT_FISH, job_skill::DISSECT_FISH, "Di"},
|
||||
// Metalsmithing
|
||||
{profession::METALSMITH, unit_labor::SMELT, job_skill::SMELT, "Fu"},
|
||||
{profession::METALSMITH, unit_labor::FORGE_WEAPON, job_skill::FORGE_WEAPON, "We"},
|
||||
{profession::METALSMITH, unit_labor::FORGE_ARMOR, job_skill::FORGE_ARMOR, "Ar"},
|
||||
{profession::METALSMITH, unit_labor::FORGE_FURNITURE, job_skill::FORGE_FURNITURE, "Bl"},
|
||||
{profession::METALSMITH, unit_labor::METAL_CRAFT, job_skill::METALCRAFT, "Cr"},
|
||||
// Jewelry
|
||||
{profession::JEWELER, unit_labor::CUT_GEM, job_skill::CUTGEM, "Cu"},
|
||||
{profession::JEWELER, unit_labor::ENCRUST_GEM, job_skill::ENCRUSTGEM, "Se"},
|
||||
// Crafts
|
||||
{profession::CRAFTSMAN, unit_labor::LEATHER, job_skill::LEATHERWORK, "Le"},
|
||||
{profession::CRAFTSMAN, unit_labor::WOOD_CRAFT, job_skill::WOODCRAFT, "Wo"},
|
||||
{profession::CRAFTSMAN, unit_labor::STONE_CRAFT, job_skill::STONECRAFT, "St"},
|
||||
{profession::CRAFTSMAN, unit_labor::BONE_CARVE, job_skill::BONECARVE, "Bo"},
|
||||
{profession::CRAFTSMAN, unit_labor::GLASSMAKER, job_skill::GLASSMAKER, "Gl"},
|
||||
{profession::CRAFTSMAN, unit_labor::WEAVER, job_skill::WEAVING, "We"},
|
||||
{profession::CRAFTSMAN, unit_labor::CLOTHESMAKER, job_skill::CLOTHESMAKING, "Cl"},
|
||||
{profession::CRAFTSMAN, unit_labor::EXTRACT_STRAND, job_skill::EXTRACT_STRAND, "Ad"},
|
||||
{profession::CRAFTSMAN, unit_labor::POTTERY, job_skill::POTTERY, "Po"},
|
||||
{profession::CRAFTSMAN, unit_labor::GLAZING, job_skill::GLAZING, "Gl"},
|
||||
{profession::CRAFTSMAN, unit_labor::WAX_WORKING, job_skill::WAX_WORKING, "Wx"},
|
||||
// Engineering
|
||||
{profession::ENGINEER, unit_labor::SIEGECRAFT, job_skill::SIEGECRAFT, "En"},
|
||||
{profession::ENGINEER, unit_labor::SIEGEOPERATE, job_skill::SIEGEOPERATE, "Op"},
|
||||
{profession::ENGINEER, unit_labor::MECHANIC, job_skill::MECHANICS, "Me"},
|
||||
{profession::ENGINEER, unit_labor::OPERATE_PUMP, job_skill::OPERATE_PUMP, "Pu"},
|
||||
// Hauling
|
||||
{profession::STANDARD, unit_labor::HAUL_STONE, job_skill::NONE, "St"},
|
||||
{profession::STANDARD, unit_labor::HAUL_WOOD, job_skill::NONE, "Wo"},
|
||||
{profession::STANDARD, unit_labor::HAUL_ITEM, job_skill::NONE, "It"},
|
||||
{profession::STANDARD, unit_labor::HAUL_BODY, job_skill::NONE, "Bu"},
|
||||
{profession::STANDARD, unit_labor::HAUL_FOOD, job_skill::NONE, "Fo"},
|
||||
{profession::STANDARD, unit_labor::HAUL_REFUSE, job_skill::NONE, "Re"},
|
||||
{profession::STANDARD, unit_labor::HAUL_FURNITURE, job_skill::NONE, "Fu"},
|
||||
{profession::STANDARD, unit_labor::HAUL_ANIMAL, job_skill::NONE, "An"},
|
||||
{profession::STANDARD, unit_labor::PUSH_HAUL_VEHICLE, job_skill::NONE, "Ve"},
|
||||
// Other Jobs
|
||||
{profession::CHILD, unit_labor::ARCHITECT, job_skill::DESIGNBUILDING, "Ar"},
|
||||
{profession::CHILD, unit_labor::ALCHEMIST, job_skill::ALCHEMY, "Al"},
|
||||
{profession::CHILD, unit_labor::CLEAN, job_skill::NONE, "Cl"},
|
||||
// Military
|
||||
{profession::WRESTLER, unit_labor::NONE, job_skill::WRESTLING, "Wr"},
|
||||
{profession::WRESTLER, unit_labor::NONE, job_skill::AXE, "Ax"},
|
||||
{profession::WRESTLER, unit_labor::NONE, job_skill::SWORD, "Sw"},
|
||||
{profession::WRESTLER, unit_labor::NONE, job_skill::MACE, "Mc"},
|
||||
{profession::WRESTLER, unit_labor::NONE, job_skill::HAMMER, "Ha"},
|
||||
{profession::WRESTLER, unit_labor::NONE, job_skill::SPEAR, "Sp"},
|
||||
{profession::WRESTLER, unit_labor::NONE, job_skill::DAGGER, "Kn"},
|
||||
{profession::WRESTLER, unit_labor::NONE, job_skill::CROSSBOW, "Cb"},
|
||||
{profession::WRESTLER, unit_labor::NONE, job_skill::BOW, "Bo"},
|
||||
{profession::WRESTLER, unit_labor::NONE, job_skill::BLOWGUN, "Bl"},
|
||||
{profession::WRESTLER, unit_labor::NONE, job_skill::PIKE, "Pk"},
|
||||
{profession::WRESTLER, unit_labor::NONE, job_skill::WHIP, "La"},
|
||||
{profession::WRESTLER, unit_labor::NONE, job_skill::ARMOR, "Ar"},
|
||||
{profession::WRESTLER, unit_labor::NONE, job_skill::SHIELD, "Sh"},
|
||||
{profession::WRESTLER, unit_labor::NONE, job_skill::BITE, "Bi"},
|
||||
{profession::WRESTLER, unit_labor::NONE, job_skill::GRASP_STRIKE, "Pu"},
|
||||
{profession::WRESTLER, unit_labor::NONE, job_skill::STANCE_STRIKE, "Ki"},
|
||||
{profession::WRESTLER, unit_labor::NONE, job_skill::DODGING, "Do"},
|
||||
{profession::WRESTLER, unit_labor::NONE, job_skill::MISC_WEAPON, "Mi"},
|
||||
{profession::WRESTLER, unit_labor::NONE, job_skill::MELEE_COMBAT, "Fi"},
|
||||
{profession::WRESTLER, unit_labor::NONE, job_skill::RANGED_COMBAT, "Ar"},
|
||||
|
||||
{profession::RECRUIT, unit_labor::NONE, job_skill::LEADERSHIP, "Le"},
|
||||
{profession::RECRUIT, unit_labor::NONE, job_skill::TEACHING, "Te"},
|
||||
{profession::RECRUIT, unit_labor::NONE, job_skill::KNOWLEDGE_ACQUISITION, "Lr"},
|
||||
{profession::RECRUIT, unit_labor::NONE, job_skill::DISCIPLINE, "Di"},
|
||||
{profession::RECRUIT, unit_labor::NONE, job_skill::CONCENTRATION, "Co"},
|
||||
{profession::RECRUIT, unit_labor::NONE, job_skill::SITUATIONAL_AWARENESS, "Aw"},
|
||||
{profession::RECRUIT, unit_labor::NONE, job_skill::COORDINATION, "Cr"},
|
||||
{profession::RECRUIT, unit_labor::NONE, job_skill::BALANCE, "Ba"},
|
||||
|
||||
// Social
|
||||
{profession::STANDARD, unit_labor::NONE, job_skill::PERSUASION, "Pe"},
|
||||
{profession::STANDARD, unit_labor::NONE, job_skill::NEGOTIATION, "Ne"},
|
||||
{profession::STANDARD, unit_labor::NONE, job_skill::JUDGING_INTENT, "Ju"},
|
||||
{profession::STANDARD, unit_labor::NONE, job_skill::LYING, "Ly"},
|
||||
{profession::STANDARD, unit_labor::NONE, job_skill::INTIMIDATION, "In"},
|
||||
{profession::STANDARD, unit_labor::NONE, job_skill::CONVERSATION, "Cn"},
|
||||
{profession::STANDARD, unit_labor::NONE, job_skill::COMEDY, "Cm"},
|
||||
{profession::STANDARD, unit_labor::NONE, job_skill::FLATTERY, "Fl"},
|
||||
{profession::STANDARD, unit_labor::NONE, job_skill::CONSOLE, "Cs"},
|
||||
{profession::STANDARD, unit_labor::NONE, job_skill::PACIFY, "Pc"},
|
||||
{profession::ADMINISTRATOR, unit_labor::NONE, job_skill::APPRAISAL, "Ap"},
|
||||
{profession::ADMINISTRATOR, unit_labor::NONE, job_skill::ORGANIZATION, "Or"},
|
||||
{profession::ADMINISTRATOR, unit_labor::NONE, job_skill::RECORD_KEEPING, "RK"},
|
||||
|
||||
// Miscellaneous
|
||||
{profession::STANDARD, unit_labor::NONE, job_skill::THROW, "Th"},
|
||||
{profession::STANDARD, unit_labor::NONE, job_skill::CRUTCH_WALK, "CW"},
|
||||
{profession::STANDARD, unit_labor::NONE, job_skill::SWIMMING, "Sw"},
|
||||
{profession::STANDARD, unit_labor::NONE, job_skill::KNAPPING, "Kn"},
|
||||
|
||||
{profession::DRUNK, unit_labor::NONE, job_skill::WRITING, "Wr"},
|
||||
{profession::DRUNK, unit_labor::NONE, job_skill::PROSE, "Pr"},
|
||||
{profession::DRUNK, unit_labor::NONE, job_skill::POETRY, "Po"},
|
||||
{profession::DRUNK, unit_labor::NONE, job_skill::READING, "Rd"},
|
||||
{profession::DRUNK, unit_labor::NONE, job_skill::SPEAKING, "Sp"},
|
||||
|
||||
{profession::ADMINISTRATOR, unit_labor::NONE, job_skill::MILITARY_TACTICS, "MT"},
|
||||
{profession::ADMINISTRATOR, unit_labor::NONE, job_skill::TRACKING, "Tr"},
|
||||
{profession::ADMINISTRATOR, unit_labor::NONE, job_skill::MAGIC_NATURE, "Dr"},
|
||||
};
|
||||
|
||||
struct UnitInfo
|
||||
{
|
||||
df::unit *unit;
|
||||
bool allowEdit;
|
||||
string name;
|
||||
string transname;
|
||||
string profession;
|
||||
int8_t color;
|
||||
};
|
||||
|
||||
#define FILTER_NONWORKERS 0x0001
|
||||
#define FILTER_NONDWARVES 0x0002
|
||||
#define FILTER_NONCIV 0x0004
|
||||
#define FILTER_ANIMALS 0x0008
|
||||
#define FILTER_LIVING 0x0010
|
||||
#define FILTER_DEAD 0x0020
|
||||
|
||||
class viewscreen_unitlaborsst : public dfhack_viewscreen {
|
||||
public:
|
||||
static viewscreen_unitlaborsst *create (char pushtype, df::viewscreen *scr = NULL);
|
||||
|
||||
void feed(set<df::interface_key> *events);
|
||||
|
||||
void render();
|
||||
void resize(int w, int h) { calcSize(); }
|
||||
|
||||
void help() { }
|
||||
|
||||
std::string getFocusString() { return "unitlabors"; }
|
||||
|
||||
viewscreen_unitlaborsst();
|
||||
~viewscreen_unitlaborsst() { };
|
||||
|
||||
protected:
|
||||
vector<UnitInfo *> units;
|
||||
int filter;
|
||||
|
||||
int first_row, sel_row;
|
||||
int first_column, sel_column;
|
||||
|
||||
int height, name_width, prof_width, labors_width;
|
||||
|
||||
// bool descending;
|
||||
// int sort_skill;
|
||||
// int sort_labor;
|
||||
|
||||
void readUnits ();
|
||||
void calcSize ();
|
||||
};
|
||||
|
||||
viewscreen_unitlaborsst::viewscreen_unitlaborsst()
|
||||
{
|
||||
filter = FILTER_LIVING;
|
||||
first_row = sel_row = 0;
|
||||
first_column = sel_column = 0;
|
||||
calcSize();
|
||||
readUnits();
|
||||
}
|
||||
|
||||
void viewscreen_unitlaborsst::readUnits ()
|
||||
{
|
||||
for (size_t i = 0; i < units.size(); i++)
|
||||
delete units[i];
|
||||
units.clear();
|
||||
|
||||
UnitInfo *cur = new UnitInfo;
|
||||
for (size_t i = 0; i < world->units.active.size(); i++)
|
||||
{
|
||||
df::unit *unit = world->units.active[i];
|
||||
cur->unit = unit;
|
||||
cur->allowEdit = true;
|
||||
|
||||
if (unit->race != ui->race_id)
|
||||
{
|
||||
cur->allowEdit = false;
|
||||
if (!(filter & FILTER_NONDWARVES))
|
||||
continue;
|
||||
}
|
||||
|
||||
if (unit->civ_id != ui->civ_id)
|
||||
{
|
||||
cur->allowEdit = false;
|
||||
if (!(filter & FILTER_NONCIV))
|
||||
continue;
|
||||
}
|
||||
|
||||
if (unit->flags1.bits.dead)
|
||||
{
|
||||
cur->allowEdit = false;
|
||||
if (!(filter & FILTER_DEAD))
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!(filter & FILTER_LIVING))
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!ENUM_ATTR(profession, can_assign_labor, unit->profession))
|
||||
{
|
||||
cur->allowEdit = false;
|
||||
if (!(filter & FILTER_NONWORKERS))
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!unit->name.first_name.length())
|
||||
{
|
||||
if (!(filter & FILTER_ANIMALS))
|
||||
continue;
|
||||
}
|
||||
|
||||
cur->name = Translation::TranslateName(&unit->name, false);
|
||||
cur->transname = Translation::TranslateName(&unit->name, true);
|
||||
cur->profession = Units::getProfessionName(unit);
|
||||
cur->color = Units::getProfessionColor(unit);
|
||||
|
||||
units.push_back(cur);
|
||||
cur = new UnitInfo;
|
||||
}
|
||||
delete cur;
|
||||
}
|
||||
|
||||
void viewscreen_unitlaborsst::calcSize()
|
||||
{
|
||||
height = gps->dimy - 10;
|
||||
if (height > units.size())
|
||||
height = units.size();
|
||||
|
||||
name_width = prof_width = labors_width = 0;
|
||||
for (int i = 4; i < gps->dimx; i++)
|
||||
{
|
||||
// 20% for Name, 20% for Profession, 60% for Labors
|
||||
switch ((i - 4) % 5)
|
||||
{
|
||||
case 0: case 2: case 4:
|
||||
labors_width++;
|
||||
break;
|
||||
case 1:
|
||||
name_width++;
|
||||
break;
|
||||
case 3:
|
||||
prof_width++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
while (labors_width > NUM_COLUMNS)
|
||||
{
|
||||
if (labors_width & 1)
|
||||
name_width++;
|
||||
else
|
||||
prof_width++;
|
||||
labors_width--;
|
||||
}
|
||||
|
||||
// don't adjust scroll position immediately after the window opened
|
||||
if (units.size() == 0)
|
||||
return;
|
||||
|
||||
// if the window grows vertically, scroll upward to eliminate blank rows from the bottom
|
||||
if (first_row > units.size() - height)
|
||||
first_row = units.size() - height;
|
||||
|
||||
// if it shrinks vertically, scroll downward to keep the cursor visible
|
||||
if (first_row < sel_row - height + 1)
|
||||
first_row = sel_row - height + 1;
|
||||
|
||||
// if the window grows horizontally, scroll to the left to eliminate blank columns from the right
|
||||
if (first_column > NUM_COLUMNS - labors_width)
|
||||
first_column = NUM_COLUMNS - labors_width;
|
||||
|
||||
// if it shrinks horizontally, scroll to the right to keep the cursor visible
|
||||
if (first_column < sel_column - labors_width + 1)
|
||||
first_column = sel_column - labors_width + 1;
|
||||
}
|
||||
|
||||
void viewscreen_unitlaborsst::feed(set<df::interface_key> *events)
|
||||
{
|
||||
if (events->count(interface_key::LEAVESCREEN))
|
||||
{
|
||||
events->clear();
|
||||
Screen::dismiss(this);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO - allow modifying filters
|
||||
|
||||
if (!units.size())
|
||||
return;
|
||||
|
||||
if (events->count(interface_key::CURSOR_UP) || events->count(interface_key::CURSOR_UPLEFT) || events->count(interface_key::CURSOR_UPRIGHT))
|
||||
sel_row--;
|
||||
if (events->count(interface_key::CURSOR_UP_FAST) || events->count(interface_key::CURSOR_UPLEFT_FAST) || events->count(interface_key::CURSOR_UPRIGHT_FAST))
|
||||
sel_row -= 10;
|
||||
if (events->count(interface_key::CURSOR_DOWN) || events->count(interface_key::CURSOR_DOWNLEFT) || events->count(interface_key::CURSOR_DOWNRIGHT))
|
||||
sel_row++;
|
||||
if (events->count(interface_key::CURSOR_DOWN_FAST) || events->count(interface_key::CURSOR_DOWNLEFT_FAST) || events->count(interface_key::CURSOR_DOWNRIGHT_FAST))
|
||||
sel_row += 10;
|
||||
|
||||
if (sel_row < 0)
|
||||
sel_row = 0;
|
||||
if (sel_row > units.size() - 1)
|
||||
sel_row = units.size() - 1;
|
||||
|
||||
if (sel_row < first_row)
|
||||
first_row = sel_row;
|
||||
if (first_row < sel_row - height + 1)
|
||||
first_row = sel_row - height + 1;
|
||||
|
||||
if (events->count(interface_key::CURSOR_LEFT) || events->count(interface_key::CURSOR_UPLEFT) || events->count(interface_key::CURSOR_DOWNLEFT))
|
||||
sel_column--;
|
||||
if (events->count(interface_key::CURSOR_LEFT_FAST) || events->count(interface_key::CURSOR_UPLEFT_FAST) || events->count(interface_key::CURSOR_DOWNLEFT_FAST))
|
||||
sel_column -= 10;
|
||||
if (events->count(interface_key::CURSOR_RIGHT) || events->count(interface_key::CURSOR_UPRIGHT) || events->count(interface_key::CURSOR_DOWNRIGHT))
|
||||
sel_column++;
|
||||
if (events->count(interface_key::CURSOR_RIGHT_FAST) || events->count(interface_key::CURSOR_UPRIGHT_FAST) || events->count(interface_key::CURSOR_DOWNRIGHT_FAST))
|
||||
sel_column += 10;
|
||||
|
||||
if ((sel_column != 0) && events->count(interface_key::CURSOR_UP_Z))
|
||||
{
|
||||
// go to beginning of current column group; if already at the beginning, go to the beginning of the previous one
|
||||
sel_column--;
|
||||
df::profession cur = columns[sel_column].profession;
|
||||
while ((sel_column > 0) && columns[sel_column - 1].profession == cur)
|
||||
sel_column--;
|
||||
}
|
||||
if ((sel_column != NUM_COLUMNS - 1) && events->count(interface_key::CURSOR_DOWN_Z))
|
||||
{
|
||||
// go to end of current column group; if already at the end, go to the end of the next one
|
||||
sel_column++;
|
||||
df::profession cur = columns[sel_column].profession;
|
||||
while ((sel_column < NUM_COLUMNS - 1) && columns[sel_column + 1].profession == cur)
|
||||
sel_column++;
|
||||
}
|
||||
|
||||
if (sel_column < 0)
|
||||
sel_column = 0;
|
||||
if (sel_column > NUM_COLUMNS - 1)
|
||||
sel_column = NUM_COLUMNS - 1;
|
||||
|
||||
if (sel_column < first_column)
|
||||
first_column = sel_column;
|
||||
if (first_column < sel_column - labors_width + 1)
|
||||
first_column = sel_column - labors_width + 1;
|
||||
|
||||
UnitInfo *cur = units[sel_row];
|
||||
if (events->count(interface_key::SELECT) && (cur->allowEdit) && (columns[sel_column].labor != unit_labor::NONE))
|
||||
{
|
||||
df::unit *unit = cur->unit;
|
||||
const SkillColumn &col = columns[sel_column];
|
||||
if (col.special)
|
||||
{
|
||||
if (!unit->status.labors[col.labor])
|
||||
{
|
||||
for (int i = 0; i < NUM_COLUMNS; i++)
|
||||
{
|
||||
if ((columns[i].labor != unit_labor::NONE) && columns[i].special)
|
||||
unit->status.labors[columns[i].labor] = false;
|
||||
}
|
||||
}
|
||||
unit->military.pickup_flags.bits.update = true;
|
||||
}
|
||||
unit->status.labors[col.labor] = !unit->status.labors[col.labor];
|
||||
}
|
||||
|
||||
// TODO: add sorting
|
||||
}
|
||||
|
||||
void viewscreen_unitlaborsst::render()
|
||||
{
|
||||
if (Screen::isDismissed(this))
|
||||
return;
|
||||
|
||||
dfhack_viewscreen::render();
|
||||
|
||||
Screen::clear();
|
||||
Screen::drawBorder(" Manage Labors ");
|
||||
|
||||
for (int col = 0; col < labors_width; col++)
|
||||
{
|
||||
int col_offset = col + first_column;
|
||||
if (col_offset >= NUM_COLUMNS)
|
||||
break;
|
||||
|
||||
int8_t fg = Units::getCasteProfessionColor(ui->race_id, -1, columns[col_offset].profession);
|
||||
int8_t bg = 0;
|
||||
|
||||
if (col_offset == sel_column)
|
||||
{
|
||||
fg = 0;
|
||||
bg = 7;
|
||||
}
|
||||
Screen::paintTile(Screen::Pen(columns[col_offset].label[0], fg, bg), 1 + name_width + 1 + prof_width + 1 + col, 1);
|
||||
Screen::paintTile(Screen::Pen(columns[col_offset].label[1], fg, bg), 1 + name_width + 1 + prof_width + 1 + col, 2);
|
||||
}
|
||||
|
||||
for (int row = 0; row < height; row++)
|
||||
{
|
||||
int row_offset = row + first_row;
|
||||
if (row_offset >= units.size())
|
||||
break;
|
||||
UnitInfo *cur = units[row_offset];
|
||||
df::unit *unit = cur->unit;
|
||||
int8_t fg = 15, bg = 0;
|
||||
if (row_offset == sel_row)
|
||||
{
|
||||
fg = 0;
|
||||
bg = 7;
|
||||
}
|
||||
|
||||
string name = cur->name;
|
||||
name.resize(name_width);
|
||||
Screen::paintString(Screen::Pen(' ', fg, bg), 1, 3 + row, name);
|
||||
|
||||
string profession = cur->profession;
|
||||
profession.resize(prof_width);
|
||||
fg = cur->color;
|
||||
bg = 0;
|
||||
Screen::paintString(Screen::Pen(' ', fg, bg), 1 + prof_width + 1, 3 + row, profession);
|
||||
|
||||
// Print unit's skills and labor assignments
|
||||
for (int col = 0; col < labors_width; col++)
|
||||
{
|
||||
int col_offset = col + first_column;
|
||||
fg = 15;
|
||||
bg = 0;
|
||||
if ((col_offset == sel_column) && (row_offset == sel_row))
|
||||
fg = 9;
|
||||
if ((columns[col_offset].labor != unit_labor::NONE) && (unit->status.labors[columns[col_offset].labor]))
|
||||
bg = 7;
|
||||
char c = '-';
|
||||
if (columns[col_offset].skill != job_skill::NONE)
|
||||
{
|
||||
df::unit_skill *skill = binsearch_in_vector<df::unit_skill,df::enum_field<df::job_skill,int16_t>>(unit->status.current_soul->skills, &df::unit_skill::id, columns[col_offset].skill);
|
||||
if ((skill != NULL) && (skill->rating || skill->experience))
|
||||
{
|
||||
int level = skill->rating;
|
||||
if (level > NUM_SKILL_LEVELS - 1)
|
||||
level = NUM_SKILL_LEVELS - 1;
|
||||
c = skill_levels[level].abbrev;
|
||||
}
|
||||
}
|
||||
Screen::paintTile(Screen::Pen(c, fg, bg), 1 + name_width + 1 + prof_width + 1 + col, 3 + row);
|
||||
}
|
||||
}
|
||||
|
||||
UnitInfo *cur = units[sel_row];
|
||||
if (cur != NULL)
|
||||
{
|
||||
df::unit *unit = cur->unit;
|
||||
string str = cur->transname;
|
||||
if (str.length())
|
||||
str += ", ";
|
||||
str += cur->profession;
|
||||
str += ":";
|
||||
|
||||
Screen::paintString(Screen::Pen(' ', 15, 0), 1, 3 + height + 2, str);
|
||||
int y = 1 + str.length() + 1;
|
||||
|
||||
if (columns[sel_column].skill == job_skill::NONE)
|
||||
{
|
||||
str = ENUM_ATTR_STR(unit_labor, caption, columns[sel_column].labor);
|
||||
if (unit->status.labors[columns[sel_column].labor])
|
||||
str += " Enabled";
|
||||
else
|
||||
str += " Not Enabled";
|
||||
Screen::paintString(Screen::Pen(' ', 9, 0), y, 3 + height + 2, str);
|
||||
}
|
||||
else
|
||||
{
|
||||
df::unit_skill *skill = binsearch_in_vector<df::unit_skill,df::enum_field<df::job_skill,int16_t>>(unit->status.current_soul->skills, &df::unit_skill::id, columns[sel_column].skill);
|
||||
if (skill)
|
||||
{
|
||||
int level = skill->rating;
|
||||
if (level > NUM_SKILL_LEVELS - 1)
|
||||
level = NUM_SKILL_LEVELS - 1;
|
||||
str = stl_sprintf("%s %s", skill_levels[level].name, ENUM_ATTR_STR(job_skill, caption_noun, columns[sel_column].skill));
|
||||
if (level != NUM_SKILL_LEVELS - 1)
|
||||
str += stl_sprintf(" (%d/%d)", skill->experience, skill_levels[level].points);
|
||||
}
|
||||
else
|
||||
str = stl_sprintf("Not %s (0/500)", ENUM_ATTR_STR(job_skill, caption_noun, columns[sel_column].skill));
|
||||
Screen::paintString(Screen::Pen(' ', 9, 0), y, 3 + height + 2, str);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO - print command help info
|
||||
}
|
||||
|
||||
struct unitlist_hook : df::viewscreen_unitlistst
|
||||
{
|
||||
typedef df::viewscreen_unitlistst interpose_base;
|
||||
|
||||
DEFINE_VMETHOD_INTERPOSE(void, feed, (set<df::interface_key> *input))
|
||||
{
|
||||
if (input->count(interface_key::UNITVIEW_PRF_PROF))
|
||||
{
|
||||
Screen::dismiss(this);
|
||||
Screen::show(new viewscreen_unitlaborsst());
|
||||
return;
|
||||
}
|
||||
INTERPOSE_NEXT(feed)(input);
|
||||
}
|
||||
};
|
||||
|
||||
IMPLEMENT_VMETHOD_INTERPOSE(unitlist_hook, feed);
|
||||
|
||||
DFhackCExport command_result plugin_init ( color_ostream &out, vector <PluginCommand> &commands)
|
||||
{
|
||||
if (gps)
|
||||
{
|
||||
if (!INTERPOSE_HOOK(unitlist_hook, feed).apply())
|
||||
out.printerr("Could not interpose viewscreen_unitlistst::feed\n");
|
||||
}
|
||||
|
||||
return CR_OK;
|
||||
}
|
||||
|
||||
DFhackCExport command_result plugin_shutdown ( color_ostream &out )
|
||||
{
|
||||
INTERPOSE_HOOK(unitlist_hook, feed).remove();
|
||||
return CR_OK;
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
-- Test lua viewscreens.
|
||||
|
||||
local gui = require 'gui'
|
||||
|
||||
local text = 'Woohoo, lua viewscreen :)'
|
||||
|
||||
local screen = mkinstance(gui.FramedScreen, {
|
||||
frame_style = gui.GREY_LINE_FRAME,
|
||||
frame_title = 'Hello World',
|
||||
frame_width = #text+6,
|
||||
frame_height = 3,
|
||||
onRenderBody = function(self, dc)
|
||||
dc:seek(3,1):string(text, COLOR_LIGHTGREEN)
|
||||
end,
|
||||
onInput = function(self,keys)
|
||||
if keys.LEAVESCREEN or keys.SELECT then
|
||||
self:dismiss()
|
||||
end
|
||||
end
|
||||
})
|
||||
|
||||
screen:show()
|
@ -0,0 +1,154 @@
|
||||
-- Shows mechanisms linked to the current building.
|
||||
|
||||
local utils = require 'utils'
|
||||
local gui = require 'gui'
|
||||
local guidm = require 'gui.dwarfmode'
|
||||
|
||||
function getBuildingName(building)
|
||||
return utils.call_with_string(building, 'getName')
|
||||
end
|
||||
|
||||
function getBuildingCenter(building)
|
||||
return xyz2pos(building.centerx, building.centery, building.z)
|
||||
end
|
||||
|
||||
function listMechanismLinks(building)
|
||||
local lst = {}
|
||||
local function push(item, mode)
|
||||
if item then
|
||||
lst[#lst+1] = {
|
||||
obj = item, mode = mode,
|
||||
name = getBuildingName(item),
|
||||
center = getBuildingCenter(item)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
push(building, 'self')
|
||||
|
||||
if not df.building_actual:is_instance(building) then
|
||||
return lst
|
||||
end
|
||||
|
||||
local item, tref, tgt
|
||||
for _,v in ipairs(building.contained_items) do
|
||||
item = v.item
|
||||
if df.item_trappartsst:is_instance(item) then
|
||||
tref = dfhack.items.getGeneralRef(item, df.general_ref_type.BUILDING_TRIGGER)
|
||||
if tref then
|
||||
push(tref:getBuilding(), 'trigger')
|
||||
end
|
||||
tref = dfhack.items.getGeneralRef(item, df.general_ref_type.BUILDING_TRIGGERTARGET)
|
||||
if tref then
|
||||
push(tref:getBuilding(), 'target')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return lst
|
||||
end
|
||||
|
||||
MechanismList = defclass(MechanismList, guidm.MenuOverlay)
|
||||
|
||||
MechanismList.focus_path = 'mechanisms'
|
||||
|
||||
function MechanismList.new(building)
|
||||
local self = {
|
||||
links = {},
|
||||
selected = 1
|
||||
}
|
||||
return mkinstance(MechanismList, self):init(building)
|
||||
end
|
||||
|
||||
function MechanismList:init(building)
|
||||
local links = listMechanismLinks(building)
|
||||
|
||||
links[1].viewport = self:getViewport()
|
||||
links[1].cursor = guidm.getCursorPos()
|
||||
if #links <= 1 then
|
||||
links[1].mode = 'none'
|
||||
end
|
||||
|
||||
self.links = links
|
||||
self.selected = 1
|
||||
return self
|
||||
end
|
||||
|
||||
local colors = {
|
||||
self = COLOR_CYAN, none = COLOR_CYAN,
|
||||
trigger = COLOR_GREEN, target = COLOR_GREEN
|
||||
}
|
||||
local icons = {
|
||||
self = 128, none = 63, trigger = 27, target = 26
|
||||
}
|
||||
|
||||
function MechanismList:onRenderBody(dc)
|
||||
dc:clear()
|
||||
dc:seek(1,1):string("Mechanism Links", COLOR_WHITE):newline()
|
||||
|
||||
for i,v in ipairs(self.links) do
|
||||
local pen = { fg=colors[v.mode], bold = (i == self.selected) }
|
||||
dc:newline(1):pen(pen):char(icons[v.mode])
|
||||
dc:advance(1):string(v.name)
|
||||
end
|
||||
|
||||
local nlinks = #self.links
|
||||
|
||||
if nlinks <= 1 then
|
||||
dc:newline():newline(1):string("This building has no links", COLOR_LIGHTRED)
|
||||
end
|
||||
|
||||
dc:newline():newline(1):pen(COLOR_WHITE)
|
||||
dc:string("Esc", COLOR_LIGHTGREEN):string(": Back, ")
|
||||
dc:string("Enter", COLOR_LIGHTGREEN):string(": Switch")
|
||||
end
|
||||
|
||||
function MechanismList:zoomToLink(link,back)
|
||||
df.global.world.selected_building = link.obj
|
||||
|
||||
if back then
|
||||
guidm.setCursorPos(link.cursor)
|
||||
self:getViewport(link.viewport):set()
|
||||
else
|
||||
guidm.setCursorPos(link.center)
|
||||
self:getViewport():reveal(link.center, 5, 0, 10):set()
|
||||
end
|
||||
end
|
||||
|
||||
function MechanismList:changeSelected(delta)
|
||||
if #self.links <= 1 then return end
|
||||
self.selected = 1 + (self.selected + delta - 1) % #self.links
|
||||
self:zoomToLink(self.links[self.selected])
|
||||
end
|
||||
|
||||
function MechanismList:onInput(keys)
|
||||
if keys.SECONDSCROLL_UP then
|
||||
self:changeSelected(-1)
|
||||
elseif keys.SECONDSCROLL_DOWN then
|
||||
self:changeSelected(1)
|
||||
elseif keys.LEAVESCREEN then
|
||||
self:dismiss()
|
||||
if self.selected ~= 1 then
|
||||
self:zoomToLink(self.links[1], true)
|
||||
end
|
||||
elseif keys.SELECT_ALL then
|
||||
if self.selected > 1 then
|
||||
self:init(self.links[self.selected].obj)
|
||||
end
|
||||
elseif keys.SELECT then
|
||||
self:dismiss()
|
||||
elseif self:simulateViewScroll(keys) then
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
if not df.viewscreen_dwarfmodest:is_instance(dfhack.gui.getCurViewscreen()) then
|
||||
qerror("This script requires the main dwarfmode view")
|
||||
end
|
||||
if df.global.ui.main.mode ~= df.ui_sidebar_mode.QueryBuilding then
|
||||
qerror("This script requires the 'q' interface mode")
|
||||
end
|
||||
|
||||
local list = MechanismList.new(df.global.world.selected_building)
|
||||
list:show()
|
||||
list:changeSelected(1)
|
Loading…
Reference in New Issue