diff --git a/data/CMakeLists.txt b/data/CMakeLists.txt index 412ffa347..8991f1c24 100644 --- a/data/CMakeLists.txt +++ b/data/CMakeLists.txt @@ -10,6 +10,9 @@ install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/quickfort/ install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/orders/ DESTINATION dfhack-config/orders/library) +install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/art/ + DESTINATION "${DFHACK_DATA_DESTINATION}/data/art") + install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/examples/ DESTINATION "${DFHACK_DATA_DESTINATION}/examples") diff --git a/data/art/dfhack.png b/data/art/dfhack.png new file mode 100644 index 000000000..6f146dc2f Binary files /dev/null and b/data/art/dfhack.png differ diff --git a/docs/changelog.txt b/docs/changelog.txt index 5d3fb25b2..bd07b58ac 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -40,6 +40,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: ## Misc Improvements - Scrollable widgets now react to mouse wheel events when the mouse is over the widget - the ``dfhack-config/scripts/`` folder is now searched for scripts by default +- `hotkeys`: overlay hotspot widget now shows the DFHack logo in graphics mode and "DFHack" in text mode ## Documentation - `overlay-dev-guide`: added troubleshooting tips and common development workflows diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index 8c0992a79..0edf10c0b 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -141,6 +141,7 @@ set(MODULE_HEADERS include/modules/Random.h include/modules/Renderer.h include/modules/Screen.h + include/modules/Textures.h include/modules/Translation.h include/modules/Units.h include/modules/World.h @@ -166,6 +167,7 @@ set(MODULE_SOURCES modules/Random.cpp modules/Renderer.cpp modules/Screen.cpp + modules/Textures.cpp modules/Translation.cpp modules/Units.cpp modules/World.cpp diff --git a/library/Core.cpp b/library/Core.cpp index 1e76baffe..69e033c02 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -52,6 +52,7 @@ using namespace std; #include "modules/EventManager.h" #include "modules/Filesystem.h" #include "modules/Gui.h" +#include "modules/Textures.h" #include "modules/World.h" #include "modules/Persistence.h" #include "RemoteServer.h" @@ -1670,8 +1671,10 @@ bool Core::Init() return false; } + cerr << "Initializing textures.\n"; + Textures::init(con); // create mutex for syncing with interactive tasks - cerr << "Initializing Plugins.\n"; + cerr << "Initializing plugins.\n"; // create plugin manager plug_mgr = new PluginManager(this); plug_mgr->init(); @@ -1765,12 +1768,7 @@ bool Core::Init() cerr << "DFHack is running.\n"; - { - auto L = Lua::Core::State; - Lua::StackUnwinder top(L); - Lua::CallLuaModuleFunction(con, L, "script-manager", "reload"); - onStateChange(con, SC_CORE_INITIALIZED); - } + onStateChange(con, SC_CORE_INITIALIZED); return true; } @@ -2138,6 +2136,13 @@ void Core::onStateChange(color_ostream &out, state_change_event event) switch (event) { + case SC_CORE_INITIALIZED: + { + auto L = Lua::Core::State; + Lua::StackUnwinder top(L); + Lua::CallLuaModuleFunction(con, L, "script-manager", "reload"); + } + break; case SC_WORLD_LOADED: case SC_WORLD_UNLOADED: case SC_MAP_LOADED: @@ -2171,6 +2176,10 @@ void Core::onStateChange(color_ostream &out, state_change_event event) evtlog << std::endl; } } + break; + case SC_VIEWSCREEN_CHANGED: + Textures::init(out); + break; default: break; } @@ -2248,6 +2257,7 @@ int Core::Shutdown ( void ) } // invalidate all modules allModules.clear(); + Textures::cleanup(); memset(&(s_mods), 0, sizeof(s_mods)); d.reset(); return -1; diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 1352b7f1a..2f1d612b2 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -57,6 +57,7 @@ distribution. #include "modules/Materials.h" #include "modules/Random.h" #include "modules/Screen.h" +#include "modules/Textures.h" #include "modules/Translation.h" #include "modules/Units.h" #include "modules/World.h" @@ -1670,6 +1671,13 @@ static const luaL_Reg dfhack_job_funcs[] = { { NULL, NULL } }; +/***** Textures module *****/ + +static const LuaWrapper::FunctionReg dfhack_textures_module[] = { + WRAPM(Textures, getDfhackLogoTexposStart), + { NULL, NULL } +}; + /***** Units module *****/ static const LuaWrapper::FunctionReg dfhack_units_module[] = { @@ -3357,6 +3365,7 @@ void OpenDFHackApi(lua_State *state) luaL_setfuncs(state, dfhack_funcs, 0); OpenModule(state, "gui", dfhack_gui_module, dfhack_gui_funcs); OpenModule(state, "job", dfhack_job_module, dfhack_job_funcs); + OpenModule(state, "textures", dfhack_textures_module); OpenModule(state, "units", dfhack_units_module, dfhack_units_funcs); OpenModule(state, "items", dfhack_items_module, dfhack_items_funcs); OpenModule(state, "maps", dfhack_maps_module, dfhack_maps_funcs); diff --git a/library/include/modules/Graphic.h b/library/include/modules/Graphic.h index fa2cc30c6..ba5bf98c3 100644 --- a/library/include/modules/Graphic.h +++ b/library/include/modules/Graphic.h @@ -45,9 +45,29 @@ namespace DFHack uint16_t w, h; } DFSDL_Rect; typedef struct + { + void *palette; // SDL_Palette* + uint8_t BitsPerPixel; + uint8_t BytesPerPixel; + uint8_t Rloss; + uint8_t Gloss; + uint8_t Bloss; + uint8_t Aloss; + uint8_t Rshift; + uint8_t Gshift; + uint8_t Bshift; + uint8_t Ashift; + uint32_t Rmask; + uint32_t Gmask; + uint32_t Bmask; + uint32_t Amask; + uint32_t colorkey; + uint8_t alpha; + } DFSDL_PixelFormat; + typedef struct { uint32_t flags; - void* format; // PixelFormat* + DFSDL_PixelFormat* format; int w, h; int pitch; void* pixels; diff --git a/library/include/modules/Textures.h b/library/include/modules/Textures.h new file mode 100644 index 000000000..390bbeed6 --- /dev/null +++ b/library/include/modules/Textures.h @@ -0,0 +1,35 @@ +#pragma once + +#include "Export.h" + +#include "ColorText.h" + +namespace DFHack { + +/** + * The Textures module - loads and provides access to DFHack textures + * \ingroup grp_modules + * \ingroup grp_textures + */ +namespace Textures { + +/** + * Call this on DFHack init and on every viewscreen change so we can reload + * and reindex textures as needed. + */ +void init(DFHack::color_ostream &out); + +/** + * Call this when DFHack is being unloaded. + * + */ +void cleanup(); + +/** + * Get first texpos for the DFHack logo. This texpos and the next 11 make up the + * 4x3 grid of logo textures that can be displayed on the UI layer. + */ +DFHACK_EXPORT long getDfhackLogoTexposStart(); + +} +} diff --git a/library/modules/Textures.cpp b/library/modules/Textures.cpp new file mode 100644 index 000000000..efbd1cdf4 --- /dev/null +++ b/library/modules/Textures.cpp @@ -0,0 +1,208 @@ +#include "Internal.h" + +#include "modules/Textures.h" + +#include "Debug.h" +#include "PluginManager.h" + +#include "df/enabler.h" + +using df::global::enabler; +using namespace DFHack; + +namespace DFHack { + DBG_DECLARE(core, textures, DebugCategory::LINFO); +} + +static bool g_loaded = false; +static long g_num_dfhack_textures = 0; +static long g_dfhack_logo_texpos_start = -1; + +static DFLibrary *g_sdl_handle = nullptr; +static DFLibrary *g_sdl_image_handle = nullptr; +static const std::vector SDL_LIBS { + "SDLreal.dll", + "SDL.framework/Versions/A/SDL", + "SDL.framework/SDL", + "libSDL-1.2.so.0" +}; +static const std::vector SDL_IMAGE_LIBS { + "SDL_image.dll", + "SDL_image.framework/Versions/A/SDL_image", + "SDL_image.framework/SDL_image", + "libSDL_image-1.2.so.0" +}; + +DFSDL_Surface * (*g_IMG_Load)(const char *) = nullptr; +int (*g_SDL_SetAlpha)(DFSDL_Surface *, uint32_t, uint8_t) = nullptr; +DFSDL_Surface * (*g_SDL_CreateRGBSurface)(uint32_t, int, int, int, uint32_t, uint32_t, uint32_t, uint32_t); +int (*g_SDL_UpperBlit)(DFSDL_Surface *, DFSDL_Rect *, DFSDL_Surface *, DFSDL_Rect *); +DFSDL_Surface * (*g_SDL_ConvertSurface)(DFSDL_Surface *, const DFSDL_PixelFormat *, uint32_t); + +void (*g_SDL_FreeSurface)(DFSDL_Surface *); + +// Converts an arbitrary Surface to something like the display format +// (32-bit RGBA), and converts magenta to transparency if convert_magenta is set +// and the source surface didn't already have an alpha channel. +// It also deletes the source surface. +// +// It uses the same pixel format (RGBA, R at lowest address) regardless of +// hardware. +DFSDL_Surface * canonicalize_format(DFSDL_Surface *src) { + DFSDL_PixelFormat fmt; + fmt.palette = NULL; + fmt.BitsPerPixel = 32; + fmt.BytesPerPixel = 4; + fmt.Rloss = fmt.Gloss = fmt.Bloss = fmt.Aloss = 0; +//#if SDL_BYTEORDER == SDL_BIG_ENDIAN +// fmt.Rshift = 24; fmt.Gshift = 16; fmt.Bshift = 8; fmt.Ashift = 0; +//#else + fmt.Rshift = 0; fmt.Gshift = 8; fmt.Bshift = 16; fmt.Ashift = 24; +//#endif + fmt.Rmask = 255 << fmt.Rshift; + fmt.Gmask = 255 << fmt.Gshift; + fmt.Bmask = 255 << fmt.Bshift; + fmt.Amask = 255 << fmt.Ashift; + fmt.colorkey = 0; + fmt.alpha = 255; + + DFSDL_Surface *tgt = g_SDL_ConvertSurface(src, &fmt, 0); // SDL_SWSURFACE + g_SDL_FreeSurface(src); + return tgt; +} + +const uint32_t TILE_WIDTH_PX = 8; +const uint32_t TILE_HEIGHT_PX = 12; + +static size_t load_textures(color_ostream & out, const char * fname, + long *texpos_start) { + if (!g_sdl_handle || !g_sdl_image_handle || !g_IMG_Load || !g_SDL_SetAlpha + || !g_SDL_CreateRGBSurface || !g_SDL_UpperBlit + || !g_SDL_ConvertSurface || !g_SDL_FreeSurface) + return 0; + + DFSDL_Surface *s = g_IMG_Load(fname); + if (!s) { + out.printerr("unable to load textures from '%s'\n", fname); + return 0; + } + + s = canonicalize_format(s); + g_SDL_SetAlpha(s, 0, 255); + int dimx = s->w / TILE_WIDTH_PX; + int dimy = s->h / TILE_HEIGHT_PX; + long count = 0; + for (int y = 0; y < dimy; y++) { + for (int x = 0; x < dimx; x++) { + DFSDL_Surface *tile = g_SDL_CreateRGBSurface(0, // SDL_SWSURFACE + TILE_WIDTH_PX, TILE_HEIGHT_PX, 32, + s->format->Rmask, s->format->Gmask, s->format->Bmask, + s->format->Amask); + g_SDL_SetAlpha(tile, 0,255); + DFSDL_Rect vp; + vp.x = TILE_WIDTH_PX * x; + vp.y = TILE_HEIGHT_PX * y; + vp.w = TILE_WIDTH_PX; + vp.h = TILE_HEIGHT_PX; + g_SDL_UpperBlit(s, &vp, tile, NULL); + if (!count++) + *texpos_start = enabler->textures.raws.size(); + enabler->textures.raws.push_back(tile); + } + } + g_SDL_FreeSurface(s); + + DEBUG(textures,out).print("loaded %zd textures from '%s'\n", count, fname); + return count; +} + +// DFHack could conceivably be loaded at any time, so we need to be able to +// handle loading textures before or after a world is loaded. +// If a world is already loaded, then append our textures to the raws. they'll +// be freed when the world is unloaded and we'll reload when we get to the title +// screen. If it's pre-world, append our textures and then adjust the "init" +// texture count so our textures will no longer be freed when worlds are +// unloaded. +// +void Textures::init(color_ostream &out) { + auto & textures = enabler->textures; + long num_textures = textures.raws.size(); + if (num_textures <= g_dfhack_logo_texpos_start) + g_loaded = false; + + if (g_loaded) + return; + + for (auto &lib_str : SDL_LIBS) { + if ((g_sdl_handle = OpenPlugin(lib_str.c_str()))) + break; + } + for (auto &lib_str : SDL_IMAGE_LIBS) { + if ((g_sdl_image_handle = OpenPlugin(lib_str.c_str()))) + break; + } + + if (!g_sdl_handle) { + out.printerr("Could not find SDL; DFHack textures not loaded.\n"); + } else if (!g_sdl_image_handle) { + out.printerr("Could not find SDL_image; DFHack textures not loaded.\n"); + } else { + #define bind(handle, name) \ + g_##name = (decltype(g_##name))LookupPlugin(handle, #name); \ + if (!g_##name) { \ + out.printerr("Could not find: " #name "; DFHack textures not loaded\n"); \ + } + + bind(g_sdl_image_handle, IMG_Load); + bind(g_sdl_handle, SDL_SetAlpha); + bind(g_sdl_handle, SDL_CreateRGBSurface); + bind(g_sdl_handle, SDL_UpperBlit); + bind(g_sdl_handle, SDL_ConvertSurface); + bind(g_sdl_handle, SDL_FreeSurface); + #undef bind + } + + bool is_pre_world = num_textures == textures.init_texture_size; + + g_num_dfhack_textures = load_textures(out, "hack/data/art/dfhack.png", + &g_dfhack_logo_texpos_start); + + DEBUG(textures,out).print("loaded %zd textures\n", g_num_dfhack_textures); + + if (is_pre_world) + textures.init_texture_size += g_num_dfhack_textures; + + // NOTE: when GL modes are supported, we'll have to re-upload textures here + + g_loaded = true; +} + +// It's ok to leave NULLs in the raws list (according to usage in g_src) +void Textures::cleanup() { + if (!g_loaded) + return; + + auto & textures = enabler->textures; + auto &raws = textures.raws; + size_t texpos_end = g_dfhack_logo_texpos_start + g_num_dfhack_textures; + for (size_t idx = g_dfhack_logo_texpos_start; idx <= texpos_end; ++idx) { + SDL_FreeSurface(raws[idx]); + raws[idx] = NULL; + } + + if (g_dfhack_logo_texpos_start == textures.init_texture_size - g_num_dfhack_textures) + textures.init_texture_size -= g_num_dfhack_textures; + + g_loaded = false; + g_num_dfhack_textures = 0; + g_dfhack_logo_texpos_start = -1; + + if (g_sdl_handle) { + ClosePlugin(g_sdl_handle); + g_sdl_handle = nullptr; + } +} + +long Textures::getDfhackLogoTexposStart() { + return g_dfhack_logo_texpos_start; +}