From: Vitaliy Date: Wed, 8 Nov 2017 22:56:20 +0000 (+0300) Subject: Move files to subdirectories (#6599) X-Git-Tag: 5.0.0~736 X-Git-Url: https://git.librecmc.org/?a=commitdiff_plain;h=20a85d76d94c9c5c7fbe198c3d0e1fbee97a485f;p=oweals%2Fminetest.git Move files to subdirectories (#6599) * Move files around --- diff --git a/build/android/jni/Android.mk b/build/android/jni/Android.mk index 854ab75a7..375ab5ce4 100644 --- a/build/android/jni/Android.mk +++ b/build/android/jni/Android.mk @@ -114,7 +114,7 @@ LOCAL_C_INCLUDES := \ LOCAL_SRC_FILES := \ jni/src/ban.cpp \ jni/src/camera.cpp \ - jni/src/cavegen.cpp \ + jni/src/mapgen/cavegen.cpp \ jni/src/chat.cpp \ jni/src/client.cpp \ jni/src/clientenvironment.cpp \ @@ -132,13 +132,13 @@ LOCAL_SRC_FILES := \ jni/src/content_sao.cpp \ jni/src/convert_json.cpp \ jni/src/craftdef.cpp \ - jni/src/database-dummy.cpp \ - jni/src/database-files.cpp \ - jni/src/database-sqlite3.cpp \ - jni/src/database.cpp \ + jni/src/database/database-dummy.cpp \ + jni/src/database/database-files.cpp \ + jni/src/database/database-sqlite3.cpp \ + jni/src/database/database.cpp \ jni/src/debug.cpp \ jni/src/defaultsettings.cpp \ - jni/src/dungeongen.cpp \ + jni/src/mapgen/dungeongen.cpp \ jni/src/emerge.cpp \ jni/src/environment.cpp \ jni/src/face_position_cache.cpp \ @@ -148,20 +148,20 @@ LOCAL_SRC_FILES := \ jni/src/game.cpp \ jni/src/genericobject.cpp \ jni/src/gettext.cpp \ - jni/src/guiChatConsole.cpp \ - jni/src/guiEditBoxWithScrollbar.cpp \ - jni/src/guiEngine.cpp \ - jni/src/guiPathSelectMenu.cpp \ - jni/src/guiFormSpecMenu.cpp \ - jni/src/guiKeyChangeMenu.cpp \ - jni/src/guiPasswordChange.cpp \ - jni/src/guiTable.cpp \ + jni/src/gui/guiChatConsole.cpp \ + jni/src/gui/guiEditBoxWithScrollbar.cpp \ + jni/src/gui/guiEngine.cpp \ + jni/src/gui/guiPathSelectMenu.cpp \ + jni/src/gui/guiFormSpecMenu.cpp \ + jni/src/gui/guiKeyChangeMenu.cpp \ + jni/src/gui/guiPasswordChange.cpp \ + jni/src/gui/guiTable.cpp \ jni/src/guiscalingfilter.cpp \ - jni/src/guiVolumeChange.cpp \ + jni/src/gui/guiVolumeChange.cpp \ jni/src/httpfetch.cpp \ jni/src/hud.cpp \ jni/src/imagefilters.cpp \ - jni/src/intlGUIEditBox.cpp \ + jni/src/gui/intlGUIEditBox.cpp \ jni/src/inventory.cpp \ jni/src/inventorymanager.cpp \ jni/src/itemdef.cpp \ @@ -175,24 +175,24 @@ LOCAL_SRC_FILES := \ jni/src/map_settings_manager.cpp \ jni/src/mapblock.cpp \ jni/src/mapblock_mesh.cpp \ - jni/src/mapgen.cpp \ - jni/src/mapgen_carpathian.cpp \ - jni/src/mapgen_flat.cpp \ - jni/src/mapgen_fractal.cpp \ - jni/src/mapgen_singlenode.cpp \ - jni/src/mapgen_v5.cpp \ - jni/src/mapgen_v6.cpp \ - jni/src/mapgen_v7.cpp \ - jni/src/mapgen_valleys.cpp \ + jni/src/mapgen/mapgen.cpp \ + jni/src/mapgen/mapgen_carpathian.cpp \ + jni/src/mapgen/mapgen_flat.cpp \ + jni/src/mapgen/mapgen_fractal.cpp \ + jni/src/mapgen/mapgen_singlenode.cpp \ + jni/src/mapgen/mapgen_v5.cpp \ + jni/src/mapgen/mapgen_v6.cpp \ + jni/src/mapgen/mapgen_v7.cpp \ + jni/src/mapgen/mapgen_valleys.cpp \ jni/src/mapnode.cpp \ jni/src/mapsector.cpp \ jni/src/mesh.cpp \ jni/src/mesh_generator_thread.cpp \ jni/src/metadata.cpp \ - jni/src/mg_biome.cpp \ - jni/src/mg_decoration.cpp \ - jni/src/mg_ore.cpp \ - jni/src/mg_schematic.cpp \ + jni/src/mapgen/mg_biome.cpp \ + jni/src/mapgen/mg_decoration.cpp \ + jni/src/mapgen/mg_ore.cpp \ + jni/src/mapgen/mg_schematic.cpp \ jni/src/minimap.cpp \ jni/src/mods.cpp \ jni/src/nameidmapping.cpp \ @@ -228,7 +228,7 @@ LOCAL_SRC_FILES := \ jni/src/subgame.cpp \ jni/src/tileanimation.cpp \ jni/src/tool.cpp \ - jni/src/treegen.cpp \ + jni/src/mapgen/treegen.cpp \ jni/src/version.cpp \ jni/src/voxel.cpp \ jni/src/voxelalgorithms.cpp \ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 82f60be86..9964b828b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -371,6 +371,9 @@ add_custom_target(GenerateVersion add_subdirectory(threading) +add_subdirectory(database) +add_subdirectory(gui) +add_subdirectory(mapgen) add_subdirectory(network) add_subdirectory(script) add_subdirectory(unittest) @@ -378,8 +381,9 @@ add_subdirectory(util) add_subdirectory(irrlicht_changes) set(common_SRCS + ${database_SRCS} + ${mapgen_SRCS} ban.cpp - cavegen.cpp chat.cpp clientiface.cpp collision.cpp @@ -388,16 +392,8 @@ set(common_SRCS content_sao.cpp convert_json.cpp craftdef.cpp - database-dummy.cpp - database-files.cpp - database-leveldb.cpp - database-postgresql.cpp - database-redis.cpp - database-sqlite3.cpp - database.cpp debug.cpp defaultsettings.cpp - dungeongen.cpp emerge.cpp environment.cpp face_position_cache.cpp @@ -414,22 +410,9 @@ set(common_SRCS map.cpp map_settings_manager.cpp mapblock.cpp - mapgen.cpp - mapgen_carpathian.cpp - mapgen_flat.cpp - mapgen_fractal.cpp - mapgen_singlenode.cpp - mapgen_v5.cpp - mapgen_v6.cpp - mapgen_v7.cpp - mapgen_valleys.cpp mapnode.cpp mapsector.cpp metadata.cpp - mg_biome.cpp - mg_decoration.cpp - mg_ore.cpp - mg_schematic.cpp modchannels.cpp mods.cpp nameidmapping.cpp @@ -462,7 +445,6 @@ set(common_SRCS tileanimation.cpp tool.cpp translation.cpp - treegen.cpp version.cpp voxel.cpp voxelalgorithms.cpp @@ -503,6 +485,7 @@ endif(BUILD_CLIENT) set(client_SRCS ${client_SRCS} ${common_SRCS} + ${gui_SRCS} ${sound_SRCS} ${client_network_SRCS} ${client_irrlicht_changes_SRCS} @@ -520,19 +503,9 @@ set(client_SRCS filecache.cpp fontengine.cpp game.cpp - guiChatConsole.cpp - guiEditBoxWithScrollbar.cpp - guiEngine.cpp - guiPathSelectMenu.cpp - guiFormSpecMenu.cpp - guiKeyChangeMenu.cpp - guiPasswordChange.cpp guiscalingfilter.cpp - guiTable.cpp - guiVolumeChange.cpp hud.cpp imagefilters.cpp - intlGUIEditBox.cpp keycode.cpp localplayer.cpp main.cpp diff --git a/src/cavegen.cpp b/src/cavegen.cpp deleted file mode 100644 index e66634517..000000000 --- a/src/cavegen.cpp +++ /dev/null @@ -1,882 +0,0 @@ -/* -Minetest -Copyright (C) 2010-2013 celeron55, Perttu Ahola - -This program is free software; you can redistribute it and/or modify -it under the terms of the GNU Lesser General Public License as published by -the Free Software Foundation; either version 2.1 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public License along -with this program; if not, write to the Free Software Foundation, Inc., -51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -*/ - -#include "util/numeric.h" -#include "map.h" -#include "mapgen.h" -#include "mapgen_v5.h" -#include "mapgen_v6.h" -#include "mapgen_v7.h" -#include "mg_biome.h" -#include "cavegen.h" - -static NoiseParams nparams_caveliquids(0, 1, v3f(150.0, 150.0, 150.0), 776, 3, 0.6, 2.0); - - -//// -//// CavesNoiseIntersection -//// - -CavesNoiseIntersection::CavesNoiseIntersection( - INodeDefManager *nodedef, BiomeManager *biomemgr, v3s16 chunksize, - NoiseParams *np_cave1, NoiseParams *np_cave2, s32 seed, float cave_width) -{ - assert(nodedef); - assert(biomemgr); - - m_ndef = nodedef; - m_bmgr = biomemgr; - - m_csize = chunksize; - m_cave_width = cave_width; - - m_ystride = m_csize.X; - m_zstride_1d = m_csize.X * (m_csize.Y + 1); - - // Noises are created using 1-down overgeneration - // A Nx-by-1-by-Nz-sized plane is at the bottom of the desired for - // re-carving the solid overtop placed for blocking sunlight - noise_cave1 = new Noise(np_cave1, seed, m_csize.X, m_csize.Y + 1, m_csize.Z); - noise_cave2 = new Noise(np_cave2, seed, m_csize.X, m_csize.Y + 1, m_csize.Z); -} - - -CavesNoiseIntersection::~CavesNoiseIntersection() -{ - delete noise_cave1; - delete noise_cave2; -} - - -void CavesNoiseIntersection::generateCaves(MMVManip *vm, - v3s16 nmin, v3s16 nmax, u8 *biomemap) -{ - assert(vm); - assert(biomemap); - - noise_cave1->perlinMap3D(nmin.X, nmin.Y - 1, nmin.Z); - noise_cave2->perlinMap3D(nmin.X, nmin.Y - 1, nmin.Z); - - const v3s16 &em = vm->m_area.getExtent(); - u32 index2d = 0; // Biomemap index - - for (s16 z = nmin.Z; z <= nmax.Z; z++) - for (s16 x = nmin.X; x <= nmax.X; x++, index2d++) { - bool column_is_open = false; // Is column open to overground - bool is_under_river = false; // Is column under river water - bool is_under_tunnel = false; // Is tunnel or is under tunnel - bool is_top_filler_above = false; // Is top or filler above node - // Indexes at column top - u32 vi = vm->m_area.index(x, nmax.Y, z); - u32 index3d = (z - nmin.Z) * m_zstride_1d + m_csize.Y * m_ystride + - (x - nmin.X); // 3D noise index - // Biome of column - Biome *biome = (Biome *)m_bmgr->getRaw(biomemap[index2d]); - u16 depth_top = biome->depth_top; - u16 base_filler = depth_top + biome->depth_filler; - u16 depth_riverbed = biome->depth_riverbed; - u16 nplaced = 0; - // Don't excavate the overgenerated stone at nmax.Y + 1, - // this creates a 'roof' over the tunnel, preventing light in - // tunnels at mapchunk borders when generating mapchunks upwards. - // This 'roof' is removed when the mapchunk above is generated. - for (s16 y = nmax.Y; y >= nmin.Y - 1; y--, - index3d -= m_ystride, - vm->m_area.add_y(em, vi, -1)) { - content_t c = vm->m_data[vi].getContent(); - - if (c == CONTENT_AIR || c == biome->c_water_top || - c == biome->c_water) { - column_is_open = true; - is_top_filler_above = false; - continue; - } - - if (c == biome->c_river_water) { - column_is_open = true; - is_under_river = true; - is_top_filler_above = false; - continue; - } - - // Ground - float d1 = contour(noise_cave1->result[index3d]); - float d2 = contour(noise_cave2->result[index3d]); - - if (d1 * d2 > m_cave_width && m_ndef->get(c).is_ground_content) { - // In tunnel and ground content, excavate - vm->m_data[vi] = MapNode(CONTENT_AIR); - is_under_tunnel = true; - // If tunnel roof is top or filler, replace with stone - if (is_top_filler_above) - vm->m_data[vi + em.X] = MapNode(biome->c_stone); - is_top_filler_above = false; - } else if (column_is_open && is_under_tunnel && - (c == biome->c_stone || c == biome->c_filler)) { - // Tunnel entrance floor, place biome surface nodes - if (is_under_river) { - if (nplaced < depth_riverbed) { - vm->m_data[vi] = MapNode(biome->c_riverbed); - is_top_filler_above = true; - nplaced++; - } else { - // Disable top/filler placement - column_is_open = false; - is_under_river = false; - is_under_tunnel = false; - } - } else if (nplaced < depth_top) { - vm->m_data[vi] = MapNode(biome->c_top); - is_top_filler_above = true; - nplaced++; - } else if (nplaced < base_filler) { - vm->m_data[vi] = MapNode(biome->c_filler); - is_top_filler_above = true; - nplaced++; - } else { - // Disable top/filler placement - column_is_open = false; - is_under_tunnel = false; - } - } else { - // Not tunnel or tunnel entrance floor - // Check node for possible replacing with stone for tunnel roof - if (c == biome->c_top || c == biome->c_filler) - is_top_filler_above = true; - - column_is_open = false; - } - } - } -} - - -//// -//// CavernsNoise -//// - -CavernsNoise::CavernsNoise( - INodeDefManager *nodedef, v3s16 chunksize, NoiseParams *np_cavern, - s32 seed, float cavern_limit, float cavern_taper, float cavern_threshold) -{ - assert(nodedef); - - m_ndef = nodedef; - - m_csize = chunksize; - m_cavern_limit = cavern_limit; - m_cavern_taper = cavern_taper; - m_cavern_threshold = cavern_threshold; - - m_ystride = m_csize.X; - m_zstride_1d = m_csize.X * (m_csize.Y + 1); - - // Noise is created using 1-down overgeneration - // A Nx-by-1-by-Nz-sized plane is at the bottom of the desired for - // re-carving the solid overtop placed for blocking sunlight - noise_cavern = new Noise(np_cavern, seed, m_csize.X, m_csize.Y + 1, m_csize.Z); - - c_water_source = m_ndef->getId("mapgen_water_source"); - if (c_water_source == CONTENT_IGNORE) - c_water_source = CONTENT_AIR; - - c_lava_source = m_ndef->getId("mapgen_lava_source"); - if (c_lava_source == CONTENT_IGNORE) - c_lava_source = CONTENT_AIR; -} - - -CavernsNoise::~CavernsNoise() -{ - delete noise_cavern; -} - - -bool CavernsNoise::generateCaverns(MMVManip *vm, v3s16 nmin, v3s16 nmax) -{ - assert(vm); - - // Calculate noise - noise_cavern->perlinMap3D(nmin.X, nmin.Y - 1, nmin.Z); - - // Cache cavern_amp values - float *cavern_amp = new float[m_csize.Y + 1]; - u8 cavern_amp_index = 0; // Index zero at column top - for (s16 y = nmax.Y; y >= nmin.Y - 1; y--, cavern_amp_index++) { - cavern_amp[cavern_amp_index] = - MYMIN((m_cavern_limit - y) / (float)m_cavern_taper, 1.0f); - } - - //// Place nodes - bool near_cavern = false; - const v3s16 &em = vm->m_area.getExtent(); - u32 index2d = 0; - - for (s16 z = nmin.Z; z <= nmax.Z; z++) - for (s16 x = nmin.X; x <= nmax.X; x++, index2d++) { - // Reset cave_amp index to column top - cavern_amp_index = 0; - // Initial voxelmanip index at column top - u32 vi = vm->m_area.index(x, nmax.Y, z); - // Initial 3D noise index at column top - u32 index3d = (z - nmin.Z) * m_zstride_1d + m_csize.Y * m_ystride + - (x - nmin.X); - // Don't excavate the overgenerated stone at node_max.Y + 1, - // this creates a 'roof' over the cavern, preventing light in - // caverns at mapchunk borders when generating mapchunks upwards. - // This 'roof' is excavated when the mapchunk above is generated. - for (s16 y = nmax.Y; y >= nmin.Y - 1; y--, - index3d -= m_ystride, - vm->m_area.add_y(em, vi, -1), - cavern_amp_index++) { - content_t c = vm->m_data[vi].getContent(); - float n_absamp_cavern = fabs(noise_cavern->result[index3d]) * - cavern_amp[cavern_amp_index]; - // Disable CavesRandomWalk at a safe distance from caverns - // to avoid excessively spreading liquids in caverns. - if (n_absamp_cavern > m_cavern_threshold - 0.1f) { - near_cavern = true; - if (n_absamp_cavern > m_cavern_threshold && - m_ndef->get(c).is_ground_content) - vm->m_data[vi] = MapNode(CONTENT_AIR); - } - } - } - - delete[] cavern_amp; - - return near_cavern; -} - - -//// -//// CavesRandomWalk -//// - -CavesRandomWalk::CavesRandomWalk( - INodeDefManager *ndef, - GenerateNotifier *gennotify, - s32 seed, - int water_level, - content_t water_source, - content_t lava_source, - int lava_depth) -{ - assert(ndef); - - this->ndef = ndef; - this->gennotify = gennotify; - this->seed = seed; - this->water_level = water_level; - this->np_caveliquids = &nparams_caveliquids; - this->lava_depth = lava_depth; - - c_water_source = water_source; - if (c_water_source == CONTENT_IGNORE) - c_water_source = ndef->getId("mapgen_water_source"); - if (c_water_source == CONTENT_IGNORE) - c_water_source = CONTENT_AIR; - - c_lava_source = lava_source; - if (c_lava_source == CONTENT_IGNORE) - c_lava_source = ndef->getId("mapgen_lava_source"); - if (c_lava_source == CONTENT_IGNORE) - c_lava_source = CONTENT_AIR; -} - - -void CavesRandomWalk::makeCave(MMVManip *vm, v3s16 nmin, v3s16 nmax, - PseudoRandom *ps, bool is_large_cave, int max_stone_height, s16 *heightmap) -{ - assert(vm); - assert(ps); - - this->vm = vm; - this->ps = ps; - this->node_min = nmin; - this->node_max = nmax; - this->heightmap = heightmap; - this->large_cave = is_large_cave; - - this->ystride = nmax.X - nmin.X + 1; - - // Set initial parameters from randomness - int dswitchint = ps->range(1, 14); - flooded = ps->range(1, 2) == 2; - - if (large_cave) { - part_max_length_rs = ps->range(2, 4); - tunnel_routepoints = ps->range(5, ps->range(15, 30)); - min_tunnel_diameter = 5; - max_tunnel_diameter = ps->range(7, ps->range(8, 24)); - } else { - part_max_length_rs = ps->range(2, 9); - tunnel_routepoints = ps->range(10, ps->range(15, 30)); - min_tunnel_diameter = 2; - max_tunnel_diameter = ps->range(2, 6); - } - - large_cave_is_flat = (ps->range(0, 1) == 0); - - main_direction = v3f(0, 0, 0); - - // Allowed route area size in nodes - ar = node_max - node_min + v3s16(1, 1, 1); - // Area starting point in nodes - of = node_min; - - // Allow a bit more - //(this should be more than the maximum radius of the tunnel) - const s16 insure = 10; - s16 more = MYMAX(MAP_BLOCKSIZE - max_tunnel_diameter / 2 - insure, 1); - ar += v3s16(1, 0, 1) * more * 2; - of -= v3s16(1, 0, 1) * more; - - route_y_min = 0; - // Allow half a diameter + 7 over stone surface - route_y_max = -of.Y + max_stone_y + max_tunnel_diameter / 2 + 7; - - // Limit maximum to area - route_y_max = rangelim(route_y_max, 0, ar.Y - 1); - - if (large_cave) { - s16 minpos = 0; - if (node_min.Y < water_level && node_max.Y > water_level) { - minpos = water_level - max_tunnel_diameter / 3 - of.Y; - route_y_max = water_level + max_tunnel_diameter / 3 - of.Y; - } - route_y_min = ps->range(minpos, minpos + max_tunnel_diameter); - route_y_min = rangelim(route_y_min, 0, route_y_max); - } - - s16 route_start_y_min = route_y_min; - s16 route_start_y_max = route_y_max; - - route_start_y_min = rangelim(route_start_y_min, 0, ar.Y - 1); - route_start_y_max = rangelim(route_start_y_max, route_start_y_min, ar.Y - 1); - - // Randomize starting position - orp.Z = (float)(ps->next() % ar.Z) + 0.5f; - orp.Y = (float)(ps->range(route_start_y_min, route_start_y_max)) + 0.5f; - orp.X = (float)(ps->next() % ar.X) + 0.5f; - - // Add generation notify begin event - if (gennotify) { - v3s16 abs_pos(of.X + orp.X, of.Y + orp.Y, of.Z + orp.Z); - GenNotifyType notifytype = large_cave ? - GENNOTIFY_LARGECAVE_BEGIN : GENNOTIFY_CAVE_BEGIN; - gennotify->addEvent(notifytype, abs_pos); - } - - // Generate some tunnel starting from orp - for (u16 j = 0; j < tunnel_routepoints; j++) - makeTunnel(j % dswitchint == 0); - - // Add generation notify end event - if (gennotify) { - v3s16 abs_pos(of.X + orp.X, of.Y + orp.Y, of.Z + orp.Z); - GenNotifyType notifytype = large_cave ? - GENNOTIFY_LARGECAVE_END : GENNOTIFY_CAVE_END; - gennotify->addEvent(notifytype, abs_pos); - } -} - - -void CavesRandomWalk::makeTunnel(bool dirswitch) -{ - if (dirswitch && !large_cave) { - main_direction.Z = ((float)(ps->next() % 20) - (float)10) / 10; - main_direction.Y = ((float)(ps->next() % 20) - (float)10) / 30; - main_direction.X = ((float)(ps->next() % 20) - (float)10) / 10; - - main_direction *= (float)ps->range(0, 10) / 10; - } - - // Randomize size - s16 min_d = min_tunnel_diameter; - s16 max_d = max_tunnel_diameter; - rs = ps->range(min_d, max_d); - s16 rs_part_max_length_rs = rs * part_max_length_rs; - - v3s16 maxlen; - if (large_cave) { - maxlen = v3s16( - rs_part_max_length_rs, - rs_part_max_length_rs / 2, - rs_part_max_length_rs - ); - } else { - maxlen = v3s16( - rs_part_max_length_rs, - ps->range(1, rs_part_max_length_rs), - rs_part_max_length_rs - ); - } - - v3f vec; - // Jump downward sometimes - if (!large_cave && ps->range(0, 12) == 0) { - vec.Z = (float)(ps->next() % (maxlen.Z * 1)) - (float)maxlen.Z / 2; - vec.Y = (float)(ps->next() % (maxlen.Y * 2)) - (float)maxlen.Y; - vec.X = (float)(ps->next() % (maxlen.X * 1)) - (float)maxlen.X / 2; - } else { - vec.Z = (float)(ps->next() % (maxlen.Z * 1)) - (float)maxlen.Z / 2; - vec.Y = (float)(ps->next() % (maxlen.Y * 1)) - (float)maxlen.Y / 2; - vec.X = (float)(ps->next() % (maxlen.X * 1)) - (float)maxlen.X / 2; - } - - // Do not make caves that are above ground. - // It is only necessary to check the startpoint and endpoint. - v3s16 p1 = v3s16(orp.X, orp.Y, orp.Z) + of + rs / 2; - v3s16 p2 = v3s16(vec.X, vec.Y, vec.Z) + p1; - if (isPosAboveSurface(p1) || isPosAboveSurface(p2)) - return; - - vec += main_direction; - - v3f rp = orp + vec; - if (rp.X < 0) - rp.X = 0; - else if (rp.X >= ar.X) - rp.X = ar.X - 1; - - if (rp.Y < route_y_min) - rp.Y = route_y_min; - else if (rp.Y >= route_y_max) - rp.Y = route_y_max - 1; - - if (rp.Z < 0) - rp.Z = 0; - else if (rp.Z >= ar.Z) - rp.Z = ar.Z - 1; - - vec = rp - orp; - - float veclen = vec.getLength(); - if (veclen < 0.05f) - veclen = 1.0f; - - // Every second section is rough - bool randomize_xz = (ps->range(1, 2) == 1); - - // Carve routes - for (float f = 0.f; f < 1.0f; f += 1.0f / veclen) - carveRoute(vec, f, randomize_xz); - - orp = rp; -} - - -void CavesRandomWalk::carveRoute(v3f vec, float f, bool randomize_xz) -{ - MapNode airnode(CONTENT_AIR); - MapNode waternode(c_water_source); - MapNode lavanode(c_lava_source); - - v3s16 startp(orp.X, orp.Y, orp.Z); - startp += of; - - float nval = NoisePerlin3D(np_caveliquids, startp.X, - startp.Y, startp.Z, seed); - MapNode liquidnode = (nval < 0.40f && node_max.Y < lava_depth) ? - lavanode : waternode; - - v3f fp = orp + vec * f; - fp.X += 0.1f * ps->range(-10, 10); - fp.Z += 0.1f * ps->range(-10, 10); - v3s16 cp(fp.X, fp.Y, fp.Z); - - s16 d0 = -rs / 2; - s16 d1 = d0 + rs; - if (randomize_xz) { - d0 += ps->range(-1, 1); - d1 += ps->range(-1, 1); - } - - bool flat_cave_floor = !large_cave && ps->range(0, 2) == 2; - - for (s16 z0 = d0; z0 <= d1; z0++) { - s16 si = rs / 2 - MYMAX(0, abs(z0) - rs / 7 - 1); - for (s16 x0 = -si - ps->range(0,1); x0 <= si - 1 + ps->range(0,1); x0++) { - s16 maxabsxz = MYMAX(abs(x0), abs(z0)); - - s16 si2 = rs / 2 - MYMAX(0, maxabsxz - rs / 7 - 1); - - for (s16 y0 = -si2; y0 <= si2; y0++) { - // Make better floors in small caves - if (flat_cave_floor && y0 <= -rs / 2 && rs <= 7) - continue; - - if (large_cave_is_flat) { - // Make large caves not so tall - if (rs > 7 && abs(y0) >= rs / 3) - continue; - } - - v3s16 p(cp.X + x0, cp.Y + y0, cp.Z + z0); - p += of; - - if (!vm->m_area.contains(p)) - continue; - - u32 i = vm->m_area.index(p); - content_t c = vm->m_data[i].getContent(); - if (!ndef->get(c).is_ground_content) - continue; - - if (large_cave) { - int full_ymin = node_min.Y - MAP_BLOCKSIZE; - int full_ymax = node_max.Y + MAP_BLOCKSIZE; - - if (flooded && full_ymin < water_level && full_ymax > water_level) - vm->m_data[i] = (p.Y <= water_level) ? waternode : airnode; - else if (flooded && full_ymax < water_level) - vm->m_data[i] = (p.Y < startp.Y - 4) ? liquidnode : airnode; - else - vm->m_data[i] = airnode; - } else { - if (c == CONTENT_IGNORE) - continue; - - vm->m_data[i] = airnode; - vm->m_flags[i] |= VMANIP_FLAG_CAVE; - } - } - } - } -} - - -inline bool CavesRandomWalk::isPosAboveSurface(v3s16 p) -{ - if (heightmap != NULL && - p.Z >= node_min.Z && p.Z <= node_max.Z && - p.X >= node_min.X && p.X <= node_max.X) { - u32 index = (p.Z - node_min.Z) * ystride + (p.X - node_min.X); - if (heightmap[index] < p.Y) - return true; - } else if (p.Y > water_level) { - return true; - } - - return false; -} - - -//// -//// CavesV6 -//// - -CavesV6::CavesV6(INodeDefManager *ndef, GenerateNotifier *gennotify, - int water_level, content_t water_source, content_t lava_source) -{ - assert(ndef); - - this->ndef = ndef; - this->gennotify = gennotify; - this->water_level = water_level; - - c_water_source = water_source; - if (c_water_source == CONTENT_IGNORE) - c_water_source = ndef->getId("mapgen_water_source"); - if (c_water_source == CONTENT_IGNORE) - c_water_source = CONTENT_AIR; - - c_lava_source = lava_source; - if (c_lava_source == CONTENT_IGNORE) - c_lava_source = ndef->getId("mapgen_lava_source"); - if (c_lava_source == CONTENT_IGNORE) - c_lava_source = CONTENT_AIR; -} - - -void CavesV6::makeCave(MMVManip *vm, v3s16 nmin, v3s16 nmax, - PseudoRandom *ps, PseudoRandom *ps2, - bool is_large_cave, int max_stone_height, s16 *heightmap) -{ - assert(vm); - assert(ps); - assert(ps2); - - this->vm = vm; - this->ps = ps; - this->ps2 = ps2; - this->node_min = nmin; - this->node_max = nmax; - this->heightmap = heightmap; - this->large_cave = is_large_cave; - - this->ystride = nmax.X - nmin.X + 1; - - // Set initial parameters from randomness - min_tunnel_diameter = 2; - max_tunnel_diameter = ps->range(2, 6); - int dswitchint = ps->range(1, 14); - if (large_cave) { - part_max_length_rs = ps->range(2, 4); - tunnel_routepoints = ps->range(5, ps->range(15, 30)); - min_tunnel_diameter = 5; - max_tunnel_diameter = ps->range(7, ps->range(8, 24)); - } else { - part_max_length_rs = ps->range(2, 9); - tunnel_routepoints = ps->range(10, ps->range(15, 30)); - } - large_cave_is_flat = (ps->range(0, 1) == 0); - - main_direction = v3f(0, 0, 0); - - // Allowed route area size in nodes - ar = node_max - node_min + v3s16(1, 1, 1); - // Area starting point in nodes - of = node_min; - - // Allow a bit more - //(this should be more than the maximum radius of the tunnel) - const s16 max_spread_amount = MAP_BLOCKSIZE; - const s16 insure = 10; - s16 more = MYMAX(max_spread_amount - max_tunnel_diameter / 2 - insure, 1); - ar += v3s16(1, 0, 1) * more * 2; - of -= v3s16(1, 0, 1) * more; - - route_y_min = 0; - // Allow half a diameter + 7 over stone surface - route_y_max = -of.Y + max_stone_height + max_tunnel_diameter / 2 + 7; - - // Limit maximum to area - route_y_max = rangelim(route_y_max, 0, ar.Y - 1); - - if (large_cave) { - s16 minpos = 0; - if (node_min.Y < water_level && node_max.Y > water_level) { - minpos = water_level - max_tunnel_diameter / 3 - of.Y; - route_y_max = water_level + max_tunnel_diameter / 3 - of.Y; - } - route_y_min = ps->range(minpos, minpos + max_tunnel_diameter); - route_y_min = rangelim(route_y_min, 0, route_y_max); - } - - s16 route_start_y_min = route_y_min; - s16 route_start_y_max = route_y_max; - - route_start_y_min = rangelim(route_start_y_min, 0, ar.Y - 1); - route_start_y_max = rangelim(route_start_y_max, route_start_y_min, ar.Y - 1); - - // Randomize starting position - orp.Z = (float)(ps->next() % ar.Z) + 0.5f; - orp.Y = (float)(ps->range(route_start_y_min, route_start_y_max)) + 0.5f; - orp.X = (float)(ps->next() % ar.X) + 0.5f; - - // Add generation notify begin event - if (gennotify != NULL) { - v3s16 abs_pos(of.X + orp.X, of.Y + orp.Y, of.Z + orp.Z); - GenNotifyType notifytype = large_cave ? - GENNOTIFY_LARGECAVE_BEGIN : GENNOTIFY_CAVE_BEGIN; - gennotify->addEvent(notifytype, abs_pos); - } - - // Generate some tunnel starting from orp - for (u16 j = 0; j < tunnel_routepoints; j++) - makeTunnel(j % dswitchint == 0); - - // Add generation notify end event - if (gennotify != NULL) { - v3s16 abs_pos(of.X + orp.X, of.Y + orp.Y, of.Z + orp.Z); - GenNotifyType notifytype = large_cave ? - GENNOTIFY_LARGECAVE_END : GENNOTIFY_CAVE_END; - gennotify->addEvent(notifytype, abs_pos); - } -} - - -void CavesV6::makeTunnel(bool dirswitch) -{ - if (dirswitch && !large_cave) { - main_direction.Z = ((float)(ps->next() % 20) - (float)10) / 10; - main_direction.Y = ((float)(ps->next() % 20) - (float)10) / 30; - main_direction.X = ((float)(ps->next() % 20) - (float)10) / 10; - - main_direction *= (float)ps->range(0, 10) / 10; - } - - // Randomize size - s16 min_d = min_tunnel_diameter; - s16 max_d = max_tunnel_diameter; - rs = ps->range(min_d, max_d); - s16 rs_part_max_length_rs = rs * part_max_length_rs; - - v3s16 maxlen; - if (large_cave) { - maxlen = v3s16( - rs_part_max_length_rs, - rs_part_max_length_rs / 2, - rs_part_max_length_rs - ); - } else { - maxlen = v3s16( - rs_part_max_length_rs, - ps->range(1, rs_part_max_length_rs), - rs_part_max_length_rs - ); - } - - v3f vec; - vec.Z = (float)(ps->next() % maxlen.Z) - (float)maxlen.Z / 2; - vec.Y = (float)(ps->next() % maxlen.Y) - (float)maxlen.Y / 2; - vec.X = (float)(ps->next() % maxlen.X) - (float)maxlen.X / 2; - - // Jump downward sometimes - if (!large_cave && ps->range(0, 12) == 0) { - vec.Z = (float)(ps->next() % maxlen.Z) - (float)maxlen.Z / 2; - vec.Y = (float)(ps->next() % (maxlen.Y * 2)) - (float)maxlen.Y; - vec.X = (float)(ps->next() % maxlen.X) - (float)maxlen.X / 2; - } - - // Do not make caves that are entirely above ground, to fix shadow bugs - // caused by overgenerated large caves. - // It is only necessary to check the startpoint and endpoint. - v3s16 p1 = v3s16(orp.X, orp.Y, orp.Z) + of + rs / 2; - v3s16 p2 = v3s16(vec.X, vec.Y, vec.Z) + p1; - - // If startpoint and endpoint are above ground, disable placement of nodes - // in carveRoute while still running all PseudoRandom calls to ensure caves - // are consistent with existing worlds. - bool tunnel_above_ground = - p1.Y > getSurfaceFromHeightmap(p1) && - p2.Y > getSurfaceFromHeightmap(p2); - - vec += main_direction; - - v3f rp = orp + vec; - if (rp.X < 0) - rp.X = 0; - else if (rp.X >= ar.X) - rp.X = ar.X - 1; - - if (rp.Y < route_y_min) - rp.Y = route_y_min; - else if (rp.Y >= route_y_max) - rp.Y = route_y_max - 1; - - if (rp.Z < 0) - rp.Z = 0; - else if (rp.Z >= ar.Z) - rp.Z = ar.Z - 1; - - vec = rp - orp; - - float veclen = vec.getLength(); - // As odd as it sounds, veclen is *exactly* 0.0 sometimes, causing a FPE - if (veclen < 0.05f) - veclen = 1.0f; - - // Every second section is rough - bool randomize_xz = (ps2->range(1, 2) == 1); - - // Carve routes - for (float f = 0.f; f < 1.0f; f += 1.0f / veclen) - carveRoute(vec, f, randomize_xz, tunnel_above_ground); - - orp = rp; -} - - -void CavesV6::carveRoute(v3f vec, float f, bool randomize_xz, - bool tunnel_above_ground) -{ - MapNode airnode(CONTENT_AIR); - MapNode waternode(c_water_source); - MapNode lavanode(c_lava_source); - - v3s16 startp(orp.X, orp.Y, orp.Z); - startp += of; - - v3f fp = orp + vec * f; - fp.X += 0.1f * ps->range(-10, 10); - fp.Z += 0.1f * ps->range(-10, 10); - v3s16 cp(fp.X, fp.Y, fp.Z); - - s16 d0 = -rs / 2; - s16 d1 = d0 + rs; - if (randomize_xz) { - d0 += ps->range(-1, 1); - d1 += ps->range(-1, 1); - } - - for (s16 z0 = d0; z0 <= d1; z0++) { - s16 si = rs / 2 - MYMAX(0, abs(z0) - rs / 7 - 1); - for (s16 x0 = -si - ps->range(0,1); x0 <= si - 1 + ps->range(0,1); x0++) { - if (tunnel_above_ground) - continue; - - s16 maxabsxz = MYMAX(abs(x0), abs(z0)); - s16 si2 = rs / 2 - MYMAX(0, maxabsxz - rs / 7 - 1); - for (s16 y0 = -si2; y0 <= si2; y0++) { - if (large_cave_is_flat) { - // Make large caves not so tall - if (rs > 7 && abs(y0) >= rs / 3) - continue; - } - - v3s16 p(cp.X + x0, cp.Y + y0, cp.Z + z0); - p += of; - - if (!vm->m_area.contains(p)) - continue; - - u32 i = vm->m_area.index(p); - content_t c = vm->m_data[i].getContent(); - if (!ndef->get(c).is_ground_content) - continue; - - if (large_cave) { - int full_ymin = node_min.Y - MAP_BLOCKSIZE; - int full_ymax = node_max.Y + MAP_BLOCKSIZE; - - if (full_ymin < water_level && full_ymax > water_level) { - vm->m_data[i] = (p.Y <= water_level) ? waternode : airnode; - } else if (full_ymax < water_level) { - vm->m_data[i] = (p.Y < startp.Y - 2) ? lavanode : airnode; - } else { - vm->m_data[i] = airnode; - } - } else { - if (c == CONTENT_IGNORE || c == CONTENT_AIR) - continue; - - vm->m_data[i] = airnode; - vm->m_flags[i] |= VMANIP_FLAG_CAVE; - } - } - } - } -} - - -inline s16 CavesV6::getSurfaceFromHeightmap(v3s16 p) -{ - if (heightmap != NULL && - p.Z >= node_min.Z && p.Z <= node_max.Z && - p.X >= node_min.X && p.X <= node_max.X) { - u32 index = (p.Z - node_min.Z) * ystride + (p.X - node_min.X); - return heightmap[index]; - } - - return water_level; - -} diff --git a/src/cavegen.h b/src/cavegen.h deleted file mode 100644 index ce146e0cd..000000000 --- a/src/cavegen.h +++ /dev/null @@ -1,242 +0,0 @@ -/* -Minetest -Copyright (C) 2010-2013 kwolekr, Ryan Kwolek - -This program is free software; you can redistribute it and/or modify -it under the terms of the GNU Lesser General Public License as published by -the Free Software Foundation; either version 2.1 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public License along -with this program; if not, write to the Free Software Foundation, Inc., -51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -*/ - -#pragma once - -#define VMANIP_FLAG_CAVE VOXELFLAG_CHECKED1 - -class GenerateNotifier; - -/* - CavesNoiseIntersection is a cave digging algorithm that carves smooth, - web-like, continuous tunnels at points where the density of the intersection - between two separate 3d noises is above a certain value. This value, - cave_width, can be modified to set the effective width of these tunnels. - - This algorithm is relatively heavyweight, taking ~80ms to generate an - 80x80x80 chunk of map on a modern processor. Use sparingly! - - TODO(hmmmm): Remove dependency on biomes - TODO(hmmmm): Find alternative to overgeneration as solution for sunlight issue -*/ -class CavesNoiseIntersection -{ -public: - CavesNoiseIntersection(INodeDefManager *nodedef, BiomeManager *biomemgr, - v3s16 chunksize, NoiseParams *np_cave1, NoiseParams *np_cave2, - s32 seed, float cave_width); - ~CavesNoiseIntersection(); - - void generateCaves(MMVManip *vm, v3s16 nmin, v3s16 nmax, u8 *biomemap); - -private: - INodeDefManager *m_ndef; - BiomeManager *m_bmgr; - - // configurable parameters - v3s16 m_csize; - float m_cave_width; - - // intermediate state variables - u16 m_ystride; - u16 m_zstride_1d; - - Noise *noise_cave1; - Noise *noise_cave2; -}; - -/* - CavernsNoise is a cave digging algorithm -*/ -class CavernsNoise -{ -public: - CavernsNoise(INodeDefManager *nodedef, v3s16 chunksize, NoiseParams *np_cavern, - s32 seed, float cavern_limit, float cavern_taper, - float cavern_threshold); - ~CavernsNoise(); - - bool generateCaverns(MMVManip *vm, v3s16 nmin, v3s16 nmax); - -private: - INodeDefManager *m_ndef; - - // configurable parameters - v3s16 m_csize; - float m_cavern_limit; - float m_cavern_taper; - float m_cavern_threshold; - - // intermediate state variables - u16 m_ystride; - u16 m_zstride_1d; - - Noise *noise_cavern; - - content_t c_water_source; - content_t c_lava_source; -}; - -/* - CavesRandomWalk is an implementation of a cave-digging algorithm that - operates on the principle of a "random walk" to approximate the stochiastic - activity of cavern development. - - In summary, this algorithm works by carving a randomly sized tunnel in a - random direction a random amount of times, randomly varying in width. - All randomness here is uniformly distributed; alternative distributions have - not yet been implemented. - - This algorithm is very fast, executing in less than 1ms on average for an - 80x80x80 chunk of map on a modern processor. -*/ -class CavesRandomWalk -{ -public: - MMVManip *vm; - INodeDefManager *ndef; - GenerateNotifier *gennotify; - s16 *heightmap; - - // configurable parameters - s32 seed; - int water_level; - int lava_depth; - NoiseParams *np_caveliquids; - - // intermediate state variables - u16 ystride; - - s16 min_tunnel_diameter; - s16 max_tunnel_diameter; - u16 tunnel_routepoints; - int part_max_length_rs; - - bool large_cave; - bool large_cave_is_flat; - bool flooded; - - s16 max_stone_y; - v3s16 node_min; - v3s16 node_max; - - v3f orp; // starting point, relative to caved space - v3s16 of; // absolute coordinates of caved space - v3s16 ar; // allowed route area - s16 rs; // tunnel radius size - v3f main_direction; - - s16 route_y_min; - s16 route_y_max; - - PseudoRandom *ps; - - content_t c_water_source; - content_t c_lava_source; - - // ndef is a mandatory parameter. - // If gennotify is NULL, generation events are not logged. - CavesRandomWalk(INodeDefManager *ndef, GenerateNotifier *gennotify = NULL, - s32 seed = 0, int water_level = 1, - content_t water_source = CONTENT_IGNORE, - content_t lava_source = CONTENT_IGNORE, int lava_depth = -256); - - // vm and ps are mandatory parameters. - // If heightmap is NULL, the surface level at all points is assumed to - // be water_level. - void makeCave(MMVManip *vm, v3s16 nmin, v3s16 nmax, PseudoRandom *ps, - bool is_large_cave, int max_stone_height, s16 *heightmap); - -private: - void makeTunnel(bool dirswitch); - void carveRoute(v3f vec, float f, bool randomize_xz); - - inline bool isPosAboveSurface(v3s16 p); -}; - -/* - CavesV6 is the original version of caves used with Mapgen V6. - - Though it uses the same fundamental algorithm as CavesRandomWalk, it is made - separate to preserve the exact sequence of PseudoRandom calls - any change - to this ordering results in the output being radically different. - Because caves in Mapgen V6 are responsible for a large portion of the basic - terrain shape, modifying this will break our contract of reverse - compatibility for a 'stable' mapgen such as V6. - - tl;dr, - *** DO NOT TOUCH THIS CLASS UNLESS YOU KNOW WHAT YOU ARE DOING *** -*/ -class CavesV6 -{ -public: - MMVManip *vm; - INodeDefManager *ndef; - GenerateNotifier *gennotify; - PseudoRandom *ps; - PseudoRandom *ps2; - - // configurable parameters - s16 *heightmap; - content_t c_water_source; - content_t c_lava_source; - int water_level; - - // intermediate state variables - u16 ystride; - - s16 min_tunnel_diameter; - s16 max_tunnel_diameter; - u16 tunnel_routepoints; - int part_max_length_rs; - - bool large_cave; - bool large_cave_is_flat; - - v3s16 node_min; - v3s16 node_max; - - v3f orp; // starting point, relative to caved space - v3s16 of; // absolute coordinates of caved space - v3s16 ar; // allowed route area - s16 rs; // tunnel radius size - v3f main_direction; - - s16 route_y_min; - s16 route_y_max; - - // ndef is a mandatory parameter. - // If gennotify is NULL, generation events are not logged. - CavesV6(INodeDefManager *ndef, GenerateNotifier *gennotify = NULL, - int water_level = 1, content_t water_source = CONTENT_IGNORE, - content_t lava_source = CONTENT_IGNORE); - - // vm, ps, and ps2 are mandatory parameters. - // If heightmap is NULL, the surface level at all points is assumed to - // be water_level. - void makeCave(MMVManip *vm, v3s16 nmin, v3s16 nmax, PseudoRandom *ps, - PseudoRandom *ps2, bool is_large_cave, int max_stone_height, - s16 *heightmap = NULL); - -private: - void makeTunnel(bool dirswitch); - void carveRoute(v3f vec, float f, bool randomize_xz, bool tunnel_above_ground); - - inline s16 getSurfaceFromHeightmap(v3s16 p); -}; diff --git a/src/client.cpp b/src/client.cpp index 2538f52aa..bca9f41d0 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -47,7 +47,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "clientmap.h" #include "clientmedia.h" #include "version.h" -#include "database-sqlite3.h" +#include "database/database-sqlite3.h" #include "serialization.h" #include "guiscalingfilter.h" #include "script/scripting_client.h" diff --git a/src/client/clientlauncher.cpp b/src/client/clientlauncher.cpp index dbaba8040..741a90d9f 100644 --- a/src/client/clientlauncher.cpp +++ b/src/client/clientlauncher.cpp @@ -17,18 +17,18 @@ with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -#include "mainmenumanager.h" +#include "gui/mainmenumanager.h" #include "clouds.h" #include "server.h" #include "filesys.h" -#include "guiMainMenu.h" +#include "gui/guiMainMenu.h" #include "game.h" #include "player.h" #include "chat.h" #include "gettext.h" #include "profiler.h" #include "serverlist.h" -#include "guiEngine.h" +#include "gui/guiEngine.h" #include "fontengine.h" #include "clientlauncher.h" #include "version.h" diff --git a/src/client/inputhandler.cpp b/src/client/inputhandler.cpp index b176f3ad7..48b94ae95 100644 --- a/src/client/inputhandler.cpp +++ b/src/client/inputhandler.cpp @@ -20,7 +20,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "util/numeric.h" #include "inputhandler.h" -#include "mainmenumanager.h" +#include "gui/mainmenumanager.h" bool MyEventReceiver::OnEvent(const SEvent &event) { diff --git a/src/client/inputhandler.h b/src/client/inputhandler.h index 249336947..165c75990 100644 --- a/src/client/inputhandler.h +++ b/src/client/inputhandler.h @@ -26,7 +26,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "renderingengine.h" #ifdef HAVE_TOUCHSCREENGUI -#include "touchscreengui.h" +#include "gui/touchscreengui.h" #endif class KeyList : private std::list diff --git a/src/client/joystick_controller.cpp b/src/client/joystick_controller.cpp index 95bd77bc4..c29e8b639 100644 --- a/src/client/joystick_controller.cpp +++ b/src/client/joystick_controller.cpp @@ -23,7 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "settings.h" #include "gettime.h" #include "porting.h" -#include "../util/string.h" +#include "util/string.h" bool JoystickButtonCmb::isTriggered(const irr::SEvent::SJoystickEvent &ev) const { diff --git a/src/database-dummy.cpp b/src/database-dummy.cpp deleted file mode 100644 index a3d8cd579..000000000 --- a/src/database-dummy.cpp +++ /dev/null @@ -1,59 +0,0 @@ -/* -Minetest -Copyright (C) 2013 celeron55, Perttu Ahola - -This program is free software; you can redistribute it and/or modify -it under the terms of the GNU Lesser General Public License as published by -the Free Software Foundation; either version 2.1 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public License along -with this program; if not, write to the Free Software Foundation, Inc., -51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -*/ - -/* -Dummy database class -*/ - -#include "database-dummy.h" - - -bool Database_Dummy::saveBlock(const v3s16 &pos, const std::string &data) -{ - m_database[getBlockAsInteger(pos)] = data; - return true; -} - -void Database_Dummy::loadBlock(const v3s16 &pos, std::string *block) -{ - s64 i = getBlockAsInteger(pos); - auto it = m_database.find(i); - if (it == m_database.end()) { - *block = ""; - return; - } - - *block = it->second; -} - -bool Database_Dummy::deleteBlock(const v3s16 &pos) -{ - m_database.erase(getBlockAsInteger(pos)); - return true; -} - -void Database_Dummy::listAllLoadableBlocks(std::vector &dst) -{ - dst.reserve(m_database.size()); - for (std::map::const_iterator x = m_database.begin(); - x != m_database.end(); ++x) { - dst.push_back(getIntegerAsBlock(x->first)); - } -} - diff --git a/src/database-dummy.h b/src/database-dummy.h deleted file mode 100644 index 2d87d58f6..000000000 --- a/src/database-dummy.h +++ /dev/null @@ -1,45 +0,0 @@ -/* -Minetest -Copyright (C) 2013 celeron55, Perttu Ahola - -This program is free software; you can redistribute it and/or modify -it under the terms of the GNU Lesser General Public License as published by -the Free Software Foundation; either version 2.1 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public License along -with this program; if not, write to the Free Software Foundation, Inc., -51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -*/ - -#pragma once - -#include -#include -#include "database.h" -#include "irrlichttypes.h" - -class Database_Dummy : public MapDatabase, public PlayerDatabase -{ -public: - bool saveBlock(const v3s16 &pos, const std::string &data); - void loadBlock(const v3s16 &pos, std::string *block); - bool deleteBlock(const v3s16 &pos); - void listAllLoadableBlocks(std::vector &dst); - - void savePlayer(RemotePlayer *player) {} - bool loadPlayer(RemotePlayer *player, PlayerSAO *sao) { return true; } - bool removePlayer(const std::string &name) { return true; } - void listPlayers(std::vector &res) {} - - void beginSave() {} - void endSave() {} - -private: - std::map m_database; -}; diff --git a/src/database-files.cpp b/src/database-files.cpp deleted file mode 100644 index 70de8c8d2..000000000 --- a/src/database-files.cpp +++ /dev/null @@ -1,179 +0,0 @@ -/* -Minetest -Copyright (C) 2017 nerzhul, Loic Blot - -This program is free software; you can redistribute it and/or modify -it under the terms of the GNU Lesser General Public License as published by -the Free Software Foundation; either version 2.1 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public License along -with this program; if not, write to the Free Software Foundation, Inc., -51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -*/ - -#include -#include -#include "database-files.h" -#include "content_sao.h" -#include "remoteplayer.h" -#include "settings.h" -#include "porting.h" -#include "filesys.h" - -// !!! WARNING !!! -// This backend is intended to be used on Minetest 0.4.16 only for the transition backend -// for player files - -void PlayerDatabaseFiles::serialize(std::ostringstream &os, RemotePlayer *player) -{ - // Utilize a Settings object for storing values - Settings args; - args.setS32("version", 1); - args.set("name", player->getName()); - - sanity_check(player->getPlayerSAO()); - args.setS32("hp", player->getPlayerSAO()->getHP()); - args.setV3F("position", player->getPlayerSAO()->getBasePosition()); - args.setFloat("pitch", player->getPlayerSAO()->getPitch()); - args.setFloat("yaw", player->getPlayerSAO()->getYaw()); - args.setS32("breath", player->getPlayerSAO()->getBreath()); - - std::string extended_attrs; - player->serializeExtraAttributes(extended_attrs); - args.set("extended_attributes", extended_attrs); - - args.writeLines(os); - - os << "PlayerArgsEnd\n"; - - player->inventory.serialize(os); -} - -void PlayerDatabaseFiles::savePlayer(RemotePlayer *player) -{ - std::string savedir = m_savedir + DIR_DELIM; - std::string path = savedir + player->getName(); - bool path_found = false; - RemotePlayer testplayer("", NULL); - - for (u32 i = 0; i < PLAYER_FILE_ALTERNATE_TRIES && !path_found; i++) { - if (!fs::PathExists(path)) { - path_found = true; - continue; - } - - // Open and deserialize file to check player name - std::ifstream is(path.c_str(), std::ios_base::binary); - if (!is.good()) { - errorstream << "Failed to open " << path << std::endl; - return; - } - - testplayer.deSerialize(is, path, NULL); - is.close(); - if (strcmp(testplayer.getName(), player->getName()) == 0) { - path_found = true; - continue; - } - - path = savedir + player->getName() + itos(i); - } - - if (!path_found) { - errorstream << "Didn't find free file for player " << player->getName() - << std::endl; - return; - } - - // Open and serialize file - std::ostringstream ss(std::ios_base::binary); - serialize(ss, player); - if (!fs::safeWriteToFile(path, ss.str())) { - infostream << "Failed to write " << path << std::endl; - } - player->setModified(false); -} - -bool PlayerDatabaseFiles::removePlayer(const std::string &name) -{ - std::string players_path = m_savedir + DIR_DELIM; - std::string path = players_path + name; - - RemotePlayer temp_player("", NULL); - for (u32 i = 0; i < PLAYER_FILE_ALTERNATE_TRIES; i++) { - // Open file and deserialize - std::ifstream is(path.c_str(), std::ios_base::binary); - if (!is.good()) - continue; - - temp_player.deSerialize(is, path, NULL); - is.close(); - - if (temp_player.getName() == name) { - fs::DeleteSingleFileOrEmptyDirectory(path); - return true; - } - - path = players_path + name + itos(i); - } - - return false; -} - -bool PlayerDatabaseFiles::loadPlayer(RemotePlayer *player, PlayerSAO *sao) -{ - std::string players_path = m_savedir + DIR_DELIM; - std::string path = players_path + player->getName(); - - const std::string player_to_load = player->getName(); - for (u32 i = 0; i < PLAYER_FILE_ALTERNATE_TRIES; i++) { - // Open file and deserialize - std::ifstream is(path.c_str(), std::ios_base::binary); - if (!is.good()) - continue; - - player->deSerialize(is, path, sao); - is.close(); - - if (player->getName() == player_to_load) - return true; - - path = players_path + player_to_load + itos(i); - } - - infostream << "Player file for player " << player_to_load << " not found" << std::endl; - return false; -} - -void PlayerDatabaseFiles::listPlayers(std::vector &res) -{ - std::vector files = fs::GetDirListing(m_savedir); - // list files into players directory - for (std::vector::const_iterator it = files.begin(); it != - files.end(); ++it) { - // Ignore directories - if (it->dir) - continue; - - const std::string &filename = it->name; - std::string full_path = m_savedir + DIR_DELIM + filename; - std::ifstream is(full_path.c_str(), std::ios_base::binary); - if (!is.good()) - continue; - - RemotePlayer player(filename.c_str(), NULL); - // Null env & dummy peer_id - PlayerSAO playerSAO(NULL, &player, 15789, false); - - player.deSerialize(is, "", &playerSAO); - is.close(); - - res.emplace_back(player.getName()); - } -} diff --git a/src/database-files.h b/src/database-files.h deleted file mode 100644 index f0824a304..000000000 --- a/src/database-files.h +++ /dev/null @@ -1,43 +0,0 @@ -/* -Minetest -Copyright (C) 2017 nerzhul, Loic Blot - -This program is free software; you can redistribute it and/or modify -it under the terms of the GNU Lesser General Public License as published by -the Free Software Foundation; either version 2.1 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public License along -with this program; if not, write to the Free Software Foundation, Inc., -51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -*/ - -#pragma once - -// !!! WARNING !!! -// This backend is intended to be used on Minetest 0.4.16 only for the transition backend -// for player files - -#include "database.h" - -class PlayerDatabaseFiles : public PlayerDatabase -{ -public: - PlayerDatabaseFiles(const std::string &savedir) : m_savedir(savedir) {} - virtual ~PlayerDatabaseFiles() = default; - - void savePlayer(RemotePlayer *player); - bool loadPlayer(RemotePlayer *player, PlayerSAO *sao); - bool removePlayer(const std::string &name); - void listPlayers(std::vector &res); - -private: - void serialize(std::ostringstream &os, RemotePlayer *player); - - std::string m_savedir; -}; diff --git a/src/database-leveldb.cpp b/src/database-leveldb.cpp deleted file mode 100644 index 4a4904c6a..000000000 --- a/src/database-leveldb.cpp +++ /dev/null @@ -1,101 +0,0 @@ -/* -Minetest -Copyright (C) 2013 celeron55, Perttu Ahola - -This program is free software; you can redistribute it and/or modify -it under the terms of the GNU Lesser General Public License as published by -the Free Software Foundation; either version 2.1 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public License along -with this program; if not, write to the Free Software Foundation, Inc., -51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -*/ - -#include "config.h" - -#if USE_LEVELDB - -#include "database-leveldb.h" - -#include "log.h" -#include "filesys.h" -#include "exceptions.h" -#include "util/string.h" - -#include "leveldb/db.h" - - -#define ENSURE_STATUS_OK(s) \ - if (!(s).ok()) { \ - throw DatabaseException(std::string("LevelDB error: ") + \ - (s).ToString()); \ - } - - -Database_LevelDB::Database_LevelDB(const std::string &savedir) -{ - leveldb::Options options; - options.create_if_missing = true; - leveldb::Status status = leveldb::DB::Open(options, - savedir + DIR_DELIM + "map.db", &m_database); - ENSURE_STATUS_OK(status); -} - -Database_LevelDB::~Database_LevelDB() -{ - delete m_database; -} - -bool Database_LevelDB::saveBlock(const v3s16 &pos, const std::string &data) -{ - leveldb::Status status = m_database->Put(leveldb::WriteOptions(), - i64tos(getBlockAsInteger(pos)), data); - if (!status.ok()) { - warningstream << "saveBlock: LevelDB error saving block " - << PP(pos) << ": " << status.ToString() << std::endl; - return false; - } - - return true; -} - -void Database_LevelDB::loadBlock(const v3s16 &pos, std::string *block) -{ - std::string datastr; - leveldb::Status status = m_database->Get(leveldb::ReadOptions(), - i64tos(getBlockAsInteger(pos)), &datastr); - - *block = (status.ok()) ? datastr : ""; -} - -bool Database_LevelDB::deleteBlock(const v3s16 &pos) -{ - leveldb::Status status = m_database->Delete(leveldb::WriteOptions(), - i64tos(getBlockAsInteger(pos))); - if (!status.ok()) { - warningstream << "deleteBlock: LevelDB error deleting block " - << PP(pos) << ": " << status.ToString() << std::endl; - return false; - } - - return true; -} - -void Database_LevelDB::listAllLoadableBlocks(std::vector &dst) -{ - leveldb::Iterator* it = m_database->NewIterator(leveldb::ReadOptions()); - for (it->SeekToFirst(); it->Valid(); it->Next()) { - dst.push_back(getIntegerAsBlock(stoi64(it->key().ToString()))); - } - ENSURE_STATUS_OK(it->status()); // Check for any errors found during the scan - delete it; -} - -#endif // USE_LEVELDB - diff --git a/src/database-leveldb.h b/src/database-leveldb.h deleted file mode 100644 index d30f9f8f5..000000000 --- a/src/database-leveldb.h +++ /dev/null @@ -1,48 +0,0 @@ -/* -Minetest -Copyright (C) 2013 celeron55, Perttu Ahola - -This program is free software; you can redistribute it and/or modify -it under the terms of the GNU Lesser General Public License as published by -the Free Software Foundation; either version 2.1 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public License along -with this program; if not, write to the Free Software Foundation, Inc., -51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -*/ - -#pragma once - -#include "config.h" - -#if USE_LEVELDB - -#include -#include "database.h" -#include "leveldb/db.h" - -class Database_LevelDB : public MapDatabase -{ -public: - Database_LevelDB(const std::string &savedir); - ~Database_LevelDB(); - - bool saveBlock(const v3s16 &pos, const std::string &data); - void loadBlock(const v3s16 &pos, std::string *block); - bool deleteBlock(const v3s16 &pos); - void listAllLoadableBlocks(std::vector &dst); - - void beginSave() {} - void endSave() {} - -private: - leveldb::DB *m_database; -}; - -#endif // USE_LEVELDB diff --git a/src/database-postgresql.cpp b/src/database-postgresql.cpp deleted file mode 100644 index 74651135a..000000000 --- a/src/database-postgresql.cpp +++ /dev/null @@ -1,631 +0,0 @@ -/* -Copyright (C) 2016 Loic Blot - -This program is free software; you can redistribute it and/or modify -it under the terms of the GNU Lesser General Public License as published by -the Free Software Foundation; either version 2.1 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public License along -with this program; if not, write to the Free Software Foundation, Inc., -51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -*/ - -#include "config.h" - -#if USE_POSTGRESQL - -#include "database-postgresql.h" - -#ifdef _WIN32 - // Without this some of the network functions are not found on mingw - #ifndef _WIN32_WINNT - #define _WIN32_WINNT 0x0501 - #endif - #include - #include -#else -#include -#endif - -#include "debug.h" -#include "exceptions.h" -#include "settings.h" -#include "content_sao.h" -#include "remoteplayer.h" - -Database_PostgreSQL::Database_PostgreSQL(const std::string &connect_string) : - m_connect_string(connect_string) -{ - if (m_connect_string.empty()) { - throw SettingNotFoundException( - "Set pgsql_connection string in world.mt to " - "use the postgresql backend\n" - "Notes:\n" - "pgsql_connection has the following form: \n" - "\tpgsql_connection = host=127.0.0.1 port=5432 user=mt_user " - "password=mt_password dbname=minetest_world\n" - "mt_user should have CREATE TABLE, INSERT, SELECT, UPDATE and " - "DELETE rights on the database.\n" - "Don't create mt_user as a SUPERUSER!"); - } -} - -Database_PostgreSQL::~Database_PostgreSQL() -{ - PQfinish(m_conn); -} - -void Database_PostgreSQL::connectToDatabase() -{ - m_conn = PQconnectdb(m_connect_string.c_str()); - - if (PQstatus(m_conn) != CONNECTION_OK) { - throw DatabaseException(std::string( - "PostgreSQL database error: ") + - PQerrorMessage(m_conn)); - } - - m_pgversion = PQserverVersion(m_conn); - - /* - * We are using UPSERT feature from PostgreSQL 9.5 - * to have the better performance where possible. - */ - if (m_pgversion < 90500) { - warningstream << "Your PostgreSQL server lacks UPSERT " - << "support. Use version 9.5 or better if possible." - << std::endl; - } - - infostream << "PostgreSQL Database: Version " << m_pgversion - << " Connection made." << std::endl; - - createDatabase(); - initStatements(); -} - -void Database_PostgreSQL::verifyDatabase() -{ - if (PQstatus(m_conn) == CONNECTION_OK) - return; - - PQreset(m_conn); - ping(); -} - -void Database_PostgreSQL::ping() -{ - if (PQping(m_connect_string.c_str()) != PQPING_OK) { - throw DatabaseException(std::string( - "PostgreSQL database error: ") + - PQerrorMessage(m_conn)); - } -} - -bool Database_PostgreSQL::initialized() const -{ - return (PQstatus(m_conn) == CONNECTION_OK); -} - -PGresult *Database_PostgreSQL::checkResults(PGresult *result, bool clear) -{ - ExecStatusType statusType = PQresultStatus(result); - - switch (statusType) { - case PGRES_COMMAND_OK: - case PGRES_TUPLES_OK: - break; - case PGRES_FATAL_ERROR: - default: - throw DatabaseException( - std::string("PostgreSQL database error: ") + - PQresultErrorMessage(result)); - } - - if (clear) - PQclear(result); - - return result; -} - -void Database_PostgreSQL::createTableIfNotExists(const std::string &table_name, - const std::string &definition) -{ - std::string sql_check_table = "SELECT relname FROM pg_class WHERE relname='" + - table_name + "';"; - PGresult *result = checkResults(PQexec(m_conn, sql_check_table.c_str()), false); - - // If table doesn't exist, create it - if (!PQntuples(result)) { - checkResults(PQexec(m_conn, definition.c_str())); - } - - PQclear(result); -} - -void Database_PostgreSQL::beginSave() -{ - verifyDatabase(); - checkResults(PQexec(m_conn, "BEGIN;")); -} - -void Database_PostgreSQL::endSave() -{ - checkResults(PQexec(m_conn, "COMMIT;")); -} - -MapDatabasePostgreSQL::MapDatabasePostgreSQL(const std::string &connect_string): - Database_PostgreSQL(connect_string), - MapDatabase() -{ - connectToDatabase(); -} - - -void MapDatabasePostgreSQL::createDatabase() -{ - createTableIfNotExists("blocks", - "CREATE TABLE blocks (" - "posX INT NOT NULL," - "posY INT NOT NULL," - "posZ INT NOT NULL," - "data BYTEA," - "PRIMARY KEY (posX,posY,posZ)" - ");" - ); - - infostream << "PostgreSQL: Map Database was initialized." << std::endl; -} - -void MapDatabasePostgreSQL::initStatements() -{ - prepareStatement("read_block", - "SELECT data FROM blocks " - "WHERE posX = $1::int4 AND posY = $2::int4 AND " - "posZ = $3::int4"); - - if (getPGVersion() < 90500) { - prepareStatement("write_block_insert", - "INSERT INTO blocks (posX, posY, posZ, data) SELECT " - "$1::int4, $2::int4, $3::int4, $4::bytea " - "WHERE NOT EXISTS (SELECT true FROM blocks " - "WHERE posX = $1::int4 AND posY = $2::int4 AND " - "posZ = $3::int4)"); - - prepareStatement("write_block_update", - "UPDATE blocks SET data = $4::bytea " - "WHERE posX = $1::int4 AND posY = $2::int4 AND " - "posZ = $3::int4"); - } else { - prepareStatement("write_block", - "INSERT INTO blocks (posX, posY, posZ, data) VALUES " - "($1::int4, $2::int4, $3::int4, $4::bytea) " - "ON CONFLICT ON CONSTRAINT blocks_pkey DO " - "UPDATE SET data = $4::bytea"); - } - - prepareStatement("delete_block", "DELETE FROM blocks WHERE " - "posX = $1::int4 AND posY = $2::int4 AND posZ = $3::int4"); - - prepareStatement("list_all_loadable_blocks", - "SELECT posX, posY, posZ FROM blocks"); -} - -bool MapDatabasePostgreSQL::saveBlock(const v3s16 &pos, const std::string &data) -{ - // Verify if we don't overflow the platform integer with the mapblock size - if (data.size() > INT_MAX) { - errorstream << "Database_PostgreSQL::saveBlock: Data truncation! " - << "data.size() over 0xFFFFFFFF (== " << data.size() - << ")" << std::endl; - return false; - } - - verifyDatabase(); - - s32 x, y, z; - x = htonl(pos.X); - y = htonl(pos.Y); - z = htonl(pos.Z); - - const void *args[] = { &x, &y, &z, data.c_str() }; - const int argLen[] = { - sizeof(x), sizeof(y), sizeof(z), (int)data.size() - }; - const int argFmt[] = { 1, 1, 1, 1 }; - - if (getPGVersion() < 90500) { - execPrepared("write_block_update", ARRLEN(args), args, argLen, argFmt); - execPrepared("write_block_insert", ARRLEN(args), args, argLen, argFmt); - } else { - execPrepared("write_block", ARRLEN(args), args, argLen, argFmt); - } - return true; -} - -void MapDatabasePostgreSQL::loadBlock(const v3s16 &pos, std::string *block) -{ - verifyDatabase(); - - s32 x, y, z; - x = htonl(pos.X); - y = htonl(pos.Y); - z = htonl(pos.Z); - - const void *args[] = { &x, &y, &z }; - const int argLen[] = { sizeof(x), sizeof(y), sizeof(z) }; - const int argFmt[] = { 1, 1, 1 }; - - PGresult *results = execPrepared("read_block", ARRLEN(args), args, - argLen, argFmt, false); - - *block = ""; - - if (PQntuples(results)) - *block = std::string(PQgetvalue(results, 0, 0), PQgetlength(results, 0, 0)); - - PQclear(results); -} - -bool MapDatabasePostgreSQL::deleteBlock(const v3s16 &pos) -{ - verifyDatabase(); - - s32 x, y, z; - x = htonl(pos.X); - y = htonl(pos.Y); - z = htonl(pos.Z); - - const void *args[] = { &x, &y, &z }; - const int argLen[] = { sizeof(x), sizeof(y), sizeof(z) }; - const int argFmt[] = { 1, 1, 1 }; - - execPrepared("delete_block", ARRLEN(args), args, argLen, argFmt); - - return true; -} - -void MapDatabasePostgreSQL::listAllLoadableBlocks(std::vector &dst) -{ - verifyDatabase(); - - PGresult *results = execPrepared("list_all_loadable_blocks", 0, - NULL, NULL, NULL, false, false); - - int numrows = PQntuples(results); - - for (int row = 0; row < numrows; ++row) - dst.push_back(pg_to_v3s16(results, 0, 0)); - - PQclear(results); -} - -/* - * Player Database - */ -PlayerDatabasePostgreSQL::PlayerDatabasePostgreSQL(const std::string &connect_string): - Database_PostgreSQL(connect_string), - PlayerDatabase() -{ - connectToDatabase(); -} - - -void PlayerDatabasePostgreSQL::createDatabase() -{ - createTableIfNotExists("player", - "CREATE TABLE player (" - "name VARCHAR(60) NOT NULL," - "pitch NUMERIC(15, 7) NOT NULL," - "yaw NUMERIC(15, 7) NOT NULL," - "posX NUMERIC(15, 7) NOT NULL," - "posY NUMERIC(15, 7) NOT NULL," - "posZ NUMERIC(15, 7) NOT NULL," - "hp INT NOT NULL," - "breath INT NOT NULL," - "creation_date TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW()," - "modification_date TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW()," - "PRIMARY KEY (name)" - ");" - ); - - createTableIfNotExists("player_inventories", - "CREATE TABLE player_inventories (" - "player VARCHAR(60) NOT NULL," - "inv_id INT NOT NULL," - "inv_width INT NOT NULL," - "inv_name TEXT NOT NULL DEFAULT ''," - "inv_size INT NOT NULL," - "PRIMARY KEY(player, inv_id)," - "CONSTRAINT player_inventories_fkey FOREIGN KEY (player) REFERENCES " - "player (name) ON DELETE CASCADE" - ");" - ); - - createTableIfNotExists("player_inventory_items", - "CREATE TABLE player_inventory_items (" - "player VARCHAR(60) NOT NULL," - "inv_id INT NOT NULL," - "slot_id INT NOT NULL," - "item TEXT NOT NULL DEFAULT ''," - "PRIMARY KEY(player, inv_id, slot_id)," - "CONSTRAINT player_inventory_items_fkey FOREIGN KEY (player) REFERENCES " - "player (name) ON DELETE CASCADE" - ");" - ); - - createTableIfNotExists("player_metadata", - "CREATE TABLE player_metadata (" - "player VARCHAR(60) NOT NULL," - "attr VARCHAR(256) NOT NULL," - "value TEXT," - "PRIMARY KEY(player, attr)," - "CONSTRAINT player_metadata_fkey FOREIGN KEY (player) REFERENCES " - "player (name) ON DELETE CASCADE" - ");" - ); - - infostream << "PostgreSQL: Player Database was inited." << std::endl; -} - -void PlayerDatabasePostgreSQL::initStatements() -{ - if (getPGVersion() < 90500) { - prepareStatement("create_player", - "INSERT INTO player(name, pitch, yaw, posX, posY, posZ, hp, breath) VALUES " - "($1, $2, $3, $4, $5, $6, $7::int, $8::int)"); - - prepareStatement("update_player", - "UPDATE SET pitch = $2, yaw = $3, posX = $4, posY = $5, posZ = $6, hp = $7::int, " - "breath = $8::int, modification_date = NOW() WHERE name = $1"); - } else { - prepareStatement("save_player", - "INSERT INTO player(name, pitch, yaw, posX, posY, posZ, hp, breath) VALUES " - "($1, $2, $3, $4, $5, $6, $7::int, $8::int)" - "ON CONFLICT ON CONSTRAINT player_pkey DO UPDATE SET pitch = $2, yaw = $3, " - "posX = $4, posY = $5, posZ = $6, hp = $7::int, breath = $8::int, " - "modification_date = NOW()"); - } - - prepareStatement("remove_player", "DELETE FROM player WHERE name = $1"); - - prepareStatement("load_player_list", "SELECT name FROM player"); - - prepareStatement("remove_player_inventories", - "DELETE FROM player_inventories WHERE player = $1"); - - prepareStatement("remove_player_inventory_items", - "DELETE FROM player_inventory_items WHERE player = $1"); - - prepareStatement("add_player_inventory", - "INSERT INTO player_inventories (player, inv_id, inv_width, inv_name, inv_size) VALUES " - "($1, $2::int, $3::int, $4, $5::int)"); - - prepareStatement("add_player_inventory_item", - "INSERT INTO player_inventory_items (player, inv_id, slot_id, item) VALUES " - "($1, $2::int, $3::int, $4)"); - - prepareStatement("load_player_inventories", - "SELECT inv_id, inv_width, inv_name, inv_size FROM player_inventories " - "WHERE player = $1 ORDER BY inv_id"); - - prepareStatement("load_player_inventory_items", - "SELECT slot_id, item FROM player_inventory_items WHERE " - "player = $1 AND inv_id = $2::int"); - - prepareStatement("load_player", - "SELECT pitch, yaw, posX, posY, posZ, hp, breath FROM player WHERE name = $1"); - - prepareStatement("remove_player_metadata", - "DELETE FROM player_metadata WHERE player = $1"); - - prepareStatement("save_player_metadata", - "INSERT INTO player_metadata (player, attr, value) VALUES ($1, $2, $3)"); - - prepareStatement("load_player_metadata", - "SELECT attr, value FROM player_metadata WHERE player = $1"); - -} - -bool PlayerDatabasePostgreSQL::playerDataExists(const std::string &playername) -{ - verifyDatabase(); - - const char *values[] = { playername.c_str() }; - PGresult *results = execPrepared("load_player", 1, values, false); - - bool res = (PQntuples(results) > 0); - PQclear(results); - return res; -} - -void PlayerDatabasePostgreSQL::savePlayer(RemotePlayer *player) -{ - PlayerSAO* sao = player->getPlayerSAO(); - if (!sao) - return; - - verifyDatabase(); - - v3f pos = sao->getBasePosition(); - std::string pitch = ftos(sao->getPitch()); - std::string yaw = ftos(sao->getYaw()); - std::string posx = ftos(pos.X); - std::string posy = ftos(pos.Y); - std::string posz = ftos(pos.Z); - std::string hp = itos(sao->getHP()); - std::string breath = itos(sao->getBreath()); - const char *values[] = { - player->getName(), - pitch.c_str(), - yaw.c_str(), - posx.c_str(), posy.c_str(), posz.c_str(), - hp.c_str(), - breath.c_str() - }; - - const char* rmvalues[] = { player->getName() }; - beginSave(); - - if (getPGVersion() < 90500) { - if (!playerDataExists(player->getName())) - execPrepared("create_player", 8, values, true, false); - else - execPrepared("update_player", 8, values, true, false); - } - else - execPrepared("save_player", 8, values, true, false); - - // Write player inventories - execPrepared("remove_player_inventories", 1, rmvalues); - execPrepared("remove_player_inventory_items", 1, rmvalues); - - std::vector inventory_lists = sao->getInventory()->getLists(); - for (u16 i = 0; i < inventory_lists.size(); i++) { - const InventoryList* list = inventory_lists[i]; - const std::string &name = list->getName(); - std::string width = itos(list->getWidth()), - inv_id = itos(i), lsize = itos(list->getSize()); - - const char* inv_values[] = { - player->getName(), - inv_id.c_str(), - width.c_str(), - name.c_str(), - lsize.c_str() - }; - execPrepared("add_player_inventory", 5, inv_values); - - for (u32 j = 0; j < list->getSize(); j++) { - std::ostringstream os; - list->getItem(j).serialize(os); - std::string itemStr = os.str(), slotId = itos(j); - - const char* invitem_values[] = { - player->getName(), - inv_id.c_str(), - slotId.c_str(), - itemStr.c_str() - }; - execPrepared("add_player_inventory_item", 4, invitem_values); - } - } - - execPrepared("remove_player_metadata", 1, rmvalues); - const PlayerAttributes &attrs = sao->getExtendedAttributes(); - for (const auto &attr : attrs) { - const char *meta_values[] = { - player->getName(), - attr.first.c_str(), - attr.second.c_str() - }; - execPrepared("save_player_metadata", 3, meta_values); - } - endSave(); -} - -bool PlayerDatabasePostgreSQL::loadPlayer(RemotePlayer *player, PlayerSAO *sao) -{ - sanity_check(sao); - verifyDatabase(); - - const char *values[] = { player->getName() }; - PGresult *results = execPrepared("load_player", 1, values, false, false); - - // Player not found, return not found - if (!PQntuples(results)) { - PQclear(results); - return false; - } - - sao->setPitch(pg_to_float(results, 0, 0)); - sao->setYaw(pg_to_float(results, 0, 1)); - sao->setBasePosition(v3f( - pg_to_float(results, 0, 2), - pg_to_float(results, 0, 3), - pg_to_float(results, 0, 4)) - ); - sao->setHPRaw((s16) pg_to_int(results, 0, 5)); - sao->setBreath((u16) pg_to_int(results, 0, 6), false); - - PQclear(results); - - // Load inventory - results = execPrepared("load_player_inventories", 1, values, false, false); - - int resultCount = PQntuples(results); - - for (int row = 0; row < resultCount; ++row) { - InventoryList* invList = player->inventory. - addList(PQgetvalue(results, row, 2), pg_to_uint(results, row, 3)); - invList->setWidth(pg_to_uint(results, row, 1)); - - u32 invId = pg_to_uint(results, row, 0); - std::string invIdStr = itos(invId); - - const char* values2[] = { - player->getName(), - invIdStr.c_str() - }; - PGresult *results2 = execPrepared("load_player_inventory_items", 2, - values2, false, false); - - int resultCount2 = PQntuples(results2); - for (int row2 = 0; row2 < resultCount2; row2++) { - const std::string itemStr = PQgetvalue(results2, row2, 1); - if (itemStr.length() > 0) { - ItemStack stack; - stack.deSerialize(itemStr); - invList->changeItem(pg_to_uint(results2, row2, 0), stack); - } - } - PQclear(results2); - } - - PQclear(results); - - results = execPrepared("load_player_metadata", 1, values, false); - - int numrows = PQntuples(results); - for (int row = 0; row < numrows; row++) { - sao->setExtendedAttribute(PQgetvalue(results, row, 0),PQgetvalue(results, row, 1)); - } - - PQclear(results); - - return true; -} - -bool PlayerDatabasePostgreSQL::removePlayer(const std::string &name) -{ - if (!playerDataExists(name)) - return false; - - verifyDatabase(); - - const char *values[] = { name.c_str() }; - execPrepared("remove_player", 1, values); - - return true; -} - -void PlayerDatabasePostgreSQL::listPlayers(std::vector &res) -{ - verifyDatabase(); - - PGresult *results = execPrepared("load_player_list", 0, NULL, false); - - int numrows = PQntuples(results); - for (int row = 0; row < numrows; row++) - res.emplace_back(PQgetvalue(results, row, 0)); - - PQclear(results); -} - -#endif // USE_POSTGRESQL diff --git a/src/database-postgresql.h b/src/database-postgresql.h deleted file mode 100644 index db0b505c9..000000000 --- a/src/database-postgresql.h +++ /dev/null @@ -1,146 +0,0 @@ -/* -Minetest -Copyright (C) 2013 celeron55, Perttu Ahola - -This program is free software; you can redistribute it and/or modify -it under the terms of the GNU Lesser General Public License as published by -the Free Software Foundation; either version 2.1 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public License along -with this program; if not, write to the Free Software Foundation, Inc., -51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -*/ - -#pragma once - -#include -#include -#include "database.h" -#include "util/basic_macros.h" - -class Settings; - -class Database_PostgreSQL: public Database -{ -public: - Database_PostgreSQL(const std::string &connect_string); - ~Database_PostgreSQL(); - - void beginSave(); - void endSave(); - - bool initialized() const; - - -protected: - // Conversion helpers - inline int pg_to_int(PGresult *res, int row, int col) - { - return atoi(PQgetvalue(res, row, col)); - } - - inline u32 pg_to_uint(PGresult *res, int row, int col) - { - return (u32) atoi(PQgetvalue(res, row, col)); - } - - inline float pg_to_float(PGresult *res, int row, int col) - { - return (float) atof(PQgetvalue(res, row, col)); - } - - inline v3s16 pg_to_v3s16(PGresult *res, int row, int col) - { - return v3s16( - pg_to_int(res, row, col), - pg_to_int(res, row, col + 1), - pg_to_int(res, row, col + 2) - ); - } - - inline PGresult *execPrepared(const char *stmtName, const int paramsNumber, - const void **params, - const int *paramsLengths = NULL, const int *paramsFormats = NULL, - bool clear = true, bool nobinary = true) - { - return checkResults(PQexecPrepared(m_conn, stmtName, paramsNumber, - (const char* const*) params, paramsLengths, paramsFormats, - nobinary ? 1 : 0), clear); - } - - inline PGresult *execPrepared(const char *stmtName, const int paramsNumber, - const char **params, bool clear = true, bool nobinary = true) - { - return execPrepared(stmtName, paramsNumber, - (const void **)params, NULL, NULL, clear, nobinary); - } - - void createTableIfNotExists(const std::string &table_name, const std::string &definition); - void verifyDatabase(); - - // Database initialization - void connectToDatabase(); - virtual void createDatabase() = 0; - virtual void initStatements() = 0; - inline void prepareStatement(const std::string &name, const std::string &sql) - { - checkResults(PQprepare(m_conn, name.c_str(), sql.c_str(), 0, NULL)); - } - - const int getPGVersion() const { return m_pgversion; } -private: - // Database connectivity checks - void ping(); - - // Database usage - PGresult *checkResults(PGresult *res, bool clear = true); - - // Attributes - std::string m_connect_string; - PGconn *m_conn = nullptr; - int m_pgversion = 0; -}; - -class MapDatabasePostgreSQL : private Database_PostgreSQL, public MapDatabase -{ -public: - MapDatabasePostgreSQL(const std::string &connect_string); - virtual ~MapDatabasePostgreSQL() = default; - - bool saveBlock(const v3s16 &pos, const std::string &data); - void loadBlock(const v3s16 &pos, std::string *block); - bool deleteBlock(const v3s16 &pos); - void listAllLoadableBlocks(std::vector &dst); - - void beginSave() { Database_PostgreSQL::beginSave(); } - void endSave() { Database_PostgreSQL::endSave(); } - -protected: - virtual void createDatabase(); - virtual void initStatements(); -}; - -class PlayerDatabasePostgreSQL : private Database_PostgreSQL, public PlayerDatabase -{ -public: - PlayerDatabasePostgreSQL(const std::string &connect_string); - virtual ~PlayerDatabasePostgreSQL() = default; - - void savePlayer(RemotePlayer *player); - bool loadPlayer(RemotePlayer *player, PlayerSAO *sao); - bool removePlayer(const std::string &name); - void listPlayers(std::vector &res); - -protected: - virtual void createDatabase(); - virtual void initStatements(); - -private: - bool playerDataExists(const std::string &playername); -}; diff --git a/src/database-redis.cpp b/src/database-redis.cpp deleted file mode 100644 index 096ea504d..000000000 --- a/src/database-redis.cpp +++ /dev/null @@ -1,203 +0,0 @@ -/* -Minetest -Copyright (C) 2014 celeron55, Perttu Ahola - -This program is free software; you can redistribute it and/or modify -it under the terms of the GNU Lesser General Public License as published by -the Free Software Foundation; either version 2.1 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public License along -with this program; if not, write to the Free Software Foundation, Inc., -51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -*/ - -#include "config.h" - -#if USE_REDIS - -#include "database-redis.h" - -#include "settings.h" -#include "log.h" -#include "exceptions.h" -#include "util/string.h" - -#include -#include - - -Database_Redis::Database_Redis(Settings &conf) -{ - std::string tmp; - try { - tmp = conf.get("redis_address"); - hash = conf.get("redis_hash"); - } catch (SettingNotFoundException &) { - throw SettingNotFoundException("Set redis_address and " - "redis_hash in world.mt to use the redis backend"); - } - const char *addr = tmp.c_str(); - int port = conf.exists("redis_port") ? conf.getU16("redis_port") : 6379; - // if redis_address contains '/' assume unix socket, else hostname/ip - ctx = tmp.find('/') != std::string::npos ? redisConnectUnix(addr) : redisConnect(addr, port); - if (!ctx) { - throw DatabaseException("Cannot allocate redis context"); - } else if (ctx->err) { - std::string err = std::string("Connection error: ") + ctx->errstr; - redisFree(ctx); - throw DatabaseException(err); - } - if (conf.exists("redis_password")) { - tmp = conf.get("redis_password"); - redisReply *reply = static_cast(redisCommand(ctx, "AUTH %s", tmp.c_str())); - if (!reply) - throw DatabaseException("Redis authentication failed"); - if (reply->type == REDIS_REPLY_ERROR) { - std::string err = "Redis authentication failed: " + std::string(reply->str, reply->len); - freeReplyObject(reply); - throw DatabaseException(err); - } - freeReplyObject(reply); - } -} - -Database_Redis::~Database_Redis() -{ - redisFree(ctx); -} - -void Database_Redis::beginSave() { - redisReply *reply = static_cast(redisCommand(ctx, "MULTI")); - if (!reply) { - throw DatabaseException(std::string( - "Redis command 'MULTI' failed: ") + ctx->errstr); - } - freeReplyObject(reply); -} - -void Database_Redis::endSave() { - redisReply *reply = static_cast(redisCommand(ctx, "EXEC")); - if (!reply) { - throw DatabaseException(std::string( - "Redis command 'EXEC' failed: ") + ctx->errstr); - } - freeReplyObject(reply); -} - -bool Database_Redis::saveBlock(const v3s16 &pos, const std::string &data) -{ - std::string tmp = i64tos(getBlockAsInteger(pos)); - - redisReply *reply = static_cast(redisCommand(ctx, "HSET %s %s %b", - hash.c_str(), tmp.c_str(), data.c_str(), data.size())); - if (!reply) { - warningstream << "saveBlock: redis command 'HSET' failed on " - "block " << PP(pos) << ": " << ctx->errstr << std::endl; - freeReplyObject(reply); - return false; - } - - if (reply->type == REDIS_REPLY_ERROR) { - warningstream << "saveBlock: saving block " << PP(pos) - << " failed: " << std::string(reply->str, reply->len) << std::endl; - freeReplyObject(reply); - return false; - } - - freeReplyObject(reply); - return true; -} - -void Database_Redis::loadBlock(const v3s16 &pos, std::string *block) -{ - std::string tmp = i64tos(getBlockAsInteger(pos)); - redisReply *reply = static_cast(redisCommand(ctx, - "HGET %s %s", hash.c_str(), tmp.c_str())); - - if (!reply) { - throw DatabaseException(std::string( - "Redis command 'HGET %s %s' failed: ") + ctx->errstr); - } - - switch (reply->type) { - case REDIS_REPLY_STRING: { - *block = std::string(reply->str, reply->len); - // std::string copies the memory so this won't cause any problems - freeReplyObject(reply); - return; - } - case REDIS_REPLY_ERROR: { - std::string errstr(reply->str, reply->len); - freeReplyObject(reply); - errorstream << "loadBlock: loading block " << PP(pos) - << " failed: " << errstr << std::endl; - throw DatabaseException(std::string( - "Redis command 'HGET %s %s' errored: ") + errstr); - } - case REDIS_REPLY_NIL: { - *block = ""; - // block not found in database - freeReplyObject(reply); - return; - } - } - - errorstream << "loadBlock: loading block " << PP(pos) - << " returned invalid reply type " << reply->type - << ": " << std::string(reply->str, reply->len) << std::endl; - freeReplyObject(reply); - throw DatabaseException(std::string( - "Redis command 'HGET %s %s' gave invalid reply.")); -} - -bool Database_Redis::deleteBlock(const v3s16 &pos) -{ - std::string tmp = i64tos(getBlockAsInteger(pos)); - - redisReply *reply = static_cast(redisCommand(ctx, - "HDEL %s %s", hash.c_str(), tmp.c_str())); - if (!reply) { - throw DatabaseException(std::string( - "Redis command 'HDEL %s %s' failed: ") + ctx->errstr); - } else if (reply->type == REDIS_REPLY_ERROR) { - warningstream << "deleteBlock: deleting block " << PP(pos) - << " failed: " << std::string(reply->str, reply->len) << std::endl; - freeReplyObject(reply); - return false; - } - - freeReplyObject(reply); - return true; -} - -void Database_Redis::listAllLoadableBlocks(std::vector &dst) -{ - redisReply *reply = static_cast(redisCommand(ctx, "HKEYS %s", hash.c_str())); - if (!reply) { - throw DatabaseException(std::string( - "Redis command 'HKEYS %s' failed: ") + ctx->errstr); - } - switch (reply->type) { - case REDIS_REPLY_ARRAY: - dst.reserve(reply->elements); - for (size_t i = 0; i < reply->elements; i++) { - assert(reply->element[i]->type == REDIS_REPLY_STRING); - dst.push_back(getIntegerAsBlock(stoi64(reply->element[i]->str))); - } - break; - case REDIS_REPLY_ERROR: - throw DatabaseException(std::string( - "Failed to get keys from database: ") + - std::string(reply->str, reply->len)); - } - freeReplyObject(reply); -} - -#endif // USE_REDIS - diff --git a/src/database-redis.h b/src/database-redis.h deleted file mode 100644 index 6bea563bc..000000000 --- a/src/database-redis.h +++ /dev/null @@ -1,51 +0,0 @@ -/* -Minetest -Copyright (C) 2014 celeron55, Perttu Ahola - -This program is free software; you can redistribute it and/or modify -it under the terms of the GNU Lesser General Public License as published by -the Free Software Foundation; either version 2.1 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public License along -with this program; if not, write to the Free Software Foundation, Inc., -51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -*/ - -#pragma once - -#include "config.h" - -#if USE_REDIS - -#include -#include -#include "database.h" - -class Settings; - -class Database_Redis : public MapDatabase -{ -public: - Database_Redis(Settings &conf); - ~Database_Redis(); - - void beginSave(); - void endSave(); - - bool saveBlock(const v3s16 &pos, const std::string &data); - void loadBlock(const v3s16 &pos, std::string *block); - bool deleteBlock(const v3s16 &pos); - void listAllLoadableBlocks(std::vector &dst); - -private: - redisContext *ctx = nullptr; - std::string hash = ""; -}; - -#endif // USE_REDIS diff --git a/src/database-sqlite3.cpp b/src/database-sqlite3.cpp deleted file mode 100644 index 78c182f86..000000000 --- a/src/database-sqlite3.cpp +++ /dev/null @@ -1,606 +0,0 @@ -/* -Minetest -Copyright (C) 2013 celeron55, Perttu Ahola - -This program is free software; you can redistribute it and/or modify -it under the terms of the GNU Lesser General Public License as published by -the Free Software Foundation; either version 2.1 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public License along -with this program; if not, write to the Free Software Foundation, Inc., -51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -*/ - -/* -SQLite format specification: - blocks: - (PK) INT id - BLOB data -*/ - - -#include "database-sqlite3.h" - -#include "log.h" -#include "filesys.h" -#include "exceptions.h" -#include "settings.h" -#include "porting.h" -#include "util/string.h" -#include "content_sao.h" -#include "remoteplayer.h" - -#include - -// When to print messages when the database is being held locked by another process -// Note: I've seen occasional delays of over 250ms while running minetestmapper. -#define BUSY_INFO_TRESHOLD 100 // Print first informational message after 100ms. -#define BUSY_WARNING_TRESHOLD 250 // Print warning message after 250ms. Lag is increased. -#define BUSY_ERROR_TRESHOLD 1000 // Print error message after 1000ms. Significant lag. -#define BUSY_FATAL_TRESHOLD 3000 // Allow SQLITE_BUSY to be returned, which will cause a minetest crash. -#define BUSY_ERROR_INTERVAL 10000 // Safety net: report again every 10 seconds - - -#define SQLRES(s, r, m) \ - if ((s) != (r)) { \ - throw DatabaseException(std::string(m) + ": " +\ - sqlite3_errmsg(m_database)); \ - } -#define SQLOK(s, m) SQLRES(s, SQLITE_OK, m) - -#define PREPARE_STATEMENT(name, query) \ - SQLOK(sqlite3_prepare_v2(m_database, query, -1, &m_stmt_##name, NULL),\ - "Failed to prepare query '" query "'") - -#define SQLOK_ERRSTREAM(s, m) \ - if ((s) != SQLITE_OK) { \ - errorstream << (m) << ": " \ - << sqlite3_errmsg(m_database) << std::endl; \ - } - -#define FINALIZE_STATEMENT(statement) SQLOK_ERRSTREAM(sqlite3_finalize(statement), \ - "Failed to finalize " #statement) - -int Database_SQLite3::busyHandler(void *data, int count) -{ - s64 &first_time = reinterpret_cast(data)[0]; - s64 &prev_time = reinterpret_cast(data)[1]; - s64 cur_time = porting::getTimeMs(); - - if (count == 0) { - first_time = cur_time; - prev_time = first_time; - } else { - while (cur_time < prev_time) - cur_time += s64(1)<<32; - } - - if (cur_time - first_time < BUSY_INFO_TRESHOLD) { - ; // do nothing - } else if (cur_time - first_time >= BUSY_INFO_TRESHOLD && - prev_time - first_time < BUSY_INFO_TRESHOLD) { - infostream << "SQLite3 database has been locked for " - << cur_time - first_time << " ms." << std::endl; - } else if (cur_time - first_time >= BUSY_WARNING_TRESHOLD && - prev_time - first_time < BUSY_WARNING_TRESHOLD) { - warningstream << "SQLite3 database has been locked for " - << cur_time - first_time << " ms." << std::endl; - } else if (cur_time - first_time >= BUSY_ERROR_TRESHOLD && - prev_time - first_time < BUSY_ERROR_TRESHOLD) { - errorstream << "SQLite3 database has been locked for " - << cur_time - first_time << " ms; this causes lag." << std::endl; - } else if (cur_time - first_time >= BUSY_FATAL_TRESHOLD && - prev_time - first_time < BUSY_FATAL_TRESHOLD) { - errorstream << "SQLite3 database has been locked for " - << cur_time - first_time << " ms - giving up!" << std::endl; - } else if ((cur_time - first_time) / BUSY_ERROR_INTERVAL != - (prev_time - first_time) / BUSY_ERROR_INTERVAL) { - // Safety net: keep reporting every BUSY_ERROR_INTERVAL - errorstream << "SQLite3 database has been locked for " - << (cur_time - first_time) / 1000 << " seconds!" << std::endl; - } - - prev_time = cur_time; - - // Make sqlite transaction fail if delay exceeds BUSY_FATAL_TRESHOLD - return cur_time - first_time < BUSY_FATAL_TRESHOLD; -} - - -Database_SQLite3::Database_SQLite3(const std::string &savedir, const std::string &dbname) : - m_savedir(savedir), - m_dbname(dbname) -{ -} - -void Database_SQLite3::beginSave() -{ - verifyDatabase(); - SQLRES(sqlite3_step(m_stmt_begin), SQLITE_DONE, - "Failed to start SQLite3 transaction"); - sqlite3_reset(m_stmt_begin); -} - -void Database_SQLite3::endSave() -{ - verifyDatabase(); - SQLRES(sqlite3_step(m_stmt_end), SQLITE_DONE, - "Failed to commit SQLite3 transaction"); - sqlite3_reset(m_stmt_end); -} - -void Database_SQLite3::openDatabase() -{ - if (m_database) return; - - std::string dbp = m_savedir + DIR_DELIM + m_dbname + ".sqlite"; - - // Open the database connection - - if (!fs::CreateAllDirs(m_savedir)) { - infostream << "Database_SQLite3: Failed to create directory \"" - << m_savedir << "\"" << std::endl; - throw FileNotGoodException("Failed to create database " - "save directory"); - } - - bool needs_create = !fs::PathExists(dbp); - - SQLOK(sqlite3_open_v2(dbp.c_str(), &m_database, - SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL), - std::string("Failed to open SQLite3 database file ") + dbp); - - SQLOK(sqlite3_busy_handler(m_database, Database_SQLite3::busyHandler, - m_busy_handler_data), "Failed to set SQLite3 busy handler"); - - if (needs_create) { - createDatabase(); - } - - std::string query_str = std::string("PRAGMA synchronous = ") - + itos(g_settings->getU16("sqlite_synchronous")); - SQLOK(sqlite3_exec(m_database, query_str.c_str(), NULL, NULL, NULL), - "Failed to modify sqlite3 synchronous mode"); - SQLOK(sqlite3_exec(m_database, "PRAGMA foreign_keys = ON", NULL, NULL, NULL), - "Failed to enable sqlite3 foreign key support"); -} - -void Database_SQLite3::verifyDatabase() -{ - if (m_initialized) return; - - openDatabase(); - - PREPARE_STATEMENT(begin, "BEGIN;"); - PREPARE_STATEMENT(end, "COMMIT;"); - - initStatements(); - - m_initialized = true; -} - -Database_SQLite3::~Database_SQLite3() -{ - FINALIZE_STATEMENT(m_stmt_begin) - FINALIZE_STATEMENT(m_stmt_end) - - SQLOK_ERRSTREAM(sqlite3_close(m_database), "Failed to close database"); -} - -/* - * Map database - */ - -MapDatabaseSQLite3::MapDatabaseSQLite3(const std::string &savedir): - Database_SQLite3(savedir, "map"), - MapDatabase() -{ -} - -MapDatabaseSQLite3::~MapDatabaseSQLite3() -{ - FINALIZE_STATEMENT(m_stmt_read) - FINALIZE_STATEMENT(m_stmt_write) - FINALIZE_STATEMENT(m_stmt_list) - FINALIZE_STATEMENT(m_stmt_delete) -} - - -void MapDatabaseSQLite3::createDatabase() -{ - assert(m_database); // Pre-condition - - SQLOK(sqlite3_exec(m_database, - "CREATE TABLE IF NOT EXISTS `blocks` (\n" - " `pos` INT PRIMARY KEY,\n" - " `data` BLOB\n" - ");\n", - NULL, NULL, NULL), - "Failed to create database table"); -} - -void MapDatabaseSQLite3::initStatements() -{ - PREPARE_STATEMENT(read, "SELECT `data` FROM `blocks` WHERE `pos` = ? LIMIT 1"); -#ifdef __ANDROID__ - PREPARE_STATEMENT(write, "INSERT INTO `blocks` (`pos`, `data`) VALUES (?, ?)"); -#else - PREPARE_STATEMENT(write, "REPLACE INTO `blocks` (`pos`, `data`) VALUES (?, ?)"); -#endif - PREPARE_STATEMENT(delete, "DELETE FROM `blocks` WHERE `pos` = ?"); - PREPARE_STATEMENT(list, "SELECT `pos` FROM `blocks`"); - - verbosestream << "ServerMap: SQLite3 database opened." << std::endl; -} - -inline void MapDatabaseSQLite3::bindPos(sqlite3_stmt *stmt, const v3s16 &pos, int index) -{ - SQLOK(sqlite3_bind_int64(stmt, index, getBlockAsInteger(pos)), - "Internal error: failed to bind query at " __FILE__ ":" TOSTRING(__LINE__)); -} - -bool MapDatabaseSQLite3::deleteBlock(const v3s16 &pos) -{ - verifyDatabase(); - - bindPos(m_stmt_delete, pos); - - bool good = sqlite3_step(m_stmt_delete) == SQLITE_DONE; - sqlite3_reset(m_stmt_delete); - - if (!good) { - warningstream << "deleteBlock: Block failed to delete " - << PP(pos) << ": " << sqlite3_errmsg(m_database) << std::endl; - } - return good; -} - -bool MapDatabaseSQLite3::saveBlock(const v3s16 &pos, const std::string &data) -{ - verifyDatabase(); - -#ifdef __ANDROID__ - /** - * Note: For some unknown reason SQLite3 fails to REPLACE blocks on Android, - * deleting them and then inserting works. - */ - bindPos(m_stmt_read, pos); - - if (sqlite3_step(m_stmt_read) == SQLITE_ROW) { - deleteBlock(pos); - } - sqlite3_reset(m_stmt_read); -#endif - - bindPos(m_stmt_write, pos); - SQLOK(sqlite3_bind_blob(m_stmt_write, 2, data.data(), data.size(), NULL), - "Internal error: failed to bind query at " __FILE__ ":" TOSTRING(__LINE__)); - - SQLRES(sqlite3_step(m_stmt_write), SQLITE_DONE, "Failed to save block") - sqlite3_reset(m_stmt_write); - - return true; -} - -void MapDatabaseSQLite3::loadBlock(const v3s16 &pos, std::string *block) -{ - verifyDatabase(); - - bindPos(m_stmt_read, pos); - - if (sqlite3_step(m_stmt_read) != SQLITE_ROW) { - sqlite3_reset(m_stmt_read); - return; - } - - const char *data = (const char *) sqlite3_column_blob(m_stmt_read, 0); - size_t len = sqlite3_column_bytes(m_stmt_read, 0); - - *block = (data) ? std::string(data, len) : ""; - - sqlite3_step(m_stmt_read); - // We should never get more than 1 row, so ok to reset - sqlite3_reset(m_stmt_read); -} - -void MapDatabaseSQLite3::listAllLoadableBlocks(std::vector &dst) -{ - verifyDatabase(); - - while (sqlite3_step(m_stmt_list) == SQLITE_ROW) - dst.push_back(getIntegerAsBlock(sqlite3_column_int64(m_stmt_list, 0))); - - sqlite3_reset(m_stmt_list); -} - -/* - * Player Database - */ - -PlayerDatabaseSQLite3::PlayerDatabaseSQLite3(const std::string &savedir): - Database_SQLite3(savedir, "players"), - PlayerDatabase() -{ -} - -PlayerDatabaseSQLite3::~PlayerDatabaseSQLite3() -{ - FINALIZE_STATEMENT(m_stmt_player_load) - FINALIZE_STATEMENT(m_stmt_player_add) - FINALIZE_STATEMENT(m_stmt_player_update) - FINALIZE_STATEMENT(m_stmt_player_remove) - FINALIZE_STATEMENT(m_stmt_player_list) - FINALIZE_STATEMENT(m_stmt_player_add_inventory) - FINALIZE_STATEMENT(m_stmt_player_add_inventory_items) - FINALIZE_STATEMENT(m_stmt_player_remove_inventory) - FINALIZE_STATEMENT(m_stmt_player_remove_inventory_items) - FINALIZE_STATEMENT(m_stmt_player_load_inventory) - FINALIZE_STATEMENT(m_stmt_player_load_inventory_items) - FINALIZE_STATEMENT(m_stmt_player_metadata_load) - FINALIZE_STATEMENT(m_stmt_player_metadata_add) - FINALIZE_STATEMENT(m_stmt_player_metadata_remove) -}; - - -void PlayerDatabaseSQLite3::createDatabase() -{ - assert(m_database); // Pre-condition - - SQLOK(sqlite3_exec(m_database, - "CREATE TABLE IF NOT EXISTS `player` (" - "`name` VARCHAR(50) NOT NULL," - "`pitch` NUMERIC(11, 4) NOT NULL," - "`yaw` NUMERIC(11, 4) NOT NULL," - "`posX` NUMERIC(11, 4) NOT NULL," - "`posY` NUMERIC(11, 4) NOT NULL," - "`posZ` NUMERIC(11, 4) NOT NULL," - "`hp` INT NOT NULL," - "`breath` INT NOT NULL," - "`creation_date` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP," - "`modification_date` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP," - "PRIMARY KEY (`name`));", - NULL, NULL, NULL), - "Failed to create player table"); - - SQLOK(sqlite3_exec(m_database, - "CREATE TABLE IF NOT EXISTS `player_metadata` (" - " `player` VARCHAR(50) NOT NULL," - " `metadata` VARCHAR(256) NOT NULL," - " `value` TEXT," - " PRIMARY KEY(`player`, `metadata`)," - " FOREIGN KEY (`player`) REFERENCES player (`name`) ON DELETE CASCADE );", - NULL, NULL, NULL), - "Failed to create player metadata table"); - - SQLOK(sqlite3_exec(m_database, - "CREATE TABLE IF NOT EXISTS `player_inventories` (" - " `player` VARCHAR(50) NOT NULL," - " `inv_id` INT NOT NULL," - " `inv_width` INT NOT NULL," - " `inv_name` TEXT NOT NULL DEFAULT ''," - " `inv_size` INT NOT NULL," - " PRIMARY KEY(player, inv_id)," - " FOREIGN KEY (`player`) REFERENCES player (`name`) ON DELETE CASCADE );", - NULL, NULL, NULL), - "Failed to create player inventory table"); - - SQLOK(sqlite3_exec(m_database, - "CREATE TABLE `player_inventory_items` (" - " `player` VARCHAR(50) NOT NULL," - " `inv_id` INT NOT NULL," - " `slot_id` INT NOT NULL," - " `item` TEXT NOT NULL DEFAULT ''," - " PRIMARY KEY(player, inv_id, slot_id)," - " FOREIGN KEY (`player`) REFERENCES player (`name`) ON DELETE CASCADE );", - NULL, NULL, NULL), - "Failed to create player inventory items table"); -} - -void PlayerDatabaseSQLite3::initStatements() -{ - PREPARE_STATEMENT(player_load, "SELECT `pitch`, `yaw`, `posX`, `posY`, `posZ`, `hp`, " - "`breath`" - "FROM `player` WHERE `name` = ?") - PREPARE_STATEMENT(player_add, "INSERT INTO `player` (`name`, `pitch`, `yaw`, `posX`, " - "`posY`, `posZ`, `hp`, `breath`) VALUES (?, ?, ?, ?, ?, ?, ?, ?)") - PREPARE_STATEMENT(player_update, "UPDATE `player` SET `pitch` = ?, `yaw` = ?, " - "`posX` = ?, `posY` = ?, `posZ` = ?, `hp` = ?, `breath` = ?, " - "`modification_date` = CURRENT_TIMESTAMP WHERE `name` = ?") - PREPARE_STATEMENT(player_remove, "DELETE FROM `player` WHERE `name` = ?") - PREPARE_STATEMENT(player_list, "SELECT `name` FROM `player`") - - PREPARE_STATEMENT(player_add_inventory, "INSERT INTO `player_inventories` " - "(`player`, `inv_id`, `inv_width`, `inv_name`, `inv_size`) VALUES (?, ?, ?, ?, ?)") - PREPARE_STATEMENT(player_add_inventory_items, "INSERT INTO `player_inventory_items` " - "(`player`, `inv_id`, `slot_id`, `item`) VALUES (?, ?, ?, ?)") - PREPARE_STATEMENT(player_remove_inventory, "DELETE FROM `player_inventories` " - "WHERE `player` = ?") - PREPARE_STATEMENT(player_remove_inventory_items, "DELETE FROM `player_inventory_items` " - "WHERE `player` = ?") - PREPARE_STATEMENT(player_load_inventory, "SELECT `inv_id`, `inv_width`, `inv_name`, " - "`inv_size` FROM `player_inventories` WHERE `player` = ? ORDER BY inv_id") - PREPARE_STATEMENT(player_load_inventory_items, "SELECT `slot_id`, `item` " - "FROM `player_inventory_items` WHERE `player` = ? AND `inv_id` = ?") - - PREPARE_STATEMENT(player_metadata_load, "SELECT `metadata`, `value` FROM " - "`player_metadata` WHERE `player` = ?") - PREPARE_STATEMENT(player_metadata_add, "INSERT INTO `player_metadata` " - "(`player`, `metadata`, `value`) VALUES (?, ?, ?)") - PREPARE_STATEMENT(player_metadata_remove, "DELETE FROM `player_metadata` " - "WHERE `player` = ?") - verbosestream << "ServerEnvironment: SQLite3 database opened (players)." << std::endl; -} - -bool PlayerDatabaseSQLite3::playerDataExists(const std::string &name) -{ - verifyDatabase(); - str_to_sqlite(m_stmt_player_load, 1, name); - bool res = (sqlite3_step(m_stmt_player_load) == SQLITE_ROW); - sqlite3_reset(m_stmt_player_load); - return res; -} - -void PlayerDatabaseSQLite3::savePlayer(RemotePlayer *player) -{ - PlayerSAO* sao = player->getPlayerSAO(); - sanity_check(sao); - - const v3f &pos = sao->getBasePosition(); - // Begin save in brace is mandatory - if (!playerDataExists(player->getName())) { - beginSave(); - str_to_sqlite(m_stmt_player_add, 1, player->getName()); - double_to_sqlite(m_stmt_player_add, 2, sao->getPitch()); - double_to_sqlite(m_stmt_player_add, 3, sao->getYaw()); - double_to_sqlite(m_stmt_player_add, 4, pos.X); - double_to_sqlite(m_stmt_player_add, 5, pos.Y); - double_to_sqlite(m_stmt_player_add, 6, pos.Z); - int64_to_sqlite(m_stmt_player_add, 7, sao->getHP()); - int64_to_sqlite(m_stmt_player_add, 8, sao->getBreath()); - - sqlite3_vrfy(sqlite3_step(m_stmt_player_add), SQLITE_DONE); - sqlite3_reset(m_stmt_player_add); - } else { - beginSave(); - double_to_sqlite(m_stmt_player_update, 1, sao->getPitch()); - double_to_sqlite(m_stmt_player_update, 2, sao->getYaw()); - double_to_sqlite(m_stmt_player_update, 3, pos.X); - double_to_sqlite(m_stmt_player_update, 4, pos.Y); - double_to_sqlite(m_stmt_player_update, 5, pos.Z); - int64_to_sqlite(m_stmt_player_update, 6, sao->getHP()); - int64_to_sqlite(m_stmt_player_update, 7, sao->getBreath()); - str_to_sqlite(m_stmt_player_update, 8, player->getName()); - - sqlite3_vrfy(sqlite3_step(m_stmt_player_update), SQLITE_DONE); - sqlite3_reset(m_stmt_player_update); - } - - // Write player inventories - str_to_sqlite(m_stmt_player_remove_inventory, 1, player->getName()); - sqlite3_vrfy(sqlite3_step(m_stmt_player_remove_inventory), SQLITE_DONE); - sqlite3_reset(m_stmt_player_remove_inventory); - - str_to_sqlite(m_stmt_player_remove_inventory_items, 1, player->getName()); - sqlite3_vrfy(sqlite3_step(m_stmt_player_remove_inventory_items), SQLITE_DONE); - sqlite3_reset(m_stmt_player_remove_inventory_items); - - std::vector inventory_lists = sao->getInventory()->getLists(); - for (u16 i = 0; i < inventory_lists.size(); i++) { - const InventoryList* list = inventory_lists[i]; - - str_to_sqlite(m_stmt_player_add_inventory, 1, player->getName()); - int_to_sqlite(m_stmt_player_add_inventory, 2, i); - int_to_sqlite(m_stmt_player_add_inventory, 3, list->getWidth()); - str_to_sqlite(m_stmt_player_add_inventory, 4, list->getName()); - int_to_sqlite(m_stmt_player_add_inventory, 5, list->getSize()); - sqlite3_vrfy(sqlite3_step(m_stmt_player_add_inventory), SQLITE_DONE); - sqlite3_reset(m_stmt_player_add_inventory); - - for (u32 j = 0; j < list->getSize(); j++) { - std::ostringstream os; - list->getItem(j).serialize(os); - std::string itemStr = os.str(); - - str_to_sqlite(m_stmt_player_add_inventory_items, 1, player->getName()); - int_to_sqlite(m_stmt_player_add_inventory_items, 2, i); - int_to_sqlite(m_stmt_player_add_inventory_items, 3, j); - str_to_sqlite(m_stmt_player_add_inventory_items, 4, itemStr); - sqlite3_vrfy(sqlite3_step(m_stmt_player_add_inventory_items), SQLITE_DONE); - sqlite3_reset(m_stmt_player_add_inventory_items); - } - } - - str_to_sqlite(m_stmt_player_metadata_remove, 1, player->getName()); - sqlite3_vrfy(sqlite3_step(m_stmt_player_metadata_remove), SQLITE_DONE); - sqlite3_reset(m_stmt_player_metadata_remove); - - const PlayerAttributes &attrs = sao->getExtendedAttributes(); - for (const auto &attr : attrs) { - str_to_sqlite(m_stmt_player_metadata_add, 1, player->getName()); - str_to_sqlite(m_stmt_player_metadata_add, 2, attr.first); - str_to_sqlite(m_stmt_player_metadata_add, 3, attr.second); - sqlite3_vrfy(sqlite3_step(m_stmt_player_metadata_add), SQLITE_DONE); - sqlite3_reset(m_stmt_player_metadata_add); - } - - endSave(); -} - -bool PlayerDatabaseSQLite3::loadPlayer(RemotePlayer *player, PlayerSAO *sao) -{ - verifyDatabase(); - - str_to_sqlite(m_stmt_player_load, 1, player->getName()); - if (sqlite3_step(m_stmt_player_load) != SQLITE_ROW) { - sqlite3_reset(m_stmt_player_load); - return false; - } - sao->setPitch(sqlite_to_float(m_stmt_player_load, 0)); - sao->setYaw(sqlite_to_float(m_stmt_player_load, 1)); - sao->setBasePosition(sqlite_to_v3f(m_stmt_player_load, 2)); - sao->setHPRaw((s16) MYMIN(sqlite_to_int(m_stmt_player_load, 5), S16_MAX)); - sao->setBreath((u16) MYMIN(sqlite_to_int(m_stmt_player_load, 6), U16_MAX), false); - sqlite3_reset(m_stmt_player_load); - - // Load inventory - str_to_sqlite(m_stmt_player_load_inventory, 1, player->getName()); - while (sqlite3_step(m_stmt_player_load_inventory) == SQLITE_ROW) { - InventoryList *invList = player->inventory.addList( - sqlite_to_string(m_stmt_player_load_inventory, 2), - sqlite_to_uint(m_stmt_player_load_inventory, 3)); - invList->setWidth(sqlite_to_uint(m_stmt_player_load_inventory, 1)); - - u32 invId = sqlite_to_uint(m_stmt_player_load_inventory, 0); - - str_to_sqlite(m_stmt_player_load_inventory_items, 1, player->getName()); - int_to_sqlite(m_stmt_player_load_inventory_items, 2, invId); - while (sqlite3_step(m_stmt_player_load_inventory_items) == SQLITE_ROW) { - const std::string itemStr = sqlite_to_string(m_stmt_player_load_inventory_items, 1); - if (itemStr.length() > 0) { - ItemStack stack; - stack.deSerialize(itemStr); - invList->changeItem(sqlite_to_uint(m_stmt_player_load_inventory_items, 0), stack); - } - } - sqlite3_reset(m_stmt_player_load_inventory_items); - } - - sqlite3_reset(m_stmt_player_load_inventory); - - str_to_sqlite(m_stmt_player_metadata_load, 1, sao->getPlayer()->getName()); - while (sqlite3_step(m_stmt_player_metadata_load) == SQLITE_ROW) { - std::string attr = sqlite_to_string(m_stmt_player_metadata_load, 0); - std::string value = sqlite_to_string(m_stmt_player_metadata_load, 1); - - sao->setExtendedAttribute(attr, value); - } - sqlite3_reset(m_stmt_player_metadata_load); - return true; -} - -bool PlayerDatabaseSQLite3::removePlayer(const std::string &name) -{ - if (!playerDataExists(name)) - return false; - - str_to_sqlite(m_stmt_player_remove, 1, name); - sqlite3_vrfy(sqlite3_step(m_stmt_player_remove), SQLITE_DONE); - sqlite3_reset(m_stmt_player_remove); - return true; -} - -void PlayerDatabaseSQLite3::listPlayers(std::vector &res) -{ - verifyDatabase(); - - while (sqlite3_step(m_stmt_player_list) == SQLITE_ROW) - res.push_back(sqlite_to_string(m_stmt_player_list, 0)); - - sqlite3_reset(m_stmt_player_list); -} diff --git a/src/database-sqlite3.h b/src/database-sqlite3.h deleted file mode 100644 index 8d9f91f21..000000000 --- a/src/database-sqlite3.h +++ /dev/null @@ -1,193 +0,0 @@ -/* -Minetest -Copyright (C) 2013 celeron55, Perttu Ahola - -This program is free software; you can redistribute it and/or modify -it under the terms of the GNU Lesser General Public License as published by -the Free Software Foundation; either version 2.1 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public License along -with this program; if not, write to the Free Software Foundation, Inc., -51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -*/ - -#pragma once - -#include -#include -#include "database.h" -#include "exceptions.h" - -extern "C" { -#include "sqlite3.h" -} - -class Database_SQLite3 : public Database -{ -public: - virtual ~Database_SQLite3(); - - void beginSave(); - void endSave(); - - bool initialized() const { return m_initialized; } -protected: - Database_SQLite3(const std::string &savedir, const std::string &dbname); - - // Open and initialize the database if needed - void verifyDatabase(); - - // Convertors - inline void str_to_sqlite(sqlite3_stmt *s, int iCol, const std::string &str) const - { - sqlite3_vrfy(sqlite3_bind_text(s, iCol, str.c_str(), str.size(), NULL)); - } - - inline void str_to_sqlite(sqlite3_stmt *s, int iCol, const char *str) const - { - sqlite3_vrfy(sqlite3_bind_text(s, iCol, str, strlen(str), NULL)); - } - - inline void int_to_sqlite(sqlite3_stmt *s, int iCol, int val) const - { - sqlite3_vrfy(sqlite3_bind_int(s, iCol, val)); - } - - inline void int64_to_sqlite(sqlite3_stmt *s, int iCol, s64 val) const - { - sqlite3_vrfy(sqlite3_bind_int64(s, iCol, (sqlite3_int64) val)); - } - - inline void double_to_sqlite(sqlite3_stmt *s, int iCol, double val) const - { - sqlite3_vrfy(sqlite3_bind_double(s, iCol, val)); - } - - inline std::string sqlite_to_string(sqlite3_stmt *s, int iCol) - { - const char* text = reinterpret_cast(sqlite3_column_text(s, iCol)); - return std::string(text ? text : ""); - } - - inline s32 sqlite_to_int(sqlite3_stmt *s, int iCol) - { - return sqlite3_column_int(s, iCol); - } - - inline u32 sqlite_to_uint(sqlite3_stmt *s, int iCol) - { - return (u32) sqlite3_column_int(s, iCol); - } - - inline float sqlite_to_float(sqlite3_stmt *s, int iCol) - { - return (float) sqlite3_column_double(s, iCol); - } - - inline const v3f sqlite_to_v3f(sqlite3_stmt *s, int iCol) - { - return v3f(sqlite_to_float(s, iCol), sqlite_to_float(s, iCol + 1), - sqlite_to_float(s, iCol + 2)); - } - - // Query verifiers helpers - inline void sqlite3_vrfy(int s, const std::string &m = "", int r = SQLITE_OK) const - { - if (s != r) - throw DatabaseException(m + ": " + sqlite3_errmsg(m_database)); - } - - inline void sqlite3_vrfy(const int s, const int r, const std::string &m = "") const - { - sqlite3_vrfy(s, m, r); - } - - // Create the database structure - virtual void createDatabase() = 0; - virtual void initStatements() = 0; - - sqlite3 *m_database = nullptr; -private: - // Open the database - void openDatabase(); - - bool m_initialized = false; - - std::string m_savedir = ""; - std::string m_dbname = ""; - - sqlite3_stmt *m_stmt_begin = nullptr; - sqlite3_stmt *m_stmt_end = nullptr; - - s64 m_busy_handler_data[2]; - - static int busyHandler(void *data, int count); -}; - -class MapDatabaseSQLite3 : private Database_SQLite3, public MapDatabase -{ -public: - MapDatabaseSQLite3(const std::string &savedir); - virtual ~MapDatabaseSQLite3(); - - bool saveBlock(const v3s16 &pos, const std::string &data); - void loadBlock(const v3s16 &pos, std::string *block); - bool deleteBlock(const v3s16 &pos); - void listAllLoadableBlocks(std::vector &dst); - - void beginSave() { Database_SQLite3::beginSave(); } - void endSave() { Database_SQLite3::endSave(); } -protected: - virtual void createDatabase(); - virtual void initStatements(); - -private: - void bindPos(sqlite3_stmt *stmt, const v3s16 &pos, int index = 1); - - // Map - sqlite3_stmt *m_stmt_read = nullptr; - sqlite3_stmt *m_stmt_write = nullptr; - sqlite3_stmt *m_stmt_list = nullptr; - sqlite3_stmt *m_stmt_delete = nullptr; -}; - -class PlayerDatabaseSQLite3 : private Database_SQLite3, public PlayerDatabase -{ -public: - PlayerDatabaseSQLite3(const std::string &savedir); - virtual ~PlayerDatabaseSQLite3(); - - void savePlayer(RemotePlayer *player); - bool loadPlayer(RemotePlayer *player, PlayerSAO *sao); - bool removePlayer(const std::string &name); - void listPlayers(std::vector &res); - -protected: - virtual void createDatabase(); - virtual void initStatements(); - -private: - bool playerDataExists(const std::string &name); - - // Players - sqlite3_stmt *m_stmt_player_load = nullptr; - sqlite3_stmt *m_stmt_player_add = nullptr; - sqlite3_stmt *m_stmt_player_update = nullptr; - sqlite3_stmt *m_stmt_player_remove = nullptr; - sqlite3_stmt *m_stmt_player_list = nullptr; - sqlite3_stmt *m_stmt_player_load_inventory = nullptr; - sqlite3_stmt *m_stmt_player_load_inventory_items = nullptr; - sqlite3_stmt *m_stmt_player_add_inventory = nullptr; - sqlite3_stmt *m_stmt_player_add_inventory_items = nullptr; - sqlite3_stmt *m_stmt_player_remove_inventory = nullptr; - sqlite3_stmt *m_stmt_player_remove_inventory_items = nullptr; - sqlite3_stmt *m_stmt_player_metadata_load = nullptr; - sqlite3_stmt *m_stmt_player_metadata_remove = nullptr; - sqlite3_stmt *m_stmt_player_metadata_add = nullptr; -}; diff --git a/src/database.cpp b/src/database.cpp deleted file mode 100644 index 12e0e1a0f..000000000 --- a/src/database.cpp +++ /dev/null @@ -1,69 +0,0 @@ -/* -Minetest -Copyright (C) 2013 celeron55, Perttu Ahola - -This program is free software; you can redistribute it and/or modify -it under the terms of the GNU Lesser General Public License as published by -the Free Software Foundation; either version 2.1 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public License along -with this program; if not, write to the Free Software Foundation, Inc., -51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -*/ - -#include "database.h" -#include "irrlichttypes.h" - - -/**************** - * Black magic! * - **************** - * The position hashing is very messed up. - * It's a lot more complicated than it looks. - */ - -static inline s16 unsigned_to_signed(u16 i, u16 max_positive) -{ - if (i < max_positive) { - return i; - } - - return i - (max_positive * 2); -} - - -// Modulo of a negative number does not work consistently in C -static inline s64 pythonmodulo(s64 i, s16 mod) -{ - if (i >= 0) { - return i % mod; - } - return mod - ((-i) % mod); -} - - -s64 MapDatabase::getBlockAsInteger(const v3s16 &pos) -{ - return (u64) pos.Z * 0x1000000 + - (u64) pos.Y * 0x1000 + - (u64) pos.X; -} - - -v3s16 MapDatabase::getIntegerAsBlock(s64 i) -{ - v3s16 pos; - pos.X = unsigned_to_signed(pythonmodulo(i, 4096), 2048); - i = (i - pos.X) / 4096; - pos.Y = unsigned_to_signed(pythonmodulo(i, 4096), 2048); - i = (i - pos.Y) / 4096; - pos.Z = unsigned_to_signed(pythonmodulo(i, 4096), 2048); - return pos; -} - diff --git a/src/database.h b/src/database.h deleted file mode 100644 index 9926c7b93..000000000 --- a/src/database.h +++ /dev/null @@ -1,63 +0,0 @@ -/* -Minetest -Copyright (C) 2013 celeron55, Perttu Ahola - -This program is free software; you can redistribute it and/or modify -it under the terms of the GNU Lesser General Public License as published by -the Free Software Foundation; either version 2.1 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public License along -with this program; if not, write to the Free Software Foundation, Inc., -51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -*/ - -#pragma once - -#include -#include -#include "irr_v3d.h" -#include "irrlichttypes.h" -#include "util/basic_macros.h" - -class Database -{ -public: - virtual void beginSave() = 0; - virtual void endSave() = 0; - virtual bool initialized() const { return true; } -}; - -class MapDatabase : public Database -{ -public: - virtual ~MapDatabase() = default; - - virtual bool saveBlock(const v3s16 &pos, const std::string &data) = 0; - virtual void loadBlock(const v3s16 &pos, std::string *block) = 0; - virtual bool deleteBlock(const v3s16 &pos) = 0; - - static s64 getBlockAsInteger(const v3s16 &pos); - static v3s16 getIntegerAsBlock(s64 i); - - virtual void listAllLoadableBlocks(std::vector &dst) = 0; -}; - -class PlayerSAO; -class RemotePlayer; - -class PlayerDatabase -{ -public: - virtual ~PlayerDatabase() = default; - - virtual void savePlayer(RemotePlayer *player) = 0; - virtual bool loadPlayer(RemotePlayer *player, PlayerSAO *sao) = 0; - virtual bool removePlayer(const std::string &name) = 0; - virtual void listPlayers(std::vector &res) = 0; -}; diff --git a/src/database/CMakeLists.txt b/src/database/CMakeLists.txt new file mode 100644 index 000000000..e9d157c29 --- /dev/null +++ b/src/database/CMakeLists.txt @@ -0,0 +1,10 @@ +set(database_SRCS + ${CMAKE_CURRENT_SOURCE_DIR}/database.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/database-dummy.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/database-files.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/database-leveldb.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/database-postgresql.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/database-redis.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/database-sqlite3.cpp + PARENT_SCOPE +) diff --git a/src/database/database-dummy.cpp b/src/database/database-dummy.cpp new file mode 100644 index 000000000..a3d8cd579 --- /dev/null +++ b/src/database/database-dummy.cpp @@ -0,0 +1,59 @@ +/* +Minetest +Copyright (C) 2013 celeron55, Perttu Ahola + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +/* +Dummy database class +*/ + +#include "database-dummy.h" + + +bool Database_Dummy::saveBlock(const v3s16 &pos, const std::string &data) +{ + m_database[getBlockAsInteger(pos)] = data; + return true; +} + +void Database_Dummy::loadBlock(const v3s16 &pos, std::string *block) +{ + s64 i = getBlockAsInteger(pos); + auto it = m_database.find(i); + if (it == m_database.end()) { + *block = ""; + return; + } + + *block = it->second; +} + +bool Database_Dummy::deleteBlock(const v3s16 &pos) +{ + m_database.erase(getBlockAsInteger(pos)); + return true; +} + +void Database_Dummy::listAllLoadableBlocks(std::vector &dst) +{ + dst.reserve(m_database.size()); + for (std::map::const_iterator x = m_database.begin(); + x != m_database.end(); ++x) { + dst.push_back(getIntegerAsBlock(x->first)); + } +} + diff --git a/src/database/database-dummy.h b/src/database/database-dummy.h new file mode 100644 index 000000000..2d87d58f6 --- /dev/null +++ b/src/database/database-dummy.h @@ -0,0 +1,45 @@ +/* +Minetest +Copyright (C) 2013 celeron55, Perttu Ahola + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#pragma once + +#include +#include +#include "database.h" +#include "irrlichttypes.h" + +class Database_Dummy : public MapDatabase, public PlayerDatabase +{ +public: + bool saveBlock(const v3s16 &pos, const std::string &data); + void loadBlock(const v3s16 &pos, std::string *block); + bool deleteBlock(const v3s16 &pos); + void listAllLoadableBlocks(std::vector &dst); + + void savePlayer(RemotePlayer *player) {} + bool loadPlayer(RemotePlayer *player, PlayerSAO *sao) { return true; } + bool removePlayer(const std::string &name) { return true; } + void listPlayers(std::vector &res) {} + + void beginSave() {} + void endSave() {} + +private: + std::map m_database; +}; diff --git a/src/database/database-files.cpp b/src/database/database-files.cpp new file mode 100644 index 000000000..70de8c8d2 --- /dev/null +++ b/src/database/database-files.cpp @@ -0,0 +1,179 @@ +/* +Minetest +Copyright (C) 2017 nerzhul, Loic Blot + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#include +#include +#include "database-files.h" +#include "content_sao.h" +#include "remoteplayer.h" +#include "settings.h" +#include "porting.h" +#include "filesys.h" + +// !!! WARNING !!! +// This backend is intended to be used on Minetest 0.4.16 only for the transition backend +// for player files + +void PlayerDatabaseFiles::serialize(std::ostringstream &os, RemotePlayer *player) +{ + // Utilize a Settings object for storing values + Settings args; + args.setS32("version", 1); + args.set("name", player->getName()); + + sanity_check(player->getPlayerSAO()); + args.setS32("hp", player->getPlayerSAO()->getHP()); + args.setV3F("position", player->getPlayerSAO()->getBasePosition()); + args.setFloat("pitch", player->getPlayerSAO()->getPitch()); + args.setFloat("yaw", player->getPlayerSAO()->getYaw()); + args.setS32("breath", player->getPlayerSAO()->getBreath()); + + std::string extended_attrs; + player->serializeExtraAttributes(extended_attrs); + args.set("extended_attributes", extended_attrs); + + args.writeLines(os); + + os << "PlayerArgsEnd\n"; + + player->inventory.serialize(os); +} + +void PlayerDatabaseFiles::savePlayer(RemotePlayer *player) +{ + std::string savedir = m_savedir + DIR_DELIM; + std::string path = savedir + player->getName(); + bool path_found = false; + RemotePlayer testplayer("", NULL); + + for (u32 i = 0; i < PLAYER_FILE_ALTERNATE_TRIES && !path_found; i++) { + if (!fs::PathExists(path)) { + path_found = true; + continue; + } + + // Open and deserialize file to check player name + std::ifstream is(path.c_str(), std::ios_base::binary); + if (!is.good()) { + errorstream << "Failed to open " << path << std::endl; + return; + } + + testplayer.deSerialize(is, path, NULL); + is.close(); + if (strcmp(testplayer.getName(), player->getName()) == 0) { + path_found = true; + continue; + } + + path = savedir + player->getName() + itos(i); + } + + if (!path_found) { + errorstream << "Didn't find free file for player " << player->getName() + << std::endl; + return; + } + + // Open and serialize file + std::ostringstream ss(std::ios_base::binary); + serialize(ss, player); + if (!fs::safeWriteToFile(path, ss.str())) { + infostream << "Failed to write " << path << std::endl; + } + player->setModified(false); +} + +bool PlayerDatabaseFiles::removePlayer(const std::string &name) +{ + std::string players_path = m_savedir + DIR_DELIM; + std::string path = players_path + name; + + RemotePlayer temp_player("", NULL); + for (u32 i = 0; i < PLAYER_FILE_ALTERNATE_TRIES; i++) { + // Open file and deserialize + std::ifstream is(path.c_str(), std::ios_base::binary); + if (!is.good()) + continue; + + temp_player.deSerialize(is, path, NULL); + is.close(); + + if (temp_player.getName() == name) { + fs::DeleteSingleFileOrEmptyDirectory(path); + return true; + } + + path = players_path + name + itos(i); + } + + return false; +} + +bool PlayerDatabaseFiles::loadPlayer(RemotePlayer *player, PlayerSAO *sao) +{ + std::string players_path = m_savedir + DIR_DELIM; + std::string path = players_path + player->getName(); + + const std::string player_to_load = player->getName(); + for (u32 i = 0; i < PLAYER_FILE_ALTERNATE_TRIES; i++) { + // Open file and deserialize + std::ifstream is(path.c_str(), std::ios_base::binary); + if (!is.good()) + continue; + + player->deSerialize(is, path, sao); + is.close(); + + if (player->getName() == player_to_load) + return true; + + path = players_path + player_to_load + itos(i); + } + + infostream << "Player file for player " << player_to_load << " not found" << std::endl; + return false; +} + +void PlayerDatabaseFiles::listPlayers(std::vector &res) +{ + std::vector files = fs::GetDirListing(m_savedir); + // list files into players directory + for (std::vector::const_iterator it = files.begin(); it != + files.end(); ++it) { + // Ignore directories + if (it->dir) + continue; + + const std::string &filename = it->name; + std::string full_path = m_savedir + DIR_DELIM + filename; + std::ifstream is(full_path.c_str(), std::ios_base::binary); + if (!is.good()) + continue; + + RemotePlayer player(filename.c_str(), NULL); + // Null env & dummy peer_id + PlayerSAO playerSAO(NULL, &player, 15789, false); + + player.deSerialize(is, "", &playerSAO); + is.close(); + + res.emplace_back(player.getName()); + } +} diff --git a/src/database/database-files.h b/src/database/database-files.h new file mode 100644 index 000000000..f0824a304 --- /dev/null +++ b/src/database/database-files.h @@ -0,0 +1,43 @@ +/* +Minetest +Copyright (C) 2017 nerzhul, Loic Blot + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#pragma once + +// !!! WARNING !!! +// This backend is intended to be used on Minetest 0.4.16 only for the transition backend +// for player files + +#include "database.h" + +class PlayerDatabaseFiles : public PlayerDatabase +{ +public: + PlayerDatabaseFiles(const std::string &savedir) : m_savedir(savedir) {} + virtual ~PlayerDatabaseFiles() = default; + + void savePlayer(RemotePlayer *player); + bool loadPlayer(RemotePlayer *player, PlayerSAO *sao); + bool removePlayer(const std::string &name); + void listPlayers(std::vector &res); + +private: + void serialize(std::ostringstream &os, RemotePlayer *player); + + std::string m_savedir; +}; diff --git a/src/database/database-leveldb.cpp b/src/database/database-leveldb.cpp new file mode 100644 index 000000000..4a4904c6a --- /dev/null +++ b/src/database/database-leveldb.cpp @@ -0,0 +1,101 @@ +/* +Minetest +Copyright (C) 2013 celeron55, Perttu Ahola + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#include "config.h" + +#if USE_LEVELDB + +#include "database-leveldb.h" + +#include "log.h" +#include "filesys.h" +#include "exceptions.h" +#include "util/string.h" + +#include "leveldb/db.h" + + +#define ENSURE_STATUS_OK(s) \ + if (!(s).ok()) { \ + throw DatabaseException(std::string("LevelDB error: ") + \ + (s).ToString()); \ + } + + +Database_LevelDB::Database_LevelDB(const std::string &savedir) +{ + leveldb::Options options; + options.create_if_missing = true; + leveldb::Status status = leveldb::DB::Open(options, + savedir + DIR_DELIM + "map.db", &m_database); + ENSURE_STATUS_OK(status); +} + +Database_LevelDB::~Database_LevelDB() +{ + delete m_database; +} + +bool Database_LevelDB::saveBlock(const v3s16 &pos, const std::string &data) +{ + leveldb::Status status = m_database->Put(leveldb::WriteOptions(), + i64tos(getBlockAsInteger(pos)), data); + if (!status.ok()) { + warningstream << "saveBlock: LevelDB error saving block " + << PP(pos) << ": " << status.ToString() << std::endl; + return false; + } + + return true; +} + +void Database_LevelDB::loadBlock(const v3s16 &pos, std::string *block) +{ + std::string datastr; + leveldb::Status status = m_database->Get(leveldb::ReadOptions(), + i64tos(getBlockAsInteger(pos)), &datastr); + + *block = (status.ok()) ? datastr : ""; +} + +bool Database_LevelDB::deleteBlock(const v3s16 &pos) +{ + leveldb::Status status = m_database->Delete(leveldb::WriteOptions(), + i64tos(getBlockAsInteger(pos))); + if (!status.ok()) { + warningstream << "deleteBlock: LevelDB error deleting block " + << PP(pos) << ": " << status.ToString() << std::endl; + return false; + } + + return true; +} + +void Database_LevelDB::listAllLoadableBlocks(std::vector &dst) +{ + leveldb::Iterator* it = m_database->NewIterator(leveldb::ReadOptions()); + for (it->SeekToFirst(); it->Valid(); it->Next()) { + dst.push_back(getIntegerAsBlock(stoi64(it->key().ToString()))); + } + ENSURE_STATUS_OK(it->status()); // Check for any errors found during the scan + delete it; +} + +#endif // USE_LEVELDB + diff --git a/src/database/database-leveldb.h b/src/database/database-leveldb.h new file mode 100644 index 000000000..d30f9f8f5 --- /dev/null +++ b/src/database/database-leveldb.h @@ -0,0 +1,48 @@ +/* +Minetest +Copyright (C) 2013 celeron55, Perttu Ahola + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#pragma once + +#include "config.h" + +#if USE_LEVELDB + +#include +#include "database.h" +#include "leveldb/db.h" + +class Database_LevelDB : public MapDatabase +{ +public: + Database_LevelDB(const std::string &savedir); + ~Database_LevelDB(); + + bool saveBlock(const v3s16 &pos, const std::string &data); + void loadBlock(const v3s16 &pos, std::string *block); + bool deleteBlock(const v3s16 &pos); + void listAllLoadableBlocks(std::vector &dst); + + void beginSave() {} + void endSave() {} + +private: + leveldb::DB *m_database; +}; + +#endif // USE_LEVELDB diff --git a/src/database/database-postgresql.cpp b/src/database/database-postgresql.cpp new file mode 100644 index 000000000..74651135a --- /dev/null +++ b/src/database/database-postgresql.cpp @@ -0,0 +1,631 @@ +/* +Copyright (C) 2016 Loic Blot + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#include "config.h" + +#if USE_POSTGRESQL + +#include "database-postgresql.h" + +#ifdef _WIN32 + // Without this some of the network functions are not found on mingw + #ifndef _WIN32_WINNT + #define _WIN32_WINNT 0x0501 + #endif + #include + #include +#else +#include +#endif + +#include "debug.h" +#include "exceptions.h" +#include "settings.h" +#include "content_sao.h" +#include "remoteplayer.h" + +Database_PostgreSQL::Database_PostgreSQL(const std::string &connect_string) : + m_connect_string(connect_string) +{ + if (m_connect_string.empty()) { + throw SettingNotFoundException( + "Set pgsql_connection string in world.mt to " + "use the postgresql backend\n" + "Notes:\n" + "pgsql_connection has the following form: \n" + "\tpgsql_connection = host=127.0.0.1 port=5432 user=mt_user " + "password=mt_password dbname=minetest_world\n" + "mt_user should have CREATE TABLE, INSERT, SELECT, UPDATE and " + "DELETE rights on the database.\n" + "Don't create mt_user as a SUPERUSER!"); + } +} + +Database_PostgreSQL::~Database_PostgreSQL() +{ + PQfinish(m_conn); +} + +void Database_PostgreSQL::connectToDatabase() +{ + m_conn = PQconnectdb(m_connect_string.c_str()); + + if (PQstatus(m_conn) != CONNECTION_OK) { + throw DatabaseException(std::string( + "PostgreSQL database error: ") + + PQerrorMessage(m_conn)); + } + + m_pgversion = PQserverVersion(m_conn); + + /* + * We are using UPSERT feature from PostgreSQL 9.5 + * to have the better performance where possible. + */ + if (m_pgversion < 90500) { + warningstream << "Your PostgreSQL server lacks UPSERT " + << "support. Use version 9.5 or better if possible." + << std::endl; + } + + infostream << "PostgreSQL Database: Version " << m_pgversion + << " Connection made." << std::endl; + + createDatabase(); + initStatements(); +} + +void Database_PostgreSQL::verifyDatabase() +{ + if (PQstatus(m_conn) == CONNECTION_OK) + return; + + PQreset(m_conn); + ping(); +} + +void Database_PostgreSQL::ping() +{ + if (PQping(m_connect_string.c_str()) != PQPING_OK) { + throw DatabaseException(std::string( + "PostgreSQL database error: ") + + PQerrorMessage(m_conn)); + } +} + +bool Database_PostgreSQL::initialized() const +{ + return (PQstatus(m_conn) == CONNECTION_OK); +} + +PGresult *Database_PostgreSQL::checkResults(PGresult *result, bool clear) +{ + ExecStatusType statusType = PQresultStatus(result); + + switch (statusType) { + case PGRES_COMMAND_OK: + case PGRES_TUPLES_OK: + break; + case PGRES_FATAL_ERROR: + default: + throw DatabaseException( + std::string("PostgreSQL database error: ") + + PQresultErrorMessage(result)); + } + + if (clear) + PQclear(result); + + return result; +} + +void Database_PostgreSQL::createTableIfNotExists(const std::string &table_name, + const std::string &definition) +{ + std::string sql_check_table = "SELECT relname FROM pg_class WHERE relname='" + + table_name + "';"; + PGresult *result = checkResults(PQexec(m_conn, sql_check_table.c_str()), false); + + // If table doesn't exist, create it + if (!PQntuples(result)) { + checkResults(PQexec(m_conn, definition.c_str())); + } + + PQclear(result); +} + +void Database_PostgreSQL::beginSave() +{ + verifyDatabase(); + checkResults(PQexec(m_conn, "BEGIN;")); +} + +void Database_PostgreSQL::endSave() +{ + checkResults(PQexec(m_conn, "COMMIT;")); +} + +MapDatabasePostgreSQL::MapDatabasePostgreSQL(const std::string &connect_string): + Database_PostgreSQL(connect_string), + MapDatabase() +{ + connectToDatabase(); +} + + +void MapDatabasePostgreSQL::createDatabase() +{ + createTableIfNotExists("blocks", + "CREATE TABLE blocks (" + "posX INT NOT NULL," + "posY INT NOT NULL," + "posZ INT NOT NULL," + "data BYTEA," + "PRIMARY KEY (posX,posY,posZ)" + ");" + ); + + infostream << "PostgreSQL: Map Database was initialized." << std::endl; +} + +void MapDatabasePostgreSQL::initStatements() +{ + prepareStatement("read_block", + "SELECT data FROM blocks " + "WHERE posX = $1::int4 AND posY = $2::int4 AND " + "posZ = $3::int4"); + + if (getPGVersion() < 90500) { + prepareStatement("write_block_insert", + "INSERT INTO blocks (posX, posY, posZ, data) SELECT " + "$1::int4, $2::int4, $3::int4, $4::bytea " + "WHERE NOT EXISTS (SELECT true FROM blocks " + "WHERE posX = $1::int4 AND posY = $2::int4 AND " + "posZ = $3::int4)"); + + prepareStatement("write_block_update", + "UPDATE blocks SET data = $4::bytea " + "WHERE posX = $1::int4 AND posY = $2::int4 AND " + "posZ = $3::int4"); + } else { + prepareStatement("write_block", + "INSERT INTO blocks (posX, posY, posZ, data) VALUES " + "($1::int4, $2::int4, $3::int4, $4::bytea) " + "ON CONFLICT ON CONSTRAINT blocks_pkey DO " + "UPDATE SET data = $4::bytea"); + } + + prepareStatement("delete_block", "DELETE FROM blocks WHERE " + "posX = $1::int4 AND posY = $2::int4 AND posZ = $3::int4"); + + prepareStatement("list_all_loadable_blocks", + "SELECT posX, posY, posZ FROM blocks"); +} + +bool MapDatabasePostgreSQL::saveBlock(const v3s16 &pos, const std::string &data) +{ + // Verify if we don't overflow the platform integer with the mapblock size + if (data.size() > INT_MAX) { + errorstream << "Database_PostgreSQL::saveBlock: Data truncation! " + << "data.size() over 0xFFFFFFFF (== " << data.size() + << ")" << std::endl; + return false; + } + + verifyDatabase(); + + s32 x, y, z; + x = htonl(pos.X); + y = htonl(pos.Y); + z = htonl(pos.Z); + + const void *args[] = { &x, &y, &z, data.c_str() }; + const int argLen[] = { + sizeof(x), sizeof(y), sizeof(z), (int)data.size() + }; + const int argFmt[] = { 1, 1, 1, 1 }; + + if (getPGVersion() < 90500) { + execPrepared("write_block_update", ARRLEN(args), args, argLen, argFmt); + execPrepared("write_block_insert", ARRLEN(args), args, argLen, argFmt); + } else { + execPrepared("write_block", ARRLEN(args), args, argLen, argFmt); + } + return true; +} + +void MapDatabasePostgreSQL::loadBlock(const v3s16 &pos, std::string *block) +{ + verifyDatabase(); + + s32 x, y, z; + x = htonl(pos.X); + y = htonl(pos.Y); + z = htonl(pos.Z); + + const void *args[] = { &x, &y, &z }; + const int argLen[] = { sizeof(x), sizeof(y), sizeof(z) }; + const int argFmt[] = { 1, 1, 1 }; + + PGresult *results = execPrepared("read_block", ARRLEN(args), args, + argLen, argFmt, false); + + *block = ""; + + if (PQntuples(results)) + *block = std::string(PQgetvalue(results, 0, 0), PQgetlength(results, 0, 0)); + + PQclear(results); +} + +bool MapDatabasePostgreSQL::deleteBlock(const v3s16 &pos) +{ + verifyDatabase(); + + s32 x, y, z; + x = htonl(pos.X); + y = htonl(pos.Y); + z = htonl(pos.Z); + + const void *args[] = { &x, &y, &z }; + const int argLen[] = { sizeof(x), sizeof(y), sizeof(z) }; + const int argFmt[] = { 1, 1, 1 }; + + execPrepared("delete_block", ARRLEN(args), args, argLen, argFmt); + + return true; +} + +void MapDatabasePostgreSQL::listAllLoadableBlocks(std::vector &dst) +{ + verifyDatabase(); + + PGresult *results = execPrepared("list_all_loadable_blocks", 0, + NULL, NULL, NULL, false, false); + + int numrows = PQntuples(results); + + for (int row = 0; row < numrows; ++row) + dst.push_back(pg_to_v3s16(results, 0, 0)); + + PQclear(results); +} + +/* + * Player Database + */ +PlayerDatabasePostgreSQL::PlayerDatabasePostgreSQL(const std::string &connect_string): + Database_PostgreSQL(connect_string), + PlayerDatabase() +{ + connectToDatabase(); +} + + +void PlayerDatabasePostgreSQL::createDatabase() +{ + createTableIfNotExists("player", + "CREATE TABLE player (" + "name VARCHAR(60) NOT NULL," + "pitch NUMERIC(15, 7) NOT NULL," + "yaw NUMERIC(15, 7) NOT NULL," + "posX NUMERIC(15, 7) NOT NULL," + "posY NUMERIC(15, 7) NOT NULL," + "posZ NUMERIC(15, 7) NOT NULL," + "hp INT NOT NULL," + "breath INT NOT NULL," + "creation_date TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW()," + "modification_date TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW()," + "PRIMARY KEY (name)" + ");" + ); + + createTableIfNotExists("player_inventories", + "CREATE TABLE player_inventories (" + "player VARCHAR(60) NOT NULL," + "inv_id INT NOT NULL," + "inv_width INT NOT NULL," + "inv_name TEXT NOT NULL DEFAULT ''," + "inv_size INT NOT NULL," + "PRIMARY KEY(player, inv_id)," + "CONSTRAINT player_inventories_fkey FOREIGN KEY (player) REFERENCES " + "player (name) ON DELETE CASCADE" + ");" + ); + + createTableIfNotExists("player_inventory_items", + "CREATE TABLE player_inventory_items (" + "player VARCHAR(60) NOT NULL," + "inv_id INT NOT NULL," + "slot_id INT NOT NULL," + "item TEXT NOT NULL DEFAULT ''," + "PRIMARY KEY(player, inv_id, slot_id)," + "CONSTRAINT player_inventory_items_fkey FOREIGN KEY (player) REFERENCES " + "player (name) ON DELETE CASCADE" + ");" + ); + + createTableIfNotExists("player_metadata", + "CREATE TABLE player_metadata (" + "player VARCHAR(60) NOT NULL," + "attr VARCHAR(256) NOT NULL," + "value TEXT," + "PRIMARY KEY(player, attr)," + "CONSTRAINT player_metadata_fkey FOREIGN KEY (player) REFERENCES " + "player (name) ON DELETE CASCADE" + ");" + ); + + infostream << "PostgreSQL: Player Database was inited." << std::endl; +} + +void PlayerDatabasePostgreSQL::initStatements() +{ + if (getPGVersion() < 90500) { + prepareStatement("create_player", + "INSERT INTO player(name, pitch, yaw, posX, posY, posZ, hp, breath) VALUES " + "($1, $2, $3, $4, $5, $6, $7::int, $8::int)"); + + prepareStatement("update_player", + "UPDATE SET pitch = $2, yaw = $3, posX = $4, posY = $5, posZ = $6, hp = $7::int, " + "breath = $8::int, modification_date = NOW() WHERE name = $1"); + } else { + prepareStatement("save_player", + "INSERT INTO player(name, pitch, yaw, posX, posY, posZ, hp, breath) VALUES " + "($1, $2, $3, $4, $5, $6, $7::int, $8::int)" + "ON CONFLICT ON CONSTRAINT player_pkey DO UPDATE SET pitch = $2, yaw = $3, " + "posX = $4, posY = $5, posZ = $6, hp = $7::int, breath = $8::int, " + "modification_date = NOW()"); + } + + prepareStatement("remove_player", "DELETE FROM player WHERE name = $1"); + + prepareStatement("load_player_list", "SELECT name FROM player"); + + prepareStatement("remove_player_inventories", + "DELETE FROM player_inventories WHERE player = $1"); + + prepareStatement("remove_player_inventory_items", + "DELETE FROM player_inventory_items WHERE player = $1"); + + prepareStatement("add_player_inventory", + "INSERT INTO player_inventories (player, inv_id, inv_width, inv_name, inv_size) VALUES " + "($1, $2::int, $3::int, $4, $5::int)"); + + prepareStatement("add_player_inventory_item", + "INSERT INTO player_inventory_items (player, inv_id, slot_id, item) VALUES " + "($1, $2::int, $3::int, $4)"); + + prepareStatement("load_player_inventories", + "SELECT inv_id, inv_width, inv_name, inv_size FROM player_inventories " + "WHERE player = $1 ORDER BY inv_id"); + + prepareStatement("load_player_inventory_items", + "SELECT slot_id, item FROM player_inventory_items WHERE " + "player = $1 AND inv_id = $2::int"); + + prepareStatement("load_player", + "SELECT pitch, yaw, posX, posY, posZ, hp, breath FROM player WHERE name = $1"); + + prepareStatement("remove_player_metadata", + "DELETE FROM player_metadata WHERE player = $1"); + + prepareStatement("save_player_metadata", + "INSERT INTO player_metadata (player, attr, value) VALUES ($1, $2, $3)"); + + prepareStatement("load_player_metadata", + "SELECT attr, value FROM player_metadata WHERE player = $1"); + +} + +bool PlayerDatabasePostgreSQL::playerDataExists(const std::string &playername) +{ + verifyDatabase(); + + const char *values[] = { playername.c_str() }; + PGresult *results = execPrepared("load_player", 1, values, false); + + bool res = (PQntuples(results) > 0); + PQclear(results); + return res; +} + +void PlayerDatabasePostgreSQL::savePlayer(RemotePlayer *player) +{ + PlayerSAO* sao = player->getPlayerSAO(); + if (!sao) + return; + + verifyDatabase(); + + v3f pos = sao->getBasePosition(); + std::string pitch = ftos(sao->getPitch()); + std::string yaw = ftos(sao->getYaw()); + std::string posx = ftos(pos.X); + std::string posy = ftos(pos.Y); + std::string posz = ftos(pos.Z); + std::string hp = itos(sao->getHP()); + std::string breath = itos(sao->getBreath()); + const char *values[] = { + player->getName(), + pitch.c_str(), + yaw.c_str(), + posx.c_str(), posy.c_str(), posz.c_str(), + hp.c_str(), + breath.c_str() + }; + + const char* rmvalues[] = { player->getName() }; + beginSave(); + + if (getPGVersion() < 90500) { + if (!playerDataExists(player->getName())) + execPrepared("create_player", 8, values, true, false); + else + execPrepared("update_player", 8, values, true, false); + } + else + execPrepared("save_player", 8, values, true, false); + + // Write player inventories + execPrepared("remove_player_inventories", 1, rmvalues); + execPrepared("remove_player_inventory_items", 1, rmvalues); + + std::vector inventory_lists = sao->getInventory()->getLists(); + for (u16 i = 0; i < inventory_lists.size(); i++) { + const InventoryList* list = inventory_lists[i]; + const std::string &name = list->getName(); + std::string width = itos(list->getWidth()), + inv_id = itos(i), lsize = itos(list->getSize()); + + const char* inv_values[] = { + player->getName(), + inv_id.c_str(), + width.c_str(), + name.c_str(), + lsize.c_str() + }; + execPrepared("add_player_inventory", 5, inv_values); + + for (u32 j = 0; j < list->getSize(); j++) { + std::ostringstream os; + list->getItem(j).serialize(os); + std::string itemStr = os.str(), slotId = itos(j); + + const char* invitem_values[] = { + player->getName(), + inv_id.c_str(), + slotId.c_str(), + itemStr.c_str() + }; + execPrepared("add_player_inventory_item", 4, invitem_values); + } + } + + execPrepared("remove_player_metadata", 1, rmvalues); + const PlayerAttributes &attrs = sao->getExtendedAttributes(); + for (const auto &attr : attrs) { + const char *meta_values[] = { + player->getName(), + attr.first.c_str(), + attr.second.c_str() + }; + execPrepared("save_player_metadata", 3, meta_values); + } + endSave(); +} + +bool PlayerDatabasePostgreSQL::loadPlayer(RemotePlayer *player, PlayerSAO *sao) +{ + sanity_check(sao); + verifyDatabase(); + + const char *values[] = { player->getName() }; + PGresult *results = execPrepared("load_player", 1, values, false, false); + + // Player not found, return not found + if (!PQntuples(results)) { + PQclear(results); + return false; + } + + sao->setPitch(pg_to_float(results, 0, 0)); + sao->setYaw(pg_to_float(results, 0, 1)); + sao->setBasePosition(v3f( + pg_to_float(results, 0, 2), + pg_to_float(results, 0, 3), + pg_to_float(results, 0, 4)) + ); + sao->setHPRaw((s16) pg_to_int(results, 0, 5)); + sao->setBreath((u16) pg_to_int(results, 0, 6), false); + + PQclear(results); + + // Load inventory + results = execPrepared("load_player_inventories", 1, values, false, false); + + int resultCount = PQntuples(results); + + for (int row = 0; row < resultCount; ++row) { + InventoryList* invList = player->inventory. + addList(PQgetvalue(results, row, 2), pg_to_uint(results, row, 3)); + invList->setWidth(pg_to_uint(results, row, 1)); + + u32 invId = pg_to_uint(results, row, 0); + std::string invIdStr = itos(invId); + + const char* values2[] = { + player->getName(), + invIdStr.c_str() + }; + PGresult *results2 = execPrepared("load_player_inventory_items", 2, + values2, false, false); + + int resultCount2 = PQntuples(results2); + for (int row2 = 0; row2 < resultCount2; row2++) { + const std::string itemStr = PQgetvalue(results2, row2, 1); + if (itemStr.length() > 0) { + ItemStack stack; + stack.deSerialize(itemStr); + invList->changeItem(pg_to_uint(results2, row2, 0), stack); + } + } + PQclear(results2); + } + + PQclear(results); + + results = execPrepared("load_player_metadata", 1, values, false); + + int numrows = PQntuples(results); + for (int row = 0; row < numrows; row++) { + sao->setExtendedAttribute(PQgetvalue(results, row, 0),PQgetvalue(results, row, 1)); + } + + PQclear(results); + + return true; +} + +bool PlayerDatabasePostgreSQL::removePlayer(const std::string &name) +{ + if (!playerDataExists(name)) + return false; + + verifyDatabase(); + + const char *values[] = { name.c_str() }; + execPrepared("remove_player", 1, values); + + return true; +} + +void PlayerDatabasePostgreSQL::listPlayers(std::vector &res) +{ + verifyDatabase(); + + PGresult *results = execPrepared("load_player_list", 0, NULL, false); + + int numrows = PQntuples(results); + for (int row = 0; row < numrows; row++) + res.emplace_back(PQgetvalue(results, row, 0)); + + PQclear(results); +} + +#endif // USE_POSTGRESQL diff --git a/src/database/database-postgresql.h b/src/database/database-postgresql.h new file mode 100644 index 000000000..db0b505c9 --- /dev/null +++ b/src/database/database-postgresql.h @@ -0,0 +1,146 @@ +/* +Minetest +Copyright (C) 2013 celeron55, Perttu Ahola + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#pragma once + +#include +#include +#include "database.h" +#include "util/basic_macros.h" + +class Settings; + +class Database_PostgreSQL: public Database +{ +public: + Database_PostgreSQL(const std::string &connect_string); + ~Database_PostgreSQL(); + + void beginSave(); + void endSave(); + + bool initialized() const; + + +protected: + // Conversion helpers + inline int pg_to_int(PGresult *res, int row, int col) + { + return atoi(PQgetvalue(res, row, col)); + } + + inline u32 pg_to_uint(PGresult *res, int row, int col) + { + return (u32) atoi(PQgetvalue(res, row, col)); + } + + inline float pg_to_float(PGresult *res, int row, int col) + { + return (float) atof(PQgetvalue(res, row, col)); + } + + inline v3s16 pg_to_v3s16(PGresult *res, int row, int col) + { + return v3s16( + pg_to_int(res, row, col), + pg_to_int(res, row, col + 1), + pg_to_int(res, row, col + 2) + ); + } + + inline PGresult *execPrepared(const char *stmtName, const int paramsNumber, + const void **params, + const int *paramsLengths = NULL, const int *paramsFormats = NULL, + bool clear = true, bool nobinary = true) + { + return checkResults(PQexecPrepared(m_conn, stmtName, paramsNumber, + (const char* const*) params, paramsLengths, paramsFormats, + nobinary ? 1 : 0), clear); + } + + inline PGresult *execPrepared(const char *stmtName, const int paramsNumber, + const char **params, bool clear = true, bool nobinary = true) + { + return execPrepared(stmtName, paramsNumber, + (const void **)params, NULL, NULL, clear, nobinary); + } + + void createTableIfNotExists(const std::string &table_name, const std::string &definition); + void verifyDatabase(); + + // Database initialization + void connectToDatabase(); + virtual void createDatabase() = 0; + virtual void initStatements() = 0; + inline void prepareStatement(const std::string &name, const std::string &sql) + { + checkResults(PQprepare(m_conn, name.c_str(), sql.c_str(), 0, NULL)); + } + + const int getPGVersion() const { return m_pgversion; } +private: + // Database connectivity checks + void ping(); + + // Database usage + PGresult *checkResults(PGresult *res, bool clear = true); + + // Attributes + std::string m_connect_string; + PGconn *m_conn = nullptr; + int m_pgversion = 0; +}; + +class MapDatabasePostgreSQL : private Database_PostgreSQL, public MapDatabase +{ +public: + MapDatabasePostgreSQL(const std::string &connect_string); + virtual ~MapDatabasePostgreSQL() = default; + + bool saveBlock(const v3s16 &pos, const std::string &data); + void loadBlock(const v3s16 &pos, std::string *block); + bool deleteBlock(const v3s16 &pos); + void listAllLoadableBlocks(std::vector &dst); + + void beginSave() { Database_PostgreSQL::beginSave(); } + void endSave() { Database_PostgreSQL::endSave(); } + +protected: + virtual void createDatabase(); + virtual void initStatements(); +}; + +class PlayerDatabasePostgreSQL : private Database_PostgreSQL, public PlayerDatabase +{ +public: + PlayerDatabasePostgreSQL(const std::string &connect_string); + virtual ~PlayerDatabasePostgreSQL() = default; + + void savePlayer(RemotePlayer *player); + bool loadPlayer(RemotePlayer *player, PlayerSAO *sao); + bool removePlayer(const std::string &name); + void listPlayers(std::vector &res); + +protected: + virtual void createDatabase(); + virtual void initStatements(); + +private: + bool playerDataExists(const std::string &playername); +}; diff --git a/src/database/database-redis.cpp b/src/database/database-redis.cpp new file mode 100644 index 000000000..096ea504d --- /dev/null +++ b/src/database/database-redis.cpp @@ -0,0 +1,203 @@ +/* +Minetest +Copyright (C) 2014 celeron55, Perttu Ahola + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#include "config.h" + +#if USE_REDIS + +#include "database-redis.h" + +#include "settings.h" +#include "log.h" +#include "exceptions.h" +#include "util/string.h" + +#include +#include + + +Database_Redis::Database_Redis(Settings &conf) +{ + std::string tmp; + try { + tmp = conf.get("redis_address"); + hash = conf.get("redis_hash"); + } catch (SettingNotFoundException &) { + throw SettingNotFoundException("Set redis_address and " + "redis_hash in world.mt to use the redis backend"); + } + const char *addr = tmp.c_str(); + int port = conf.exists("redis_port") ? conf.getU16("redis_port") : 6379; + // if redis_address contains '/' assume unix socket, else hostname/ip + ctx = tmp.find('/') != std::string::npos ? redisConnectUnix(addr) : redisConnect(addr, port); + if (!ctx) { + throw DatabaseException("Cannot allocate redis context"); + } else if (ctx->err) { + std::string err = std::string("Connection error: ") + ctx->errstr; + redisFree(ctx); + throw DatabaseException(err); + } + if (conf.exists("redis_password")) { + tmp = conf.get("redis_password"); + redisReply *reply = static_cast(redisCommand(ctx, "AUTH %s", tmp.c_str())); + if (!reply) + throw DatabaseException("Redis authentication failed"); + if (reply->type == REDIS_REPLY_ERROR) { + std::string err = "Redis authentication failed: " + std::string(reply->str, reply->len); + freeReplyObject(reply); + throw DatabaseException(err); + } + freeReplyObject(reply); + } +} + +Database_Redis::~Database_Redis() +{ + redisFree(ctx); +} + +void Database_Redis::beginSave() { + redisReply *reply = static_cast(redisCommand(ctx, "MULTI")); + if (!reply) { + throw DatabaseException(std::string( + "Redis command 'MULTI' failed: ") + ctx->errstr); + } + freeReplyObject(reply); +} + +void Database_Redis::endSave() { + redisReply *reply = static_cast(redisCommand(ctx, "EXEC")); + if (!reply) { + throw DatabaseException(std::string( + "Redis command 'EXEC' failed: ") + ctx->errstr); + } + freeReplyObject(reply); +} + +bool Database_Redis::saveBlock(const v3s16 &pos, const std::string &data) +{ + std::string tmp = i64tos(getBlockAsInteger(pos)); + + redisReply *reply = static_cast(redisCommand(ctx, "HSET %s %s %b", + hash.c_str(), tmp.c_str(), data.c_str(), data.size())); + if (!reply) { + warningstream << "saveBlock: redis command 'HSET' failed on " + "block " << PP(pos) << ": " << ctx->errstr << std::endl; + freeReplyObject(reply); + return false; + } + + if (reply->type == REDIS_REPLY_ERROR) { + warningstream << "saveBlock: saving block " << PP(pos) + << " failed: " << std::string(reply->str, reply->len) << std::endl; + freeReplyObject(reply); + return false; + } + + freeReplyObject(reply); + return true; +} + +void Database_Redis::loadBlock(const v3s16 &pos, std::string *block) +{ + std::string tmp = i64tos(getBlockAsInteger(pos)); + redisReply *reply = static_cast(redisCommand(ctx, + "HGET %s %s", hash.c_str(), tmp.c_str())); + + if (!reply) { + throw DatabaseException(std::string( + "Redis command 'HGET %s %s' failed: ") + ctx->errstr); + } + + switch (reply->type) { + case REDIS_REPLY_STRING: { + *block = std::string(reply->str, reply->len); + // std::string copies the memory so this won't cause any problems + freeReplyObject(reply); + return; + } + case REDIS_REPLY_ERROR: { + std::string errstr(reply->str, reply->len); + freeReplyObject(reply); + errorstream << "loadBlock: loading block " << PP(pos) + << " failed: " << errstr << std::endl; + throw DatabaseException(std::string( + "Redis command 'HGET %s %s' errored: ") + errstr); + } + case REDIS_REPLY_NIL: { + *block = ""; + // block not found in database + freeReplyObject(reply); + return; + } + } + + errorstream << "loadBlock: loading block " << PP(pos) + << " returned invalid reply type " << reply->type + << ": " << std::string(reply->str, reply->len) << std::endl; + freeReplyObject(reply); + throw DatabaseException(std::string( + "Redis command 'HGET %s %s' gave invalid reply.")); +} + +bool Database_Redis::deleteBlock(const v3s16 &pos) +{ + std::string tmp = i64tos(getBlockAsInteger(pos)); + + redisReply *reply = static_cast(redisCommand(ctx, + "HDEL %s %s", hash.c_str(), tmp.c_str())); + if (!reply) { + throw DatabaseException(std::string( + "Redis command 'HDEL %s %s' failed: ") + ctx->errstr); + } else if (reply->type == REDIS_REPLY_ERROR) { + warningstream << "deleteBlock: deleting block " << PP(pos) + << " failed: " << std::string(reply->str, reply->len) << std::endl; + freeReplyObject(reply); + return false; + } + + freeReplyObject(reply); + return true; +} + +void Database_Redis::listAllLoadableBlocks(std::vector &dst) +{ + redisReply *reply = static_cast(redisCommand(ctx, "HKEYS %s", hash.c_str())); + if (!reply) { + throw DatabaseException(std::string( + "Redis command 'HKEYS %s' failed: ") + ctx->errstr); + } + switch (reply->type) { + case REDIS_REPLY_ARRAY: + dst.reserve(reply->elements); + for (size_t i = 0; i < reply->elements; i++) { + assert(reply->element[i]->type == REDIS_REPLY_STRING); + dst.push_back(getIntegerAsBlock(stoi64(reply->element[i]->str))); + } + break; + case REDIS_REPLY_ERROR: + throw DatabaseException(std::string( + "Failed to get keys from database: ") + + std::string(reply->str, reply->len)); + } + freeReplyObject(reply); +} + +#endif // USE_REDIS + diff --git a/src/database/database-redis.h b/src/database/database-redis.h new file mode 100644 index 000000000..6bea563bc --- /dev/null +++ b/src/database/database-redis.h @@ -0,0 +1,51 @@ +/* +Minetest +Copyright (C) 2014 celeron55, Perttu Ahola + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#pragma once + +#include "config.h" + +#if USE_REDIS + +#include +#include +#include "database.h" + +class Settings; + +class Database_Redis : public MapDatabase +{ +public: + Database_Redis(Settings &conf); + ~Database_Redis(); + + void beginSave(); + void endSave(); + + bool saveBlock(const v3s16 &pos, const std::string &data); + void loadBlock(const v3s16 &pos, std::string *block); + bool deleteBlock(const v3s16 &pos); + void listAllLoadableBlocks(std::vector &dst); + +private: + redisContext *ctx = nullptr; + std::string hash = ""; +}; + +#endif // USE_REDIS diff --git a/src/database/database-sqlite3.cpp b/src/database/database-sqlite3.cpp new file mode 100644 index 000000000..78c182f86 --- /dev/null +++ b/src/database/database-sqlite3.cpp @@ -0,0 +1,606 @@ +/* +Minetest +Copyright (C) 2013 celeron55, Perttu Ahola + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +/* +SQLite format specification: + blocks: + (PK) INT id + BLOB data +*/ + + +#include "database-sqlite3.h" + +#include "log.h" +#include "filesys.h" +#include "exceptions.h" +#include "settings.h" +#include "porting.h" +#include "util/string.h" +#include "content_sao.h" +#include "remoteplayer.h" + +#include + +// When to print messages when the database is being held locked by another process +// Note: I've seen occasional delays of over 250ms while running minetestmapper. +#define BUSY_INFO_TRESHOLD 100 // Print first informational message after 100ms. +#define BUSY_WARNING_TRESHOLD 250 // Print warning message after 250ms. Lag is increased. +#define BUSY_ERROR_TRESHOLD 1000 // Print error message after 1000ms. Significant lag. +#define BUSY_FATAL_TRESHOLD 3000 // Allow SQLITE_BUSY to be returned, which will cause a minetest crash. +#define BUSY_ERROR_INTERVAL 10000 // Safety net: report again every 10 seconds + + +#define SQLRES(s, r, m) \ + if ((s) != (r)) { \ + throw DatabaseException(std::string(m) + ": " +\ + sqlite3_errmsg(m_database)); \ + } +#define SQLOK(s, m) SQLRES(s, SQLITE_OK, m) + +#define PREPARE_STATEMENT(name, query) \ + SQLOK(sqlite3_prepare_v2(m_database, query, -1, &m_stmt_##name, NULL),\ + "Failed to prepare query '" query "'") + +#define SQLOK_ERRSTREAM(s, m) \ + if ((s) != SQLITE_OK) { \ + errorstream << (m) << ": " \ + << sqlite3_errmsg(m_database) << std::endl; \ + } + +#define FINALIZE_STATEMENT(statement) SQLOK_ERRSTREAM(sqlite3_finalize(statement), \ + "Failed to finalize " #statement) + +int Database_SQLite3::busyHandler(void *data, int count) +{ + s64 &first_time = reinterpret_cast(data)[0]; + s64 &prev_time = reinterpret_cast(data)[1]; + s64 cur_time = porting::getTimeMs(); + + if (count == 0) { + first_time = cur_time; + prev_time = first_time; + } else { + while (cur_time < prev_time) + cur_time += s64(1)<<32; + } + + if (cur_time - first_time < BUSY_INFO_TRESHOLD) { + ; // do nothing + } else if (cur_time - first_time >= BUSY_INFO_TRESHOLD && + prev_time - first_time < BUSY_INFO_TRESHOLD) { + infostream << "SQLite3 database has been locked for " + << cur_time - first_time << " ms." << std::endl; + } else if (cur_time - first_time >= BUSY_WARNING_TRESHOLD && + prev_time - first_time < BUSY_WARNING_TRESHOLD) { + warningstream << "SQLite3 database has been locked for " + << cur_time - first_time << " ms." << std::endl; + } else if (cur_time - first_time >= BUSY_ERROR_TRESHOLD && + prev_time - first_time < BUSY_ERROR_TRESHOLD) { + errorstream << "SQLite3 database has been locked for " + << cur_time - first_time << " ms; this causes lag." << std::endl; + } else if (cur_time - first_time >= BUSY_FATAL_TRESHOLD && + prev_time - first_time < BUSY_FATAL_TRESHOLD) { + errorstream << "SQLite3 database has been locked for " + << cur_time - first_time << " ms - giving up!" << std::endl; + } else if ((cur_time - first_time) / BUSY_ERROR_INTERVAL != + (prev_time - first_time) / BUSY_ERROR_INTERVAL) { + // Safety net: keep reporting every BUSY_ERROR_INTERVAL + errorstream << "SQLite3 database has been locked for " + << (cur_time - first_time) / 1000 << " seconds!" << std::endl; + } + + prev_time = cur_time; + + // Make sqlite transaction fail if delay exceeds BUSY_FATAL_TRESHOLD + return cur_time - first_time < BUSY_FATAL_TRESHOLD; +} + + +Database_SQLite3::Database_SQLite3(const std::string &savedir, const std::string &dbname) : + m_savedir(savedir), + m_dbname(dbname) +{ +} + +void Database_SQLite3::beginSave() +{ + verifyDatabase(); + SQLRES(sqlite3_step(m_stmt_begin), SQLITE_DONE, + "Failed to start SQLite3 transaction"); + sqlite3_reset(m_stmt_begin); +} + +void Database_SQLite3::endSave() +{ + verifyDatabase(); + SQLRES(sqlite3_step(m_stmt_end), SQLITE_DONE, + "Failed to commit SQLite3 transaction"); + sqlite3_reset(m_stmt_end); +} + +void Database_SQLite3::openDatabase() +{ + if (m_database) return; + + std::string dbp = m_savedir + DIR_DELIM + m_dbname + ".sqlite"; + + // Open the database connection + + if (!fs::CreateAllDirs(m_savedir)) { + infostream << "Database_SQLite3: Failed to create directory \"" + << m_savedir << "\"" << std::endl; + throw FileNotGoodException("Failed to create database " + "save directory"); + } + + bool needs_create = !fs::PathExists(dbp); + + SQLOK(sqlite3_open_v2(dbp.c_str(), &m_database, + SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL), + std::string("Failed to open SQLite3 database file ") + dbp); + + SQLOK(sqlite3_busy_handler(m_database, Database_SQLite3::busyHandler, + m_busy_handler_data), "Failed to set SQLite3 busy handler"); + + if (needs_create) { + createDatabase(); + } + + std::string query_str = std::string("PRAGMA synchronous = ") + + itos(g_settings->getU16("sqlite_synchronous")); + SQLOK(sqlite3_exec(m_database, query_str.c_str(), NULL, NULL, NULL), + "Failed to modify sqlite3 synchronous mode"); + SQLOK(sqlite3_exec(m_database, "PRAGMA foreign_keys = ON", NULL, NULL, NULL), + "Failed to enable sqlite3 foreign key support"); +} + +void Database_SQLite3::verifyDatabase() +{ + if (m_initialized) return; + + openDatabase(); + + PREPARE_STATEMENT(begin, "BEGIN;"); + PREPARE_STATEMENT(end, "COMMIT;"); + + initStatements(); + + m_initialized = true; +} + +Database_SQLite3::~Database_SQLite3() +{ + FINALIZE_STATEMENT(m_stmt_begin) + FINALIZE_STATEMENT(m_stmt_end) + + SQLOK_ERRSTREAM(sqlite3_close(m_database), "Failed to close database"); +} + +/* + * Map database + */ + +MapDatabaseSQLite3::MapDatabaseSQLite3(const std::string &savedir): + Database_SQLite3(savedir, "map"), + MapDatabase() +{ +} + +MapDatabaseSQLite3::~MapDatabaseSQLite3() +{ + FINALIZE_STATEMENT(m_stmt_read) + FINALIZE_STATEMENT(m_stmt_write) + FINALIZE_STATEMENT(m_stmt_list) + FINALIZE_STATEMENT(m_stmt_delete) +} + + +void MapDatabaseSQLite3::createDatabase() +{ + assert(m_database); // Pre-condition + + SQLOK(sqlite3_exec(m_database, + "CREATE TABLE IF NOT EXISTS `blocks` (\n" + " `pos` INT PRIMARY KEY,\n" + " `data` BLOB\n" + ");\n", + NULL, NULL, NULL), + "Failed to create database table"); +} + +void MapDatabaseSQLite3::initStatements() +{ + PREPARE_STATEMENT(read, "SELECT `data` FROM `blocks` WHERE `pos` = ? LIMIT 1"); +#ifdef __ANDROID__ + PREPARE_STATEMENT(write, "INSERT INTO `blocks` (`pos`, `data`) VALUES (?, ?)"); +#else + PREPARE_STATEMENT(write, "REPLACE INTO `blocks` (`pos`, `data`) VALUES (?, ?)"); +#endif + PREPARE_STATEMENT(delete, "DELETE FROM `blocks` WHERE `pos` = ?"); + PREPARE_STATEMENT(list, "SELECT `pos` FROM `blocks`"); + + verbosestream << "ServerMap: SQLite3 database opened." << std::endl; +} + +inline void MapDatabaseSQLite3::bindPos(sqlite3_stmt *stmt, const v3s16 &pos, int index) +{ + SQLOK(sqlite3_bind_int64(stmt, index, getBlockAsInteger(pos)), + "Internal error: failed to bind query at " __FILE__ ":" TOSTRING(__LINE__)); +} + +bool MapDatabaseSQLite3::deleteBlock(const v3s16 &pos) +{ + verifyDatabase(); + + bindPos(m_stmt_delete, pos); + + bool good = sqlite3_step(m_stmt_delete) == SQLITE_DONE; + sqlite3_reset(m_stmt_delete); + + if (!good) { + warningstream << "deleteBlock: Block failed to delete " + << PP(pos) << ": " << sqlite3_errmsg(m_database) << std::endl; + } + return good; +} + +bool MapDatabaseSQLite3::saveBlock(const v3s16 &pos, const std::string &data) +{ + verifyDatabase(); + +#ifdef __ANDROID__ + /** + * Note: For some unknown reason SQLite3 fails to REPLACE blocks on Android, + * deleting them and then inserting works. + */ + bindPos(m_stmt_read, pos); + + if (sqlite3_step(m_stmt_read) == SQLITE_ROW) { + deleteBlock(pos); + } + sqlite3_reset(m_stmt_read); +#endif + + bindPos(m_stmt_write, pos); + SQLOK(sqlite3_bind_blob(m_stmt_write, 2, data.data(), data.size(), NULL), + "Internal error: failed to bind query at " __FILE__ ":" TOSTRING(__LINE__)); + + SQLRES(sqlite3_step(m_stmt_write), SQLITE_DONE, "Failed to save block") + sqlite3_reset(m_stmt_write); + + return true; +} + +void MapDatabaseSQLite3::loadBlock(const v3s16 &pos, std::string *block) +{ + verifyDatabase(); + + bindPos(m_stmt_read, pos); + + if (sqlite3_step(m_stmt_read) != SQLITE_ROW) { + sqlite3_reset(m_stmt_read); + return; + } + + const char *data = (const char *) sqlite3_column_blob(m_stmt_read, 0); + size_t len = sqlite3_column_bytes(m_stmt_read, 0); + + *block = (data) ? std::string(data, len) : ""; + + sqlite3_step(m_stmt_read); + // We should never get more than 1 row, so ok to reset + sqlite3_reset(m_stmt_read); +} + +void MapDatabaseSQLite3::listAllLoadableBlocks(std::vector &dst) +{ + verifyDatabase(); + + while (sqlite3_step(m_stmt_list) == SQLITE_ROW) + dst.push_back(getIntegerAsBlock(sqlite3_column_int64(m_stmt_list, 0))); + + sqlite3_reset(m_stmt_list); +} + +/* + * Player Database + */ + +PlayerDatabaseSQLite3::PlayerDatabaseSQLite3(const std::string &savedir): + Database_SQLite3(savedir, "players"), + PlayerDatabase() +{ +} + +PlayerDatabaseSQLite3::~PlayerDatabaseSQLite3() +{ + FINALIZE_STATEMENT(m_stmt_player_load) + FINALIZE_STATEMENT(m_stmt_player_add) + FINALIZE_STATEMENT(m_stmt_player_update) + FINALIZE_STATEMENT(m_stmt_player_remove) + FINALIZE_STATEMENT(m_stmt_player_list) + FINALIZE_STATEMENT(m_stmt_player_add_inventory) + FINALIZE_STATEMENT(m_stmt_player_add_inventory_items) + FINALIZE_STATEMENT(m_stmt_player_remove_inventory) + FINALIZE_STATEMENT(m_stmt_player_remove_inventory_items) + FINALIZE_STATEMENT(m_stmt_player_load_inventory) + FINALIZE_STATEMENT(m_stmt_player_load_inventory_items) + FINALIZE_STATEMENT(m_stmt_player_metadata_load) + FINALIZE_STATEMENT(m_stmt_player_metadata_add) + FINALIZE_STATEMENT(m_stmt_player_metadata_remove) +}; + + +void PlayerDatabaseSQLite3::createDatabase() +{ + assert(m_database); // Pre-condition + + SQLOK(sqlite3_exec(m_database, + "CREATE TABLE IF NOT EXISTS `player` (" + "`name` VARCHAR(50) NOT NULL," + "`pitch` NUMERIC(11, 4) NOT NULL," + "`yaw` NUMERIC(11, 4) NOT NULL," + "`posX` NUMERIC(11, 4) NOT NULL," + "`posY` NUMERIC(11, 4) NOT NULL," + "`posZ` NUMERIC(11, 4) NOT NULL," + "`hp` INT NOT NULL," + "`breath` INT NOT NULL," + "`creation_date` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP," + "`modification_date` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP," + "PRIMARY KEY (`name`));", + NULL, NULL, NULL), + "Failed to create player table"); + + SQLOK(sqlite3_exec(m_database, + "CREATE TABLE IF NOT EXISTS `player_metadata` (" + " `player` VARCHAR(50) NOT NULL," + " `metadata` VARCHAR(256) NOT NULL," + " `value` TEXT," + " PRIMARY KEY(`player`, `metadata`)," + " FOREIGN KEY (`player`) REFERENCES player (`name`) ON DELETE CASCADE );", + NULL, NULL, NULL), + "Failed to create player metadata table"); + + SQLOK(sqlite3_exec(m_database, + "CREATE TABLE IF NOT EXISTS `player_inventories` (" + " `player` VARCHAR(50) NOT NULL," + " `inv_id` INT NOT NULL," + " `inv_width` INT NOT NULL," + " `inv_name` TEXT NOT NULL DEFAULT ''," + " `inv_size` INT NOT NULL," + " PRIMARY KEY(player, inv_id)," + " FOREIGN KEY (`player`) REFERENCES player (`name`) ON DELETE CASCADE );", + NULL, NULL, NULL), + "Failed to create player inventory table"); + + SQLOK(sqlite3_exec(m_database, + "CREATE TABLE `player_inventory_items` (" + " `player` VARCHAR(50) NOT NULL," + " `inv_id` INT NOT NULL," + " `slot_id` INT NOT NULL," + " `item` TEXT NOT NULL DEFAULT ''," + " PRIMARY KEY(player, inv_id, slot_id)," + " FOREIGN KEY (`player`) REFERENCES player (`name`) ON DELETE CASCADE );", + NULL, NULL, NULL), + "Failed to create player inventory items table"); +} + +void PlayerDatabaseSQLite3::initStatements() +{ + PREPARE_STATEMENT(player_load, "SELECT `pitch`, `yaw`, `posX`, `posY`, `posZ`, `hp`, " + "`breath`" + "FROM `player` WHERE `name` = ?") + PREPARE_STATEMENT(player_add, "INSERT INTO `player` (`name`, `pitch`, `yaw`, `posX`, " + "`posY`, `posZ`, `hp`, `breath`) VALUES (?, ?, ?, ?, ?, ?, ?, ?)") + PREPARE_STATEMENT(player_update, "UPDATE `player` SET `pitch` = ?, `yaw` = ?, " + "`posX` = ?, `posY` = ?, `posZ` = ?, `hp` = ?, `breath` = ?, " + "`modification_date` = CURRENT_TIMESTAMP WHERE `name` = ?") + PREPARE_STATEMENT(player_remove, "DELETE FROM `player` WHERE `name` = ?") + PREPARE_STATEMENT(player_list, "SELECT `name` FROM `player`") + + PREPARE_STATEMENT(player_add_inventory, "INSERT INTO `player_inventories` " + "(`player`, `inv_id`, `inv_width`, `inv_name`, `inv_size`) VALUES (?, ?, ?, ?, ?)") + PREPARE_STATEMENT(player_add_inventory_items, "INSERT INTO `player_inventory_items` " + "(`player`, `inv_id`, `slot_id`, `item`) VALUES (?, ?, ?, ?)") + PREPARE_STATEMENT(player_remove_inventory, "DELETE FROM `player_inventories` " + "WHERE `player` = ?") + PREPARE_STATEMENT(player_remove_inventory_items, "DELETE FROM `player_inventory_items` " + "WHERE `player` = ?") + PREPARE_STATEMENT(player_load_inventory, "SELECT `inv_id`, `inv_width`, `inv_name`, " + "`inv_size` FROM `player_inventories` WHERE `player` = ? ORDER BY inv_id") + PREPARE_STATEMENT(player_load_inventory_items, "SELECT `slot_id`, `item` " + "FROM `player_inventory_items` WHERE `player` = ? AND `inv_id` = ?") + + PREPARE_STATEMENT(player_metadata_load, "SELECT `metadata`, `value` FROM " + "`player_metadata` WHERE `player` = ?") + PREPARE_STATEMENT(player_metadata_add, "INSERT INTO `player_metadata` " + "(`player`, `metadata`, `value`) VALUES (?, ?, ?)") + PREPARE_STATEMENT(player_metadata_remove, "DELETE FROM `player_metadata` " + "WHERE `player` = ?") + verbosestream << "ServerEnvironment: SQLite3 database opened (players)." << std::endl; +} + +bool PlayerDatabaseSQLite3::playerDataExists(const std::string &name) +{ + verifyDatabase(); + str_to_sqlite(m_stmt_player_load, 1, name); + bool res = (sqlite3_step(m_stmt_player_load) == SQLITE_ROW); + sqlite3_reset(m_stmt_player_load); + return res; +} + +void PlayerDatabaseSQLite3::savePlayer(RemotePlayer *player) +{ + PlayerSAO* sao = player->getPlayerSAO(); + sanity_check(sao); + + const v3f &pos = sao->getBasePosition(); + // Begin save in brace is mandatory + if (!playerDataExists(player->getName())) { + beginSave(); + str_to_sqlite(m_stmt_player_add, 1, player->getName()); + double_to_sqlite(m_stmt_player_add, 2, sao->getPitch()); + double_to_sqlite(m_stmt_player_add, 3, sao->getYaw()); + double_to_sqlite(m_stmt_player_add, 4, pos.X); + double_to_sqlite(m_stmt_player_add, 5, pos.Y); + double_to_sqlite(m_stmt_player_add, 6, pos.Z); + int64_to_sqlite(m_stmt_player_add, 7, sao->getHP()); + int64_to_sqlite(m_stmt_player_add, 8, sao->getBreath()); + + sqlite3_vrfy(sqlite3_step(m_stmt_player_add), SQLITE_DONE); + sqlite3_reset(m_stmt_player_add); + } else { + beginSave(); + double_to_sqlite(m_stmt_player_update, 1, sao->getPitch()); + double_to_sqlite(m_stmt_player_update, 2, sao->getYaw()); + double_to_sqlite(m_stmt_player_update, 3, pos.X); + double_to_sqlite(m_stmt_player_update, 4, pos.Y); + double_to_sqlite(m_stmt_player_update, 5, pos.Z); + int64_to_sqlite(m_stmt_player_update, 6, sao->getHP()); + int64_to_sqlite(m_stmt_player_update, 7, sao->getBreath()); + str_to_sqlite(m_stmt_player_update, 8, player->getName()); + + sqlite3_vrfy(sqlite3_step(m_stmt_player_update), SQLITE_DONE); + sqlite3_reset(m_stmt_player_update); + } + + // Write player inventories + str_to_sqlite(m_stmt_player_remove_inventory, 1, player->getName()); + sqlite3_vrfy(sqlite3_step(m_stmt_player_remove_inventory), SQLITE_DONE); + sqlite3_reset(m_stmt_player_remove_inventory); + + str_to_sqlite(m_stmt_player_remove_inventory_items, 1, player->getName()); + sqlite3_vrfy(sqlite3_step(m_stmt_player_remove_inventory_items), SQLITE_DONE); + sqlite3_reset(m_stmt_player_remove_inventory_items); + + std::vector inventory_lists = sao->getInventory()->getLists(); + for (u16 i = 0; i < inventory_lists.size(); i++) { + const InventoryList* list = inventory_lists[i]; + + str_to_sqlite(m_stmt_player_add_inventory, 1, player->getName()); + int_to_sqlite(m_stmt_player_add_inventory, 2, i); + int_to_sqlite(m_stmt_player_add_inventory, 3, list->getWidth()); + str_to_sqlite(m_stmt_player_add_inventory, 4, list->getName()); + int_to_sqlite(m_stmt_player_add_inventory, 5, list->getSize()); + sqlite3_vrfy(sqlite3_step(m_stmt_player_add_inventory), SQLITE_DONE); + sqlite3_reset(m_stmt_player_add_inventory); + + for (u32 j = 0; j < list->getSize(); j++) { + std::ostringstream os; + list->getItem(j).serialize(os); + std::string itemStr = os.str(); + + str_to_sqlite(m_stmt_player_add_inventory_items, 1, player->getName()); + int_to_sqlite(m_stmt_player_add_inventory_items, 2, i); + int_to_sqlite(m_stmt_player_add_inventory_items, 3, j); + str_to_sqlite(m_stmt_player_add_inventory_items, 4, itemStr); + sqlite3_vrfy(sqlite3_step(m_stmt_player_add_inventory_items), SQLITE_DONE); + sqlite3_reset(m_stmt_player_add_inventory_items); + } + } + + str_to_sqlite(m_stmt_player_metadata_remove, 1, player->getName()); + sqlite3_vrfy(sqlite3_step(m_stmt_player_metadata_remove), SQLITE_DONE); + sqlite3_reset(m_stmt_player_metadata_remove); + + const PlayerAttributes &attrs = sao->getExtendedAttributes(); + for (const auto &attr : attrs) { + str_to_sqlite(m_stmt_player_metadata_add, 1, player->getName()); + str_to_sqlite(m_stmt_player_metadata_add, 2, attr.first); + str_to_sqlite(m_stmt_player_metadata_add, 3, attr.second); + sqlite3_vrfy(sqlite3_step(m_stmt_player_metadata_add), SQLITE_DONE); + sqlite3_reset(m_stmt_player_metadata_add); + } + + endSave(); +} + +bool PlayerDatabaseSQLite3::loadPlayer(RemotePlayer *player, PlayerSAO *sao) +{ + verifyDatabase(); + + str_to_sqlite(m_stmt_player_load, 1, player->getName()); + if (sqlite3_step(m_stmt_player_load) != SQLITE_ROW) { + sqlite3_reset(m_stmt_player_load); + return false; + } + sao->setPitch(sqlite_to_float(m_stmt_player_load, 0)); + sao->setYaw(sqlite_to_float(m_stmt_player_load, 1)); + sao->setBasePosition(sqlite_to_v3f(m_stmt_player_load, 2)); + sao->setHPRaw((s16) MYMIN(sqlite_to_int(m_stmt_player_load, 5), S16_MAX)); + sao->setBreath((u16) MYMIN(sqlite_to_int(m_stmt_player_load, 6), U16_MAX), false); + sqlite3_reset(m_stmt_player_load); + + // Load inventory + str_to_sqlite(m_stmt_player_load_inventory, 1, player->getName()); + while (sqlite3_step(m_stmt_player_load_inventory) == SQLITE_ROW) { + InventoryList *invList = player->inventory.addList( + sqlite_to_string(m_stmt_player_load_inventory, 2), + sqlite_to_uint(m_stmt_player_load_inventory, 3)); + invList->setWidth(sqlite_to_uint(m_stmt_player_load_inventory, 1)); + + u32 invId = sqlite_to_uint(m_stmt_player_load_inventory, 0); + + str_to_sqlite(m_stmt_player_load_inventory_items, 1, player->getName()); + int_to_sqlite(m_stmt_player_load_inventory_items, 2, invId); + while (sqlite3_step(m_stmt_player_load_inventory_items) == SQLITE_ROW) { + const std::string itemStr = sqlite_to_string(m_stmt_player_load_inventory_items, 1); + if (itemStr.length() > 0) { + ItemStack stack; + stack.deSerialize(itemStr); + invList->changeItem(sqlite_to_uint(m_stmt_player_load_inventory_items, 0), stack); + } + } + sqlite3_reset(m_stmt_player_load_inventory_items); + } + + sqlite3_reset(m_stmt_player_load_inventory); + + str_to_sqlite(m_stmt_player_metadata_load, 1, sao->getPlayer()->getName()); + while (sqlite3_step(m_stmt_player_metadata_load) == SQLITE_ROW) { + std::string attr = sqlite_to_string(m_stmt_player_metadata_load, 0); + std::string value = sqlite_to_string(m_stmt_player_metadata_load, 1); + + sao->setExtendedAttribute(attr, value); + } + sqlite3_reset(m_stmt_player_metadata_load); + return true; +} + +bool PlayerDatabaseSQLite3::removePlayer(const std::string &name) +{ + if (!playerDataExists(name)) + return false; + + str_to_sqlite(m_stmt_player_remove, 1, name); + sqlite3_vrfy(sqlite3_step(m_stmt_player_remove), SQLITE_DONE); + sqlite3_reset(m_stmt_player_remove); + return true; +} + +void PlayerDatabaseSQLite3::listPlayers(std::vector &res) +{ + verifyDatabase(); + + while (sqlite3_step(m_stmt_player_list) == SQLITE_ROW) + res.push_back(sqlite_to_string(m_stmt_player_list, 0)); + + sqlite3_reset(m_stmt_player_list); +} diff --git a/src/database/database-sqlite3.h b/src/database/database-sqlite3.h new file mode 100644 index 000000000..8d9f91f21 --- /dev/null +++ b/src/database/database-sqlite3.h @@ -0,0 +1,193 @@ +/* +Minetest +Copyright (C) 2013 celeron55, Perttu Ahola + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#pragma once + +#include +#include +#include "database.h" +#include "exceptions.h" + +extern "C" { +#include "sqlite3.h" +} + +class Database_SQLite3 : public Database +{ +public: + virtual ~Database_SQLite3(); + + void beginSave(); + void endSave(); + + bool initialized() const { return m_initialized; } +protected: + Database_SQLite3(const std::string &savedir, const std::string &dbname); + + // Open and initialize the database if needed + void verifyDatabase(); + + // Convertors + inline void str_to_sqlite(sqlite3_stmt *s, int iCol, const std::string &str) const + { + sqlite3_vrfy(sqlite3_bind_text(s, iCol, str.c_str(), str.size(), NULL)); + } + + inline void str_to_sqlite(sqlite3_stmt *s, int iCol, const char *str) const + { + sqlite3_vrfy(sqlite3_bind_text(s, iCol, str, strlen(str), NULL)); + } + + inline void int_to_sqlite(sqlite3_stmt *s, int iCol, int val) const + { + sqlite3_vrfy(sqlite3_bind_int(s, iCol, val)); + } + + inline void int64_to_sqlite(sqlite3_stmt *s, int iCol, s64 val) const + { + sqlite3_vrfy(sqlite3_bind_int64(s, iCol, (sqlite3_int64) val)); + } + + inline void double_to_sqlite(sqlite3_stmt *s, int iCol, double val) const + { + sqlite3_vrfy(sqlite3_bind_double(s, iCol, val)); + } + + inline std::string sqlite_to_string(sqlite3_stmt *s, int iCol) + { + const char* text = reinterpret_cast(sqlite3_column_text(s, iCol)); + return std::string(text ? text : ""); + } + + inline s32 sqlite_to_int(sqlite3_stmt *s, int iCol) + { + return sqlite3_column_int(s, iCol); + } + + inline u32 sqlite_to_uint(sqlite3_stmt *s, int iCol) + { + return (u32) sqlite3_column_int(s, iCol); + } + + inline float sqlite_to_float(sqlite3_stmt *s, int iCol) + { + return (float) sqlite3_column_double(s, iCol); + } + + inline const v3f sqlite_to_v3f(sqlite3_stmt *s, int iCol) + { + return v3f(sqlite_to_float(s, iCol), sqlite_to_float(s, iCol + 1), + sqlite_to_float(s, iCol + 2)); + } + + // Query verifiers helpers + inline void sqlite3_vrfy(int s, const std::string &m = "", int r = SQLITE_OK) const + { + if (s != r) + throw DatabaseException(m + ": " + sqlite3_errmsg(m_database)); + } + + inline void sqlite3_vrfy(const int s, const int r, const std::string &m = "") const + { + sqlite3_vrfy(s, m, r); + } + + // Create the database structure + virtual void createDatabase() = 0; + virtual void initStatements() = 0; + + sqlite3 *m_database = nullptr; +private: + // Open the database + void openDatabase(); + + bool m_initialized = false; + + std::string m_savedir = ""; + std::string m_dbname = ""; + + sqlite3_stmt *m_stmt_begin = nullptr; + sqlite3_stmt *m_stmt_end = nullptr; + + s64 m_busy_handler_data[2]; + + static int busyHandler(void *data, int count); +}; + +class MapDatabaseSQLite3 : private Database_SQLite3, public MapDatabase +{ +public: + MapDatabaseSQLite3(const std::string &savedir); + virtual ~MapDatabaseSQLite3(); + + bool saveBlock(const v3s16 &pos, const std::string &data); + void loadBlock(const v3s16 &pos, std::string *block); + bool deleteBlock(const v3s16 &pos); + void listAllLoadableBlocks(std::vector &dst); + + void beginSave() { Database_SQLite3::beginSave(); } + void endSave() { Database_SQLite3::endSave(); } +protected: + virtual void createDatabase(); + virtual void initStatements(); + +private: + void bindPos(sqlite3_stmt *stmt, const v3s16 &pos, int index = 1); + + // Map + sqlite3_stmt *m_stmt_read = nullptr; + sqlite3_stmt *m_stmt_write = nullptr; + sqlite3_stmt *m_stmt_list = nullptr; + sqlite3_stmt *m_stmt_delete = nullptr; +}; + +class PlayerDatabaseSQLite3 : private Database_SQLite3, public PlayerDatabase +{ +public: + PlayerDatabaseSQLite3(const std::string &savedir); + virtual ~PlayerDatabaseSQLite3(); + + void savePlayer(RemotePlayer *player); + bool loadPlayer(RemotePlayer *player, PlayerSAO *sao); + bool removePlayer(const std::string &name); + void listPlayers(std::vector &res); + +protected: + virtual void createDatabase(); + virtual void initStatements(); + +private: + bool playerDataExists(const std::string &name); + + // Players + sqlite3_stmt *m_stmt_player_load = nullptr; + sqlite3_stmt *m_stmt_player_add = nullptr; + sqlite3_stmt *m_stmt_player_update = nullptr; + sqlite3_stmt *m_stmt_player_remove = nullptr; + sqlite3_stmt *m_stmt_player_list = nullptr; + sqlite3_stmt *m_stmt_player_load_inventory = nullptr; + sqlite3_stmt *m_stmt_player_load_inventory_items = nullptr; + sqlite3_stmt *m_stmt_player_add_inventory = nullptr; + sqlite3_stmt *m_stmt_player_add_inventory_items = nullptr; + sqlite3_stmt *m_stmt_player_remove_inventory = nullptr; + sqlite3_stmt *m_stmt_player_remove_inventory_items = nullptr; + sqlite3_stmt *m_stmt_player_metadata_load = nullptr; + sqlite3_stmt *m_stmt_player_metadata_remove = nullptr; + sqlite3_stmt *m_stmt_player_metadata_add = nullptr; +}; diff --git a/src/database/database.cpp b/src/database/database.cpp new file mode 100644 index 000000000..12e0e1a0f --- /dev/null +++ b/src/database/database.cpp @@ -0,0 +1,69 @@ +/* +Minetest +Copyright (C) 2013 celeron55, Perttu Ahola + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#include "database.h" +#include "irrlichttypes.h" + + +/**************** + * Black magic! * + **************** + * The position hashing is very messed up. + * It's a lot more complicated than it looks. + */ + +static inline s16 unsigned_to_signed(u16 i, u16 max_positive) +{ + if (i < max_positive) { + return i; + } + + return i - (max_positive * 2); +} + + +// Modulo of a negative number does not work consistently in C +static inline s64 pythonmodulo(s64 i, s16 mod) +{ + if (i >= 0) { + return i % mod; + } + return mod - ((-i) % mod); +} + + +s64 MapDatabase::getBlockAsInteger(const v3s16 &pos) +{ + return (u64) pos.Z * 0x1000000 + + (u64) pos.Y * 0x1000 + + (u64) pos.X; +} + + +v3s16 MapDatabase::getIntegerAsBlock(s64 i) +{ + v3s16 pos; + pos.X = unsigned_to_signed(pythonmodulo(i, 4096), 2048); + i = (i - pos.X) / 4096; + pos.Y = unsigned_to_signed(pythonmodulo(i, 4096), 2048); + i = (i - pos.Y) / 4096; + pos.Z = unsigned_to_signed(pythonmodulo(i, 4096), 2048); + return pos; +} + diff --git a/src/database/database.h b/src/database/database.h new file mode 100644 index 000000000..9926c7b93 --- /dev/null +++ b/src/database/database.h @@ -0,0 +1,63 @@ +/* +Minetest +Copyright (C) 2013 celeron55, Perttu Ahola + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#pragma once + +#include +#include +#include "irr_v3d.h" +#include "irrlichttypes.h" +#include "util/basic_macros.h" + +class Database +{ +public: + virtual void beginSave() = 0; + virtual void endSave() = 0; + virtual bool initialized() const { return true; } +}; + +class MapDatabase : public Database +{ +public: + virtual ~MapDatabase() = default; + + virtual bool saveBlock(const v3s16 &pos, const std::string &data) = 0; + virtual void loadBlock(const v3s16 &pos, std::string *block) = 0; + virtual bool deleteBlock(const v3s16 &pos) = 0; + + static s64 getBlockAsInteger(const v3s16 &pos); + static v3s16 getIntegerAsBlock(s64 i); + + virtual void listAllLoadableBlocks(std::vector &dst) = 0; +}; + +class PlayerSAO; +class RemotePlayer; + +class PlayerDatabase +{ +public: + virtual ~PlayerDatabase() = default; + + virtual void savePlayer(RemotePlayer *player) = 0; + virtual bool loadPlayer(RemotePlayer *player, PlayerSAO *sao) = 0; + virtual bool removePlayer(const std::string &name) = 0; + virtual void listPlayers(std::vector &res) = 0; +}; diff --git a/src/dungeongen.cpp b/src/dungeongen.cpp deleted file mode 100644 index fa867b398..000000000 --- a/src/dungeongen.cpp +++ /dev/null @@ -1,677 +0,0 @@ -/* -Minetest -Copyright (C) 2010-2013 celeron55, Perttu Ahola - -This program is free software; you can redistribute it and/or modify -it under the terms of the GNU Lesser General Public License as published by -the Free Software Foundation; either version 2.1 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public License along -with this program; if not, write to the Free Software Foundation, Inc., -51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -*/ - -#include "dungeongen.h" -#include "mapgen.h" -#include "voxel.h" -#include "noise.h" -#include "mapblock.h" -#include "mapnode.h" -#include "map.h" -#include "nodedef.h" -#include "settings.h" - -//#define DGEN_USE_TORCHES - -NoiseParams nparams_dungeon_density(0.9, 0.5, v3f(500.0, 500.0, 500.0), 0, 2, 0.8, 2.0); -NoiseParams nparams_dungeon_alt_wall(-0.4, 1.0, v3f(40.0, 40.0, 40.0), 32474, 6, 1.1, 2.0); - - -/////////////////////////////////////////////////////////////////////////////// - - -DungeonGen::DungeonGen(INodeDefManager *ndef, - GenerateNotifier *gennotify, DungeonParams *dparams) -{ - assert(ndef); - - this->ndef = ndef; - this->gennotify = gennotify; - -#ifdef DGEN_USE_TORCHES - c_torch = ndef->getId("default:torch"); -#endif - - if (dparams) { - memcpy(&dp, dparams, sizeof(dp)); - } else { - // Default dungeon parameters - dp.seed = 0; - - dp.c_water = ndef->getId("mapgen_water_source"); - dp.c_river_water = ndef->getId("mapgen_river_water_source"); - dp.c_wall = ndef->getId("mapgen_cobble"); - dp.c_alt_wall = ndef->getId("mapgen_mossycobble"); - dp.c_stair = ndef->getId("mapgen_stair_cobble"); - - if (dp.c_river_water == CONTENT_IGNORE) - dp.c_river_water = ndef->getId("mapgen_water_source"); - - dp.diagonal_dirs = false; - dp.only_in_ground = true; - dp.holesize = v3s16(1, 2, 1); - dp.corridor_len_min = 1; - dp.corridor_len_max = 13; - dp.room_size_min = v3s16(4, 4, 4); - dp.room_size_max = v3s16(8, 6, 8); - dp.room_size_large_min = v3s16(8, 8, 8); - dp.room_size_large_max = v3s16(16, 16, 16); - dp.rooms_min = 2; - dp.rooms_max = 16; - dp.y_min = -MAX_MAP_GENERATION_LIMIT; - dp.y_max = MAX_MAP_GENERATION_LIMIT; - dp.notifytype = GENNOTIFY_DUNGEON; - - dp.np_density = nparams_dungeon_density; - dp.np_alt_wall = nparams_dungeon_alt_wall; - } -} - - -void DungeonGen::generate(MMVManip *vm, u32 bseed, v3s16 nmin, v3s16 nmax) -{ - assert(vm); - - //TimeTaker t("gen dungeons"); - if (nmin.Y < dp.y_min || nmax.Y > dp.y_max) - return; - - float nval_density = NoisePerlin3D(&dp.np_density, nmin.X, nmin.Y, nmin.Z, dp.seed); - if (nval_density < 1.0f) - return; - - static const bool preserve_ignore = !g_settings->getBool("projecting_dungeons"); - - this->vm = vm; - this->blockseed = bseed; - random.seed(bseed + 2); - - // Dungeon generator doesn't modify places which have this set - vm->clearFlag(VMANIP_FLAG_DUNGEON_INSIDE | VMANIP_FLAG_DUNGEON_PRESERVE); - - if (dp.only_in_ground) { - // Set all air and water to be untouchable to make dungeons open to - // caves and open air. Optionally set ignore to be untouchable to - // prevent protruding dungeons. - for (s16 z = nmin.Z; z <= nmax.Z; z++) { - for (s16 y = nmin.Y; y <= nmax.Y; y++) { - u32 i = vm->m_area.index(nmin.X, y, z); - for (s16 x = nmin.X; x <= nmax.X; x++) { - content_t c = vm->m_data[i].getContent(); - if (c == CONTENT_AIR || c == dp.c_water || - (preserve_ignore && c == CONTENT_IGNORE) || - c == dp.c_river_water) - vm->m_flags[i] |= VMANIP_FLAG_DUNGEON_PRESERVE; - i++; - } - } - } - } - - // Add them - for (u32 i = 0; i < floor(nval_density); i++) - makeDungeon(v3s16(1, 1, 1) * MAP_BLOCKSIZE); - - // Optionally convert some structure to alternative structure - if (dp.c_alt_wall == CONTENT_IGNORE) - return; - - for (s16 z = nmin.Z; z <= nmax.Z; z++) - for (s16 y = nmin.Y; y <= nmax.Y; y++) { - u32 i = vm->m_area.index(nmin.X, y, z); - for (s16 x = nmin.X; x <= nmax.X; x++) { - if (vm->m_data[i].getContent() == dp.c_wall) { - if (NoisePerlin3D(&dp.np_alt_wall, x, y, z, blockseed) > 0.0f) - vm->m_data[i].setContent(dp.c_alt_wall); - } - i++; - } - } - - //printf("== gen dungeons: %dms\n", t.stop()); -} - - -void DungeonGen::makeDungeon(v3s16 start_padding) -{ - const v3s16 &areasize = vm->m_area.getExtent(); - v3s16 roomsize; - v3s16 roomplace; - - /* - Find place for first room. - There is a 1 in 4 chance of the first room being 'large', - all other rooms are not 'large'. - */ - bool fits = false; - for (u32 i = 0; i < 100 && !fits; i++) { - bool is_large_room = ((random.next() & 3) == 1); - if (is_large_room) { - roomsize.Z = random.range( - dp.room_size_large_min.Z, dp.room_size_large_max.Z); - roomsize.Y = random.range( - dp.room_size_large_min.Y, dp.room_size_large_max.Y); - roomsize.X = random.range( - dp.room_size_large_min.X, dp.room_size_large_max.X); - } else { - roomsize.Z = random.range(dp.room_size_min.Z, dp.room_size_max.Z); - roomsize.Y = random.range(dp.room_size_min.Y, dp.room_size_max.Y); - roomsize.X = random.range(dp.room_size_min.X, dp.room_size_max.X); - } - - // start_padding is used to disallow starting the generation of - // a dungeon in a neighboring generation chunk - roomplace = vm->m_area.MinEdge + start_padding; - roomplace.Z += random.range(0, areasize.Z - roomsize.Z - start_padding.Z); - roomplace.Y += random.range(0, areasize.Y - roomsize.Y - start_padding.Y); - roomplace.X += random.range(0, areasize.X - roomsize.X - start_padding.X); - - /* - Check that we're not putting the room to an unknown place, - otherwise it might end up floating in the air - */ - fits = true; - for (s16 z = 0; z < roomsize.Z; z++) - for (s16 y = 0; y < roomsize.Y; y++) - for (s16 x = 0; x < roomsize.X; x++) { - v3s16 p = roomplace + v3s16(x, y, z); - u32 vi = vm->m_area.index(p); - if ((vm->m_flags[vi] & VMANIP_FLAG_DUNGEON_UNTOUCHABLE) || - vm->m_data[vi].getContent() == CONTENT_IGNORE) { - fits = false; - break; - } - } - } - // No place found - if (!fits) - return; - - /* - Stores the center position of the last room made, so that - a new corridor can be started from the last room instead of - the new room, if chosen so. - */ - v3s16 last_room_center = roomplace + v3s16(roomsize.X / 2, 1, roomsize.Z / 2); - - u32 room_count = random.range(dp.rooms_min, dp.rooms_max); - for (u32 i = 0; i < room_count; i++) { - // Make a room to the determined place - makeRoom(roomsize, roomplace); - - v3s16 room_center = roomplace + v3s16(roomsize.X / 2, 1, roomsize.Z / 2); - if (gennotify) - gennotify->addEvent(dp.notifytype, room_center); - -#ifdef DGEN_USE_TORCHES - // Place torch at room center (for testing) - vm->m_data[vm->m_area.index(room_center)] = MapNode(c_torch); -#endif - - // Quit if last room - if (i == room_count - 1) - break; - - // Determine walker start position - - bool start_in_last_room = (random.range(0, 2) != 0); - - v3s16 walker_start_place; - - if (start_in_last_room) { - walker_start_place = last_room_center; - } else { - walker_start_place = room_center; - // Store center of current room as the last one - last_room_center = room_center; - } - - // Create walker and find a place for a door - v3s16 doorplace; - v3s16 doordir; - - m_pos = walker_start_place; - if (!findPlaceForDoor(doorplace, doordir)) - return; - - if (random.range(0, 1) == 0) - // Make the door - makeDoor(doorplace, doordir); - else - // Don't actually make a door - doorplace -= doordir; - - // Make a random corridor starting from the door - v3s16 corridor_end; - v3s16 corridor_end_dir; - makeCorridor(doorplace, doordir, corridor_end, corridor_end_dir); - - // Find a place for a random sized room - roomsize.Z = random.range(dp.room_size_min.Z, dp.room_size_max.Z); - roomsize.Y = random.range(dp.room_size_min.Y, dp.room_size_max.Y); - roomsize.X = random.range(dp.room_size_min.X, dp.room_size_max.X); - - m_pos = corridor_end; - m_dir = corridor_end_dir; - if (!findPlaceForRoomDoor(roomsize, doorplace, doordir, roomplace)) - return; - - if (random.range(0, 1) == 0) - // Make the door - makeDoor(doorplace, doordir); - else - // Don't actually make a door - roomplace -= doordir; - - } -} - - -void DungeonGen::makeRoom(v3s16 roomsize, v3s16 roomplace) -{ - MapNode n_wall(dp.c_wall); - MapNode n_air(CONTENT_AIR); - - // Make +-X walls - for (s16 z = 0; z < roomsize.Z; z++) - for (s16 y = 0; y < roomsize.Y; y++) { - { - v3s16 p = roomplace + v3s16(0, y, z); - if (!vm->m_area.contains(p)) - continue; - u32 vi = vm->m_area.index(p); - if (vm->m_flags[vi] & VMANIP_FLAG_DUNGEON_UNTOUCHABLE) - continue; - vm->m_data[vi] = n_wall; - } - { - v3s16 p = roomplace + v3s16(roomsize.X - 1, y, z); - if (!vm->m_area.contains(p)) - continue; - u32 vi = vm->m_area.index(p); - if (vm->m_flags[vi] & VMANIP_FLAG_DUNGEON_UNTOUCHABLE) - continue; - vm->m_data[vi] = n_wall; - } - } - - // Make +-Z walls - for (s16 x = 0; x < roomsize.X; x++) - for (s16 y = 0; y < roomsize.Y; y++) { - { - v3s16 p = roomplace + v3s16(x, y, 0); - if (!vm->m_area.contains(p)) - continue; - u32 vi = vm->m_area.index(p); - if (vm->m_flags[vi] & VMANIP_FLAG_DUNGEON_UNTOUCHABLE) - continue; - vm->m_data[vi] = n_wall; - } - { - v3s16 p = roomplace + v3s16(x, y, roomsize.Z - 1); - if (!vm->m_area.contains(p)) - continue; - u32 vi = vm->m_area.index(p); - if (vm->m_flags[vi] & VMANIP_FLAG_DUNGEON_UNTOUCHABLE) - continue; - vm->m_data[vi] = n_wall; - } - } - - // Make +-Y walls (floor and ceiling) - for (s16 z = 0; z < roomsize.Z; z++) - for (s16 x = 0; x < roomsize.X; x++) { - { - v3s16 p = roomplace + v3s16(x, 0, z); - if (!vm->m_area.contains(p)) - continue; - u32 vi = vm->m_area.index(p); - if (vm->m_flags[vi] & VMANIP_FLAG_DUNGEON_UNTOUCHABLE) - continue; - vm->m_data[vi] = n_wall; - } - { - v3s16 p = roomplace + v3s16(x,roomsize. Y - 1, z); - if (!vm->m_area.contains(p)) - continue; - u32 vi = vm->m_area.index(p); - if (vm->m_flags[vi] & VMANIP_FLAG_DUNGEON_UNTOUCHABLE) - continue; - vm->m_data[vi] = n_wall; - } - } - - // Fill with air - for (s16 z = 1; z < roomsize.Z - 1; z++) - for (s16 y = 1; y < roomsize.Y - 1; y++) - for (s16 x = 1; x < roomsize.X - 1; x++) { - v3s16 p = roomplace + v3s16(x, y, z); - if (!vm->m_area.contains(p)) - continue; - u32 vi = vm->m_area.index(p); - vm->m_flags[vi] |= VMANIP_FLAG_DUNGEON_UNTOUCHABLE; - vm->m_data[vi] = n_air; - } -} - - -void DungeonGen::makeFill(v3s16 place, v3s16 size, - u8 avoid_flags, MapNode n, u8 or_flags) -{ - for (s16 z = 0; z < size.Z; z++) - for (s16 y = 0; y < size.Y; y++) - for (s16 x = 0; x < size.X; x++) { - v3s16 p = place + v3s16(x, y, z); - if (!vm->m_area.contains(p)) - continue; - u32 vi = vm->m_area.index(p); - if (vm->m_flags[vi] & avoid_flags) - continue; - vm->m_flags[vi] |= or_flags; - vm->m_data[vi] = n; - } -} - - -void DungeonGen::makeHole(v3s16 place) -{ - makeFill(place, dp.holesize, 0, MapNode(CONTENT_AIR), - VMANIP_FLAG_DUNGEON_INSIDE); -} - - -void DungeonGen::makeDoor(v3s16 doorplace, v3s16 doordir) -{ - makeHole(doorplace); - -#ifdef DGEN_USE_TORCHES - // Place torch (for testing) - vm->m_data[vm->m_area.index(doorplace)] = MapNode(c_torch); -#endif -} - - -void DungeonGen::makeCorridor(v3s16 doorplace, v3s16 doordir, - v3s16 &result_place, v3s16 &result_dir) -{ - makeHole(doorplace); - v3s16 p0 = doorplace; - v3s16 dir = doordir; - u32 length = random.range(dp.corridor_len_min, dp.corridor_len_max); - u32 partlength = random.range(dp.corridor_len_min, dp.corridor_len_max); - u32 partcount = 0; - s16 make_stairs = 0; - - if (random.next() % 2 == 0 && partlength >= 3) - make_stairs = random.next() % 2 ? 1 : -1; - - for (u32 i = 0; i < length; i++) { - v3s16 p = p0 + dir; - if (partcount != 0) - p.Y += make_stairs; - - // Check segment of minimum size corridor is in voxelmanip - if (vm->m_area.contains(p) && vm->m_area.contains(p + v3s16(0, 1, 0))) { - if (make_stairs) { - makeFill(p + v3s16(-1, -1, -1), - dp.holesize + v3s16(2, 3, 2), - VMANIP_FLAG_DUNGEON_UNTOUCHABLE, - MapNode(dp.c_wall), - 0); - makeHole(p); - makeHole(p - dir); - - // TODO: fix stairs code so it works 100% - // (quite difficult) - - // exclude stairs from the bottom step - // exclude stairs from diagonal steps - if (((dir.X ^ dir.Z) & 1) && - (((make_stairs == 1) && i != 0) || - ((make_stairs == -1) && i != length - 1))) { - // rotate face 180 deg if - // making stairs backwards - int facedir = dir_to_facedir(dir * make_stairs); - v3s16 ps = p; - u16 stair_width = (dir.Z != 0) ? dp.holesize.X : dp.holesize.Z; - // Stair width direction vector - v3s16 swv = (dir.Z != 0) ? v3s16(1, 0, 0) : v3s16(0, 0, 1); - - for (u16 st = 0; st < stair_width; st++) { - u32 vi = vm->m_area.index(ps.X - dir.X, ps.Y - 1, ps.Z - dir.Z); - if (vm->m_area.contains(ps + v3s16(-dir.X, -1, -dir.Z)) && - vm->m_data[vi].getContent() == dp.c_wall) - vm->m_data[vi] = MapNode(dp.c_stair, 0, facedir); - - vi = vm->m_area.index(ps.X, ps.Y, ps.Z); - if (vm->m_area.contains(ps) && - vm->m_data[vi].getContent() == dp.c_wall) - vm->m_data[vi] = MapNode(dp.c_stair, 0, facedir); - - ps += swv; - } - } - } else { - makeFill(p + v3s16(-1, -1, -1), - dp.holesize + v3s16(2, 2, 2), - VMANIP_FLAG_DUNGEON_UNTOUCHABLE, - MapNode(dp.c_wall), - 0); - makeHole(p); - } - - p0 = p; - } else { - // Can't go here, turn away - dir = turn_xz(dir, random.range(0, 1)); - make_stairs = -make_stairs; - partcount = 0; - partlength = random.range(1, length); - continue; - } - - partcount++; - if (partcount >= partlength) { - partcount = 0; - - dir = random_turn(random, dir); - - partlength = random.range(1, length); - - make_stairs = 0; - if (random.next() % 2 == 0 && partlength >= 3) - make_stairs = random.next() % 2 ? 1 : -1; - } - } - result_place = p0; - result_dir = dir; -} - - -bool DungeonGen::findPlaceForDoor(v3s16 &result_place, v3s16 &result_dir) -{ - for (u32 i = 0; i < 100; i++) { - v3s16 p = m_pos + m_dir; - v3s16 p1 = p + v3s16(0, 1, 0); - if (!vm->m_area.contains(p) || !vm->m_area.contains(p1) || i % 4 == 0) { - randomizeDir(); - continue; - } - if (vm->getNodeNoExNoEmerge(p).getContent() == dp.c_wall && - vm->getNodeNoExNoEmerge(p1).getContent() == dp.c_wall) { - // Found wall, this is a good place! - result_place = p; - result_dir = m_dir; - // Randomize next direction - randomizeDir(); - return true; - } - /* - Determine where to move next - */ - // Jump one up if the actual space is there - if (vm->getNodeNoExNoEmerge(p + - v3s16(0, 0, 0)).getContent() == dp.c_wall && - vm->getNodeNoExNoEmerge(p + - v3s16(0, 1, 0)).getContent() == CONTENT_AIR && - vm->getNodeNoExNoEmerge(p + - v3s16(0, 2, 0)).getContent() == CONTENT_AIR) - p += v3s16(0,1,0); - // Jump one down if the actual space is there - if (vm->getNodeNoExNoEmerge(p + - v3s16(0, 1, 0)).getContent() == dp.c_wall && - vm->getNodeNoExNoEmerge(p + - v3s16(0, 0, 0)).getContent() == CONTENT_AIR && - vm->getNodeNoExNoEmerge(p + - v3s16(0, -1, 0)).getContent() == CONTENT_AIR) - p += v3s16(0, -1, 0); - // Check if walking is now possible - if (vm->getNodeNoExNoEmerge(p).getContent() != CONTENT_AIR || - vm->getNodeNoExNoEmerge(p + - v3s16(0, 1, 0)).getContent() != CONTENT_AIR) { - // Cannot continue walking here - randomizeDir(); - continue; - } - // Move there - m_pos = p; - } - return false; -} - - -bool DungeonGen::findPlaceForRoomDoor(v3s16 roomsize, v3s16 &result_doorplace, - v3s16 &result_doordir, v3s16 &result_roomplace) -{ - for (s16 trycount = 0; trycount < 30; trycount++) { - v3s16 doorplace; - v3s16 doordir; - bool r = findPlaceForDoor(doorplace, doordir); - if (!r) - continue; - v3s16 roomplace; - // X east, Z north, Y up - if (doordir == v3s16(1, 0, 0)) // X+ - roomplace = doorplace + - v3s16(0, -1, random.range(-roomsize.Z + 2, -2)); - if (doordir == v3s16(-1, 0, 0)) // X- - roomplace = doorplace + - v3s16(-roomsize.X + 1, -1, random.range(-roomsize.Z + 2, -2)); - if (doordir == v3s16(0, 0, 1)) // Z+ - roomplace = doorplace + - v3s16(random.range(-roomsize.X + 2, -2), -1, 0); - if (doordir == v3s16(0, 0, -1)) // Z- - roomplace = doorplace + - v3s16(random.range(-roomsize.X + 2, -2), -1, -roomsize.Z + 1); - - // Check fit - bool fits = true; - for (s16 z = 1; z < roomsize.Z - 1; z++) - for (s16 y = 1; y < roomsize.Y - 1; y++) - for (s16 x = 1; x < roomsize.X - 1; x++) { - v3s16 p = roomplace + v3s16(x, y, z); - if (!vm->m_area.contains(p)) { - fits = false; - break; - } - if (vm->m_flags[vm->m_area.index(p)] & VMANIP_FLAG_DUNGEON_INSIDE) { - fits = false; - break; - } - } - if (!fits) { - // Find new place - continue; - } - result_doorplace = doorplace; - result_doordir = doordir; - result_roomplace = roomplace; - return true; - } - return false; -} - - -v3s16 rand_ortho_dir(PseudoRandom &random, bool diagonal_dirs) -{ - // Make diagonal directions somewhat rare - if (diagonal_dirs && (random.next() % 4 == 0)) { - v3s16 dir; - int trycount = 0; - - do { - trycount++; - - dir.Z = random.next() % 3 - 1; - dir.Y = 0; - dir.X = random.next() % 3 - 1; - } while ((dir.X == 0 || dir.Z == 0) && trycount < 10); - - return dir; - } - - if (random.next() % 2 == 0) - return random.next() % 2 ? v3s16(-1, 0, 0) : v3s16(1, 0, 0); - - return random.next() % 2 ? v3s16(0, 0, -1) : v3s16(0, 0, 1); -} - - -v3s16 turn_xz(v3s16 olddir, int t) -{ - v3s16 dir; - if (t == 0) { - // Turn right - dir.X = olddir.Z; - dir.Z = -olddir.X; - dir.Y = olddir.Y; - } else { - // Turn left - dir.X = -olddir.Z; - dir.Z = olddir.X; - dir.Y = olddir.Y; - } - return dir; -} - - -v3s16 random_turn(PseudoRandom &random, v3s16 olddir) -{ - int turn = random.range(0, 2); - v3s16 dir; - if (turn == 0) - // Go straight - dir = olddir; - else if (turn == 1) - // Turn right - dir = turn_xz(olddir, 0); - else - // Turn left - dir = turn_xz(olddir, 1); - return dir; -} - - -int dir_to_facedir(v3s16 d) -{ - if (abs(d.X) > abs(d.Z)) - return d.X < 0 ? 3 : 1; - - return d.Z < 0 ? 2 : 0; -} diff --git a/src/dungeongen.h b/src/dungeongen.h deleted file mode 100644 index 6799db79e..000000000 --- a/src/dungeongen.h +++ /dev/null @@ -1,110 +0,0 @@ -/* -Minetest -Copyright (C) 2010-2013 celeron55, Perttu Ahola - -This program is free software; you can redistribute it and/or modify -it under the terms of the GNU Lesser General Public License as published by -the Free Software Foundation; either version 2.1 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public License along -with this program; if not, write to the Free Software Foundation, Inc., -51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -*/ - -#pragma once - -#include "voxel.h" -#include "noise.h" -#include "mapgen.h" - -#define VMANIP_FLAG_DUNGEON_INSIDE VOXELFLAG_CHECKED1 -#define VMANIP_FLAG_DUNGEON_PRESERVE VOXELFLAG_CHECKED2 -#define VMANIP_FLAG_DUNGEON_UNTOUCHABLE (\ - VMANIP_FLAG_DUNGEON_INSIDE|VMANIP_FLAG_DUNGEON_PRESERVE) - -class MMVManip; -class INodeDefManager; - -v3s16 rand_ortho_dir(PseudoRandom &random, bool diagonal_dirs); -v3s16 turn_xz(v3s16 olddir, int t); -v3s16 random_turn(PseudoRandom &random, v3s16 olddir); -int dir_to_facedir(v3s16 d); - - -struct DungeonParams { - s32 seed; - - content_t c_water; - content_t c_river_water; - content_t c_wall; - content_t c_alt_wall; - content_t c_stair; - - bool diagonal_dirs; - bool only_in_ground; - v3s16 holesize; - u16 corridor_len_min; - u16 corridor_len_max; - v3s16 room_size_min; - v3s16 room_size_max; - v3s16 room_size_large_min; - v3s16 room_size_large_max; - u16 rooms_min; - u16 rooms_max; - s16 y_min; - s16 y_max; - GenNotifyType notifytype; - - NoiseParams np_density; - NoiseParams np_alt_wall; -}; - -class DungeonGen { -public: - MMVManip *vm; - INodeDefManager *ndef; - GenerateNotifier *gennotify; - - u32 blockseed; - PseudoRandom random; - v3s16 csize; - - content_t c_torch; - DungeonParams dp; - - // RoomWalker - v3s16 m_pos; - v3s16 m_dir; - - DungeonGen(INodeDefManager *ndef, - GenerateNotifier *gennotify, DungeonParams *dparams); - - void generate(MMVManip *vm, u32 bseed, - v3s16 full_node_min, v3s16 full_node_max); - - void makeDungeon(v3s16 start_padding); - void makeRoom(v3s16 roomsize, v3s16 roomplace); - void makeCorridor(v3s16 doorplace, v3s16 doordir, - v3s16 &result_place, v3s16 &result_dir); - void makeDoor(v3s16 doorplace, v3s16 doordir); - void makeFill(v3s16 place, v3s16 size, u8 avoid_flags, MapNode n, u8 or_flags); - void makeHole(v3s16 place); - - bool findPlaceForDoor(v3s16 &result_place, v3s16 &result_dir); - bool findPlaceForRoomDoor(v3s16 roomsize, v3s16 &result_doorplace, - v3s16 &result_doordir, v3s16 &result_roomplace); - - inline void randomizeDir() - { - m_dir = rand_ortho_dir(random, dp.diagonal_dirs); - } -}; - -extern NoiseParams nparams_dungeon_density; -extern NoiseParams nparams_dungeon_alt_wall; diff --git a/src/emerge.cpp b/src/emerge.cpp index e9d96c0ba..ffe387f63 100644 --- a/src/emerge.cpp +++ b/src/emerge.cpp @@ -34,10 +34,10 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "log.h" #include "map.h" #include "mapblock.h" -#include "mg_biome.h" -#include "mg_ore.h" -#include "mg_decoration.h" -#include "mg_schematic.h" +#include "mapgen/mg_biome.h" +#include "mapgen/mg_ore.h" +#include "mapgen/mg_decoration.h" +#include "mapgen/mg_schematic.h" #include "nodedef.h" #include "profiler.h" #include "scripting_server.h" diff --git a/src/emerge.h b/src/emerge.h index 135121b39..e1f5d5ab0 100644 --- a/src/emerge.h +++ b/src/emerge.h @@ -24,7 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "network/networkprotocol.h" #include "irr_v3d.h" #include "util/container.h" -#include "mapgen.h" // for MapgenParams +#include "mapgen/mapgen.h" // for MapgenParams #include "map.h" #define BLOCK_EMERGE_ALLOW_GEN (1 << 0) diff --git a/src/game.cpp b/src/game.cpp index 200de2c59..462fd37a9 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -39,12 +39,12 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "log.h" #include "filesys.h" #include "gettext.h" -#include "guiChatConsole.h" -#include "guiFormSpecMenu.h" -#include "guiKeyChangeMenu.h" -#include "guiPasswordChange.h" -#include "guiVolumeChange.h" -#include "mainmenumanager.h" +#include "gui/guiChatConsole.h" +#include "gui/guiFormSpecMenu.h" +#include "gui/guiKeyChangeMenu.h" +#include "gui/guiPasswordChange.h" +#include "gui/guiVolumeChange.h" +#include "gui/mainmenumanager.h" #include "mapblock.h" #include "minimap.h" #include "nodedef.h" // Needed for determining pointing to nodes diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt new file mode 100644 index 000000000..067ba09a8 --- /dev/null +++ b/src/gui/CMakeLists.txt @@ -0,0 +1,13 @@ +set(gui_SRCS + ${CMAKE_CURRENT_SOURCE_DIR}/guiChatConsole.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/guiEditBoxWithScrollbar.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/guiEngine.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/guiFormSpecMenu.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/guiKeyChangeMenu.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/guiPasswordChange.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/guiPathSelectMenu.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/guiTable.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/guiVolumeChange.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/intlGUIEditBox.cpp + PARENT_SCOPE +) diff --git a/src/gui/guiChatConsole.cpp b/src/gui/guiChatConsole.cpp new file mode 100644 index 000000000..b194834e2 --- /dev/null +++ b/src/gui/guiChatConsole.cpp @@ -0,0 +1,642 @@ +/* +Minetest +Copyright (C) 2013 celeron55, Perttu Ahola + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#include "guiChatConsole.h" +#include "chat.h" +#include "client.h" +#include "debug.h" +#include "gettime.h" +#include "keycode.h" +#include "settings.h" +#include "porting.h" +#include "client/tile.h" +#include "fontengine.h" +#include "log.h" +#include "gettext.h" +#include + +#if USE_FREETYPE + #include "irrlicht_changes/CGUITTFont.h" +#endif + +inline u32 clamp_u8(s32 value) +{ + return (u32) MYMIN(MYMAX(value, 0), 255); +} + + +GUIChatConsole::GUIChatConsole( + gui::IGUIEnvironment* env, + gui::IGUIElement* parent, + s32 id, + ChatBackend* backend, + Client* client, + IMenuManager* menumgr +): + IGUIElement(gui::EGUIET_ELEMENT, env, parent, id, + core::rect(0,0,100,100)), + m_chat_backend(backend), + m_client(client), + m_menumgr(menumgr), + m_animate_time_old(porting::getTimeMs()) +{ + // load background settings + s32 console_alpha = g_settings->getS32("console_alpha"); + m_background_color.setAlpha(clamp_u8(console_alpha)); + + // load the background texture depending on settings + ITextureSource *tsrc = client->getTextureSource(); + if (tsrc->isKnownSourceImage("background_chat.jpg")) { + m_background = tsrc->getTexture("background_chat.jpg"); + m_background_color.setRed(255); + m_background_color.setGreen(255); + m_background_color.setBlue(255); + } else { + v3f console_color = g_settings->getV3F("console_color"); + m_background_color.setRed(clamp_u8(myround(console_color.X))); + m_background_color.setGreen(clamp_u8(myround(console_color.Y))); + m_background_color.setBlue(clamp_u8(myround(console_color.Z))); + } + + m_font = g_fontengine->getFont(FONT_SIZE_UNSPECIFIED, FM_Mono); + + if (!m_font) { + errorstream << "GUIChatConsole: Unable to load mono font "; + } else { + core::dimension2d dim = m_font->getDimension(L"M"); + m_fontsize = v2u32(dim.Width, dim.Height); + m_font->grab(); + } + m_fontsize.X = MYMAX(m_fontsize.X, 1); + m_fontsize.Y = MYMAX(m_fontsize.Y, 1); + + // set default cursor options + setCursor(true, true, 2.0, 0.1); +} + +GUIChatConsole::~GUIChatConsole() +{ + if (m_font) + m_font->drop(); +} + +void GUIChatConsole::openConsole(f32 scale) +{ + assert(scale > 0.0f && scale <= 1.0f); + + m_open = true; + m_desired_height_fraction = scale; + m_desired_height = scale * m_screensize.Y; + reformatConsole(); + m_animate_time_old = porting::getTimeMs(); + IGUIElement::setVisible(true); + Environment->setFocus(this); + m_menumgr->createdMenu(this); +} + +bool GUIChatConsole::isOpen() const +{ + return m_open; +} + +bool GUIChatConsole::isOpenInhibited() const +{ + return m_open_inhibited > 0; +} + +void GUIChatConsole::closeConsole() +{ + m_open = false; + Environment->removeFocus(this); + m_menumgr->deletingMenu(this); +} + +void GUIChatConsole::closeConsoleAtOnce() +{ + closeConsole(); + m_height = 0; + recalculateConsolePosition(); +} + +f32 GUIChatConsole::getDesiredHeight() const +{ + return m_desired_height_fraction; +} + +void GUIChatConsole::replaceAndAddToHistory(std::wstring line) +{ + ChatPrompt& prompt = m_chat_backend->getPrompt(); + prompt.addToHistory(prompt.getLine()); + prompt.replace(line); +} + + +void GUIChatConsole::setCursor( + bool visible, bool blinking, f32 blink_speed, f32 relative_height) +{ + if (visible) + { + if (blinking) + { + // leave m_cursor_blink unchanged + m_cursor_blink_speed = blink_speed; + } + else + { + m_cursor_blink = 0x8000; // on + m_cursor_blink_speed = 0.0; + } + } + else + { + m_cursor_blink = 0; // off + m_cursor_blink_speed = 0.0; + } + m_cursor_height = relative_height; +} + +void GUIChatConsole::draw() +{ + if(!IsVisible) + return; + + video::IVideoDriver* driver = Environment->getVideoDriver(); + + // Check screen size + v2u32 screensize = driver->getScreenSize(); + if (screensize != m_screensize) + { + // screen size has changed + // scale current console height to new window size + if (m_screensize.Y != 0) + m_height = m_height * screensize.Y / m_screensize.Y; + m_screensize = screensize; + m_desired_height = m_desired_height_fraction * m_screensize.Y; + reformatConsole(); + } + + // Animation + u64 now = porting::getTimeMs(); + animate(now - m_animate_time_old); + m_animate_time_old = now; + + // Draw console elements if visible + if (m_height > 0) + { + drawBackground(); + drawText(); + drawPrompt(); + } + + gui::IGUIElement::draw(); +} + +void GUIChatConsole::reformatConsole() +{ + s32 cols = m_screensize.X / m_fontsize.X - 2; // make room for a margin (looks better) + s32 rows = m_desired_height / m_fontsize.Y - 1; // make room for the input prompt + if (cols <= 0 || rows <= 0) + cols = rows = 0; + recalculateConsolePosition(); + m_chat_backend->reformat(cols, rows); +} + +void GUIChatConsole::recalculateConsolePosition() +{ + core::rect rect(0, 0, m_screensize.X, m_height); + DesiredRect = rect; + recalculateAbsolutePosition(false); +} + +void GUIChatConsole::animate(u32 msec) +{ + // animate the console height + s32 goal = m_open ? m_desired_height : 0; + + // Set invisible if close animation finished (reset by openConsole) + // This function (animate()) is never called once its visibility becomes false so do not + // actually set visible to false before the inhibited period is over + if (!m_open && m_height == 0 && m_open_inhibited == 0) + IGUIElement::setVisible(false); + + if (m_height != goal) + { + s32 max_change = msec * m_screensize.Y * (m_height_speed / 1000.0); + if (max_change == 0) + max_change = 1; + + if (m_height < goal) + { + // increase height + if (m_height + max_change < goal) + m_height += max_change; + else + m_height = goal; + } + else + { + // decrease height + if (m_height > goal + max_change) + m_height -= max_change; + else + m_height = goal; + } + + recalculateConsolePosition(); + } + + // blink the cursor + if (m_cursor_blink_speed != 0.0) + { + u32 blink_increase = 0x10000 * msec * (m_cursor_blink_speed / 1000.0); + if (blink_increase == 0) + blink_increase = 1; + m_cursor_blink = ((m_cursor_blink + blink_increase) & 0xffff); + } + + // decrease open inhibit counter + if (m_open_inhibited > msec) + m_open_inhibited -= msec; + else + m_open_inhibited = 0; +} + +void GUIChatConsole::drawBackground() +{ + video::IVideoDriver* driver = Environment->getVideoDriver(); + if (m_background != NULL) + { + core::rect sourcerect(0, -m_height, m_screensize.X, 0); + driver->draw2DImage( + m_background, + v2s32(0, 0), + sourcerect, + &AbsoluteClippingRect, + m_background_color, + false); + } + else + { + driver->draw2DRectangle( + m_background_color, + core::rect(0, 0, m_screensize.X, m_height), + &AbsoluteClippingRect); + } +} + +void GUIChatConsole::drawText() +{ + if (m_font == NULL) + return; + + ChatBuffer& buf = m_chat_backend->getConsoleBuffer(); + for (u32 row = 0; row < buf.getRows(); ++row) + { + const ChatFormattedLine& line = buf.getFormattedLine(row); + if (line.fragments.empty()) + continue; + + s32 line_height = m_fontsize.Y; + s32 y = row * line_height + m_height - m_desired_height; + if (y + line_height < 0) + continue; + + for (const ChatFormattedFragment &fragment : line.fragments) { + s32 x = (fragment.column + 1) * m_fontsize.X; + core::rect destrect( + x, y, x + m_fontsize.X * fragment.text.size(), y + m_fontsize.Y); + + + #if USE_FREETYPE + // Draw colored text if FreeType is enabled + irr::gui::CGUITTFont *tmp = dynamic_cast(m_font); + tmp->draw( + fragment.text, + destrect, + video::SColor(255, 255, 255, 255), + false, + false, + &AbsoluteClippingRect); + #else + // Otherwise use standard text + m_font->draw( + fragment.text.c_str(), + destrect, + video::SColor(255, 255, 255, 255), + false, + false, + &AbsoluteClippingRect); + #endif + } + } +} + +void GUIChatConsole::drawPrompt() +{ + if (!m_font) + return; + + u32 row = m_chat_backend->getConsoleBuffer().getRows(); + s32 line_height = m_fontsize.Y; + s32 y = row * line_height + m_height - m_desired_height; + + ChatPrompt& prompt = m_chat_backend->getPrompt(); + std::wstring prompt_text = prompt.getVisiblePortion(); + + // FIXME Draw string at once, not character by character + // That will only work with the cursor once we have a monospace font + for (u32 i = 0; i < prompt_text.size(); ++i) + { + wchar_t ws[2] = {prompt_text[i], 0}; + s32 x = (1 + i) * m_fontsize.X; + core::rect destrect( + x, y, x + m_fontsize.X, y + m_fontsize.Y); + m_font->draw( + ws, + destrect, + video::SColor(255, 255, 255, 255), + false, + false, + &AbsoluteClippingRect); + } + + // Draw the cursor during on periods + if ((m_cursor_blink & 0x8000) != 0) + { + s32 cursor_pos = prompt.getVisibleCursorPosition(); + if (cursor_pos >= 0) + { + s32 cursor_len = prompt.getCursorLength(); + video::IVideoDriver* driver = Environment->getVideoDriver(); + s32 x = (1 + cursor_pos) * m_fontsize.X; + core::rect destrect( + x, + y + m_fontsize.Y * (1.0 - m_cursor_height), + x + m_fontsize.X * MYMAX(cursor_len, 1), + y + m_fontsize.Y * (cursor_len ? m_cursor_height+1 : 1) + ); + video::SColor cursor_color(255,255,255,255); + driver->draw2DRectangle( + cursor_color, + destrect, + &AbsoluteClippingRect); + } + } + +} + +bool GUIChatConsole::OnEvent(const SEvent& event) +{ + + ChatPrompt &prompt = m_chat_backend->getPrompt(); + + if(event.EventType == EET_KEY_INPUT_EVENT && event.KeyInput.PressedDown) + { + // Key input + if (KeyPress(event.KeyInput) == getKeySetting("keymap_console")) { + closeConsole(); + + // inhibit open so the_game doesn't reopen immediately + m_open_inhibited = 50; + m_close_on_enter = false; + return true; + } + + if (event.KeyInput.Key == KEY_ESCAPE) { + closeConsoleAtOnce(); + m_close_on_enter = false; + // inhibit open so the_game doesn't reopen immediately + m_open_inhibited = 1; // so the ESCAPE button doesn't open the "pause menu" + return true; + } + else if(event.KeyInput.Key == KEY_PRIOR) + { + m_chat_backend->scrollPageUp(); + return true; + } + else if(event.KeyInput.Key == KEY_NEXT) + { + m_chat_backend->scrollPageDown(); + return true; + } + else if(event.KeyInput.Key == KEY_RETURN) + { + prompt.addToHistory(prompt.getLine()); + std::wstring text = prompt.replace(L""); + m_client->typeChatMessage(text); + if (m_close_on_enter) { + closeConsoleAtOnce(); + m_close_on_enter = false; + } + return true; + } + else if(event.KeyInput.Key == KEY_UP) + { + // Up pressed + // Move back in history + prompt.historyPrev(); + return true; + } + else if(event.KeyInput.Key == KEY_DOWN) + { + // Down pressed + // Move forward in history + prompt.historyNext(); + return true; + } + else if(event.KeyInput.Key == KEY_LEFT || event.KeyInput.Key == KEY_RIGHT) + { + // Left/right pressed + // Move/select character/word to the left depending on control and shift keys + ChatPrompt::CursorOp op = event.KeyInput.Shift ? + ChatPrompt::CURSOROP_SELECT : + ChatPrompt::CURSOROP_MOVE; + ChatPrompt::CursorOpDir dir = event.KeyInput.Key == KEY_LEFT ? + ChatPrompt::CURSOROP_DIR_LEFT : + ChatPrompt::CURSOROP_DIR_RIGHT; + ChatPrompt::CursorOpScope scope = event.KeyInput.Control ? + ChatPrompt::CURSOROP_SCOPE_WORD : + ChatPrompt::CURSOROP_SCOPE_CHARACTER; + prompt.cursorOperation(op, dir, scope); + return true; + } + else if(event.KeyInput.Key == KEY_HOME) + { + // Home pressed + // move to beginning of line + prompt.cursorOperation( + ChatPrompt::CURSOROP_MOVE, + ChatPrompt::CURSOROP_DIR_LEFT, + ChatPrompt::CURSOROP_SCOPE_LINE); + return true; + } + else if(event.KeyInput.Key == KEY_END) + { + // End pressed + // move to end of line + prompt.cursorOperation( + ChatPrompt::CURSOROP_MOVE, + ChatPrompt::CURSOROP_DIR_RIGHT, + ChatPrompt::CURSOROP_SCOPE_LINE); + return true; + } + else if(event.KeyInput.Key == KEY_BACK) + { + // Backspace or Ctrl-Backspace pressed + // delete character / word to the left + ChatPrompt::CursorOpScope scope = + event.KeyInput.Control ? + ChatPrompt::CURSOROP_SCOPE_WORD : + ChatPrompt::CURSOROP_SCOPE_CHARACTER; + prompt.cursorOperation( + ChatPrompt::CURSOROP_DELETE, + ChatPrompt::CURSOROP_DIR_LEFT, + scope); + return true; + } + else if(event.KeyInput.Key == KEY_DELETE) + { + // Delete or Ctrl-Delete pressed + // delete character / word to the right + ChatPrompt::CursorOpScope scope = + event.KeyInput.Control ? + ChatPrompt::CURSOROP_SCOPE_WORD : + ChatPrompt::CURSOROP_SCOPE_CHARACTER; + prompt.cursorOperation( + ChatPrompt::CURSOROP_DELETE, + ChatPrompt::CURSOROP_DIR_RIGHT, + scope); + return true; + } + else if(event.KeyInput.Key == KEY_KEY_A && event.KeyInput.Control) + { + // Ctrl-A pressed + // Select all text + prompt.cursorOperation( + ChatPrompt::CURSOROP_SELECT, + ChatPrompt::CURSOROP_DIR_LEFT, // Ignored + ChatPrompt::CURSOROP_SCOPE_LINE); + return true; + } + else if(event.KeyInput.Key == KEY_KEY_C && event.KeyInput.Control) + { + // Ctrl-C pressed + // Copy text to clipboard + if (prompt.getCursorLength() <= 0) + return true; + std::wstring wselected = prompt.getSelection(); + std::string selected(wselected.begin(), wselected.end()); + Environment->getOSOperator()->copyToClipboard(selected.c_str()); + return true; + } + else if(event.KeyInput.Key == KEY_KEY_V && event.KeyInput.Control) + { + // Ctrl-V pressed + // paste text from clipboard + if (prompt.getCursorLength() > 0) { + // Delete selected section of text + prompt.cursorOperation( + ChatPrompt::CURSOROP_DELETE, + ChatPrompt::CURSOROP_DIR_LEFT, // Ignored + ChatPrompt::CURSOROP_SCOPE_SELECTION); + } + IOSOperator *os_operator = Environment->getOSOperator(); + const c8 *text = os_operator->getTextFromClipboard(); + if (!text) + return true; + std::basic_string str((const unsigned char*)text); + prompt.input(std::wstring(str.begin(), str.end())); + return true; + } + else if(event.KeyInput.Key == KEY_KEY_X && event.KeyInput.Control) + { + // Ctrl-X pressed + // Cut text to clipboard + if (prompt.getCursorLength() <= 0) + return true; + std::wstring wselected = prompt.getSelection(); + std::string selected(wselected.begin(), wselected.end()); + Environment->getOSOperator()->copyToClipboard(selected.c_str()); + prompt.cursorOperation( + ChatPrompt::CURSOROP_DELETE, + ChatPrompt::CURSOROP_DIR_LEFT, // Ignored + ChatPrompt::CURSOROP_SCOPE_SELECTION); + return true; + } + else if(event.KeyInput.Key == KEY_KEY_U && event.KeyInput.Control) + { + // Ctrl-U pressed + // kill line to left end + prompt.cursorOperation( + ChatPrompt::CURSOROP_DELETE, + ChatPrompt::CURSOROP_DIR_LEFT, + ChatPrompt::CURSOROP_SCOPE_LINE); + return true; + } + else if(event.KeyInput.Key == KEY_KEY_K && event.KeyInput.Control) + { + // Ctrl-K pressed + // kill line to right end + prompt.cursorOperation( + ChatPrompt::CURSOROP_DELETE, + ChatPrompt::CURSOROP_DIR_RIGHT, + ChatPrompt::CURSOROP_SCOPE_LINE); + return true; + } + else if(event.KeyInput.Key == KEY_TAB) + { + // Tab or Shift-Tab pressed + // Nick completion + std::list names = m_client->getConnectedPlayerNames(); + bool backwards = event.KeyInput.Shift; + prompt.nickCompletion(names, backwards); + return true; + } else if (!iswcntrl(event.KeyInput.Char) && !event.KeyInput.Control) { + #if defined(__linux__) && (IRRLICHT_VERSION_MAJOR == 1 && IRRLICHT_VERSION_MINOR < 9) + wchar_t wc = L'_'; + mbtowc( &wc, (char *) &event.KeyInput.Char, sizeof(event.KeyInput.Char) ); + prompt.input(wc); + #else + prompt.input(event.KeyInput.Char); + #endif + return true; + } + } + else if(event.EventType == EET_MOUSE_INPUT_EVENT) + { + if(event.MouseInput.Event == EMIE_MOUSE_WHEEL) + { + s32 rows = myround(-3.0 * event.MouseInput.Wheel); + m_chat_backend->scroll(rows); + } + } + + return Parent ? Parent->OnEvent(event) : false; +} + +void GUIChatConsole::setVisible(bool visible) +{ + m_open = visible; + IGUIElement::setVisible(visible); + if (!visible) { + m_height = 0; + recalculateConsolePosition(); + } +} + diff --git a/src/gui/guiChatConsole.h b/src/gui/guiChatConsole.h new file mode 100644 index 000000000..ef8a87673 --- /dev/null +++ b/src/gui/guiChatConsole.h @@ -0,0 +1,133 @@ +/* +Minetest +Copyright (C) 2013 celeron55, Perttu Ahola + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#pragma once + +#include "irrlichttypes_extrabloated.h" +#include "modalMenu.h" +#include "chat.h" +#include "config.h" + +class Client; + +class GUIChatConsole : public gui::IGUIElement +{ +public: + GUIChatConsole(gui::IGUIEnvironment* env, + gui::IGUIElement* parent, + s32 id, + ChatBackend* backend, + Client* client, + IMenuManager* menumgr); + virtual ~GUIChatConsole(); + + // Open the console (height = desired fraction of screen size) + // This doesn't open immediately but initiates an animation. + // You should call isOpenInhibited() before this. + void openConsole(f32 scale); + + bool isOpen() const; + + // Check if the console should not be opened at the moment + // This is to avoid reopening the console immediately after closing + bool isOpenInhibited() const; + // Close the console, equivalent to openConsole(0). + // This doesn't close immediately but initiates an animation. + void closeConsole(); + // Close the console immediately, without animation. + void closeConsoleAtOnce(); + // Set whether to close the console after the user presses enter. + void setCloseOnEnter(bool close) { m_close_on_enter = close; } + + // Return the desired height (fraction of screen size) + // Zero if the console is closed or getting closed + f32 getDesiredHeight() const; + + // Replace actual line when adding the actual to the history (if there is any) + void replaceAndAddToHistory(std::wstring line); + + // Change how the cursor looks + void setCursor( + bool visible, + bool blinking = false, + f32 blink_speed = 1.0, + f32 relative_height = 1.0); + + // Irrlicht draw method + virtual void draw(); + + bool canTakeFocus(gui::IGUIElement* element) { return false; } + + virtual bool OnEvent(const SEvent& event); + + virtual void setVisible(bool visible); + +private: + void reformatConsole(); + void recalculateConsolePosition(); + + // These methods are called by draw + void animate(u32 msec); + void drawBackground(); + void drawText(); + void drawPrompt(); + +private: + ChatBackend* m_chat_backend; + Client* m_client; + IMenuManager* m_menumgr; + + // current screen size + v2u32 m_screensize; + + // used to compute how much time passed since last animate() + u64 m_animate_time_old; + + // should the console be opened or closed? + bool m_open = false; + // should it close after you press enter? + bool m_close_on_enter = false; + // current console height [pixels] + s32 m_height = 0; + // desired height [pixels] + f32 m_desired_height = 0.0f; + // desired height [screen height fraction] + f32 m_desired_height_fraction = 0.0f; + // console open/close animation speed [screen height fraction / second] + f32 m_height_speed = 5.0f; + // if nonzero, opening the console is inhibited [milliseconds] + u32 m_open_inhibited = 0; + + // cursor blink frame (16-bit value) + // cursor is off during [0,32767] and on during [32768,65535] + u32 m_cursor_blink = 0; + // cursor blink speed [on/off toggles / second] + f32 m_cursor_blink_speed = 0.0f; + // cursor height [line height] + f32 m_cursor_height = 0.0f; + + // background texture + video::ITexture *m_background = nullptr; + // background color (including alpha) + video::SColor m_background_color = video::SColor(255, 0, 0, 0); + + // font + gui::IGUIFont *m_font = nullptr; + v2u32 m_fontsize; +}; diff --git a/src/gui/guiEditBoxWithScrollbar.cpp b/src/gui/guiEditBoxWithScrollbar.cpp new file mode 100644 index 000000000..d4d2a0c1c --- /dev/null +++ b/src/gui/guiEditBoxWithScrollbar.cpp @@ -0,0 +1,1524 @@ +// Copyright (C) 2002-2012 Nikolaus Gebhardt +// Modified by Mustapha T. +// This file is part of the "Irrlicht Engine". +// For conditions of distribution and use, see copyright notice in irrlicht.h + +#include "guiEditBoxWithScrollbar.h" + +#include "IGUISkin.h" +#include "IGUIEnvironment.h" +#include "IGUIFont.h" +#include "IVideoDriver.h" +#include "rect.h" +#include "porting.h" +#include "Keycodes.h" + + +/* +todo: +optional scrollbars [done] +ctrl+left/right to select word +double click/ctrl click: word select + drag to select whole words, triple click to select line +optional? dragging selected text +numerical +*/ + + +//! constructor +GUIEditBoxWithScrollBar::GUIEditBoxWithScrollBar(const wchar_t* text, bool border, + IGUIEnvironment* environment, IGUIElement* parent, s32 id, + const core::rect& rectangle, bool writable, bool has_vscrollbar) + : IGUIEditBox(environment, parent, id, rectangle), m_mouse_marking(false), + m_border(border), m_background(true), m_override_color_enabled(false), m_mark_begin(0), m_mark_end(0), + m_override_color(video::SColor(101, 255, 255, 255)), m_override_font(0), m_last_break_font(0), + m_operator(0), m_blink_start_time(0), m_cursor_pos(0), m_hscroll_pos(0), m_vscroll_pos(0), m_max(0), + m_word_wrap(false), m_multiline(false), m_autoscroll(true), m_passwordbox(false), + m_passwordchar(L'*'), m_halign(EGUIA_UPPERLEFT), m_valign(EGUIA_CENTER), + m_current_text_rect(0, 0, 1, 1), m_frame_rect(rectangle), + m_scrollbar_width(0), m_vscrollbar(NULL), m_writable(writable), + m_bg_color_used(false) +{ +#ifdef _DEBUG + setDebugName("GUIEditBoxWithScrollBar"); +#endif + + + Text = text; + + if (Environment) + m_operator = Environment->getOSOperator(); + + if (m_operator) + m_operator->grab(); + + // this element can be tabbed to + setTabStop(true); + setTabOrder(-1); + + if (has_vscrollbar) { + createVScrollBar(); + } + + calculateFrameRect(); + breakText(); + + calculateScrollPos(); + setWritable(writable); +} + + +//! destructor +GUIEditBoxWithScrollBar::~GUIEditBoxWithScrollBar() +{ + if (m_override_font) + m_override_font->drop(); + + if (m_operator) + m_operator->drop(); + + m_vscrollbar->remove(); +} + + +//! Sets another skin independent font. +void GUIEditBoxWithScrollBar::setOverrideFont(IGUIFont* font) +{ + if (m_override_font == font) + return; + + if (m_override_font) + m_override_font->drop(); + + m_override_font = font; + + if (m_override_font) + m_override_font->grab(); + + breakText(); +} + +//! Gets the override font (if any) +IGUIFont * GUIEditBoxWithScrollBar::getOverrideFont() const +{ + return m_override_font; +} + +//! Get the font which is used right now for drawing +IGUIFont* GUIEditBoxWithScrollBar::getActiveFont() const +{ + if (m_override_font) + return m_override_font; + IGUISkin* skin = Environment->getSkin(); + if (skin) + return skin->getFont(); + return 0; +} + +//! Sets another color for the text. +void GUIEditBoxWithScrollBar::setOverrideColor(video::SColor color) +{ + m_override_color = color; + m_override_color_enabled = true; +} + + +video::SColor GUIEditBoxWithScrollBar::getOverrideColor() const +{ + return m_override_color; +} + + +//! Turns the border on or off +void GUIEditBoxWithScrollBar::setDrawBorder(bool border) +{ + m_border = border; +} + +//! Sets whether to draw the background +void GUIEditBoxWithScrollBar::setDrawBackground(bool draw) +{ + m_background = draw; +} + +//! Sets if the text should use the overide color or the color in the gui skin. +void GUIEditBoxWithScrollBar::enableOverrideColor(bool enable) +{ + m_override_color_enabled = enable; +} + +bool GUIEditBoxWithScrollBar::isOverrideColorEnabled() const +{ + _IRR_IMPLEMENT_MANAGED_MARSHALLING_BUGFIX; + return m_override_color_enabled; +} + +//! Enables or disables word wrap +void GUIEditBoxWithScrollBar::setWordWrap(bool enable) +{ + m_word_wrap = enable; + breakText(); +} + + +void GUIEditBoxWithScrollBar::updateAbsolutePosition() +{ + core::rect old_absolute_rect(AbsoluteRect); + IGUIElement::updateAbsolutePosition(); + if (old_absolute_rect != AbsoluteRect) { + calculateFrameRect(); + breakText(); + calculateScrollPos(); + } +} + +//! Checks if word wrap is enabled +bool GUIEditBoxWithScrollBar::isWordWrapEnabled() const +{ + _IRR_IMPLEMENT_MANAGED_MARSHALLING_BUGFIX; + return m_word_wrap; +} + + +//! Enables or disables newlines. +void GUIEditBoxWithScrollBar::setMultiLine(bool enable) +{ + m_multiline = enable; +} + + +//! Checks if multi line editing is enabled +bool GUIEditBoxWithScrollBar::isMultiLineEnabled() const +{ + _IRR_IMPLEMENT_MANAGED_MARSHALLING_BUGFIX; + return m_multiline; +} + + +void GUIEditBoxWithScrollBar::setPasswordBox(bool password_box, wchar_t password_char) +{ + m_passwordbox = password_box; + if (m_passwordbox) { + m_passwordchar = password_char; + setMultiLine(false); + setWordWrap(false); + m_broken_text.clear(); + } +} + + +bool GUIEditBoxWithScrollBar::isPasswordBox() const +{ + _IRR_IMPLEMENT_MANAGED_MARSHALLING_BUGFIX; + return m_passwordbox; +} + + +//! Sets text justification +void GUIEditBoxWithScrollBar::setTextAlignment(EGUI_ALIGNMENT horizontal, EGUI_ALIGNMENT vertical) +{ + m_halign = horizontal; + m_valign = vertical; +} + + +//! called if an event happened. +bool GUIEditBoxWithScrollBar::OnEvent(const SEvent& event) +{ + if (isEnabled()) { + switch (event.EventType) + { + case EET_GUI_EVENT: + if (event.GUIEvent.EventType == EGET_ELEMENT_FOCUS_LOST) { + if (event.GUIEvent.Caller == this) { + m_mouse_marking = false; + setTextMarkers(0, 0); + } + } + break; + case EET_KEY_INPUT_EVENT: + if (processKey(event)) + return true; + break; + case EET_MOUSE_INPUT_EVENT: + if (processMouse(event)) + return true; + break; + default: + break; + } + } + + return IGUIElement::OnEvent(event); +} + + +bool GUIEditBoxWithScrollBar::processKey(const SEvent& event) +{ + if (!m_writable) { + return false; + } + + if (!event.KeyInput.PressedDown) + return false; + + bool text_changed = false; + s32 new_mark_begin = m_mark_begin; + s32 new_mark_end = m_mark_end; + + // control shortcut handling + + if (event.KeyInput.Control) { + + // german backlash '\' entered with control + '?' + if (event.KeyInput.Char == '\\') { + inputChar(event.KeyInput.Char); + return true; + } + + switch (event.KeyInput.Key) { + case KEY_KEY_A: + // select all + new_mark_begin = 0; + new_mark_end = Text.size(); + break; + case KEY_KEY_C: + // copy to clipboard + if (!m_passwordbox && m_operator && m_mark_begin != m_mark_end) + { + const s32 realmbgn = m_mark_begin < m_mark_end ? m_mark_begin : m_mark_end; + const s32 realmend = m_mark_begin < m_mark_end ? m_mark_end : m_mark_begin; + + core::stringc s; + s = Text.subString(realmbgn, realmend - realmbgn).c_str(); + m_operator->copyToClipboard(s.c_str()); + } + break; + case KEY_KEY_X: + // cut to the clipboard + if (!m_passwordbox && m_operator && m_mark_begin != m_mark_end) { + const s32 realmbgn = m_mark_begin < m_mark_end ? m_mark_begin : m_mark_end; + const s32 realmend = m_mark_begin < m_mark_end ? m_mark_end : m_mark_begin; + + // copy + core::stringc sc; + sc = Text.subString(realmbgn, realmend - realmbgn).c_str(); + m_operator->copyToClipboard(sc.c_str()); + + if (isEnabled()) + { + // delete + core::stringw s; + s = Text.subString(0, realmbgn); + s.append(Text.subString(realmend, Text.size() - realmend)); + Text = s; + + m_cursor_pos = realmbgn; + new_mark_begin = 0; + new_mark_end = 0; + text_changed = true; + } + } + break; + case KEY_KEY_V: + if (!isEnabled()) + break; + + // paste from the clipboard + if (m_operator) { + const s32 realmbgn = m_mark_begin < m_mark_end ? m_mark_begin : m_mark_end; + const s32 realmend = m_mark_begin < m_mark_end ? m_mark_end : m_mark_begin; + + // add new character + const c8* p = m_operator->getTextFromClipboard(); + if (p) { + if (m_mark_begin == m_mark_end) { + // insert text + core::stringw s = Text.subString(0, m_cursor_pos); + s.append(p); + s.append(Text.subString(m_cursor_pos, Text.size() - m_cursor_pos)); + + if (!m_max || s.size() <= m_max) // thx to Fish FH for fix + { + Text = s; + s = p; + m_cursor_pos += s.size(); + } + } else { + // replace text + + core::stringw s = Text.subString(0, realmbgn); + s.append(p); + s.append(Text.subString(realmend, Text.size() - realmend)); + + if (!m_max || s.size() <= m_max) // thx to Fish FH for fix + { + Text = s; + s = p; + m_cursor_pos = realmbgn + s.size(); + } + } + } + + new_mark_begin = 0; + new_mark_end = 0; + text_changed = true; + } + break; + case KEY_HOME: + // move/highlight to start of text + if (event.KeyInput.Shift) { + new_mark_end = m_cursor_pos; + new_mark_begin = 0; + m_cursor_pos = 0; + } else { + m_cursor_pos = 0; + new_mark_begin = 0; + new_mark_end = 0; + } + break; + case KEY_END: + // move/highlight to end of text + if (event.KeyInput.Shift) { + new_mark_begin = m_cursor_pos; + new_mark_end = Text.size(); + m_cursor_pos = 0; + } else { + m_cursor_pos = Text.size(); + new_mark_begin = 0; + new_mark_end = 0; + } + break; + default: + return false; + } + } + // default keyboard handling + else + switch (event.KeyInput.Key) { + case KEY_END: + { + s32 p = Text.size(); + if (m_word_wrap || m_multiline) { + p = getLineFromPos(m_cursor_pos); + p = m_broken_text_positions[p] + (s32)m_broken_text[p].size(); + if (p > 0 && (Text[p - 1] == L'\r' || Text[p - 1] == L'\n')) + p -= 1; + } + + if (event.KeyInput.Shift) { + if (m_mark_begin == m_mark_end) + new_mark_begin = m_cursor_pos; + + new_mark_end = p; + } else { + new_mark_begin = 0; + new_mark_end = 0; + } + m_cursor_pos = p; + m_blink_start_time = porting::getTimeMs(); + } + break; + case KEY_HOME: + { + + s32 p = 0; + if (m_word_wrap || m_multiline) { + p = getLineFromPos(m_cursor_pos); + p = m_broken_text_positions[p]; + } + + if (event.KeyInput.Shift) { + if (m_mark_begin == m_mark_end) + new_mark_begin = m_cursor_pos; + new_mark_end = p; + } else { + new_mark_begin = 0; + new_mark_end = 0; + } + m_cursor_pos = p; + m_blink_start_time = porting::getTimeMs(); + } + break; + case KEY_RETURN: + if (m_multiline) { + inputChar(L'\n'); + } else { + calculateScrollPos(); + sendGuiEvent(EGET_EDITBOX_ENTER); + } + return true; + case KEY_LEFT: + + if (event.KeyInput.Shift) { + if (m_cursor_pos > 0) { + if (m_mark_begin == m_mark_end) + new_mark_begin = m_cursor_pos; + + new_mark_end = m_cursor_pos - 1; + } + } else { + new_mark_begin = 0; + new_mark_end = 0; + } + + if (m_cursor_pos > 0) + m_cursor_pos--; + m_blink_start_time = porting::getTimeMs(); + break; + + case KEY_RIGHT: + if (event.KeyInput.Shift) { + if (Text.size() > (u32)m_cursor_pos) { + if (m_mark_begin == m_mark_end) + new_mark_begin = m_cursor_pos; + + new_mark_end = m_cursor_pos + 1; + } + } else { + new_mark_begin = 0; + new_mark_end = 0; + } + + if (Text.size() > (u32)m_cursor_pos) + m_cursor_pos++; + m_blink_start_time = porting::getTimeMs(); + break; + case KEY_UP: + if (m_multiline || (m_word_wrap && m_broken_text.size() > 1)) { + s32 lineNo = getLineFromPos(m_cursor_pos); + s32 mb = (m_mark_begin == m_mark_end) ? m_cursor_pos : (m_mark_begin > m_mark_end ? m_mark_begin : m_mark_end); + if (lineNo > 0) { + s32 cp = m_cursor_pos - m_broken_text_positions[lineNo]; + if ((s32)m_broken_text[lineNo - 1].size() < cp) + m_cursor_pos = m_broken_text_positions[lineNo - 1] + core::max_((u32)1, m_broken_text[lineNo - 1].size()) - 1; + else + m_cursor_pos = m_broken_text_positions[lineNo - 1] + cp; + } + + if (event.KeyInput.Shift) { + new_mark_begin = mb; + new_mark_end = m_cursor_pos; + } else { + new_mark_begin = 0; + new_mark_end = 0; + } + } else { + return false; + } + break; + case KEY_DOWN: + if (m_multiline || (m_word_wrap && m_broken_text.size() > 1)) { + s32 lineNo = getLineFromPos(m_cursor_pos); + s32 mb = (m_mark_begin == m_mark_end) ? m_cursor_pos : (m_mark_begin < m_mark_end ? m_mark_begin : m_mark_end); + if (lineNo < (s32)m_broken_text.size() - 1) + { + s32 cp = m_cursor_pos - m_broken_text_positions[lineNo]; + if ((s32)m_broken_text[lineNo + 1].size() < cp) + m_cursor_pos = m_broken_text_positions[lineNo + 1] + core::max_((u32)1, m_broken_text[lineNo + 1].size()) - 1; + else + m_cursor_pos = m_broken_text_positions[lineNo + 1] + cp; + } + + if (event.KeyInput.Shift) { + new_mark_begin = mb; + new_mark_end = m_cursor_pos; + } else { + new_mark_begin = 0; + new_mark_end = 0; + } + + } else { + return false; + } + break; + + case KEY_BACK: + if (!isEnabled()) + break; + + if (Text.size()) { + core::stringw s; + + if (m_mark_begin != m_mark_end) { + // delete marked text + const s32 realmbgn = m_mark_begin < m_mark_end ? m_mark_begin : m_mark_end; + const s32 realmend = m_mark_begin < m_mark_end ? m_mark_end : m_mark_begin; + + s = Text.subString(0, realmbgn); + s.append(Text.subString(realmend, Text.size() - realmend)); + Text = s; + + m_cursor_pos = realmbgn; + } else { + // delete text behind cursor + if (m_cursor_pos > 0) + s = Text.subString(0, m_cursor_pos - 1); + else + s = L""; + s.append(Text.subString(m_cursor_pos, Text.size() - m_cursor_pos)); + Text = s; + --m_cursor_pos; + } + + if (m_cursor_pos < 0) + m_cursor_pos = 0; + m_blink_start_time = porting::getTimeMs(); // os::Timer::getTime(); + new_mark_begin = 0; + new_mark_end = 0; + text_changed = true; + } + break; + case KEY_DELETE: + if (!isEnabled()) + break; + + if (Text.size() != 0) { + core::stringw s; + + if (m_mark_begin != m_mark_end) { + // delete marked text + const s32 realmbgn = m_mark_begin < m_mark_end ? m_mark_begin : m_mark_end; + const s32 realmend = m_mark_begin < m_mark_end ? m_mark_end : m_mark_begin; + + s = Text.subString(0, realmbgn); + s.append(Text.subString(realmend, Text.size() - realmend)); + Text = s; + + m_cursor_pos = realmbgn; + } else { + // delete text before cursor + s = Text.subString(0, m_cursor_pos); + s.append(Text.subString(m_cursor_pos + 1, Text.size() - m_cursor_pos - 1)); + Text = s; + } + + if (m_cursor_pos > (s32)Text.size()) + m_cursor_pos = (s32)Text.size(); + + m_blink_start_time = porting::getTimeMs(); // os::Timer::getTime(); + new_mark_begin = 0; + new_mark_end = 0; + text_changed = true; + } + break; + + case KEY_ESCAPE: + case KEY_TAB: + case KEY_SHIFT: + case KEY_F1: + case KEY_F2: + case KEY_F3: + case KEY_F4: + case KEY_F5: + case KEY_F6: + case KEY_F7: + case KEY_F8: + case KEY_F9: + case KEY_F10: + case KEY_F11: + case KEY_F12: + case KEY_F13: + case KEY_F14: + case KEY_F15: + case KEY_F16: + case KEY_F17: + case KEY_F18: + case KEY_F19: + case KEY_F20: + case KEY_F21: + case KEY_F22: + case KEY_F23: + case KEY_F24: + // ignore these keys + return false; + + default: + inputChar(event.KeyInput.Char); + return true; + } + + // Set new text markers + setTextMarkers(new_mark_begin, new_mark_end); + + // break the text if it has changed + if (text_changed) { + breakText(); + calculateScrollPos(); + sendGuiEvent(EGET_EDITBOX_CHANGED); + } + else + { + calculateScrollPos(); + } + + return true; +} + + +//! draws the element and its children +void GUIEditBoxWithScrollBar::draw() +{ + if (!IsVisible) + return; + + const bool focus = Environment->hasFocus(this); + + IGUISkin* skin = Environment->getSkin(); + if (!skin) + return; + + video::SColor default_bg_color; + video::SColor bg_color; + + default_bg_color = m_writable ? skin->getColor(EGDC_WINDOW) : video::SColor(0); + bg_color = m_bg_color_used ? m_bg_color : default_bg_color; + + if (!m_border && m_background) { + skin->draw2DRectangle(this, bg_color, AbsoluteRect, &AbsoluteClippingRect); + } + + // draw the border + + if (m_border) { + + if (m_writable) { + skin->draw3DSunkenPane(this, bg_color, false, m_background, + AbsoluteRect, &AbsoluteClippingRect); + } + + calculateFrameRect(); + } + + core::rect local_clip_rect = m_frame_rect; + local_clip_rect.clipAgainst(AbsoluteClippingRect); + + // draw the text + + IGUIFont* font = getActiveFont(); + + s32 cursor_line = 0; + s32 charcursorpos = 0; + + if (font) { + if (m_last_break_font != font) { + breakText(); + } + + // calculate cursor pos + + core::stringw *txt_line = &Text; + s32 start_pos = 0; + + core::stringw s, s2; + + // get mark position + const bool ml = (!m_passwordbox && (m_word_wrap || m_multiline)); + const s32 realmbgn = m_mark_begin < m_mark_end ? m_mark_begin : m_mark_end; + const s32 realmend = m_mark_begin < m_mark_end ? m_mark_end : m_mark_begin; + const s32 hline_start = ml ? getLineFromPos(realmbgn) : 0; + const s32 hline_count = ml ? getLineFromPos(realmend) - hline_start + 1 : 1; + const s32 line_count = ml ? m_broken_text.size() : 1; + + // Save the override color information. + // Then, alter it if the edit box is disabled. + const bool prevOver = m_override_color_enabled; + const video::SColor prevColor = m_override_color; + + if (Text.size()) { + if (!isEnabled() && !m_override_color_enabled) { + m_override_color_enabled = true; + m_override_color = skin->getColor(EGDC_GRAY_TEXT); + } + + for (s32 i = 0; i < line_count; ++i) { + setTextRect(i); + + // clipping test - don't draw anything outside the visible area + core::rect c = local_clip_rect; + c.clipAgainst(m_current_text_rect); + if (!c.isValid()) + continue; + + // get current line + if (m_passwordbox) { + if (m_broken_text.size() != 1) { + m_broken_text.clear(); + m_broken_text.push_back(core::stringw()); + } + if (m_broken_text[0].size() != Text.size()){ + m_broken_text[0] = Text; + for (u32 q = 0; q < Text.size(); ++q) + { + m_broken_text[0][q] = m_passwordchar; + } + } + txt_line = &m_broken_text[0]; + start_pos = 0; + } else { + txt_line = ml ? &m_broken_text[i] : &Text; + start_pos = ml ? m_broken_text_positions[i] : 0; + } + + + // draw normal text + font->draw(txt_line->c_str(), m_current_text_rect, + m_override_color_enabled ? m_override_color : skin->getColor(EGDC_BUTTON_TEXT), + false, true, &local_clip_rect); + + // draw mark and marked text + if (focus && m_mark_begin != m_mark_end && i >= hline_start && i < hline_start + hline_count) { + + s32 mbegin = 0, mend = 0; + s32 lineStartPos = 0, lineEndPos = txt_line->size(); + + if (i == hline_start) { + // highlight start is on this line + s = txt_line->subString(0, realmbgn - start_pos); + mbegin = font->getDimension(s.c_str()).Width; + + // deal with kerning + mbegin += font->getKerningWidth( + &((*txt_line)[realmbgn - start_pos]), + realmbgn - start_pos > 0 ? &((*txt_line)[realmbgn - start_pos - 1]) : 0); + + lineStartPos = realmbgn - start_pos; + } + if (i == hline_start + hline_count - 1) { + // highlight end is on this line + s2 = txt_line->subString(0, realmend - start_pos); + mend = font->getDimension(s2.c_str()).Width; + lineEndPos = (s32)s2.size(); + } else { + mend = font->getDimension(txt_line->c_str()).Width; + } + + + m_current_text_rect.UpperLeftCorner.X += mbegin; + m_current_text_rect.LowerRightCorner.X = m_current_text_rect.UpperLeftCorner.X + mend - mbegin; + + + // draw mark + skin->draw2DRectangle(this, skin->getColor(EGDC_HIGH_LIGHT), m_current_text_rect, &local_clip_rect); + + // draw marked text + s = txt_line->subString(lineStartPos, lineEndPos - lineStartPos); + + if (s.size()) + font->draw(s.c_str(), m_current_text_rect, + m_override_color_enabled ? m_override_color : skin->getColor(EGDC_HIGH_LIGHT_TEXT), + false, true, &local_clip_rect); + + } + } + + // Return the override color information to its previous settings. + m_override_color_enabled = prevOver; + m_override_color = prevColor; + } + + // draw cursor + if (IsEnabled && m_writable) { + if (m_word_wrap || m_multiline) { + cursor_line = getLineFromPos(m_cursor_pos); + txt_line = &m_broken_text[cursor_line]; + start_pos = m_broken_text_positions[cursor_line]; + } + s = txt_line->subString(0, m_cursor_pos - start_pos); + charcursorpos = font->getDimension(s.c_str()).Width + + font->getKerningWidth(L"_", m_cursor_pos - start_pos > 0 ? &((*txt_line)[m_cursor_pos - start_pos - 1]) : 0); + + if (focus && (porting::getTimeMs() - m_blink_start_time) % 700 < 350) { + setTextRect(cursor_line); + m_current_text_rect.UpperLeftCorner.X += charcursorpos; + + font->draw(L"_", m_current_text_rect, + m_override_color_enabled ? m_override_color : skin->getColor(EGDC_BUTTON_TEXT), + false, true, &local_clip_rect); + } + } + } + + // draw children + IGUIElement::draw(); +} + + +//! Sets the new caption of this element. +void GUIEditBoxWithScrollBar::setText(const wchar_t* text) +{ + Text = text; + if (u32(m_cursor_pos) > Text.size()) + m_cursor_pos = Text.size(); + m_hscroll_pos = 0; + breakText(); +} + + +//! Enables or disables automatic scrolling with cursor position +//! \param enable: If set to true, the text will move around with the cursor position +void GUIEditBoxWithScrollBar::setAutoScroll(bool enable) +{ + m_autoscroll = enable; +} + + +//! Checks to see if automatic scrolling is enabled +//! \return true if automatic scrolling is enabled, false if not +bool GUIEditBoxWithScrollBar::isAutoScrollEnabled() const +{ + _IRR_IMPLEMENT_MANAGED_MARSHALLING_BUGFIX; + return m_autoscroll; +} + + +//! Gets the area of the text in the edit box +//! \return Returns the size in pixels of the text +core::dimension2du GUIEditBoxWithScrollBar::getTextDimension() +{ + core::rect ret; + + setTextRect(0); + ret = m_current_text_rect; + + for (u32 i = 1; i < m_broken_text.size(); ++i) { + setTextRect(i); + ret.addInternalPoint(m_current_text_rect.UpperLeftCorner); + ret.addInternalPoint(m_current_text_rect.LowerRightCorner); + } + + return core::dimension2du(ret.getSize()); +} + + +//! Sets the maximum amount of characters which may be entered in the box. +//! \param max: Maximum amount of characters. If 0, the character amount is +//! infinity. +void GUIEditBoxWithScrollBar::setMax(u32 max) +{ + m_max = max; + + if (Text.size() > m_max && m_max != 0) + Text = Text.subString(0, m_max); +} + + +//! Returns maximum amount of characters, previously set by setMax(); +u32 GUIEditBoxWithScrollBar::getMax() const +{ + return m_max; +} + + +bool GUIEditBoxWithScrollBar::processMouse(const SEvent& event) +{ + switch (event.MouseInput.Event) + { + case irr::EMIE_LMOUSE_LEFT_UP: + if (Environment->hasFocus(this)) { + m_cursor_pos = getCursorPos(event.MouseInput.X, event.MouseInput.Y); + if (m_mouse_marking) { + setTextMarkers(m_mark_begin, m_cursor_pos); + } + m_mouse_marking = false; + calculateScrollPos(); + return true; + } + break; + case irr::EMIE_MOUSE_MOVED: + { + if (m_mouse_marking) { + m_cursor_pos = getCursorPos(event.MouseInput.X, event.MouseInput.Y); + setTextMarkers(m_mark_begin, m_cursor_pos); + calculateScrollPos(); + return true; + } + } + break; + case EMIE_LMOUSE_PRESSED_DOWN: + + if (!Environment->hasFocus(this)) { + m_blink_start_time = porting::getTimeMs(); + m_mouse_marking = true; + m_cursor_pos = getCursorPos(event.MouseInput.X, event.MouseInput.Y); + setTextMarkers(m_cursor_pos, m_cursor_pos); + calculateScrollPos(); + return true; + } else { + if (!AbsoluteClippingRect.isPointInside( + core::position2d(event.MouseInput.X, event.MouseInput.Y))) { + return false; + } else { + // move cursor + m_cursor_pos = getCursorPos(event.MouseInput.X, event.MouseInput.Y); + + s32 newMarkBegin = m_mark_begin; + if (!m_mouse_marking) + newMarkBegin = m_cursor_pos; + + m_mouse_marking = true; + setTextMarkers(newMarkBegin, m_cursor_pos); + calculateScrollPos(); + return true; + } + } + default: + break; + } + + return false; +} + + +s32 GUIEditBoxWithScrollBar::getCursorPos(s32 x, s32 y) +{ + IGUIFont* font = getActiveFont(); + + const u32 line_count = (m_word_wrap || m_multiline) ? m_broken_text.size() : 1; + + core::stringw *txt_line = 0; + s32 start_pos = 0; + x += 3; + + for (u32 i = 0; i < line_count; ++i) { + setTextRect(i); + if (i == 0 && y < m_current_text_rect.UpperLeftCorner.Y) + y = m_current_text_rect.UpperLeftCorner.Y; + if (i == line_count - 1 && y > m_current_text_rect.LowerRightCorner.Y) + y = m_current_text_rect.LowerRightCorner.Y; + + // is it inside this region? + if (y >= m_current_text_rect.UpperLeftCorner.Y && y <= m_current_text_rect.LowerRightCorner.Y) { + // we've found the clicked line + txt_line = (m_word_wrap || m_multiline) ? &m_broken_text[i] : &Text; + start_pos = (m_word_wrap || m_multiline) ? m_broken_text_positions[i] : 0; + break; + } + } + + if (x < m_current_text_rect.UpperLeftCorner.X) + x = m_current_text_rect.UpperLeftCorner.X; + + if (!txt_line) + return 0; + + s32 idx = font->getCharacterFromPos(txt_line->c_str(), x - m_current_text_rect.UpperLeftCorner.X); + + // click was on or left of the line + if (idx != -1) + return idx + start_pos; + + // click was off the right edge of the line, go to end. + return txt_line->size() + start_pos; +} + + +//! Breaks the single text line. +void GUIEditBoxWithScrollBar::breakText() +{ + if ((!m_word_wrap && !m_multiline)) + return; + + m_broken_text.clear(); // need to reallocate :/ + m_broken_text_positions.clear(); + + IGUIFont* font = getActiveFont(); + if (!font) + return; + + m_last_break_font = font; + + core::stringw line; + core::stringw word; + core::stringw whitespace; + s32 last_line_start = 0; + s32 size = Text.size(); + s32 length = 0; + s32 el_width = RelativeRect.getWidth() - 6; + wchar_t c; + + for (s32 i = 0; i < size; ++i) { + c = Text[i]; + bool line_break = false; + + if (c == L'\r') { // Mac or Windows breaks + + line_break = true; + c = 0; + if (Text[i + 1] == L'\n') { // Windows breaks + // TODO: I (Michael) think that we shouldn't change the text given by the user for whatever reason. + // Instead rework the cursor positioning to be able to handle this (but not in stable release + // branch as users might already expect this behavior). + Text.erase(i + 1); + --size; + if (m_cursor_pos > i) + --m_cursor_pos; + } + } else if (c == L'\n') { // Unix breaks + line_break = true; + c = 0; + } + + // don't break if we're not a multi-line edit box + if (!m_multiline) + line_break = false; + + if (c == L' ' || c == 0 || i == (size - 1)) { + // here comes the next whitespace, look if + // we can break the last word to the next line + // We also break whitespace, otherwise cursor would vanish beside the right border. + s32 whitelgth = font->getDimension(whitespace.c_str()).Width; + s32 worldlgth = font->getDimension(word.c_str()).Width; + + if (m_word_wrap && length + worldlgth + whitelgth > el_width && line.size() > 0) { + // break to next line + length = worldlgth; + m_broken_text.push_back(line); + m_broken_text_positions.push_back(last_line_start); + last_line_start = i - (s32)word.size(); + line = word; + } else { + // add word to line + line += whitespace; + line += word; + length += whitelgth + worldlgth; + } + + word = L""; + whitespace = L""; + + + if (c) + whitespace += c; + + // compute line break + if (line_break) { + line += whitespace; + line += word; + m_broken_text.push_back(line); + m_broken_text_positions.push_back(last_line_start); + last_line_start = i + 1; + line = L""; + word = L""; + whitespace = L""; + length = 0; + } + } else { + // yippee this is a word.. + word += c; + } + } + + line += whitespace; + line += word; + m_broken_text.push_back(line); + m_broken_text_positions.push_back(last_line_start); +} + +// TODO: that function does interpret VAlign according to line-index (indexed line is placed on top-center-bottom) +// but HAlign according to line-width (pixels) and not by row. +// Intuitively I suppose HAlign handling is better as VScrollPos should handle the line-scrolling. +// But please no one change this without also rewriting (and this time fucking testing!!!) autoscrolling (I noticed this when fixing the old autoscrolling). +void GUIEditBoxWithScrollBar::setTextRect(s32 line) +{ + if (line < 0) + return; + + IGUIFont* font = getActiveFont(); + if (!font) + return; + + core::dimension2du d; + + // get text dimension + const u32 line_count = (m_word_wrap || m_multiline) ? m_broken_text.size() : 1; + if (m_word_wrap || m_multiline) { + d = font->getDimension(m_broken_text[line].c_str()); + } else { + d = font->getDimension(Text.c_str()); + d.Height = AbsoluteRect.getHeight(); + } + d.Height += font->getKerningHeight(); + + // justification + switch (m_halign) { + case EGUIA_CENTER: + // align to h centre + m_current_text_rect.UpperLeftCorner.X = (m_frame_rect.getWidth() / 2) - (d.Width / 2); + m_current_text_rect.LowerRightCorner.X = (m_frame_rect.getWidth() / 2) + (d.Width / 2); + break; + case EGUIA_LOWERRIGHT: + // align to right edge + m_current_text_rect.UpperLeftCorner.X = m_frame_rect.getWidth() - d.Width; + m_current_text_rect.LowerRightCorner.X = m_frame_rect.getWidth(); + break; + default: + // align to left edge + m_current_text_rect.UpperLeftCorner.X = 0; + m_current_text_rect.LowerRightCorner.X = d.Width; + + } + + switch (m_valign) { + case EGUIA_CENTER: + // align to v centre + m_current_text_rect.UpperLeftCorner.Y = + (m_frame_rect.getHeight() / 2) - (line_count*d.Height) / 2 + d.Height*line; + break; + case EGUIA_LOWERRIGHT: + // align to bottom edge + m_current_text_rect.UpperLeftCorner.Y = + m_frame_rect.getHeight() - line_count*d.Height + d.Height*line; + break; + default: + // align to top edge + m_current_text_rect.UpperLeftCorner.Y = d.Height*line; + break; + } + + m_current_text_rect.UpperLeftCorner.X -= m_hscroll_pos; + m_current_text_rect.LowerRightCorner.X -= m_hscroll_pos; + m_current_text_rect.UpperLeftCorner.Y -= m_vscroll_pos; + m_current_text_rect.LowerRightCorner.Y = m_current_text_rect.UpperLeftCorner.Y + d.Height; + + m_current_text_rect += m_frame_rect.UpperLeftCorner; +} + + +s32 GUIEditBoxWithScrollBar::getLineFromPos(s32 pos) +{ + if (!m_word_wrap && !m_multiline) + return 0; + + s32 i = 0; + while (i < (s32)m_broken_text_positions.size()) { + if (m_broken_text_positions[i] > pos) + return i - 1; + ++i; + } + return (s32)m_broken_text_positions.size() - 1; +} + + +void GUIEditBoxWithScrollBar::inputChar(wchar_t c) +{ + if (!isEnabled()) + return; + + if (c != 0) { + if (Text.size() < m_max || m_max == 0) { + core::stringw s; + + if (m_mark_begin != m_mark_end) { + // replace marked text + const s32 realmbgn = m_mark_begin < m_mark_end ? m_mark_begin : m_mark_end; + const s32 realmend = m_mark_begin < m_mark_end ? m_mark_end : m_mark_begin; + + s = Text.subString(0, realmbgn); + s.append(c); + s.append(Text.subString(realmend, Text.size() - realmend)); + Text = s; + m_cursor_pos = realmbgn + 1; + } else { + // add new character + s = Text.subString(0, m_cursor_pos); + s.append(c); + s.append(Text.subString(m_cursor_pos, Text.size() - m_cursor_pos)); + Text = s; + ++m_cursor_pos; + } + + m_blink_start_time = porting::getTimeMs(); + setTextMarkers(0, 0); + } + } + breakText(); + calculateScrollPos(); + sendGuiEvent(EGET_EDITBOX_CHANGED); +} + +// calculate autoscroll +void GUIEditBoxWithScrollBar::calculateScrollPos() +{ + if (!m_autoscroll) + return; + + IGUISkin* skin = Environment->getSkin(); + if (!skin) + return; + IGUIFont* font = m_override_font ? m_override_font : skin->getFont(); + if (!font) + return; + + s32 curs_line = getLineFromPos(m_cursor_pos); + if (curs_line < 0) + return; + setTextRect(curs_line); + const bool has_broken_text = m_multiline || m_word_wrap; + + // Check horizonal scrolling + // NOTE: Calculations different to vertical scrolling because setTextRect interprets VAlign relative to line but HAlign not relative to row + { + // get cursor position + IGUIFont* font = getActiveFont(); + if (!font) + return; + + // get cursor area + irr::u32 cursor_width = font->getDimension(L"_").Width; + core::stringw *txt_line = has_broken_text ? &m_broken_text[curs_line] : &Text; + s32 cpos = has_broken_text ? m_cursor_pos - m_broken_text_positions[curs_line] : m_cursor_pos; // column + s32 cstart = font->getDimension(txt_line->subString(0, cpos).c_str()).Width; // pixels from text-start + s32 cend = cstart + cursor_width; + s32 txt_width = font->getDimension(txt_line->c_str()).Width; + + if (txt_width < m_frame_rect.getWidth()) { + // TODO: Needs a clean left and right gap removal depending on HAlign, similar to vertical scrolling tests for top/bottom. + // This check just fixes the case where it was most noticable (text smaller than clipping area). + + m_hscroll_pos = 0; + setTextRect(curs_line); + } + + if (m_current_text_rect.UpperLeftCorner.X + cstart < m_frame_rect.UpperLeftCorner.X) { + // cursor to the left of the clipping area + m_hscroll_pos -= m_frame_rect.UpperLeftCorner.X - (m_current_text_rect.UpperLeftCorner.X + cstart); + setTextRect(curs_line); + + // TODO: should show more characters to the left when we're scrolling left + // and the cursor reaches the border. + } else if (m_current_text_rect.UpperLeftCorner.X + cend > m_frame_rect.LowerRightCorner.X) { + // cursor to the right of the clipping area + m_hscroll_pos += (m_current_text_rect.UpperLeftCorner.X + cend) - m_frame_rect.LowerRightCorner.X; + setTextRect(curs_line); + } + } + + // calculate vertical scrolling + if (has_broken_text) { + irr::u32 line_height = font->getDimension(L"A").Height + font->getKerningHeight(); + // only up to 1 line fits? + if (line_height >= (irr::u32)m_frame_rect.getHeight()) { + m_vscroll_pos = 0; + setTextRect(curs_line); + s32 unscrolledPos = m_current_text_rect.UpperLeftCorner.Y; + s32 pivot = m_frame_rect.UpperLeftCorner.Y; + switch (m_valign) { + case EGUIA_CENTER: + pivot += m_frame_rect.getHeight() / 2; + unscrolledPos += line_height / 2; + break; + case EGUIA_LOWERRIGHT: + pivot += m_frame_rect.getHeight(); + unscrolledPos += line_height; + break; + default: + break; + } + m_vscroll_pos = unscrolledPos - pivot; + setTextRect(curs_line); + } else { + // First 2 checks are necessary when people delete lines + setTextRect(0); + if (m_current_text_rect.UpperLeftCorner.Y > m_frame_rect.UpperLeftCorner.Y && m_valign != EGUIA_LOWERRIGHT) { + // first line is leaving a gap on top + m_vscroll_pos = 0; + } else if (m_valign != EGUIA_UPPERLEFT) { + u32 lastLine = m_broken_text_positions.empty() ? 0 : m_broken_text_positions.size() - 1; + setTextRect(lastLine); + if (m_current_text_rect.LowerRightCorner.Y < m_frame_rect.LowerRightCorner.Y) + { + // last line is leaving a gap on bottom + m_vscroll_pos -= m_frame_rect.LowerRightCorner.Y - m_current_text_rect.LowerRightCorner.Y; + } + } + + setTextRect(curs_line); + if (m_current_text_rect.UpperLeftCorner.Y < m_frame_rect.UpperLeftCorner.Y) { + // text above valid area + m_vscroll_pos -= m_frame_rect.UpperLeftCorner.Y - m_current_text_rect.UpperLeftCorner.Y; + setTextRect(curs_line); + } else if (m_current_text_rect.LowerRightCorner.Y > m_frame_rect.LowerRightCorner.Y){ + // text below valid area + m_vscroll_pos += m_current_text_rect.LowerRightCorner.Y - m_frame_rect.LowerRightCorner.Y; + setTextRect(curs_line); + } + } + } + + if (m_vscrollbar) { + m_vscrollbar->setPos(m_vscroll_pos); + } +} + +void GUIEditBoxWithScrollBar::calculateFrameRect() +{ + m_frame_rect = AbsoluteRect; + + + IGUISkin *skin = 0; + if (Environment) + skin = Environment->getSkin(); + if (m_border && skin) { + m_frame_rect.UpperLeftCorner.X += skin->getSize(EGDS_TEXT_DISTANCE_X) + 1; + m_frame_rect.UpperLeftCorner.Y += skin->getSize(EGDS_TEXT_DISTANCE_Y) + 1; + m_frame_rect.LowerRightCorner.X -= skin->getSize(EGDS_TEXT_DISTANCE_X) + 1; + m_frame_rect.LowerRightCorner.Y -= skin->getSize(EGDS_TEXT_DISTANCE_Y) + 1; + } + + updateVScrollBar(); +} + +//! set text markers +void GUIEditBoxWithScrollBar::setTextMarkers(s32 begin, s32 end) +{ + if (begin != m_mark_begin || end != m_mark_end) { + m_mark_begin = begin; + m_mark_end = end; + sendGuiEvent(EGET_EDITBOX_MARKING_CHANGED); + } +} + +//! send some gui event to parent +void GUIEditBoxWithScrollBar::sendGuiEvent(EGUI_EVENT_TYPE type) +{ + if (Parent) { + SEvent e; + e.EventType = EET_GUI_EVENT; + e.GUIEvent.Caller = this; + e.GUIEvent.Element = 0; + e.GUIEvent.EventType = type; + + Parent->OnEvent(e); + } +} + +//! create a vertical scroll bar +void GUIEditBoxWithScrollBar::createVScrollBar() +{ + IGUISkin *skin = 0; + if (Environment) + skin = Environment->getSkin(); + + m_scrollbar_width = skin ? skin->getSize(gui::EGDS_SCROLLBAR_SIZE) : 16; + + irr::core::rect scrollbarrect = m_frame_rect; + scrollbarrect.UpperLeftCorner.X += m_frame_rect.getWidth() - m_scrollbar_width; + m_vscrollbar = Environment->addScrollBar(false, scrollbarrect, getParent(), getID()); + m_vscrollbar->setVisible(false); + m_vscrollbar->setSmallStep(1); + m_vscrollbar->setLargeStep(1); +} + +void GUIEditBoxWithScrollBar::updateVScrollBar() +{ + if (!m_vscrollbar) { + return; + } + + // OnScrollBarChanged(...) + if (m_vscrollbar->getPos() != m_vscroll_pos) { + s32 deltaScrollY = m_vscrollbar->getPos() - m_vscroll_pos; + m_current_text_rect.UpperLeftCorner.Y -= deltaScrollY; + m_current_text_rect.LowerRightCorner.Y -= deltaScrollY; + + s32 scrollymax = getTextDimension().Height - m_frame_rect.getHeight(); + if (scrollymax != m_vscrollbar->getMax()) { + // manage a newline or a deleted line + m_vscrollbar->setMax(scrollymax); + calculateScrollPos(); + } else { + // manage a newline or a deleted line + m_vscroll_pos = m_vscrollbar->getPos(); + } + } + + // check if a vertical scrollbar is needed ? + if (getTextDimension().Height > (u32) m_frame_rect.getHeight()) { + m_frame_rect.LowerRightCorner.X -= m_scrollbar_width; + + s32 scrollymax = getTextDimension().Height - m_frame_rect.getHeight(); + if (scrollymax != m_vscrollbar->getMax()) { + m_vscrollbar->setMax(scrollymax); + } + + if (!m_vscrollbar->isVisible()) { + m_vscrollbar->setVisible(true); + } + } else { + if (m_vscrollbar->isVisible()) + { + m_vscrollbar->setVisible(false); + m_vscroll_pos = 0; + m_vscrollbar->setPos(0); + m_vscrollbar->setMax(1); + } + } + + +} + +//! set true if this editbox is writable +void GUIEditBoxWithScrollBar::setWritable(bool writable) +{ + m_writable = writable; +} + +//! Change the background color +void GUIEditBoxWithScrollBar::setBackgroundColor(const video::SColor &bg_color) +{ + m_bg_color = bg_color; + m_bg_color_used = true; +} + +//! Writes attributes of the element. +void GUIEditBoxWithScrollBar::serializeAttributes(io::IAttributes* out, io::SAttributeReadWriteOptions* options = 0) const +{ + // IGUIEditBox::serializeAttributes(out,options); + + out->addBool("Border", m_border); + out->addBool("Background", m_background); + out->addBool("OverrideColorEnabled", m_override_color_enabled); + out->addColor("OverrideColor", m_override_color); + // out->addFont("OverrideFont", OverrideFont); + out->addInt("MaxChars", m_max); + out->addBool("WordWrap", m_word_wrap); + out->addBool("MultiLine", m_multiline); + out->addBool("AutoScroll", m_autoscroll); + out->addBool("PasswordBox", m_passwordbox); + core::stringw ch = L" "; + ch[0] = m_passwordchar; + out->addString("PasswordChar", ch.c_str()); + out->addEnum("HTextAlign", m_halign, GUIAlignmentNames); + out->addEnum("VTextAlign", m_valign, GUIAlignmentNames); + out->addBool("Writable", m_writable); + + IGUIEditBox::serializeAttributes(out, options); +} + + +//! Reads attributes of the element +void GUIEditBoxWithScrollBar::deserializeAttributes(io::IAttributes* in, io::SAttributeReadWriteOptions* options = 0) +{ + IGUIEditBox::deserializeAttributes(in, options); + + setDrawBorder(in->getAttributeAsBool("Border")); + setDrawBackground(in->getAttributeAsBool("Background")); + setOverrideColor(in->getAttributeAsColor("OverrideColor")); + enableOverrideColor(in->getAttributeAsBool("OverrideColorEnabled")); + setMax(in->getAttributeAsInt("MaxChars")); + setWordWrap(in->getAttributeAsBool("WordWrap")); + setMultiLine(in->getAttributeAsBool("MultiLine")); + setAutoScroll(in->getAttributeAsBool("AutoScroll")); + core::stringw ch = in->getAttributeAsStringW("PasswordChar"); + + if (!ch.size()) + setPasswordBox(in->getAttributeAsBool("PasswordBox")); + else + setPasswordBox(in->getAttributeAsBool("PasswordBox"), ch[0]); + + setTextAlignment((EGUI_ALIGNMENT)in->getAttributeAsEnumeration("HTextAlign", GUIAlignmentNames), + (EGUI_ALIGNMENT)in->getAttributeAsEnumeration("VTextAlign", GUIAlignmentNames)); + + // setOverrideFont(in->getAttributeAsFont("OverrideFont")); + setWritable(in->getAttributeAsBool("Writable")); +} diff --git a/src/gui/guiEditBoxWithScrollbar.h b/src/gui/guiEditBoxWithScrollbar.h new file mode 100644 index 000000000..cca2f6536 --- /dev/null +++ b/src/gui/guiEditBoxWithScrollbar.h @@ -0,0 +1,192 @@ +// Copyright (C) 2002-2012 Nikolaus Gebhardt, Modified by Mustapha Tachouct +// This file is part of the "Irrlicht Engine". +// For conditions of distribution and use, see copyright notice in irrlicht.h + +#ifndef GUIEDITBOXWITHSCROLLBAR_HEADER +#define GUIEDITBOXWITHSCROLLBAR_HEADER + +#include "IGUIEditBox.h" +#include "IOSOperator.h" +#include "IGUIScrollBar.h" +#include + +using namespace irr; +using namespace irr::gui; + +class GUIEditBoxWithScrollBar : public IGUIEditBox +{ +public: + + //! constructor + GUIEditBoxWithScrollBar(const wchar_t* text, bool border, IGUIEnvironment* environment, + IGUIElement* parent, s32 id, const core::rect& rectangle, + bool writable = true, bool has_vscrollbar = true); + + //! destructor + virtual ~GUIEditBoxWithScrollBar(); + + //! Sets another skin independent font. + virtual void setOverrideFont(IGUIFont* font = 0); + + //! Gets the override font (if any) + /** \return The override font (may be 0) */ + virtual IGUIFont* getOverrideFont() const; + + //! Get the font which is used right now for drawing + /** Currently this is the override font when one is set and the + font of the active skin otherwise */ + virtual IGUIFont* getActiveFont() const; + + //! Sets another color for the text. + virtual void setOverrideColor(video::SColor color); + + //! Gets the override color + virtual video::SColor getOverrideColor() const; + + //! Sets if the text should use the overide color or the + //! color in the gui skin. + virtual void enableOverrideColor(bool enable); + + //! Checks if an override color is enabled + /** \return true if the override color is enabled, false otherwise */ + virtual bool isOverrideColorEnabled(void) const; + + //! Sets whether to draw the background + virtual void setDrawBackground(bool draw); + + //! Turns the border on or off + virtual void setDrawBorder(bool border); + + //! Enables or disables word wrap for using the edit box as multiline text editor. + virtual void setWordWrap(bool enable); + + //! Checks if word wrap is enabled + //! \return true if word wrap is enabled, false otherwise + virtual bool isWordWrapEnabled() const; + + //! Enables or disables newlines. + /** \param enable: If set to true, the EGET_EDITBOX_ENTER event will not be fired, + instead a newline character will be inserted. */ + virtual void setMultiLine(bool enable); + + //! Checks if multi line editing is enabled + //! \return true if mult-line is enabled, false otherwise + virtual bool isMultiLineEnabled() const; + + //! Enables or disables automatic scrolling with cursor position + //! \param enable: If set to true, the text will move around with the cursor position + virtual void setAutoScroll(bool enable); + + //! Checks to see if automatic scrolling is enabled + //! \return true if automatic scrolling is enabled, false if not + virtual bool isAutoScrollEnabled() const; + + //! Gets the size area of the text in the edit box + //! \return Returns the size in pixels of the text + virtual core::dimension2du getTextDimension(); + + //! Sets text justification + virtual void setTextAlignment(EGUI_ALIGNMENT horizontal, EGUI_ALIGNMENT vertical); + + //! called if an event happened. + virtual bool OnEvent(const SEvent& event); + + //! draws the element and its children + virtual void draw(); + + //! Sets the new caption of this element. + virtual void setText(const wchar_t* text); + + //! Sets the maximum amount of characters which may be entered in the box. + //! \param max: Maximum amount of characters. If 0, the character amount is + //! infinity. + virtual void setMax(u32 max); + + //! Returns maximum amount of characters, previously set by setMax(); + virtual u32 getMax() const; + + //! Sets whether the edit box is a password box. Setting this to true will + /** disable MultiLine, WordWrap and the ability to copy with ctrl+c or ctrl+x + \param passwordBox: true to enable password, false to disable + \param passwordChar: the character that is displayed instead of letters */ + virtual void setPasswordBox(bool passwordBox, wchar_t passwordChar = L'*'); + + //! Returns true if the edit box is currently a password box. + virtual bool isPasswordBox() const; + + //! Updates the absolute position, splits text if required + virtual void updateAbsolutePosition(); + + virtual void setWritable(bool writable); + + //! Change the background color + virtual void setBackgroundColor(const video::SColor &bg_color); + + //! Writes attributes of the element. + virtual void serializeAttributes(io::IAttributes* out, io::SAttributeReadWriteOptions* options) const; + + //! Reads attributes of the element + virtual void deserializeAttributes(io::IAttributes* in, io::SAttributeReadWriteOptions* options); + +protected: + //! Breaks the single text line. + void breakText(); + //! sets the area of the given line + void setTextRect(s32 line); + //! returns the line number that the cursor is on + s32 getLineFromPos(s32 pos); + //! adds a letter to the edit box + void inputChar(wchar_t c); + //! calculates the current scroll position + void calculateScrollPos(); + //! calculated the FrameRect + void calculateFrameRect(); + //! send some gui event to parent + void sendGuiEvent(EGUI_EVENT_TYPE type); + //! set text markers + void setTextMarkers(s32 begin, s32 end); + //! create a Vertical ScrollBar + void createVScrollBar(); + //! update the vertical scrollBar (visibilty & position) + void updateVScrollBar(); + + bool processKey(const SEvent& event); + bool processMouse(const SEvent& event); + s32 getCursorPos(s32 x, s32 y); + + bool m_mouse_marking; + bool m_border; + bool m_background; + bool m_override_color_enabled; + s32 m_mark_begin; + s32 m_mark_end; + + video::SColor m_override_color; + gui::IGUIFont *m_override_font, *m_last_break_font; + IOSOperator* m_operator; + + u32 m_blink_start_time; + s32 m_cursor_pos; + s32 m_hscroll_pos, m_vscroll_pos; // scroll position in characters + u32 m_max; + + bool m_word_wrap, m_multiline, m_autoscroll, m_passwordbox; + wchar_t m_passwordchar; + EGUI_ALIGNMENT m_halign, m_valign; + + std::vector m_broken_text; + std::vector m_broken_text_positions; + + core::rect m_current_text_rect, m_frame_rect; // temporary values + + u32 m_scrollbar_width; + IGUIScrollBar *m_vscrollbar; + bool m_writable; + + bool m_bg_color_used; + video::SColor m_bg_color; +}; + + +#endif // GUIEDITBOXWITHSCROLLBAR_HEADER + diff --git a/src/gui/guiEngine.cpp b/src/gui/guiEngine.cpp new file mode 100644 index 000000000..e9b4e54c1 --- /dev/null +++ b/src/gui/guiEngine.cpp @@ -0,0 +1,587 @@ +/* +Minetest +Copyright (C) 2013 sapier + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#include "guiEngine.h" + +#include +#include +#include "client/renderingengine.h" +#include "scripting_mainmenu.h" +#include "util/numeric.h" +#include "config.h" +#include "version.h" +#include "porting.h" +#include "filesys.h" +#include "settings.h" +#include "guiMainMenu.h" +#include "sound.h" +#include "sound_openal.h" +#include "clouds.h" +#include "httpfetch.h" +#include "log.h" +#include "fontengine.h" +#include "guiscalingfilter.h" +#include "irrlicht_changes/static_text.h" + +#ifdef __ANDROID__ +#include "client/tile.h" +#include +#endif + + +/******************************************************************************/ +void TextDestGuiEngine::gotText(const StringMap &fields) +{ + m_engine->getScriptIface()->handleMainMenuButtons(fields); +} + +/******************************************************************************/ +void TextDestGuiEngine::gotText(const std::wstring &text) +{ + m_engine->getScriptIface()->handleMainMenuEvent(wide_to_utf8(text)); +} + +/******************************************************************************/ +MenuTextureSource::~MenuTextureSource() +{ + for (const std::string &texture_to_delete : m_to_delete) { + const char *tname = texture_to_delete.c_str(); + video::ITexture *texture = m_driver->getTexture(tname); + m_driver->removeTexture(texture); + } +} + +/******************************************************************************/ +video::ITexture *MenuTextureSource::getTexture(const std::string &name, u32 *id) +{ + if(id) + *id = 0; + if(name.empty()) + return NULL; + m_to_delete.insert(name); + +#ifdef __ANDROID__ + video::IImage *image = m_driver->createImageFromFile(name.c_str()); + if (image) { + image = Align2Npot2(image, m_driver); + video::ITexture* retval = m_driver->addTexture(name.c_str(), image); + image->drop(); + return retval; + } +#endif + return m_driver->getTexture(name.c_str()); +} + +/******************************************************************************/ +/** MenuMusicFetcher */ +/******************************************************************************/ +void MenuMusicFetcher::fetchSounds(const std::string &name, + std::set &dst_paths, + std::set &dst_datas) +{ + if(m_fetched.count(name)) + return; + m_fetched.insert(name); + std::string base; + base = porting::path_share + DIR_DELIM + "sounds"; + dst_paths.insert(base + DIR_DELIM + name + ".ogg"); + int i; + for(i=0; i<10; i++) + dst_paths.insert(base + DIR_DELIM + name + "."+itos(i)+".ogg"); + base = porting::path_user + DIR_DELIM + "sounds"; + dst_paths.insert(base + DIR_DELIM + name + ".ogg"); + for(i=0; i<10; i++) + dst_paths.insert(base + DIR_DELIM + name + "."+itos(i)+".ogg"); +} + +/******************************************************************************/ +/** GUIEngine */ +/******************************************************************************/ +GUIEngine::GUIEngine(JoystickController *joystick, + gui::IGUIElement *parent, + IMenuManager *menumgr, + MainMenuData *data, + bool &kill) : + m_parent(parent), + m_menumanager(menumgr), + m_smgr(RenderingEngine::get_scene_manager()), + m_data(data), + m_kill(kill) +{ + //initialize texture pointers + for (image_definition &texture : m_textures) { + texture.texture = NULL; + } + // is deleted by guiformspec! + m_buttonhandler = new TextDestGuiEngine(this); + + //create texture source + m_texture_source = new MenuTextureSource(RenderingEngine::get_video_driver()); + + //create soundmanager + MenuMusicFetcher soundfetcher; +#if USE_SOUND + m_sound_manager = createOpenALSoundManager(&soundfetcher); +#endif + if(!m_sound_manager) + m_sound_manager = &dummySoundManager; + + //create topleft header + m_toplefttext = L""; + + core::rect rect(0, 0, g_fontengine->getTextWidth(m_toplefttext.c_str()), + g_fontengine->getTextHeight()); + rect += v2s32(4, 0); + + m_irr_toplefttext = + addStaticText(RenderingEngine::get_gui_env(), m_toplefttext, + rect, false, true, 0, -1); + + //create formspecsource + m_formspecgui = new FormspecFormSource(""); + + /* Create menu */ + m_menu = new GUIFormSpecMenu(joystick, + m_parent, + -1, + m_menumanager, + NULL /* &client */, + m_texture_source, + m_formspecgui, + m_buttonhandler, + false); + + m_menu->allowClose(false); + m_menu->lockSize(true,v2u32(800,600)); + + // Initialize scripting + + infostream << "GUIEngine: Initializing Lua" << std::endl; + + m_script = new MainMenuScripting(this); + + try { + m_script->setMainMenuData(&m_data->script_data); + m_data->script_data.errormessage = ""; + + if (!loadMainMenuScript()) { + errorstream << "No future without main menu!" << std::endl; + abort(); + } + + run(); + } catch (LuaError &e) { + errorstream << "Main menu error: " << e.what() << std::endl; + m_data->script_data.errormessage = e.what(); + } + + m_menu->quitMenu(); + m_menu->drop(); + m_menu = NULL; +} + +/******************************************************************************/ +bool GUIEngine::loadMainMenuScript() +{ + // Set main menu path (for core.get_mainmenu_path()) + m_scriptdir = g_settings->get("main_menu_path"); + if (m_scriptdir.empty()) { + m_scriptdir = porting::path_share + DIR_DELIM + "builtin" + DIR_DELIM + "mainmenu"; + } + + // Load builtin (which will load the main menu script) + std::string script = porting::path_share + DIR_DELIM "builtin" + DIR_DELIM "init.lua"; + try { + m_script->loadScript(script); + // Menu script loaded + return true; + } catch (const ModError &e) { + errorstream << "GUIEngine: execution of menu script failed: " + << e.what() << std::endl; + } + + return false; +} + +/******************************************************************************/ +void GUIEngine::run() +{ + // Always create clouds because they may or may not be + // needed based on the game selected + video::IVideoDriver *driver = RenderingEngine::get_video_driver(); + + cloudInit(); + + unsigned int text_height = g_fontengine->getTextHeight(); + + irr::core::dimension2d previous_screen_size(g_settings->getU16("screen_w"), + g_settings->getU16("screen_h")); + + while (RenderingEngine::run() && (!m_startgame) && (!m_kill)) { + + const irr::core::dimension2d ¤t_screen_size = + RenderingEngine::get_video_driver()->getScreenSize(); + // Verify if window size has changed and save it if it's the case + // Ensure evaluating settings->getBool after verifying screensize + // First condition is cheaper + if (previous_screen_size != current_screen_size && + current_screen_size != irr::core::dimension2d(0,0) && + g_settings->getBool("autosave_screensize")) { + g_settings->setU16("screen_w", current_screen_size.Width); + g_settings->setU16("screen_h", current_screen_size.Height); + previous_screen_size = current_screen_size; + } + + //check if we need to update the "upper left corner"-text + if (text_height != g_fontengine->getTextHeight()) { + updateTopLeftTextSize(); + text_height = g_fontengine->getTextHeight(); + } + + driver->beginScene(true, true, video::SColor(255,140,186,250)); + + if (m_clouds_enabled) + { + cloudPreProcess(); + drawOverlay(driver); + } + else + drawBackground(driver); + + drawHeader(driver); + drawFooter(driver); + + RenderingEngine::get_gui_env()->drawAll(); + + driver->endScene(); + + if (m_clouds_enabled) + cloudPostProcess(); + else + sleep_ms(25); + + m_script->step(); + +#ifdef __ANDROID__ + m_menu->getAndroidUIInput(); +#endif + } +} + +/******************************************************************************/ +GUIEngine::~GUIEngine() +{ + if (m_sound_manager != &dummySoundManager){ + delete m_sound_manager; + m_sound_manager = NULL; + } + + infostream<<"GUIEngine: Deinitializing scripting"<setText(L""); + + //clean up texture pointers + for (image_definition &texture : m_textures) { + if (texture.texture) + RenderingEngine::get_video_driver()->removeTexture(texture.texture); + } + + delete m_texture_source; + + if (m_cloud.clouds) + m_cloud.clouds->drop(); +} + +/******************************************************************************/ +void GUIEngine::cloudInit() +{ + m_cloud.clouds = new Clouds(m_smgr, -1, rand()); + m_cloud.clouds->setHeight(100.0f); + m_cloud.clouds->update(v3f(0, 0, 0), video::SColor(255,200,200,255)); + + m_cloud.camera = m_smgr->addCameraSceneNode(0, + v3f(0,0,0), v3f(0, 60, 100)); + m_cloud.camera->setFarValue(10000); + + m_cloud.lasttime = RenderingEngine::get_timer_time(); +} + +/******************************************************************************/ +void GUIEngine::cloudPreProcess() +{ + u32 time = RenderingEngine::get_timer_time(); + + if(time > m_cloud.lasttime) + m_cloud.dtime = (time - m_cloud.lasttime) / 1000.0; + else + m_cloud.dtime = 0; + + m_cloud.lasttime = time; + + m_cloud.clouds->step(m_cloud.dtime*3); + m_cloud.clouds->render(); + m_smgr->drawAll(); +} + +/******************************************************************************/ +void GUIEngine::cloudPostProcess() +{ + float fps_max = g_settings->getFloat("pause_fps_max"); + // Time of frame without fps limit + u32 busytime_u32; + + // not using getRealTime is necessary for wine + u32 time = RenderingEngine::get_timer_time(); + if(time > m_cloud.lasttime) + busytime_u32 = time - m_cloud.lasttime; + else + busytime_u32 = 0; + + // FPS limiter + u32 frametime_min = 1000./fps_max; + + if (busytime_u32 < frametime_min) { + u32 sleeptime = frametime_min - busytime_u32; + RenderingEngine::get_raw_device()->sleep(sleeptime); + } +} + +/******************************************************************************/ +void GUIEngine::drawBackground(video::IVideoDriver *driver) +{ + v2u32 screensize = driver->getScreenSize(); + + video::ITexture* texture = m_textures[TEX_LAYER_BACKGROUND].texture; + + /* If no texture, draw background of solid color */ + if(!texture){ + video::SColor color(255,80,58,37); + core::rect rect(0, 0, screensize.X, screensize.Y); + driver->draw2DRectangle(color, rect, NULL); + return; + } + + v2u32 sourcesize = texture->getOriginalSize(); + + if (m_textures[TEX_LAYER_BACKGROUND].tile) + { + v2u32 tilesize( + MYMAX(sourcesize.X,m_textures[TEX_LAYER_BACKGROUND].minsize), + MYMAX(sourcesize.Y,m_textures[TEX_LAYER_BACKGROUND].minsize)); + for (unsigned int x = 0; x < screensize.X; x += tilesize.X ) + { + for (unsigned int y = 0; y < screensize.Y; y += tilesize.Y ) + { + draw2DImageFilterScaled(driver, texture, + core::rect(x, y, x+tilesize.X, y+tilesize.Y), + core::rect(0, 0, sourcesize.X, sourcesize.Y), + NULL, NULL, true); + } + } + return; + } + + /* Draw background texture */ + draw2DImageFilterScaled(driver, texture, + core::rect(0, 0, screensize.X, screensize.Y), + core::rect(0, 0, sourcesize.X, sourcesize.Y), + NULL, NULL, true); +} + +/******************************************************************************/ +void GUIEngine::drawOverlay(video::IVideoDriver *driver) +{ + v2u32 screensize = driver->getScreenSize(); + + video::ITexture* texture = m_textures[TEX_LAYER_OVERLAY].texture; + + /* If no texture, draw nothing */ + if(!texture) + return; + + /* Draw background texture */ + v2u32 sourcesize = texture->getOriginalSize(); + draw2DImageFilterScaled(driver, texture, + core::rect(0, 0, screensize.X, screensize.Y), + core::rect(0, 0, sourcesize.X, sourcesize.Y), + NULL, NULL, true); +} + +/******************************************************************************/ +void GUIEngine::drawHeader(video::IVideoDriver *driver) +{ + core::dimension2d screensize = driver->getScreenSize(); + + video::ITexture* texture = m_textures[TEX_LAYER_HEADER].texture; + + /* If no texture, draw nothing */ + if(!texture) + return; + + f32 mult = (((f32)screensize.Width / 2.0)) / + ((f32)texture->getOriginalSize().Width); + + v2s32 splashsize(((f32)texture->getOriginalSize().Width) * mult, + ((f32)texture->getOriginalSize().Height) * mult); + + // Don't draw the header if there isn't enough room + s32 free_space = (((s32)screensize.Height)-320)/2; + + if (free_space > splashsize.Y) { + core::rect splashrect(0, 0, splashsize.X, splashsize.Y); + splashrect += v2s32((screensize.Width/2)-(splashsize.X/2), + ((free_space/2)-splashsize.Y/2)+10); + + video::SColor bgcolor(255,50,50,50); + + draw2DImageFilterScaled(driver, texture, splashrect, + core::rect(core::position2d(0,0), + core::dimension2di(texture->getOriginalSize())), + NULL, NULL, true); + } +} + +/******************************************************************************/ +void GUIEngine::drawFooter(video::IVideoDriver *driver) +{ + core::dimension2d screensize = driver->getScreenSize(); + + video::ITexture* texture = m_textures[TEX_LAYER_FOOTER].texture; + + /* If no texture, draw nothing */ + if(!texture) + return; + + f32 mult = (((f32)screensize.Width)) / + ((f32)texture->getOriginalSize().Width); + + v2s32 footersize(((f32)texture->getOriginalSize().Width) * mult, + ((f32)texture->getOriginalSize().Height) * mult); + + // Don't draw the footer if there isn't enough room + s32 free_space = (((s32)screensize.Height)-320)/2; + + if (free_space > footersize.Y) { + core::rect rect(0,0,footersize.X,footersize.Y); + rect += v2s32(screensize.Width/2,screensize.Height-footersize.Y); + rect -= v2s32(footersize.X/2, 0); + + draw2DImageFilterScaled(driver, texture, rect, + core::rect(core::position2d(0,0), + core::dimension2di(texture->getOriginalSize())), + NULL, NULL, true); + } +} + +/******************************************************************************/ +bool GUIEngine::setTexture(texture_layer layer, std::string texturepath, + bool tile_image, unsigned int minsize) +{ + video::IVideoDriver *driver = RenderingEngine::get_video_driver(); + + if (m_textures[layer].texture) { + driver->removeTexture(m_textures[layer].texture); + m_textures[layer].texture = NULL; + } + + if (texturepath.empty() || !fs::PathExists(texturepath)) { + return false; + } + + m_textures[layer].texture = driver->getTexture(texturepath.c_str()); + m_textures[layer].tile = tile_image; + m_textures[layer].minsize = minsize; + + if (!m_textures[layer].texture) { + return false; + } + + return true; +} + +/******************************************************************************/ +bool GUIEngine::downloadFile(const std::string &url, const std::string &target) +{ +#if USE_CURL + std::ofstream target_file(target.c_str(), std::ios::out | std::ios::binary); + + if (!target_file.good()) { + return false; + } + + HTTPFetchRequest fetch_request; + HTTPFetchResult fetch_result; + fetch_request.url = url; + fetch_request.caller = HTTPFETCH_SYNC; + fetch_request.timeout = g_settings->getS32("curl_file_download_timeout"); + httpfetch_sync(fetch_request, fetch_result); + + if (!fetch_result.succeeded) { + return false; + } + target_file << fetch_result.data; + + return true; +#else + return false; +#endif +} + +/******************************************************************************/ +void GUIEngine::setTopleftText(const std::string &text) +{ + m_toplefttext = translate_string(utf8_to_wide(text)); + + updateTopLeftTextSize(); +} + +/******************************************************************************/ +void GUIEngine::updateTopLeftTextSize() +{ + core::rect rect(0, 0, g_fontengine->getTextWidth(m_toplefttext.c_str()), + g_fontengine->getTextHeight()); + rect += v2s32(4, 0); + + m_irr_toplefttext->remove(); + m_irr_toplefttext = + addStaticText(RenderingEngine::get_gui_env(), m_toplefttext, + rect, false, true, 0, -1); +} + +/******************************************************************************/ +s32 GUIEngine::playSound(SimpleSoundSpec spec, bool looped) +{ + s32 handle = m_sound_manager->playSound(spec, looped); + return handle; +} + +/******************************************************************************/ +void GUIEngine::stopSound(s32 handle) +{ + m_sound_manager->stopSound(handle); +} + +/******************************************************************************/ +unsigned int GUIEngine::queueAsync(const std::string &serialized_func, + const std::string &serialized_params) +{ + return m_script->queueAsync(serialized_func, serialized_params); +} + diff --git a/src/gui/guiEngine.h b/src/gui/guiEngine.h new file mode 100644 index 000000000..817d76014 --- /dev/null +++ b/src/gui/guiEngine.h @@ -0,0 +1,304 @@ +/* +Minetest +Copyright (C) 2013 sapier + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#pragma once + +/******************************************************************************/ +/* Includes */ +/******************************************************************************/ +#include "irrlichttypes.h" +#include "modalMenu.h" +#include "guiFormSpecMenu.h" +#include "sound.h" +#include "client/tile.h" +#include "util/enriched_string.h" + +/******************************************************************************/ +/* Typedefs and macros */ +/******************************************************************************/ +/** texture layer ids */ +typedef enum { + TEX_LAYER_BACKGROUND = 0, + TEX_LAYER_OVERLAY, + TEX_LAYER_HEADER, + TEX_LAYER_FOOTER, + TEX_LAYER_MAX +} texture_layer; + +typedef struct { + video::ITexture *texture = nullptr; + bool tile; + unsigned int minsize; +} image_definition; + +/******************************************************************************/ +/* forward declarations */ +/******************************************************************************/ +class GUIEngine; +class MainMenuScripting; +class Clouds; +struct MainMenuData; + +/******************************************************************************/ +/* declarations */ +/******************************************************************************/ + +/** GUIEngine specific implementation of TextDest used within guiFormSpecMenu */ +class TextDestGuiEngine : public TextDest +{ +public: + /** + * default constructor + * @param engine the engine data is transmitted for further processing + */ + TextDestGuiEngine(GUIEngine* engine) : m_engine(engine) {}; + + /** + * receive fields transmitted by guiFormSpecMenu + * @param fields map containing formspec field elements currently active + */ + void gotText(const StringMap &fields); + + /** + * receive text/events transmitted by guiFormSpecMenu + * @param text textual representation of event + */ + void gotText(const std::wstring &text); + +private: + /** target to transmit data to */ + GUIEngine *m_engine = nullptr; +}; + +/** GUIEngine specific implementation of ISimpleTextureSource */ +class MenuTextureSource : public ISimpleTextureSource +{ +public: + /** + * default constructor + * @param driver the video driver to load textures from + */ + MenuTextureSource(video::IVideoDriver *driver) : m_driver(driver) {}; + + /** + * destructor, removes all loaded textures + */ + virtual ~MenuTextureSource(); + + /** + * get a texture, loading it if required + * @param name path to the texture + * @param id receives the texture ID, always 0 in this implementation + */ + video::ITexture *getTexture(const std::string &name, u32 *id = NULL); + +private: + /** driver to get textures from */ + video::IVideoDriver *m_driver = nullptr; + /** set of texture names to delete */ + std::set m_to_delete; +}; + +/** GUIEngine specific implementation of OnDemandSoundFetcher */ +class MenuMusicFetcher: public OnDemandSoundFetcher +{ +public: + /** + * get sound file paths according to sound name + * @param name sound name + * @param dst_paths receives possible paths to sound files + * @param dst_datas receives binary sound data (not used here) + */ + void fetchSounds(const std::string &name, + std::set &dst_paths, + std::set &dst_datas); + +private: + /** set of fetched sound names */ + std::set m_fetched; +}; + +/** implementation of main menu based uppon formspecs */ +class GUIEngine { + /** grant ModApiMainMenu access to private members */ + friend class ModApiMainMenu; + friend class ModApiSound; + +public: + /** + * default constructor + * @param dev device to draw at + * @param parent parent gui element + * @param menumgr manager to add menus to + * @param smgr scene manager to add scene elements to + * @param data struct to transfer data to main game handling + */ + GUIEngine(JoystickController *joystick, + gui::IGUIElement *parent, + IMenuManager *menumgr, + MainMenuData *data, + bool &kill); + + /** default destructor */ + virtual ~GUIEngine(); + + /** + * return MainMenuScripting interface + */ + MainMenuScripting *getScriptIface() + { + return m_script; + } + + /** + * return dir of current menuscript + */ + std::string getScriptDir() + { + return m_scriptdir; + } + + /** pass async callback to scriptengine **/ + unsigned int queueAsync(const std::string &serialized_fct, + const std::string &serialized_params); + +private: + + /** find and run the main menu script */ + bool loadMainMenuScript(); + + /** run main menu loop */ + void run(); + + /** update size of topleftext element */ + void updateTopLeftTextSize(); + + /** parent gui element */ + gui::IGUIElement *m_parent = nullptr; + /** manager to add menus to */ + IMenuManager *m_menumanager = nullptr; + /** scene manager to add scene elements to */ + scene::ISceneManager *m_smgr = nullptr; + /** pointer to data beeing transfered back to main game handling */ + MainMenuData *m_data = nullptr; + /** pointer to texture source */ + ISimpleTextureSource *m_texture_source = nullptr; + /** pointer to soundmanager*/ + ISoundManager *m_sound_manager = nullptr; + + /** representation of form source to be used in mainmenu formspec */ + FormspecFormSource *m_formspecgui = nullptr; + /** formspec input receiver */ + TextDestGuiEngine *m_buttonhandler = nullptr; + /** the formspec menu */ + GUIFormSpecMenu *m_menu = nullptr; + + /** reference to kill variable managed by SIGINT handler */ + bool &m_kill; + + /** variable used to abort menu and return back to main game handling */ + bool m_startgame = false; + + /** scripting interface */ + MainMenuScripting *m_script = nullptr; + + /** script basefolder */ + std::string m_scriptdir = ""; + + /** + * draw background layer + * @param driver to use for drawing + */ + void drawBackground(video::IVideoDriver *driver); + /** + * draw overlay layer + * @param driver to use for drawing + */ + void drawOverlay(video::IVideoDriver *driver); + /** + * draw header layer + * @param driver to use for drawing + */ + void drawHeader(video::IVideoDriver *driver); + /** + * draw footer layer + * @param driver to use for drawing + */ + void drawFooter(video::IVideoDriver *driver); + + /** + * load a texture for a specified layer + * @param layer draw layer to specify texture + * @param texturepath full path of texture to load + */ + bool setTexture(texture_layer layer, std::string texturepath, + bool tile_image, unsigned int minsize); + + /** + * download a file using curl + * @param url url to download + * @param target file to store to + */ + static bool downloadFile(const std::string &url, const std::string &target); + + /** array containing pointers to current specified texture layers */ + image_definition m_textures[TEX_LAYER_MAX]; + + /** + * specify text to appear as top left string + * @param text to set + */ + void setTopleftText(const std::string &text); + + /** pointer to gui element shown at topleft corner */ + irr::gui::IGUIStaticText *m_irr_toplefttext = nullptr; + /** and text that is in it */ + EnrichedString m_toplefttext; + + /** initialize cloud subsystem */ + void cloudInit(); + /** do preprocessing for cloud subsystem */ + void cloudPreProcess(); + /** do postprocessing for cloud subsystem */ + void cloudPostProcess(); + + /** internam data required for drawing clouds */ + struct clouddata { + /** delta time since last cloud processing */ + f32 dtime; + /** absolute time of last cloud processing */ + u32 lasttime; + /** pointer to cloud class */ + Clouds *clouds = nullptr; + /** camera required for drawing clouds */ + scene::ICameraSceneNode *camera = nullptr; + }; + + /** is drawing of clouds enabled atm */ + bool m_clouds_enabled = true; + /** data used to draw clouds */ + clouddata m_cloud; + + /** start playing a sound and return handle */ + s32 playSound(SimpleSoundSpec spec, bool looped); + /** stop playing a sound started with playSound() */ + void stopSound(s32 handle); + + +}; diff --git a/src/gui/guiFormSpecMenu.cpp b/src/gui/guiFormSpecMenu.cpp new file mode 100644 index 000000000..0691bc598 --- /dev/null +++ b/src/gui/guiFormSpecMenu.cpp @@ -0,0 +1,3864 @@ +/* +Minetest +Copyright (C) 2013 celeron55, Perttu Ahola + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + + +#include +#include +#include +#include +#include +#include "guiFormSpecMenu.h" +#include "guiTable.h" +#include "constants.h" +#include "gamedef.h" +#include "keycode.h" +#include "util/strfnd.h" +#include +#include +#include +#include +#include +#include +#include +#include "client/renderingengine.h" +#include "log.h" +#include "client/tile.h" // ITextureSource +#include "hud.h" // drawItemStack +#include "filesys.h" +#include "gettime.h" +#include "gettext.h" +#include "scripting_server.h" +#include "porting.h" +#include "settings.h" +#include "client.h" +#include "fontengine.h" +#include "util/hex.h" +#include "util/numeric.h" +#include "util/string.h" // for parseColorString() +#include "irrlicht_changes/static_text.h" +#include "guiscalingfilter.h" +#include "guiEditBoxWithScrollbar.h" + +#if USE_FREETYPE && IRRLICHT_VERSION_MAJOR == 1 && IRRLICHT_VERSION_MINOR < 9 +#include "intlGUIEditBox.h" +#endif + +#define MY_CHECKPOS(a,b) \ + if (v_pos.size() != 2) { \ + errorstream<< "Invalid pos for element " << a << "specified: \"" \ + << parts[b] << "\"" << std::endl; \ + return; \ + } + +#define MY_CHECKGEOM(a,b) \ + if (v_geom.size() != 2) { \ + errorstream<< "Invalid pos for element " << a << "specified: \"" \ + << parts[b] << "\"" << std::endl; \ + return; \ + } +/* + GUIFormSpecMenu +*/ +static unsigned int font_line_height(gui::IGUIFont *font) +{ + return font->getDimension(L"Ay").Height + font->getKerningHeight(); +} + +inline u32 clamp_u8(s32 value) +{ + return (u32) MYMIN(MYMAX(value, 0), 255); +} + +GUIFormSpecMenu::GUIFormSpecMenu(JoystickController *joystick, + gui::IGUIElement *parent, s32 id, IMenuManager *menumgr, + Client *client, ISimpleTextureSource *tsrc, IFormSource *fsrc, TextDest *tdst, + bool remap_dbl_click) : + GUIModalMenu(RenderingEngine::get_gui_env(), parent, id, menumgr), + m_invmgr(client), + m_tsrc(tsrc), + m_client(client), + m_form_src(fsrc), + m_text_dst(tdst), + m_joystick(joystick), + m_remap_dbl_click(remap_dbl_click) +#ifdef __ANDROID__ + , m_JavaDialogFieldName("") +#endif +{ + current_keys_pending.key_down = false; + current_keys_pending.key_up = false; + current_keys_pending.key_enter = false; + current_keys_pending.key_escape = false; + + m_doubleclickdetect[0].time = 0; + m_doubleclickdetect[1].time = 0; + + m_doubleclickdetect[0].pos = v2s32(0, 0); + m_doubleclickdetect[1].pos = v2s32(0, 0); + + m_tooltip_show_delay = (u32)g_settings->getS32("tooltip_show_delay"); + m_tooltip_append_itemname = g_settings->getBool("tooltip_append_itemname"); +} + +GUIFormSpecMenu::~GUIFormSpecMenu() +{ + removeChildren(); + + for (auto &table_it : m_tables) { + table_it.second->drop(); + } + + delete m_selected_item; + delete m_form_src; + delete m_text_dst; +} + +void GUIFormSpecMenu::removeChildren() +{ + const core::list &children = getChildren(); + + while(!children.empty()) { + (*children.getLast())->remove(); + } + + if(m_tooltip_element) { + m_tooltip_element->remove(); + m_tooltip_element->drop(); + m_tooltip_element = NULL; + } + +} + +void GUIFormSpecMenu::setInitialFocus() +{ + // Set initial focus according to following order of precedence: + // 1. first empty editbox + // 2. first editbox + // 3. first table + // 4. last button + // 5. first focusable (not statictext, not tabheader) + // 6. first child element + + core::list children = getChildren(); + + // in case "children" contains any NULL elements, remove them + for (core::list::Iterator it = children.begin(); + it != children.end();) { + if (*it) + ++it; + else + it = children.erase(it); + } + + // 1. first empty editbox + for (gui::IGUIElement *it : children) { + if (it->getType() == gui::EGUIET_EDIT_BOX + && it->getText()[0] == 0) { + Environment->setFocus(it); + return; + } + } + + // 2. first editbox + for (gui::IGUIElement *it : children) { + if (it->getType() == gui::EGUIET_EDIT_BOX) { + Environment->setFocus(it); + return; + } + } + + // 3. first table + for (gui::IGUIElement *it : children) { + if (it->getTypeName() == std::string("GUITable")) { + Environment->setFocus(it); + return; + } + } + + // 4. last button + for (core::list::Iterator it = children.getLast(); + it != children.end(); --it) { + if ((*it)->getType() == gui::EGUIET_BUTTON) { + Environment->setFocus(*it); + return; + } + } + + // 5. first focusable (not statictext, not tabheader) + for (gui::IGUIElement *it : children) { + if (it->getType() != gui::EGUIET_STATIC_TEXT && + it->getType() != gui::EGUIET_TAB_CONTROL) { + Environment->setFocus(it); + return; + } + } + + // 6. first child element + if (children.empty()) + Environment->setFocus(this); + else + Environment->setFocus(*(children.begin())); +} + +GUITable* GUIFormSpecMenu::getTable(const std::string &tablename) +{ + for (auto &table : m_tables) { + if (tablename == table.first.fname) + return table.second; + } + return 0; +} + +std::vector* GUIFormSpecMenu::getDropDownValues(const std::string &name) +{ + for (auto &dropdown : m_dropdowns) { + if (name == dropdown.first.fname) + return &dropdown.second; + } + return NULL; +} + +void GUIFormSpecMenu::parseSize(parserData* data, const std::string &element) +{ + std::vector parts = split(element,','); + + if (((parts.size() == 2) || parts.size() == 3) || + ((parts.size() > 3) && (m_formspec_version > FORMSPEC_API_VERSION))) + { + if (parts[1].find(';') != std::string::npos) + parts[1] = parts[1].substr(0,parts[1].find(';')); + + data->invsize.X = MYMAX(0, stof(parts[0])); + data->invsize.Y = MYMAX(0, stof(parts[1])); + + lockSize(false); + if (parts.size() == 3) { + if (parts[2] == "true") { + lockSize(true,v2u32(800,600)); + } + } + + data->explicit_size = true; + return; + } + errorstream<< "Invalid size element (" << parts.size() << "): '" << element << "'" << std::endl; +} + +void GUIFormSpecMenu::parseContainer(parserData* data, const std::string &element) +{ + std::vector parts = split(element, ','); + + if (parts.size() >= 2) { + if (parts[1].find(';') != std::string::npos) + parts[1] = parts[1].substr(0, parts[1].find(';')); + + container_stack.push(pos_offset); + pos_offset.X += MYMAX(0, stof(parts[0])); + pos_offset.Y += MYMAX(0, stof(parts[1])); + return; + } + errorstream<< "Invalid container start element (" << parts.size() << "): '" << element << "'" << std::endl; +} + +void GUIFormSpecMenu::parseContainerEnd(parserData* data) +{ + if (container_stack.empty()) { + errorstream<< "Invalid container end element, no matching container start element" << std::endl; + } else { + pos_offset = container_stack.top(); + container_stack.pop(); + } +} + +void GUIFormSpecMenu::parseList(parserData* data, const std::string &element) +{ + if (m_client == 0) { + warningstream<<"invalid use of 'list' with m_client==0"< parts = split(element,';'); + + if (((parts.size() == 4) || (parts.size() == 5)) || + ((parts.size() > 5) && (m_formspec_version > FORMSPEC_API_VERSION))) + { + std::string location = parts[0]; + std::string listname = parts[1]; + std::vector v_pos = split(parts[2],','); + std::vector v_geom = split(parts[3],','); + std::string startindex; + if (parts.size() == 5) + startindex = parts[4]; + + MY_CHECKPOS("list",2); + MY_CHECKGEOM("list",3); + + InventoryLocation loc; + + if(location == "context" || location == "current_name") + loc = m_current_inventory_location; + else + loc.deSerialize(location); + + v2s32 pos = padding + AbsoluteRect.UpperLeftCorner + pos_offset * spacing; + pos.X += stof(v_pos[0]) * (float)spacing.X; + pos.Y += stof(v_pos[1]) * (float)spacing.Y; + + v2s32 geom; + geom.X = stoi(v_geom[0]); + geom.Y = stoi(v_geom[1]); + + s32 start_i = 0; + if (!startindex.empty()) + start_i = stoi(startindex); + + if (geom.X < 0 || geom.Y < 0 || start_i < 0) { + errorstream<< "Invalid list element: '" << element << "'" << std::endl; + return; + } + + if(!data->explicit_size) + warningstream<<"invalid use of list without a size[] element"< parts = split(element, ';'); + + if (parts.size() == 2) { + std::string location = parts[0]; + std::string listname = parts[1]; + + InventoryLocation loc; + + if (location == "context" || location == "current_name") + loc = m_current_inventory_location; + else + loc.deSerialize(location); + + m_inventory_rings.emplace_back(loc, listname); + return; + } + + if (element.empty() && m_inventorylists.size() > 1) { + size_t siz = m_inventorylists.size(); + // insert the last two inv list elements into the list ring + const ListDrawSpec &spa = m_inventorylists[siz - 2]; + const ListDrawSpec &spb = m_inventorylists[siz - 1]; + m_inventory_rings.emplace_back(spa.inventoryloc, spa.listname); + m_inventory_rings.emplace_back(spb.inventoryloc, spb.listname); + return; + } + + errorstream<< "Invalid list ring element(" << parts.size() << ", " + << m_inventorylists.size() << "): '" << element << "'" << std::endl; +} + +void GUIFormSpecMenu::parseCheckbox(parserData* data, const std::string &element) +{ + std::vector parts = split(element,';'); + + if (((parts.size() >= 3) && (parts.size() <= 4)) || + ((parts.size() > 4) && (m_formspec_version > FORMSPEC_API_VERSION))) + { + std::vector v_pos = split(parts[0],','); + std::string name = parts[1]; + std::string label = parts[2]; + std::string selected; + + if (parts.size() >= 4) + selected = parts[3]; + + MY_CHECKPOS("checkbox",0); + + v2s32 pos = padding + pos_offset * spacing; + pos.X += stof(v_pos[0]) * (float) spacing.X; + pos.Y += stof(v_pos[1]) * (float) spacing.Y; + + bool fselected = false; + + if (selected == "true") + fselected = true; + + std::wstring wlabel = translate_string(utf8_to_wide(unescape_string(label))); + + core::rect rect = core::rect( + pos.X, pos.Y + ((imgsize.Y/2) - m_btn_height), + pos.X + m_font->getDimension(wlabel.c_str()).Width + 25, // text size + size of checkbox + pos.Y + ((imgsize.Y/2) + m_btn_height)); + + FieldSpec spec( + name, + wlabel, //Needed for displaying text on MSVC + wlabel, + 258+m_fields.size() + ); + + spec.ftype = f_CheckBox; + + gui::IGUICheckBox* e = Environment->addCheckBox(fselected, rect, this, + spec.fid, spec.flabel.c_str()); + + if (spec.fname == data->focused_fieldname) { + Environment->setFocus(e); + } + + m_checkboxes.emplace_back(spec,e); + m_fields.push_back(spec); + return; + } + errorstream<< "Invalid checkbox element(" << parts.size() << "): '" << element << "'" << std::endl; +} + +void GUIFormSpecMenu::parseScrollBar(parserData* data, const std::string &element) +{ + std::vector parts = split(element,';'); + + if (parts.size() >= 5) { + std::vector v_pos = split(parts[0],','); + std::vector v_dim = split(parts[1],','); + std::string name = parts[3]; + std::string value = parts[4]; + + MY_CHECKPOS("scrollbar",0); + + v2s32 pos = padding + pos_offset * spacing; + pos.X += stof(v_pos[0]) * (float) spacing.X; + pos.Y += stof(v_pos[1]) * (float) spacing.Y; + + if (v_dim.size() != 2) { + errorstream<< "Invalid size for element " << "scrollbar" + << "specified: \"" << parts[1] << "\"" << std::endl; + return; + } + + v2s32 dim; + dim.X = stof(v_dim[0]) * (float) spacing.X; + dim.Y = stof(v_dim[1]) * (float) spacing.Y; + + core::rect rect = + core::rect(pos.X, pos.Y, pos.X + dim.X, pos.Y + dim.Y); + + FieldSpec spec( + name, + L"", + L"", + 258+m_fields.size() + ); + + bool is_horizontal = true; + + if (parts[2] == "vertical") + is_horizontal = false; + + spec.ftype = f_ScrollBar; + spec.send = true; + gui::IGUIScrollBar* e = + Environment->addScrollBar(is_horizontal,rect,this,spec.fid); + + e->setMax(1000); + e->setMin(0); + e->setPos(stoi(parts[4])); + e->setSmallStep(10); + e->setLargeStep(100); + + m_scrollbars.emplace_back(spec,e); + m_fields.push_back(spec); + return; + } + errorstream<< "Invalid scrollbar element(" << parts.size() << "): '" << element << "'" << std::endl; +} + +void GUIFormSpecMenu::parseImage(parserData* data, const std::string &element) +{ + std::vector parts = split(element,';'); + + if ((parts.size() == 3) || + ((parts.size() > 3) && (m_formspec_version > FORMSPEC_API_VERSION))) + { + std::vector v_pos = split(parts[0],','); + std::vector v_geom = split(parts[1],','); + std::string name = unescape_string(parts[2]); + + MY_CHECKPOS("image", 0); + MY_CHECKGEOM("image", 1); + + v2s32 pos = padding + AbsoluteRect.UpperLeftCorner + pos_offset * spacing; + pos.X += stof(v_pos[0]) * (float) spacing.X; + pos.Y += stof(v_pos[1]) * (float) spacing.Y; + + v2s32 geom; + geom.X = stof(v_geom[0]) * (float)imgsize.X; + geom.Y = stof(v_geom[1]) * (float)imgsize.Y; + + if (!data->explicit_size) + warningstream<<"invalid use of image without a size[] element"< v_pos = split(parts[0],','); + std::string name = unescape_string(parts[1]); + + MY_CHECKPOS("image", 0); + + v2s32 pos = padding + AbsoluteRect.UpperLeftCorner + pos_offset * spacing; + pos.X += stof(v_pos[0]) * (float) spacing.X; + pos.Y += stof(v_pos[1]) * (float) spacing.Y; + + if (!data->explicit_size) + warningstream<<"invalid use of image without a size[] element"< parts = split(element,';'); + + if ((parts.size() == 3) || + ((parts.size() > 3) && (m_formspec_version > FORMSPEC_API_VERSION))) + { + std::vector v_pos = split(parts[0],','); + std::vector v_geom = split(parts[1],','); + std::string name = parts[2]; + + MY_CHECKPOS("itemimage",0); + MY_CHECKGEOM("itemimage",1); + + v2s32 pos = padding + AbsoluteRect.UpperLeftCorner + pos_offset * spacing; + pos.X += stof(v_pos[0]) * (float) spacing.X; + pos.Y += stof(v_pos[1]) * (float) spacing.Y; + + v2s32 geom; + geom.X = stof(v_geom[0]) * (float)imgsize.X; + geom.Y = stof(v_geom[1]) * (float)imgsize.Y; + + if(!data->explicit_size) + warningstream<<"invalid use of item_image without a size[] element"< parts = split(element,';'); + + if ((parts.size() == 4) || + ((parts.size() > 4) && (m_formspec_version > FORMSPEC_API_VERSION))) + { + std::vector v_pos = split(parts[0],','); + std::vector v_geom = split(parts[1],','); + std::string name = parts[2]; + std::string label = parts[3]; + + MY_CHECKPOS("button",0); + MY_CHECKGEOM("button",1); + + v2s32 pos = padding + pos_offset * spacing; + pos.X += stof(v_pos[0]) * (float)spacing.X; + pos.Y += stof(v_pos[1]) * (float)spacing.Y; + + v2s32 geom; + geom.X = (stof(v_geom[0]) * (float)spacing.X)-(spacing.X-imgsize.X); + pos.Y += (stof(v_geom[1]) * (float)imgsize.Y)/2; + + core::rect rect = + core::rect(pos.X, pos.Y - m_btn_height, + pos.X + geom.X, pos.Y + m_btn_height); + + if(!data->explicit_size) + warningstream<<"invalid use of button without a size[] element"<addButton(rect, this, spec.fid, + spec.flabel.c_str()); + + if (spec.fname == data->focused_fieldname) { + Environment->setFocus(e); + } + + m_fields.push_back(spec); + return; + } + errorstream<< "Invalid button element(" << parts.size() << "): '" << element << "'" << std::endl; +} + +void GUIFormSpecMenu::parseBackground(parserData* data, const std::string &element) +{ + std::vector parts = split(element,';'); + + if (((parts.size() == 3) || (parts.size() == 4)) || + ((parts.size() > 4) && (m_formspec_version > FORMSPEC_API_VERSION))) + { + std::vector v_pos = split(parts[0],','); + std::vector v_geom = split(parts[1],','); + std::string name = unescape_string(parts[2]); + + MY_CHECKPOS("background",0); + MY_CHECKGEOM("background",1); + + v2s32 pos = padding + AbsoluteRect.UpperLeftCorner + pos_offset * spacing; + pos.X += stof(v_pos[0]) * (float)spacing.X - ((float)spacing.X - (float)imgsize.X)/2; + pos.Y += stof(v_pos[1]) * (float)spacing.Y - ((float)spacing.Y - (float)imgsize.Y)/2; + + v2s32 geom; + geom.X = stof(v_geom[0]) * (float)spacing.X; + geom.Y = stof(v_geom[1]) * (float)spacing.Y; + + if (!data->explicit_size) + warningstream<<"invalid use of background without a size[] element"< parts = split(element,';'); + + data->table_options.clear(); + for (const std::string &part : parts) { + // Parse table option + std::string opt = unescape_string(part); + data->table_options.push_back(GUITable::splitOption(opt)); + } +} + +void GUIFormSpecMenu::parseTableColumns(parserData* data, const std::string &element) +{ + std::vector parts = split(element,';'); + + data->table_columns.clear(); + for (const std::string &part : parts) { + std::vector col_parts = split(part,','); + GUITable::TableColumn column; + // Parse column type + if (!col_parts.empty()) + column.type = col_parts[0]; + // Parse column options + for (size_t j = 1; j < col_parts.size(); ++j) { + std::string opt = unescape_string(col_parts[j]); + column.options.push_back(GUITable::splitOption(opt)); + } + data->table_columns.push_back(column); + } +} + +void GUIFormSpecMenu::parseTable(parserData* data, const std::string &element) +{ + std::vector parts = split(element,';'); + + if (((parts.size() == 4) || (parts.size() == 5)) || + ((parts.size() > 5) && (m_formspec_version > FORMSPEC_API_VERSION))) + { + std::vector v_pos = split(parts[0],','); + std::vector v_geom = split(parts[1],','); + std::string name = parts[2]; + std::vector items = split(parts[3],','); + std::string str_initial_selection; + std::string str_transparent = "false"; + + if (parts.size() >= 5) + str_initial_selection = parts[4]; + + MY_CHECKPOS("table",0); + MY_CHECKGEOM("table",1); + + v2s32 pos = padding + pos_offset * spacing; + pos.X += stof(v_pos[0]) * (float)spacing.X; + pos.Y += stof(v_pos[1]) * (float)spacing.Y; + + v2s32 geom; + geom.X = stof(v_geom[0]) * (float)spacing.X; + geom.Y = stof(v_geom[1]) * (float)spacing.Y; + + core::rect rect = core::rect(pos.X, pos.Y, pos.X+geom.X, pos.Y+geom.Y); + + FieldSpec spec( + name, + L"", + L"", + 258+m_fields.size() + ); + + spec.ftype = f_Table; + + for (std::string &item : items) { + item = wide_to_utf8(unescape_translate(utf8_to_wide(unescape_string(item)))); + } + + //now really show table + GUITable *e = new GUITable(Environment, this, spec.fid, rect, + m_tsrc); + + if (spec.fname == data->focused_fieldname) { + Environment->setFocus(e); + } + + e->setTable(data->table_options, data->table_columns, items); + + if (data->table_dyndata.find(name) != data->table_dyndata.end()) { + e->setDynamicData(data->table_dyndata[name]); + } + + if (!str_initial_selection.empty() && str_initial_selection != "0") + e->setSelected(stoi(str_initial_selection)); + + m_tables.emplace_back(spec, e); + m_fields.push_back(spec); + return; + } + errorstream<< "Invalid table element(" << parts.size() << "): '" << element << "'" << std::endl; +} + +void GUIFormSpecMenu::parseTextList(parserData* data, const std::string &element) +{ + std::vector parts = split(element,';'); + + if (((parts.size() == 4) || (parts.size() == 5) || (parts.size() == 6)) || + ((parts.size() > 6) && (m_formspec_version > FORMSPEC_API_VERSION))) + { + std::vector v_pos = split(parts[0],','); + std::vector v_geom = split(parts[1],','); + std::string name = parts[2]; + std::vector items = split(parts[3],','); + std::string str_initial_selection; + std::string str_transparent = "false"; + + if (parts.size() >= 5) + str_initial_selection = parts[4]; + + if (parts.size() >= 6) + str_transparent = parts[5]; + + MY_CHECKPOS("textlist",0); + MY_CHECKGEOM("textlist",1); + + v2s32 pos = padding + pos_offset * spacing; + pos.X += stof(v_pos[0]) * (float)spacing.X; + pos.Y += stof(v_pos[1]) * (float)spacing.Y; + + v2s32 geom; + geom.X = stof(v_geom[0]) * (float)spacing.X; + geom.Y = stof(v_geom[1]) * (float)spacing.Y; + + + core::rect rect = core::rect(pos.X, pos.Y, pos.X+geom.X, pos.Y+geom.Y); + + FieldSpec spec( + name, + L"", + L"", + 258+m_fields.size() + ); + + spec.ftype = f_Table; + + for (std::string &item : items) { + item = wide_to_utf8(unescape_translate(utf8_to_wide(unescape_string(item)))); + } + + //now really show list + GUITable *e = new GUITable(Environment, this, spec.fid, rect, + m_tsrc); + + if (spec.fname == data->focused_fieldname) { + Environment->setFocus(e); + } + + e->setTextList(items, is_yes(str_transparent)); + + if (data->table_dyndata.find(name) != data->table_dyndata.end()) { + e->setDynamicData(data->table_dyndata[name]); + } + + if (!str_initial_selection.empty() && str_initial_selection != "0") + e->setSelected(stoi(str_initial_selection)); + + m_tables.emplace_back(spec, e); + m_fields.push_back(spec); + return; + } + errorstream<< "Invalid textlist element(" << parts.size() << "): '" << element << "'" << std::endl; +} + + +void GUIFormSpecMenu::parseDropDown(parserData* data, const std::string &element) +{ + std::vector parts = split(element,';'); + + if ((parts.size() == 5) || + ((parts.size() > 5) && (m_formspec_version > FORMSPEC_API_VERSION))) + { + std::vector v_pos = split(parts[0],','); + std::string name = parts[2]; + std::vector items = split(parts[3],','); + std::string str_initial_selection; + str_initial_selection = parts[4]; + + MY_CHECKPOS("dropdown",0); + + v2s32 pos = padding + pos_offset * spacing; + pos.X += stof(v_pos[0]) * (float)spacing.X; + pos.Y += stof(v_pos[1]) * (float)spacing.Y; + + s32 width = stof(parts[1]) * (float)spacing.Y; + + core::rect rect = core::rect(pos.X, pos.Y, + pos.X + width, pos.Y + (m_btn_height * 2)); + + FieldSpec spec( + name, + L"", + L"", + 258+m_fields.size() + ); + + spec.ftype = f_DropDown; + spec.send = true; + + //now really show list + gui::IGUIComboBox *e = Environment->addComboBox(rect, this,spec.fid); + + if (spec.fname == data->focused_fieldname) { + Environment->setFocus(e); + } + + for (const std::string &item : items) { + e->addItem(unescape_translate(unescape_string( + utf8_to_wide(item))).c_str()); + } + + if (!str_initial_selection.empty()) + e->setSelected(stoi(str_initial_selection)-1); + + m_fields.push_back(spec); + + m_dropdowns.emplace_back(spec, std::vector()); + std::vector &values = m_dropdowns.back().second; + for (const std::string &item : items) { + values.push_back(unescape_string(item)); + } + + return; + } + errorstream << "Invalid dropdown element(" << parts.size() << "): '" + << element << "'" << std::endl; +} + +void GUIFormSpecMenu::parseFieldCloseOnEnter(parserData *data, const std::string &element) +{ + std::vector parts = split(element,';'); + if (parts.size() == 2 || + (parts.size() > 2 && m_formspec_version > FORMSPEC_API_VERSION)) { + field_close_on_enter[parts[0]] = is_yes(parts[1]); + } +} + +void GUIFormSpecMenu::parsePwdField(parserData* data, const std::string &element) +{ + std::vector parts = split(element,';'); + + if ((parts.size() == 4) || (parts.size() == 5) || + ((parts.size() > 5) && (m_formspec_version > FORMSPEC_API_VERSION))) + { + std::vector v_pos = split(parts[0],','); + std::vector v_geom = split(parts[1],','); + std::string name = parts[2]; + std::string label = parts[3]; + + MY_CHECKPOS("pwdfield",0); + MY_CHECKGEOM("pwdfield",1); + + v2s32 pos = pos_offset * spacing; + pos.X += stof(v_pos[0]) * (float)spacing.X; + pos.Y += stof(v_pos[1]) * (float)spacing.Y; + + v2s32 geom; + geom.X = (stof(v_geom[0]) * (float)spacing.X)-(spacing.X-imgsize.X); + + pos.Y += (stof(v_geom[1]) * (float)imgsize.Y)/2; + pos.Y -= m_btn_height; + geom.Y = m_btn_height*2; + + core::rect rect = core::rect(pos.X, pos.Y, pos.X+geom.X, pos.Y+geom.Y); + + std::wstring wlabel = translate_string(utf8_to_wide(unescape_string(label))); + + FieldSpec spec( + name, + wlabel, + L"", + 258+m_fields.size() + ); + + spec.send = true; + gui::IGUIEditBox * e = Environment->addEditBox(0, rect, true, this, spec.fid); + + if (spec.fname == data->focused_fieldname) { + Environment->setFocus(e); + } + + if (label.length() >= 1) + { + int font_height = g_fontengine->getTextHeight(); + rect.UpperLeftCorner.Y -= font_height; + rect.LowerRightCorner.Y = rect.UpperLeftCorner.Y + font_height; + addStaticText(Environment, spec.flabel.c_str(), rect, false, true, this, 0); + } + + e->setPasswordBox(true,L'*'); + + irr::SEvent evt; + evt.EventType = EET_KEY_INPUT_EVENT; + evt.KeyInput.Key = KEY_END; + evt.KeyInput.Char = 0; + evt.KeyInput.Control = false; + evt.KeyInput.Shift = false; + evt.KeyInput.PressedDown = true; + e->OnEvent(evt); + + if (parts.size() >= 5) { + // TODO: remove after 2016-11-03 + warningstream << "pwdfield: use field_close_on_enter[name, enabled]" << + " instead of the 5th param" << std::endl; + field_close_on_enter[name] = is_yes(parts[4]); + } + + m_fields.push_back(spec); + return; + } + errorstream<< "Invalid pwdfield element(" << parts.size() << "): '" << element << "'" << std::endl; +} + +void GUIFormSpecMenu::parseSimpleField(parserData* data, + std::vector &parts) +{ + std::string name = parts[0]; + std::string label = parts[1]; + std::string default_val = parts[2]; + + core::rect rect; + + if(data->explicit_size) + warningstream<<"invalid use of unpositioned \"field\" in inventory"<(size.X / 2 - 150, pos.Y, + (size.X / 2 - 150) + 300, pos.Y + (m_btn_height*2)); + + + if(m_form_src) + default_val = m_form_src->resolveText(default_val); + + + std::wstring wlabel = translate_string(utf8_to_wide(unescape_string(label))); + + FieldSpec spec( + name, + wlabel, + utf8_to_wide(unescape_string(default_val)), + 258+m_fields.size() + ); + + if (name.empty()) { + // spec field id to 0, this stops submit searching for a value that isn't there + addStaticText(Environment, spec.flabel.c_str(), rect, false, true, this, spec.fid); + } else { + spec.send = true; + gui::IGUIElement *e; +#if USE_FREETYPE && IRRLICHT_VERSION_MAJOR == 1 && IRRLICHT_VERSION_MINOR < 9 + if (g_settings->getBool("freetype")) { + e = (gui::IGUIElement *) new gui::intlGUIEditBox(spec.fdefault.c_str(), + true, Environment, this, spec.fid, rect); + e->drop(); + } else { +#else + { +#endif + e = Environment->addEditBox(spec.fdefault.c_str(), rect, true, this, spec.fid); + } + if (spec.fname == data->focused_fieldname) { + Environment->setFocus(e); + } + + irr::SEvent evt; + evt.EventType = EET_KEY_INPUT_EVENT; + evt.KeyInput.Key = KEY_END; + evt.KeyInput.Char = 0; + evt.KeyInput.Control = 0; + evt.KeyInput.Shift = 0; + evt.KeyInput.PressedDown = true; + e->OnEvent(evt); + + if (label.length() >= 1) + { + int font_height = g_fontengine->getTextHeight(); + rect.UpperLeftCorner.Y -= font_height; + rect.LowerRightCorner.Y = rect.UpperLeftCorner.Y + font_height; + addStaticText(Environment, spec.flabel.c_str(), rect, false, true, this, 0); + } + } + + if (parts.size() >= 4) { + // TODO: remove after 2016-11-03 + warningstream << "field/simple: use field_close_on_enter[name, enabled]" << + " instead of the 4th param" << std::endl; + field_close_on_enter[name] = is_yes(parts[3]); + } + + m_fields.push_back(spec); +} + +void GUIFormSpecMenu::parseTextArea(parserData* data, std::vector& parts, + const std::string &type) +{ + + std::vector v_pos = split(parts[0],','); + std::vector v_geom = split(parts[1],','); + std::string name = parts[2]; + std::string label = parts[3]; + std::string default_val = parts[4]; + + MY_CHECKPOS(type,0); + MY_CHECKGEOM(type,1); + + v2s32 pos = pos_offset * spacing; + pos.X += stof(v_pos[0]) * (float) spacing.X; + pos.Y += stof(v_pos[1]) * (float) spacing.Y; + + v2s32 geom; + + geom.X = (stof(v_geom[0]) * (float)spacing.X)-(spacing.X-imgsize.X); + + if (type == "textarea") + { + geom.Y = (stof(v_geom[1]) * (float)imgsize.Y) - (spacing.Y-imgsize.Y); + pos.Y += m_btn_height; + } + else + { + pos.Y += (stof(v_geom[1]) * (float)imgsize.Y)/2; + pos.Y -= m_btn_height; + geom.Y = m_btn_height*2; + } + + core::rect rect = core::rect(pos.X, pos.Y, pos.X+geom.X, pos.Y+geom.Y); + + if(!data->explicit_size) + warningstream<<"invalid use of positioned "<resolveText(default_val); + + + std::wstring wlabel = translate_string(utf8_to_wide(unescape_string(label))); + + FieldSpec spec( + name, + wlabel, + utf8_to_wide(unescape_string(default_val)), + 258+m_fields.size() + ); + + bool is_editable = !name.empty(); + + if (is_editable) + spec.send = true; + + gui::IGUIEditBox *e = nullptr; + const wchar_t *text = spec.fdefault.empty() ? + wlabel.c_str() : spec.fdefault.c_str(); + +#if USE_FREETYPE && IRRLICHT_VERSION_MAJOR == 1 && IRRLICHT_VERSION_MINOR < 9 + if (g_settings->getBool("freetype")) { + e = (gui::IGUIEditBox *) new gui::intlGUIEditBox(text, + true, Environment, this, spec.fid, rect, is_editable, true); + e->drop(); + } else { +#else + { +#endif + e = new GUIEditBoxWithScrollBar(text, true, + Environment, this, spec.fid, rect, is_editable, true); + } + + if (is_editable && spec.fname == data->focused_fieldname) + Environment->setFocus(e); + + if (e) { + if (type == "textarea") + { + e->setMultiLine(true); + e->setWordWrap(true); + e->setTextAlignment(gui::EGUIA_UPPERLEFT, gui::EGUIA_UPPERLEFT); + } else { + irr::SEvent evt; + evt.EventType = EET_KEY_INPUT_EVENT; + evt.KeyInput.Key = KEY_END; + evt.KeyInput.Char = 0; + evt.KeyInput.Control = 0; + evt.KeyInput.Shift = 0; + evt.KeyInput.PressedDown = true; + e->OnEvent(evt); + } + } + + if (is_editable && !label.empty()) { + int font_height = g_fontengine->getTextHeight(); + rect.UpperLeftCorner.Y -= font_height; + rect.LowerRightCorner.Y = rect.UpperLeftCorner.Y + font_height; + addStaticText(Environment, spec.flabel.c_str(), rect, false, true, this, 0); + } + + if (parts.size() >= 6) { + // TODO: remove after 2016-11-03 + warningstream << "field/textarea: use field_close_on_enter[name, enabled]" << + " instead of the 6th param" << std::endl; + field_close_on_enter[name] = is_yes(parts[5]); + } + + m_fields.push_back(spec); +} + +void GUIFormSpecMenu::parseField(parserData* data, const std::string &element, + const std::string &type) +{ + std::vector parts = split(element,';'); + + if (parts.size() == 3 || parts.size() == 4) { + parseSimpleField(data,parts); + return; + } + + if ((parts.size() == 5) || (parts.size() == 6) || + ((parts.size() > 6) && (m_formspec_version > FORMSPEC_API_VERSION))) + { + parseTextArea(data,parts,type); + return; + } + errorstream<< "Invalid field element(" << parts.size() << "): '" << element << "'" << std::endl; +} + +void GUIFormSpecMenu::parseLabel(parserData* data, const std::string &element) +{ + std::vector parts = split(element,';'); + + if ((parts.size() == 2) || + ((parts.size() > 2) && (m_formspec_version > FORMSPEC_API_VERSION))) + { + std::vector v_pos = split(parts[0],','); + std::string text = parts[1]; + + MY_CHECKPOS("label",0); + + v2s32 pos = padding + pos_offset * spacing; + pos.X += stof(v_pos[0]) * (float)spacing.X; + pos.Y += (stof(v_pos[1]) + 7.0/30.0) * (float)spacing.Y; + + if(!data->explicit_size) + warningstream<<"invalid use of label without a size[] element"< lines = split(text, '\n'); + + for (unsigned int i = 0; i != lines.size(); i++) { + // Lines are spaced at the nominal distance of + // 2/5 inventory slot, even if the font doesn't + // quite match that. This provides consistent + // form layout, at the expense of sometimes + // having sub-optimal spacing for the font. + // We multiply by 2 and then divide by 5, rather + // than multiply by 0.4, to get exact results + // in the integer cases: 0.4 is not exactly + // representable in binary floating point. + s32 posy = pos.Y + ((float)i) * spacing.Y * 2.0 / 5.0; + std::wstring wlabel = utf8_to_wide(unescape_string(lines[i])); + core::rect rect = core::rect( + pos.X, posy - m_btn_height, + pos.X + m_font->getDimension(wlabel.c_str()).Width, + posy + m_btn_height); + FieldSpec spec( + "", + wlabel, + L"", + 258+m_fields.size() + ); + gui::IGUIStaticText *e = + addStaticText(Environment, spec.flabel.c_str(), + rect, false, false, this, spec.fid); + e->setTextAlignment(gui::EGUIA_UPPERLEFT, + gui::EGUIA_CENTER); + m_fields.push_back(spec); + } + + return; + } + errorstream<< "Invalid label element(" << parts.size() << "): '" << element << "'" << std::endl; +} + +void GUIFormSpecMenu::parseVertLabel(parserData* data, const std::string &element) +{ + std::vector parts = split(element,';'); + + if ((parts.size() == 2) || + ((parts.size() > 2) && (m_formspec_version > FORMSPEC_API_VERSION))) + { + std::vector v_pos = split(parts[0],','); + std::wstring text = unescape_translate( + unescape_string(utf8_to_wide(parts[1]))); + + MY_CHECKPOS("vertlabel",1); + + v2s32 pos = padding + pos_offset * spacing; + pos.X += stof(v_pos[0]) * (float)spacing.X; + pos.Y += stof(v_pos[1]) * (float)spacing.Y; + + core::rect rect = core::rect( + pos.X, pos.Y+((imgsize.Y/2)- m_btn_height), + pos.X+15, pos.Y + + font_line_height(m_font) + * (text.length()+1) + +((imgsize.Y/2)- m_btn_height)); + //actually text.length() would be correct but adding +1 avoids to break all mods + + if(!data->explicit_size) + warningstream<<"invalid use of label without a size[] element"<setTextAlignment(gui::EGUIA_CENTER, gui::EGUIA_CENTER); + m_fields.push_back(spec); + return; + } + errorstream<< "Invalid vertlabel element(" << parts.size() << "): '" << element << "'" << std::endl; +} + +void GUIFormSpecMenu::parseImageButton(parserData* data, const std::string &element, + const std::string &type) +{ + std::vector parts = split(element,';'); + + if ((((parts.size() >= 5) && (parts.size() <= 8)) && (parts.size() != 6)) || + ((parts.size() > 8) && (m_formspec_version > FORMSPEC_API_VERSION))) + { + std::vector v_pos = split(parts[0],','); + std::vector v_geom = split(parts[1],','); + std::string image_name = parts[2]; + std::string name = parts[3]; + std::string label = parts[4]; + + MY_CHECKPOS("imagebutton",0); + MY_CHECKGEOM("imagebutton",1); + + v2s32 pos = padding + pos_offset * spacing; + pos.X += stof(v_pos[0]) * (float)spacing.X; + pos.Y += stof(v_pos[1]) * (float)spacing.Y; + v2s32 geom; + geom.X = (stof(v_geom[0]) * (float)spacing.X)-(spacing.X-imgsize.X); + geom.Y = (stof(v_geom[1]) * (float)spacing.Y)-(spacing.Y-imgsize.Y); + + bool noclip = false; + bool drawborder = true; + std::string pressed_image_name; + + if (parts.size() >= 7) { + if (parts[5] == "true") + noclip = true; + if (parts[6] == "false") + drawborder = false; + } + + if (parts.size() >= 8) { + pressed_image_name = parts[7]; + } + + core::rect rect = core::rect(pos.X, pos.Y, pos.X+geom.X, pos.Y+geom.Y); + + if(!data->explicit_size) + warningstream<<"invalid use of image_button without a size[] element"<getTexture(image_name); + if (!pressed_image_name.empty()) + pressed_texture = m_tsrc->getTexture(pressed_image_name); + else + pressed_texture = texture; + + gui::IGUIButton *e = Environment->addButton(rect, this, spec.fid, spec.flabel.c_str()); + + if (spec.fname == data->focused_fieldname) { + Environment->setFocus(e); + } + + e->setUseAlphaChannel(true); + e->setImage(guiScalingImageButton( + Environment->getVideoDriver(), texture, geom.X, geom.Y)); + e->setPressedImage(guiScalingImageButton( + Environment->getVideoDriver(), pressed_texture, geom.X, geom.Y)); + e->setScaleImage(true); + e->setNotClipped(noclip); + e->setDrawBorder(drawborder); + + m_fields.push_back(spec); + return; + } + + errorstream<< "Invalid imagebutton element(" << parts.size() << "): '" << element << "'" << std::endl; +} + +void GUIFormSpecMenu::parseTabHeader(parserData* data, const std::string &element) +{ + std::vector parts = split(element,';'); + + if (((parts.size() == 4) || (parts.size() == 6)) || + ((parts.size() > 6) && (m_formspec_version > FORMSPEC_API_VERSION))) + { + std::vector v_pos = split(parts[0],','); + std::string name = parts[1]; + std::vector buttons = split(parts[2],','); + std::string str_index = parts[3]; + bool show_background = true; + bool show_border = true; + int tab_index = stoi(str_index) -1; + + MY_CHECKPOS("tabheader",0); + + if (parts.size() == 6) { + if (parts[4] == "true") + show_background = false; + if (parts[5] == "false") + show_border = false; + } + + FieldSpec spec( + name, + L"", + L"", + 258+m_fields.size() + ); + + spec.ftype = f_TabHeader; + + v2s32 pos = pos_offset * spacing; + pos.X += stof(v_pos[0]) * (float)spacing.X; + pos.Y += stof(v_pos[1]) * (float)spacing.Y - m_btn_height * 2; + v2s32 geom; + geom.X = DesiredRect.getWidth(); + geom.Y = m_btn_height*2; + + core::rect rect = core::rect(pos.X, pos.Y, pos.X+geom.X, + pos.Y+geom.Y); + + gui::IGUITabControl *e = Environment->addTabControl(rect, this, + show_background, show_border, spec.fid); + e->setAlignment(irr::gui::EGUIA_UPPERLEFT, irr::gui::EGUIA_UPPERLEFT, + irr::gui::EGUIA_UPPERLEFT, irr::gui::EGUIA_LOWERRIGHT); + e->setTabHeight(m_btn_height*2); + + if (spec.fname == data->focused_fieldname) { + Environment->setFocus(e); + } + + e->setNotClipped(true); + + for (const std::string &button : buttons) { + e->addTab(unescape_translate(unescape_string( + utf8_to_wide(button))).c_str(), -1); + } + + if ((tab_index >= 0) && + (buttons.size() < INT_MAX) && + (tab_index < (int) buttons.size())) + e->setActiveTab(tab_index); + + m_fields.push_back(spec); + return; + } + errorstream << "Invalid TabHeader element(" << parts.size() << "): '" + << element << "'" << std::endl; +} + +void GUIFormSpecMenu::parseItemImageButton(parserData* data, const std::string &element) +{ + + if (m_client == 0) { + warningstream << "invalid use of item_image_button with m_client==0" + << std::endl; + return; + } + + std::vector parts = split(element,';'); + + if ((parts.size() == 5) || + ((parts.size() > 5) && (m_formspec_version > FORMSPEC_API_VERSION))) + { + std::vector v_pos = split(parts[0],','); + std::vector v_geom = split(parts[1],','); + std::string item_name = parts[2]; + std::string name = parts[3]; + std::string label = parts[4]; + + label = unescape_string(label); + item_name = unescape_string(item_name); + + MY_CHECKPOS("itemimagebutton",0); + MY_CHECKGEOM("itemimagebutton",1); + + v2s32 pos = padding + pos_offset * spacing; + pos.X += stof(v_pos[0]) * (float)spacing.X; + pos.Y += stof(v_pos[1]) * (float)spacing.Y; + v2s32 geom; + geom.X = (stof(v_geom[0]) * (float)spacing.X)-(spacing.X-imgsize.X); + geom.Y = (stof(v_geom[1]) * (float)spacing.Y)-(spacing.Y-imgsize.Y); + + core::rect rect = core::rect(pos.X, pos.Y, pos.X+geom.X, pos.Y+geom.Y); + + if(!data->explicit_size) + warningstream<<"invalid use of item_image_button without a size[] element"<idef(); + ItemStack item; + item.deSerialize(item_name, idef); + + m_tooltips[name] = + TooltipSpec(utf8_to_wide(item.getDefinition(idef).description), + m_default_tooltip_bgcolor, + m_default_tooltip_color); + + FieldSpec spec( + name, + utf8_to_wide(label), + utf8_to_wide(item_name), + 258 + m_fields.size() + ); + + gui::IGUIButton *e = Environment->addButton(rect, this, spec.fid, L""); + + if (spec.fname == data->focused_fieldname) { + Environment->setFocus(e); + } + + spec.ftype = f_Button; + rect+=data->basepos-padding; + spec.rect=rect; + m_fields.push_back(spec); + pos = padding + AbsoluteRect.UpperLeftCorner + pos_offset * spacing; + pos.X += stof(v_pos[0]) * (float) spacing.X; + pos.Y += stof(v_pos[1]) * (float) spacing.Y; + m_itemimages.emplace_back("", item_name, e, pos, geom); + m_static_texts.emplace_back(utf8_to_wide(label), rect, e); + return; + } + errorstream<< "Invalid ItemImagebutton element(" << parts.size() << "): '" << element << "'" << std::endl; +} + +void GUIFormSpecMenu::parseBox(parserData* data, const std::string &element) +{ + std::vector parts = split(element,';'); + + if ((parts.size() == 3) || + ((parts.size() > 3) && (m_formspec_version > FORMSPEC_API_VERSION))) + { + std::vector v_pos = split(parts[0],','); + std::vector v_geom = split(parts[1],','); + + MY_CHECKPOS("box",0); + MY_CHECKGEOM("box",1); + + v2s32 pos = padding + AbsoluteRect.UpperLeftCorner + pos_offset * spacing; + pos.X += stof(v_pos[0]) * (float) spacing.X; + pos.Y += stof(v_pos[1]) * (float) spacing.Y; + + v2s32 geom; + geom.X = stof(v_geom[0]) * (float)spacing.X; + geom.Y = stof(v_geom[1]) * (float)spacing.Y; + + video::SColor tmp_color; + + if (parseColorString(parts[2], tmp_color, false)) { + BoxDrawSpec spec(pos, geom, tmp_color); + + m_boxes.push_back(spec); + } + else { + errorstream<< "Invalid Box element(" << parts.size() << "): '" << element << "' INVALID COLOR" << std::endl; + } + return; + } + errorstream<< "Invalid Box element(" << parts.size() << "): '" << element << "'" << std::endl; +} + +void GUIFormSpecMenu::parseBackgroundColor(parserData* data, const std::string &element) +{ + std::vector parts = split(element,';'); + + if (((parts.size() == 1) || (parts.size() == 2)) || + ((parts.size() > 2) && (m_formspec_version > FORMSPEC_API_VERSION))) { + parseColorString(parts[0], m_bgcolor, false); + + if (parts.size() == 2) { + std::string fullscreen = parts[1]; + m_bgfullscreen = is_yes(fullscreen); + } + + return; + } + + errorstream << "Invalid bgcolor element(" << parts.size() << "): '" << element << "'" + << std::endl; +} + +void GUIFormSpecMenu::parseListColors(parserData* data, const std::string &element) +{ + std::vector parts = split(element,';'); + + if (((parts.size() == 2) || (parts.size() == 3) || (parts.size() == 5)) || + ((parts.size() > 5) && (m_formspec_version > FORMSPEC_API_VERSION))) + { + parseColorString(parts[0], m_slotbg_n, false); + parseColorString(parts[1], m_slotbg_h, false); + + if (parts.size() >= 3) { + if (parseColorString(parts[2], m_slotbordercolor, false)) { + m_slotborder = true; + } + } + if (parts.size() == 5) { + video::SColor tmp_color; + + if (parseColorString(parts[3], tmp_color, false)) + m_default_tooltip_bgcolor = tmp_color; + if (parseColorString(parts[4], tmp_color, false)) + m_default_tooltip_color = tmp_color; + } + return; + } + errorstream<< "Invalid listcolors element(" << parts.size() << "): '" << element << "'" << std::endl; +} + +void GUIFormSpecMenu::parseTooltip(parserData* data, const std::string &element) +{ + std::vector parts = split(element,';'); + if (parts.size() == 2) { + std::string name = parts[0]; + m_tooltips[name] = TooltipSpec(utf8_to_wide(unescape_string(parts[1])), + m_default_tooltip_bgcolor, m_default_tooltip_color); + return; + } + + if (parts.size() == 4) { + std::string name = parts[0]; + video::SColor tmp_color1, tmp_color2; + if ( parseColorString(parts[2], tmp_color1, false) && parseColorString(parts[3], tmp_color2, false) ) { + m_tooltips[name] = TooltipSpec(utf8_to_wide(unescape_string(parts[1])), + tmp_color1, tmp_color2); + return; + } + } + errorstream<< "Invalid tooltip element(" << parts.size() << "): '" << element << "'" << std::endl; +} + +bool GUIFormSpecMenu::parseVersionDirect(const std::string &data) +{ + //some prechecks + if (data.empty()) + return false; + + std::vector parts = split(data,'['); + + if (parts.size() < 2) { + return false; + } + + if (parts[0] != "formspec_version") { + return false; + } + + if (is_number(parts[1])) { + m_formspec_version = mystoi(parts[1]); + return true; + } + + return false; +} + +bool GUIFormSpecMenu::parseSizeDirect(parserData* data, const std::string &element) +{ + if (element.empty()) + return false; + + std::vector parts = split(element,'['); + + if (parts.size() < 2) + return false; + + std::string type = trim(parts[0]); + std::string description = trim(parts[1]); + + if (type != "size" && type != "invsize") + return false; + + if (type == "invsize") + log_deprecated("Deprecated formspec element \"invsize\" is used"); + + parseSize(data, description); + + return true; +} + +bool GUIFormSpecMenu::parsePositionDirect(parserData *data, const std::string &element) +{ + if (element.empty()) + return false; + + std::vector parts = split(element, '['); + + if (parts.size() != 2) + return false; + + std::string type = trim(parts[0]); + std::string description = trim(parts[1]); + + if (type != "position") + return false; + + parsePosition(data, description); + + return true; +} + +void GUIFormSpecMenu::parsePosition(parserData *data, const std::string &element) +{ + std::vector parts = split(element, ','); + + if (parts.size() == 2) { + data->offset.X = stof(parts[0]); + data->offset.Y = stof(parts[1]); + return; + } + + errorstream << "Invalid position element (" << parts.size() << "): '" << element << "'" << std::endl; +} + +bool GUIFormSpecMenu::parseAnchorDirect(parserData *data, const std::string &element) +{ + if (element.empty()) + return false; + + std::vector parts = split(element, '['); + + if (parts.size() != 2) + return false; + + std::string type = trim(parts[0]); + std::string description = trim(parts[1]); + + if (type != "anchor") + return false; + + parseAnchor(data, description); + + return true; +} + +void GUIFormSpecMenu::parseAnchor(parserData *data, const std::string &element) +{ + std::vector parts = split(element, ','); + + if (parts.size() == 2) { + data->anchor.X = stof(parts[0]); + data->anchor.Y = stof(parts[1]); + return; + } + + errorstream << "Invalid anchor element (" << parts.size() << "): '" << element + << "'" << std::endl; +} + +void GUIFormSpecMenu::parseElement(parserData* data, const std::string &element) +{ + //some prechecks + if (element.empty()) + return; + + std::vector parts = split(element,'['); + + // ugly workaround to keep compatibility + if (parts.size() > 2) { + if (trim(parts[0]) == "image") { + for (unsigned int i=2;i< parts.size(); i++) { + parts[1] += "[" + parts[i]; + } + } + else { return; } + } + + if (parts.size() < 2) { + return; + } + + std::string type = trim(parts[0]); + std::string description = trim(parts[1]); + + if (type == "container") { + parseContainer(data, description); + return; + } + + if (type == "container_end") { + parseContainerEnd(data); + return; + } + + if (type == "list") { + parseList(data, description); + return; + } + + if (type == "listring") { + parseListRing(data, description); + return; + } + + if (type == "checkbox") { + parseCheckbox(data, description); + return; + } + + if (type == "image") { + parseImage(data, description); + return; + } + + if (type == "item_image") { + parseItemImage(data, description); + return; + } + + if (type == "button" || type == "button_exit") { + parseButton(data, description, type); + return; + } + + if (type == "background") { + parseBackground(data,description); + return; + } + + if (type == "tableoptions"){ + parseTableOptions(data,description); + return; + } + + if (type == "tablecolumns"){ + parseTableColumns(data,description); + return; + } + + if (type == "table"){ + parseTable(data,description); + return; + } + + if (type == "textlist"){ + parseTextList(data,description); + return; + } + + if (type == "dropdown"){ + parseDropDown(data,description); + return; + } + + if (type == "field_close_on_enter") { + parseFieldCloseOnEnter(data, description); + return; + } + + if (type == "pwdfield") { + parsePwdField(data,description); + return; + } + + if ((type == "field") || (type == "textarea")){ + parseField(data,description,type); + return; + } + + if (type == "label") { + parseLabel(data,description); + return; + } + + if (type == "vertlabel") { + parseVertLabel(data,description); + return; + } + + if (type == "item_image_button") { + parseItemImageButton(data,description); + return; + } + + if ((type == "image_button") || (type == "image_button_exit")) { + parseImageButton(data,description,type); + return; + } + + if (type == "tabheader") { + parseTabHeader(data,description); + return; + } + + if (type == "box") { + parseBox(data,description); + return; + } + + if (type == "bgcolor") { + parseBackgroundColor(data,description); + return; + } + + if (type == "listcolors") { + parseListColors(data,description); + return; + } + + if (type == "tooltip") { + parseTooltip(data,description); + return; + } + + if (type == "scrollbar") { + parseScrollBar(data, description); + return; + } + + // Ignore others + infostream << "Unknown DrawSpec: type=" << type << ", data=\"" << description << "\"" + << std::endl; +} + +void GUIFormSpecMenu::regenerateGui(v2u32 screensize) +{ + /* useless to regenerate without a screensize */ + if ((screensize.X <= 0) || (screensize.Y <= 0)) { + return; + } + + parserData mydata; + + //preserve tables + for (auto &m_table : m_tables) { + std::string tablename = m_table.first.fname; + GUITable *table = m_table.second; + mydata.table_dyndata[tablename] = table->getDynamicData(); + } + + //set focus + if (!m_focused_element.empty()) + mydata.focused_fieldname = m_focused_element; + + //preserve focus + gui::IGUIElement *focused_element = Environment->getFocus(); + if (focused_element && focused_element->getParent() == this) { + s32 focused_id = focused_element->getID(); + if (focused_id > 257) { + for (const GUIFormSpecMenu::FieldSpec &field : m_fields) { + if (field.fid == focused_id) { + mydata.focused_fieldname = field.fname; + break; + } + } + } + } + + // Remove children + removeChildren(); + + for (auto &table_it : m_tables) { + table_it.second->drop(); + } + + mydata.size= v2s32(100,100); + mydata.screensize = screensize; + mydata.offset = v2f32(0.5f, 0.5f); + mydata.anchor = v2f32(0.5f, 0.5f); + + // Base position of contents of form + mydata.basepos = getBasePos(); + + /* Convert m_init_draw_spec to m_inventorylists */ + + m_inventorylists.clear(); + m_images.clear(); + m_backgrounds.clear(); + m_itemimages.clear(); + m_tables.clear(); + m_checkboxes.clear(); + m_scrollbars.clear(); + m_fields.clear(); + m_boxes.clear(); + m_tooltips.clear(); + m_inventory_rings.clear(); + m_static_texts.clear(); + m_dropdowns.clear(); + + m_bgfullscreen = false; + + { + v3f formspec_bgcolor = g_settings->getV3F("formspec_default_bg_color"); + m_bgcolor = video::SColor( + (u8) clamp_u8(g_settings->getS32("formspec_default_bg_opacity")), + clamp_u8(myround(formspec_bgcolor.X)), + clamp_u8(myround(formspec_bgcolor.Y)), + clamp_u8(myround(formspec_bgcolor.Z)) + ); + } + + { + v3f formspec_bgcolor = g_settings->getV3F("formspec_fullscreen_bg_color"); + m_fullscreen_bgcolor = video::SColor( + (u8) clamp_u8(g_settings->getS32("formspec_fullscreen_bg_opacity")), + clamp_u8(myround(formspec_bgcolor.X)), + clamp_u8(myround(formspec_bgcolor.Y)), + clamp_u8(myround(formspec_bgcolor.Z)) + ); + } + + + m_slotbg_n = video::SColor(255,128,128,128); + m_slotbg_h = video::SColor(255,192,192,192); + + m_default_tooltip_bgcolor = video::SColor(255,110,130,60); + m_default_tooltip_color = video::SColor(255,255,255,255); + + m_slotbordercolor = video::SColor(200,0,0,0); + m_slotborder = false; + + // Add tooltip + { + assert(!m_tooltip_element); + // Note: parent != this so that the tooltip isn't clipped by the menu rectangle + m_tooltip_element = addStaticText(Environment, L"",core::rect(0,0,110,18)); + m_tooltip_element->enableOverrideColor(true); + m_tooltip_element->setBackgroundColor(m_default_tooltip_bgcolor); + m_tooltip_element->setDrawBackground(true); + m_tooltip_element->setDrawBorder(true); + m_tooltip_element->setOverrideColor(m_default_tooltip_color); + m_tooltip_element->setTextAlignment(gui::EGUIA_CENTER, gui::EGUIA_CENTER); + m_tooltip_element->setWordWrap(false); + //we're not parent so no autograb for this one! + m_tooltip_element->grab(); + } + + std::vector elements = split(m_formspec_string,']'); + unsigned int i = 0; + + /* try to read version from first element only */ + if (!elements.empty()) { + if ( parseVersionDirect(elements[0]) ) { + i++; + } + } + + /* we need size first in order to calculate image scale */ + mydata.explicit_size = false; + for (; i< elements.size(); i++) { + if (!parseSizeDirect(&mydata, elements[i])) { + break; + } + } + + /* "position" element is always after "size" element if it used */ + for (; i< elements.size(); i++) { + if (!parsePositionDirect(&mydata, elements[i])) { + break; + } + } + + /* "anchor" element is always after "position" (or "size" element) if it used */ + for (; i< elements.size(); i++) { + if (!parseAnchorDirect(&mydata, elements[i])) { + break; + } + } + + + if (mydata.explicit_size) { + // compute scaling for specified form size + if (m_lock) { + v2u32 current_screensize = RenderingEngine::get_video_driver()->getScreenSize(); + v2u32 delta = current_screensize - m_lockscreensize; + + if (current_screensize.Y > m_lockscreensize.Y) + delta.Y /= 2; + else + delta.Y = 0; + + if (current_screensize.X > m_lockscreensize.X) + delta.X /= 2; + else + delta.X = 0; + + offset = v2s32(delta.X,delta.Y); + + mydata.screensize = m_lockscreensize; + } else { + offset = v2s32(0,0); + } + + double gui_scaling = g_settings->getFloat("gui_scaling"); + double screen_dpi = RenderingEngine::getDisplayDensity() * 96; + + double use_imgsize; + if (m_lock) { + // In fixed-size mode, inventory image size + // is 0.53 inch multiplied by the gui_scaling + // config parameter. This magic size is chosen + // to make the main menu (15.5 inventory images + // wide, including border) just fit into the + // default window (800 pixels wide) at 96 DPI + // and default scaling (1.00). + use_imgsize = 0.5555 * screen_dpi * gui_scaling; + } else { + // In variable-size mode, we prefer to make the + // inventory image size 1/15 of screen height, + // multiplied by the gui_scaling config parameter. + // If the preferred size won't fit the whole + // form on the screen, either horizontally or + // vertically, then we scale it down to fit. + // (The magic numbers in the computation of what + // fits arise from the scaling factors in the + // following stanza, including the form border, + // help text space, and 0.1 inventory slot spare.) + // However, a minimum size is also set, that + // the image size can't be less than 0.3 inch + // multiplied by gui_scaling, even if this means + // the form doesn't fit the screen. + double prefer_imgsize = mydata.screensize.Y / 15 * + gui_scaling; + double fitx_imgsize = mydata.screensize.X / + ((5.0/4.0) * (0.5 + mydata.invsize.X)); + double fity_imgsize = mydata.screensize.Y / + ((15.0/13.0) * (0.85 * mydata.invsize.Y)); + double screen_dpi = RenderingEngine::getDisplayDensity() * 96; + double min_imgsize = 0.3 * screen_dpi * gui_scaling; + use_imgsize = MYMAX(min_imgsize, MYMIN(prefer_imgsize, + MYMIN(fitx_imgsize, fity_imgsize))); + } + + // Everything else is scaled in proportion to the + // inventory image size. The inventory slot spacing + // is 5/4 image size horizontally and 15/13 image size + // vertically. The padding around the form (incorporating + // the border of the outer inventory slots) is 3/8 + // image size. Font height (baseline to baseline) + // is 2/5 vertical inventory slot spacing, and button + // half-height is 7/8 of font height. + imgsize = v2s32(use_imgsize, use_imgsize); + spacing = v2s32(use_imgsize*5.0/4, use_imgsize*15.0/13); + padding = v2s32(use_imgsize*3.0/8, use_imgsize*3.0/8); + m_btn_height = use_imgsize*15.0/13 * 0.35; + + m_font = g_fontengine->getFont(); + + mydata.size = v2s32( + padding.X*2+spacing.X*(mydata.invsize.X-1.0)+imgsize.X, + padding.Y*2+spacing.Y*(mydata.invsize.Y-1.0)+imgsize.Y + m_btn_height*2.0/3.0 + ); + DesiredRect = mydata.rect = core::rect( + (s32)((f32)mydata.screensize.X * mydata.offset.X) - (s32)(mydata.anchor.X * (f32)mydata.size.X) + offset.X, + (s32)((f32)mydata.screensize.Y * mydata.offset.Y) - (s32)(mydata.anchor.Y * (f32)mydata.size.Y) + offset.Y, + (s32)((f32)mydata.screensize.X * mydata.offset.X) + (s32)((1.0 - mydata.anchor.X) * (f32)mydata.size.X) + offset.X, + (s32)((f32)mydata.screensize.Y * mydata.offset.Y) + (s32)((1.0 - mydata.anchor.Y) * (f32)mydata.size.Y) + offset.Y + ); + } else { + // Non-size[] form must consist only of text fields and + // implicit "Proceed" button. Use default font, and + // temporary form size which will be recalculated below. + m_font = g_fontengine->getFont(); + m_btn_height = font_line_height(m_font) * 0.875; + DesiredRect = core::rect( + (s32)((f32)mydata.screensize.X * mydata.offset.X) - (s32)(mydata.anchor.X * 580.0), + (s32)((f32)mydata.screensize.Y * mydata.offset.Y) - (s32)(mydata.anchor.Y * 300.0), + (s32)((f32)mydata.screensize.X * mydata.offset.X) + (s32)((1.0 - mydata.anchor.X) * 580.0), + (s32)((f32)mydata.screensize.Y * mydata.offset.Y) + (s32)((1.0 - mydata.anchor.Y) * 300.0) + ); + } + recalculateAbsolutePosition(false); + mydata.basepos = getBasePos(); + m_tooltip_element->setOverrideFont(m_font); + + gui::IGUISkin* skin = Environment->getSkin(); + sanity_check(skin); + gui::IGUIFont *old_font = skin->getFont(); + skin->setFont(m_font); + + pos_offset = v2s32(); + for (; i< elements.size(); i++) { + parseElement(&mydata, elements[i]); + } + + if (!container_stack.empty()) { + errorstream << "Invalid formspec string: container was never closed!" + << std::endl; + } + + // If there are fields without explicit size[], add a "Proceed" + // button and adjust size to fit all the fields. + if (!m_fields.empty() && !mydata.explicit_size) { + mydata.rect = core::rect( + mydata.screensize.X/2 - 580/2, + mydata.screensize.Y/2 - 300/2, + mydata.screensize.X/2 + 580/2, + mydata.screensize.Y/2 + 240/2+(m_fields.size()*60) + ); + DesiredRect = mydata.rect; + recalculateAbsolutePosition(false); + mydata.basepos = getBasePos(); + + { + v2s32 pos = mydata.basepos; + pos.Y = ((m_fields.size()+2)*60); + + v2s32 size = DesiredRect.getSize(); + mydata.rect = + core::rect(size.X/2-70, pos.Y, + (size.X/2-70)+140, pos.Y + (m_btn_height*2)); + const wchar_t *text = wgettext("Proceed"); + Environment->addButton(mydata.rect, this, 257, text); + delete[] text; + } + + } + + //set initial focus if parser didn't set it + focused_element = Environment->getFocus(); + if (!focused_element + || !isMyChild(focused_element) + || focused_element->getType() == gui::EGUIET_TAB_CONTROL) + setInitialFocus(); + + skin->setFont(old_font); +} + +#ifdef __ANDROID__ +bool GUIFormSpecMenu::getAndroidUIInput() +{ + /* no dialog shown */ + if (m_JavaDialogFieldName == "") { + return false; + } + + /* still waiting */ + if (porting::getInputDialogState() == -1) { + return true; + } + + std::string fieldname = m_JavaDialogFieldName; + m_JavaDialogFieldName = ""; + + /* no value abort dialog processing */ + if (porting::getInputDialogState() != 0) { + return false; + } + + for(std::vector::iterator iter = m_fields.begin(); + iter != m_fields.end(); ++iter) { + + if (iter->fname != fieldname) { + continue; + } + IGUIElement* tochange = getElementFromId(iter->fid); + + if (tochange == 0) { + return false; + } + + if (tochange->getType() != irr::gui::EGUIET_EDIT_BOX) { + return false; + } + + std::string text = porting::getInputDialogValue(); + + ((gui::IGUIEditBox*) tochange)-> + setText(utf8_to_wide(text).c_str()); + } + return false; +} +#endif + +GUIFormSpecMenu::ItemSpec GUIFormSpecMenu::getItemAtPos(v2s32 p) const +{ + core::rect imgrect(0,0,imgsize.X,imgsize.Y); + + for (const GUIFormSpecMenu::ListDrawSpec &s : m_inventorylists) { + for(s32 i=0; i rect = imgrect + s.pos + p0; + if(rect.isPointInside(p)) + { + return ItemSpec(s.inventoryloc, s.listname, item_i); + } + } + } + + return ItemSpec(InventoryLocation(), "", -1); +} + +void GUIFormSpecMenu::drawList(const ListDrawSpec &s, int phase, + bool &item_hovered) +{ + video::IVideoDriver* driver = Environment->getVideoDriver(); + + Inventory *inv = m_invmgr->getInventory(s.inventoryloc); + if(!inv){ + warningstream<<"GUIFormSpecMenu::drawList(): " + <<"The inventory location " + <<"\""<getList(s.listname); + if(!ilist){ + warningstream<<"GUIFormSpecMenu::drawList(): " + <<"The inventory list \""< imgrect(0,0,imgsize.X,imgsize.Y); + + for(s32 i=0; i= (s32) ilist->getSize()) + break; + s32 x = (i%s.geom.X) * spacing.X; + s32 y = (i/s.geom.X) * spacing.Y; + v2s32 p(x,y); + core::rect rect = imgrect + s.pos + p; + ItemStack item; + if(ilist) + item = ilist->getItem(item_i); + + bool selected = m_selected_item + && m_invmgr->getInventory(m_selected_item->inventoryloc) == inv + && m_selected_item->listname == s.listname + && m_selected_item->i == item_i; + bool hovering = rect.isPointInside(m_pointer); + ItemRotationKind rotation_kind = selected ? IT_ROT_SELECTED : + (hovering ? IT_ROT_HOVERED : IT_ROT_NONE); + + if (phase == 0) { + if (hovering) { + item_hovered = true; + driver->draw2DRectangle(m_slotbg_h, rect, &AbsoluteClippingRect); + } else { + driver->draw2DRectangle(m_slotbg_n, rect, &AbsoluteClippingRect); + } + } + + //Draw inv slot borders + if (m_slotborder) { + s32 x1 = rect.UpperLeftCorner.X; + s32 y1 = rect.UpperLeftCorner.Y; + s32 x2 = rect.LowerRightCorner.X; + s32 y2 = rect.LowerRightCorner.Y; + s32 border = 1; + driver->draw2DRectangle(m_slotbordercolor, + core::rect(v2s32(x1 - border, y1 - border), + v2s32(x2 + border, y1)), NULL); + driver->draw2DRectangle(m_slotbordercolor, + core::rect(v2s32(x1 - border, y2), + v2s32(x2 + border, y2 + border)), NULL); + driver->draw2DRectangle(m_slotbordercolor, + core::rect(v2s32(x1 - border, y1), + v2s32(x1, y2)), NULL); + driver->draw2DRectangle(m_slotbordercolor, + core::rect(v2s32(x2, y1), + v2s32(x2 + border, y2)), NULL); + } + + if(phase == 1) + { + // Draw item stack + if(selected) + { + item.takeItem(m_selected_amount); + } + if(!item.empty()) + { + drawItemStack(driver, m_font, item, + rect, &AbsoluteClippingRect, m_client, + rotation_kind); + } + + // Draw tooltip + std::wstring tooltip_text; + if (hovering && !m_selected_item) { + const std::string &desc = item.metadata.getString("description"); + if (desc.empty()) + tooltip_text = + utf8_to_wide(item.getDefinition(m_client->idef()).description); + else + tooltip_text = utf8_to_wide(desc); + + if (!item.name.empty()) { + if (tooltip_text.empty()) + tooltip_text = utf8_to_wide(item.name); + if (m_tooltip_append_itemname) + tooltip_text += utf8_to_wide(" [" + item.name + "]"); + } + } + if (!tooltip_text.empty()) { + showTooltip(tooltip_text, m_default_tooltip_color, + m_default_tooltip_bgcolor); + } + } + } +} + +void GUIFormSpecMenu::drawSelectedItem() +{ + video::IVideoDriver* driver = Environment->getVideoDriver(); + + if (!m_selected_item) { + drawItemStack(driver, m_font, ItemStack(), + core::rect(v2s32(0, 0), v2s32(0, 0)), + NULL, m_client, IT_ROT_DRAGGED); + return; + } + + Inventory *inv = m_invmgr->getInventory(m_selected_item->inventoryloc); + sanity_check(inv); + InventoryList *list = inv->getList(m_selected_item->listname); + sanity_check(list); + ItemStack stack = list->getItem(m_selected_item->i); + stack.count = m_selected_amount; + + core::rect imgrect(0,0,imgsize.X,imgsize.Y); + core::rect rect = imgrect + (m_pointer - imgrect.getCenter()); + rect.constrainTo(driver->getViewPort()); + drawItemStack(driver, m_font, stack, rect, NULL, m_client, IT_ROT_DRAGGED); +} + +void GUIFormSpecMenu::drawMenu() +{ + if (m_form_src) { + const std::string &newform = m_form_src->getForm(); + if (newform != m_formspec_string) { + m_formspec_string = newform; + regenerateGui(m_screensize_old); + } + } + + gui::IGUISkin* skin = Environment->getSkin(); + sanity_check(skin != NULL); + gui::IGUIFont *old_font = skin->getFont(); + skin->setFont(m_font); + + updateSelectedItem(); + + video::IVideoDriver* driver = Environment->getVideoDriver(); + + v2u32 screenSize = driver->getScreenSize(); + core::rect allbg(0, 0, screenSize.X, screenSize.Y); + + if (m_bgfullscreen) + driver->draw2DRectangle(m_fullscreen_bgcolor, allbg, &allbg); + else + driver->draw2DRectangle(m_bgcolor, AbsoluteRect, &AbsoluteClippingRect); + + m_tooltip_element->setVisible(false); + + /* + Draw backgrounds + */ + for (const GUIFormSpecMenu::ImageDrawSpec &spec : m_backgrounds) { + video::ITexture *texture = m_tsrc->getTexture(spec.name); + + if (texture != 0) { + // Image size on screen + core::rect imgrect(0, 0, spec.geom.X, spec.geom.Y); + // Image rectangle on screen + core::rect rect = imgrect + spec.pos; + + if (spec.clip) { + core::dimension2d absrec_size = AbsoluteRect.getSize(); + rect = core::rect(AbsoluteRect.UpperLeftCorner.X - spec.pos.X, + AbsoluteRect.UpperLeftCorner.Y - spec.pos.Y, + AbsoluteRect.UpperLeftCorner.X + absrec_size.Width + spec.pos.X, + AbsoluteRect.UpperLeftCorner.Y + absrec_size.Height + spec.pos.Y); + } + + const video::SColor color(255,255,255,255); + const video::SColor colors[] = {color,color,color,color}; + draw2DImageFilterScaled(driver, texture, rect, + core::rect(core::position2d(0,0), + core::dimension2di(texture->getOriginalSize())), + NULL/*&AbsoluteClippingRect*/, colors, true); + } else { + errorstream << "GUIFormSpecMenu::drawMenu() Draw backgrounds unable to load texture:" << std::endl; + errorstream << "\t" << spec.name << std::endl; + } + } + + /* + Draw Boxes + */ + for (const GUIFormSpecMenu::BoxDrawSpec &spec : m_boxes) { + irr::video::SColor todraw = spec.color; + + todraw.setAlpha(140); + + core::rect rect(spec.pos.X,spec.pos.Y, + spec.pos.X + spec.geom.X,spec.pos.Y + spec.geom.Y); + + driver->draw2DRectangle(todraw, rect, 0); + } + + /* + Call base class + */ + gui::IGUIElement::draw(); + + /* + Draw images + */ + for (const GUIFormSpecMenu::ImageDrawSpec &spec : m_images) { + video::ITexture *texture = m_tsrc->getTexture(spec.name); + + if (texture != 0) { + const core::dimension2d& img_origsize = texture->getOriginalSize(); + // Image size on screen + core::rect imgrect; + + if (spec.scale) + imgrect = core::rect(0,0,spec.geom.X, spec.geom.Y); + else { + + imgrect = core::rect(0,0,img_origsize.Width,img_origsize.Height); + } + // Image rectangle on screen + core::rect rect = imgrect + spec.pos; + const video::SColor color(255,255,255,255); + const video::SColor colors[] = {color,color,color,color}; + draw2DImageFilterScaled(driver, texture, rect, + core::rect(core::position2d(0,0),img_origsize), + NULL/*&AbsoluteClippingRect*/, colors, true); + } + else { + errorstream << "GUIFormSpecMenu::drawMenu() Draw images unable to load texture:" << std::endl; + errorstream << "\t" << spec.name << std::endl; + } + } + + /* + Draw item images + */ + for (const GUIFormSpecMenu::ImageDrawSpec &spec : m_itemimages) { + if (m_client == 0) + break; + + IItemDefManager *idef = m_client->idef(); + ItemStack item; + item.deSerialize(spec.item_name, idef); + core::rect imgrect(0, 0, spec.geom.X, spec.geom.Y); + // Viewport rectangle on screen + core::rect rect = imgrect + spec.pos; + if (spec.parent_button && spec.parent_button->isPressed()) { +#if (IRRLICHT_VERSION_MAJOR == 1 && IRRLICHT_VERSION_MINOR < 8) + rect += core::dimension2d( + 0.05 * (float)rect.getWidth(), 0.05 * (float)rect.getHeight()); +#else + rect += core::dimension2d( + skin->getSize(irr::gui::EGDS_BUTTON_PRESSED_IMAGE_OFFSET_X), + skin->getSize(irr::gui::EGDS_BUTTON_PRESSED_IMAGE_OFFSET_Y)); +#endif + } + drawItemStack(driver, m_font, item, rect, &AbsoluteClippingRect, + m_client, IT_ROT_NONE); + } + + /* + Draw items + Phase 0: Item slot rectangles + Phase 1: Item images; prepare tooltip + */ + bool item_hovered = false; + int start_phase = 0; + for (int phase = start_phase; phase <= 1; phase++) { + for (const GUIFormSpecMenu::ListDrawSpec &spec : m_inventorylists) { + drawList(spec, phase, item_hovered); + } + } + if (!item_hovered) { + drawItemStack(driver, m_font, ItemStack(), + core::rect(v2s32(0, 0), v2s32(0, 0)), + NULL, m_client, IT_ROT_HOVERED); + } + +/* TODO find way to show tooltips on touchscreen */ +#ifndef HAVE_TOUCHSCREENGUI + m_pointer = RenderingEngine::get_raw_device()->getCursorControl()->getPosition(); +#endif + + /* + Draw static text elements + */ + for (const GUIFormSpecMenu::StaticTextSpec &spec : m_static_texts) { + core::rect rect = spec.rect; + if (spec.parent_button && spec.parent_button->isPressed()) { +#if (IRRLICHT_VERSION_MAJOR == 1 && IRRLICHT_VERSION_MINOR < 8) + rect += core::dimension2d( + 0.05 * (float)rect.getWidth(), 0.05 * (float)rect.getHeight()); +#else + // Use image offset instead of text's because its a bit smaller + // and fits better, also TEXT_OFFSET_X is always 0 + rect += core::dimension2d( + skin->getSize(irr::gui::EGDS_BUTTON_PRESSED_IMAGE_OFFSET_X), + skin->getSize(irr::gui::EGDS_BUTTON_PRESSED_IMAGE_OFFSET_Y)); +#endif + } + video::SColor color(255, 255, 255, 255); + m_font->draw(spec.text.c_str(), rect, color, true, true, &rect); + } + + /* + Draw fields/buttons tooltips + */ + gui::IGUIElement *hovered = + Environment->getRootGUIElement()->getElementFromPoint(m_pointer); + + if (hovered != NULL) { + s32 id = hovered->getID(); + + u64 delta = 0; + if (id == -1) { + m_old_tooltip_id = id; + } else { + if (id == m_old_tooltip_id) { + delta = porting::getDeltaMs(m_hovered_time, porting::getTimeMs()); + } else { + m_hovered_time = porting::getTimeMs(); + m_old_tooltip_id = id; + } + } + + // Find and update the current tooltip + if (id != -1 && delta >= m_tooltip_show_delay) { + for (const FieldSpec &field : m_fields) { + + if (field.fid != id) + continue; + + const std::wstring &text = m_tooltips[field.fname].tooltip; + if (!text.empty()) + showTooltip(text, m_tooltips[field.fname].color, + m_tooltips[field.fname].bgcolor); + + break; + } + } + } + + m_tooltip_element->draw(); + + /* + Draw dragged item stack + */ + drawSelectedItem(); + + skin->setFont(old_font); +} + + +void GUIFormSpecMenu::showTooltip(const std::wstring &text, + const irr::video::SColor &color, const irr::video::SColor &bgcolor) +{ + const std::wstring ntext = translate_string(text); + m_tooltip_element->setOverrideColor(color); + m_tooltip_element->setBackgroundColor(bgcolor); + setStaticText(m_tooltip_element, ntext.c_str()); + + // Tooltip size and offset + s32 tooltip_width = m_tooltip_element->getTextWidth() + m_btn_height; +#if (IRRLICHT_VERSION_MAJOR <= 1 && IRRLICHT_VERSION_MINOR <= 8 && IRRLICHT_VERSION_REVISION < 2) || USE_FREETYPE == 1 + std::vector text_rows = str_split(ntext, L'\n'); + s32 tooltip_height = m_tooltip_element->getTextHeight() * text_rows.size() + 5; +#else + s32 tooltip_height = m_tooltip_element->getTextHeight() + 5; +#endif + v2u32 screenSize = Environment->getVideoDriver()->getScreenSize(); + int tooltip_offset_x = m_btn_height; + int tooltip_offset_y = m_btn_height; +#ifdef __ANDROID__ + tooltip_offset_x *= 3; + tooltip_offset_y = 0; + if (m_pointer.X > (s32)screenSize.X / 2) + tooltip_offset_x = -(tooltip_offset_x + tooltip_width); +#endif + + // Calculate and set the tooltip position + s32 tooltip_x = m_pointer.X + tooltip_offset_x; + s32 tooltip_y = m_pointer.Y + tooltip_offset_y; + if (tooltip_x + tooltip_width > (s32)screenSize.X) + tooltip_x = (s32)screenSize.X - tooltip_width - m_btn_height; + if (tooltip_y + tooltip_height > (s32)screenSize.Y) + tooltip_y = (s32)screenSize.Y - tooltip_height - m_btn_height; + + m_tooltip_element->setRelativePosition( + core::rect( + core::position2d(tooltip_x, tooltip_y), + core::dimension2d(tooltip_width, tooltip_height) + ) + ); + + // Display the tooltip + m_tooltip_element->setVisible(true); + bringToFront(m_tooltip_element); +} + +void GUIFormSpecMenu::updateSelectedItem() +{ + // If the selected stack has become empty for some reason, deselect it. + // If the selected stack has become inaccessible, deselect it. + // If the selected stack has become smaller, adjust m_selected_amount. + ItemStack selected = verifySelectedItem(); + + // WARNING: BLACK MAGIC + // See if there is a stack suited for our current guess. + // If such stack does not exist, clear the guess. + if (!m_selected_content_guess.name.empty() && + selected.name == m_selected_content_guess.name && + selected.count == m_selected_content_guess.count){ + // Selected item fits the guess. Skip the black magic. + } else if (!m_selected_content_guess.name.empty()) { + bool found = false; + for(u32 i=0; igetInventory(s.inventoryloc); + if(!inv) + continue; + InventoryList *list = inv->getList(s.listname); + if(!list) + continue; + for(s32 i=0; i= list->getSize()) + continue; + ItemStack stack = list->getItem(item_i); + if(stack.name == m_selected_content_guess.name && + stack.count == m_selected_content_guess.count){ + found = true; + infostream<<"Client: Changing selected content guess to " + <getInventory(s.inventoryloc); + InventoryList *list = inv->getList("craftresult"); + if(list && list->getSize() >= 1 && !list->getItem(0).empty()) + { + m_selected_item = new ItemSpec; + m_selected_item->inventoryloc = s.inventoryloc; + m_selected_item->listname = "craftresult"; + m_selected_item->i = 0; + m_selected_amount = 0; + m_selected_dragging = false; + break; + } + } + } + } + + // If craftresult is selected, keep the whole stack selected + if(m_selected_item && m_selected_item->listname == "craftresult") + { + m_selected_amount = verifySelectedItem().count; + } +} + +ItemStack GUIFormSpecMenu::verifySelectedItem() +{ + // If the selected stack has become empty for some reason, deselect it. + // If the selected stack has become inaccessible, deselect it. + // If the selected stack has become smaller, adjust m_selected_amount. + // Return the selected stack. + + if(m_selected_item) + { + if(m_selected_item->isValid()) + { + Inventory *inv = m_invmgr->getInventory(m_selected_item->inventoryloc); + if(inv) + { + InventoryList *list = inv->getList(m_selected_item->listname); + if(list && (u32) m_selected_item->i < list->getSize()) + { + ItemStack stack = list->getItem(m_selected_item->i); + if(m_selected_amount > stack.count) + m_selected_amount = stack.count; + if(!stack.empty()) + return stack; + } + } + } + + // selection was not valid + delete m_selected_item; + m_selected_item = NULL; + m_selected_amount = 0; + m_selected_dragging = false; + } + return ItemStack(); +} + +void GUIFormSpecMenu::acceptInput(FormspecQuitMode quitmode=quit_mode_no) +{ + if(m_text_dst) + { + StringMap fields; + + if (quitmode == quit_mode_accept) { + fields["quit"] = "true"; + } + + if (quitmode == quit_mode_cancel) { + fields["quit"] = "true"; + m_text_dst->gotText(fields); + return; + } + + if (current_keys_pending.key_down) { + fields["key_down"] = "true"; + current_keys_pending.key_down = false; + } + + if (current_keys_pending.key_up) { + fields["key_up"] = "true"; + current_keys_pending.key_up = false; + } + + if (current_keys_pending.key_enter) { + fields["key_enter"] = "true"; + current_keys_pending.key_enter = false; + } + + if (!current_field_enter_pending.empty()) { + fields["key_enter_field"] = current_field_enter_pending; + current_field_enter_pending = ""; + } + + if (current_keys_pending.key_escape) { + fields["key_escape"] = "true"; + current_keys_pending.key_escape = false; + } + + for (const GUIFormSpecMenu::FieldSpec &s : m_fields) { + if(s.send) { + std::string name = s.fname; + if (s.ftype == f_Button) { + fields[name] = wide_to_utf8(s.flabel); + } else if (s.ftype == f_Table) { + GUITable *table = getTable(s.fname); + if (table) { + fields[name] = table->checkEvent(); + } + } + else if(s.ftype == f_DropDown) { + // no dynamic cast possible due to some distributions shipped + // without rtti support in irrlicht + IGUIElement * element = getElementFromId(s.fid); + gui::IGUIComboBox *e = NULL; + if ((element) && (element->getType() == gui::EGUIET_COMBO_BOX)) { + e = static_cast(element); + } + s32 selected = e->getSelected(); + if (selected >= 0) { + std::vector *dropdown_values = + getDropDownValues(s.fname); + if (dropdown_values && selected < (s32)dropdown_values->size()) { + fields[name] = (*dropdown_values)[selected]; + } + } + } + else if (s.ftype == f_TabHeader) { + // no dynamic cast possible due to some distributions shipped + // without rttzi support in irrlicht + IGUIElement * element = getElementFromId(s.fid); + gui::IGUITabControl *e = NULL; + if ((element) && (element->getType() == gui::EGUIET_TAB_CONTROL)) { + e = static_cast(element); + } + + if (e != 0) { + std::stringstream ss; + ss << (e->getActiveTab() +1); + fields[name] = ss.str(); + } + } + else if (s.ftype == f_CheckBox) { + // no dynamic cast possible due to some distributions shipped + // without rtti support in irrlicht + IGUIElement * element = getElementFromId(s.fid); + gui::IGUICheckBox *e = NULL; + if ((element) && (element->getType() == gui::EGUIET_CHECK_BOX)) { + e = static_cast(element); + } + + if (e != 0) { + if (e->isChecked()) + fields[name] = "true"; + else + fields[name] = "false"; + } + } + else if (s.ftype == f_ScrollBar) { + // no dynamic cast possible due to some distributions shipped + // without rtti support in irrlicht + IGUIElement * element = getElementFromId(s.fid); + gui::IGUIScrollBar *e = NULL; + if ((element) && (element->getType() == gui::EGUIET_SCROLL_BAR)) { + e = static_cast(element); + } + + if (e != 0) { + std::stringstream os; + os << e->getPos(); + if (s.fdefault == L"Changed") + fields[name] = "CHG:" + os.str(); + else + fields[name] = "VAL:" + os.str(); + } + } + else + { + IGUIElement* e = getElementFromId(s.fid); + if(e != NULL) { + fields[name] = wide_to_utf8(e->getText()); + } + } + } + } + + m_text_dst->gotText(fields); + } +} + +static bool isChild(gui::IGUIElement * tocheck, gui::IGUIElement * parent) +{ + while(tocheck != NULL) { + if (tocheck == parent) { + return true; + } + tocheck = tocheck->getParent(); + } + return false; +} + +bool GUIFormSpecMenu::preprocessEvent(const SEvent& event) +{ + // The IGUITabControl renders visually using the skin's selected + // font, which we override for the duration of form drawing, + // but computes tab hotspots based on how it would have rendered + // using the font that is selected at the time of button release. + // To make these two consistent, temporarily override the skin's + // font while the IGUITabControl is processing the event. + if (event.EventType == EET_MOUSE_INPUT_EVENT && + event.MouseInput.Event == EMIE_LMOUSE_LEFT_UP) { + s32 x = event.MouseInput.X; + s32 y = event.MouseInput.Y; + gui::IGUIElement *hovered = + Environment->getRootGUIElement()->getElementFromPoint( + core::position2d(x, y)); + if (hovered && isMyChild(hovered) && + hovered->getType() == gui::EGUIET_TAB_CONTROL) { + gui::IGUISkin* skin = Environment->getSkin(); + sanity_check(skin != NULL); + gui::IGUIFont *old_font = skin->getFont(); + skin->setFont(m_font); + bool retval = hovered->OnEvent(event); + skin->setFont(old_font); + return retval; + } + } + + // Fix Esc/Return key being eaten by checkboxen and tables + if(event.EventType==EET_KEY_INPUT_EVENT) { + KeyPress kp(event.KeyInput); + if (kp == EscapeKey || kp == CancelKey + || kp == getKeySetting("keymap_inventory") + || event.KeyInput.Key==KEY_RETURN) { + gui::IGUIElement *focused = Environment->getFocus(); + if (focused && isMyChild(focused) && + (focused->getType() == gui::EGUIET_LIST_BOX || + focused->getType() == gui::EGUIET_CHECK_BOX)) { + OnEvent(event); + return true; + } + } + } + // Mouse wheel events: send to hovered element instead of focused + if(event.EventType==EET_MOUSE_INPUT_EVENT + && event.MouseInput.Event == EMIE_MOUSE_WHEEL) { + s32 x = event.MouseInput.X; + s32 y = event.MouseInput.Y; + gui::IGUIElement *hovered = + Environment->getRootGUIElement()->getElementFromPoint( + core::position2d(x, y)); + if (hovered && isMyChild(hovered)) { + hovered->OnEvent(event); + return true; + } + } + + if (event.EventType == EET_MOUSE_INPUT_EVENT) { + s32 x = event.MouseInput.X; + s32 y = event.MouseInput.Y; + gui::IGUIElement *hovered = + Environment->getRootGUIElement()->getElementFromPoint( + core::position2d(x, y)); + if (event.MouseInput.Event == EMIE_LMOUSE_PRESSED_DOWN) { + m_old_tooltip_id = -1; + } + if (!isChild(hovered,this)) { + if (DoubleClickDetection(event)) { + return true; + } + } + } + + #ifdef __ANDROID__ + // display software keyboard when clicking edit boxes + if (event.EventType == EET_MOUSE_INPUT_EVENT + && event.MouseInput.Event == EMIE_LMOUSE_PRESSED_DOWN) { + gui::IGUIElement *hovered = + Environment->getRootGUIElement()->getElementFromPoint( + core::position2d(event.MouseInput.X, event.MouseInput.Y)); + if ((hovered) && (hovered->getType() == irr::gui::EGUIET_EDIT_BOX)) { + bool retval = hovered->OnEvent(event); + if (retval) { + Environment->setFocus(hovered); + } + m_JavaDialogFieldName = getNameByID(hovered->getID()); + std::string message = gettext("Enter "); + std::string label = wide_to_utf8(getLabelByID(hovered->getID())); + if (label == "") { + label = "text"; + } + message += gettext(label) + ":"; + + /* single line text input */ + int type = 2; + + /* multi line text input */ + if (((gui::IGUIEditBox*) hovered)->isMultiLineEnabled()) { + type = 1; + } + + /* passwords are always single line */ + if (((gui::IGUIEditBox*) hovered)->isPasswordBox()) { + type = 3; + } + + porting::showInputDialog(gettext("ok"), "", + wide_to_utf8(((gui::IGUIEditBox*) hovered)->getText()), + type); + return retval; + } + } + + if (event.EventType == EET_TOUCH_INPUT_EVENT) + { + SEvent translated; + memset(&translated, 0, sizeof(SEvent)); + translated.EventType = EET_MOUSE_INPUT_EVENT; + gui::IGUIElement* root = Environment->getRootGUIElement(); + + if (!root) { + errorstream + << "GUIFormSpecMenu::preprocessEvent unable to get root element" + << std::endl; + return false; + } + gui::IGUIElement* hovered = root->getElementFromPoint( + core::position2d( + event.TouchInput.X, + event.TouchInput.Y)); + + translated.MouseInput.X = event.TouchInput.X; + translated.MouseInput.Y = event.TouchInput.Y; + translated.MouseInput.Control = false; + + bool dont_send_event = false; + + if (event.TouchInput.touchedCount == 1) { + switch (event.TouchInput.Event) { + case ETIE_PRESSED_DOWN: + m_pointer = v2s32(event.TouchInput.X,event.TouchInput.Y); + translated.MouseInput.Event = EMIE_LMOUSE_PRESSED_DOWN; + translated.MouseInput.ButtonStates = EMBSM_LEFT; + m_down_pos = m_pointer; + break; + case ETIE_MOVED: + m_pointer = v2s32(event.TouchInput.X,event.TouchInput.Y); + translated.MouseInput.Event = EMIE_MOUSE_MOVED; + translated.MouseInput.ButtonStates = EMBSM_LEFT; + break; + case ETIE_LEFT_UP: + translated.MouseInput.Event = EMIE_LMOUSE_LEFT_UP; + translated.MouseInput.ButtonStates = 0; + hovered = root->getElementFromPoint(m_down_pos); + /* we don't have a valid pointer element use last + * known pointer pos */ + translated.MouseInput.X = m_pointer.X; + translated.MouseInput.Y = m_pointer.Y; + + /* reset down pos */ + m_down_pos = v2s32(0,0); + break; + default: + dont_send_event = true; + //this is not supposed to happen + errorstream + << "GUIFormSpecMenu::preprocessEvent unexpected usecase Event=" + << event.TouchInput.Event << std::endl; + } + } else if ( (event.TouchInput.touchedCount == 2) && + (event.TouchInput.Event == ETIE_PRESSED_DOWN) ) { + hovered = root->getElementFromPoint(m_down_pos); + + translated.MouseInput.Event = EMIE_RMOUSE_PRESSED_DOWN; + translated.MouseInput.ButtonStates = EMBSM_LEFT | EMBSM_RIGHT; + translated.MouseInput.X = m_pointer.X; + translated.MouseInput.Y = m_pointer.Y; + + if (hovered) { + hovered->OnEvent(translated); + } + + translated.MouseInput.Event = EMIE_RMOUSE_LEFT_UP; + translated.MouseInput.ButtonStates = EMBSM_LEFT; + + + if (hovered) { + hovered->OnEvent(translated); + } + dont_send_event = true; + } + /* ignore unhandled 2 touch events ... accidental moving for example */ + else if (event.TouchInput.touchedCount == 2) { + dont_send_event = true; + } + else if (event.TouchInput.touchedCount > 2) { + errorstream + << "GUIFormSpecMenu::preprocessEvent to many multitouch events " + << event.TouchInput.touchedCount << " ignoring them" << std::endl; + } + + if (dont_send_event) { + return true; + } + + /* check if translated event needs to be preprocessed again */ + if (preprocessEvent(translated)) { + return true; + } + if (hovered) { + grab(); + bool retval = hovered->OnEvent(translated); + + if (event.TouchInput.Event == ETIE_LEFT_UP) { + /* reset pointer */ + m_pointer = v2s32(0,0); + } + drop(); + return retval; + } + } + #endif + + if (event.EventType == irr::EET_JOYSTICK_INPUT_EVENT) { + /* TODO add a check like: + if (event.JoystickEvent != joystick_we_listen_for) + return false; + */ + bool handled = m_joystick->handleEvent(event.JoystickEvent); + if (handled) { + if (m_joystick->wasKeyDown(KeyType::ESC)) { + tryClose(); + } else if (m_joystick->wasKeyDown(KeyType::JUMP)) { + if (m_allowclose) { + acceptInput(quit_mode_accept); + quitMenu(); + } + } + } + return handled; + } + + return false; +} + +/******************************************************************************/ +bool GUIFormSpecMenu::DoubleClickDetection(const SEvent event) +{ + /* The following code is for capturing double-clicks of the mouse button + * and translating the double-click into an EET_KEY_INPUT_EVENT event + * -- which closes the form -- under some circumstances. + * + * There have been many github issues reporting this as a bug even though it + * was an intended feature. For this reason, remapping the double-click as + * an ESC must be explicitly set when creating this class via the + * /p remap_dbl_click parameter of the constructor. + */ + + if (!m_remap_dbl_click) + return false; + + if (event.MouseInput.Event == EMIE_LMOUSE_PRESSED_DOWN) { + m_doubleclickdetect[0].pos = m_doubleclickdetect[1].pos; + m_doubleclickdetect[0].time = m_doubleclickdetect[1].time; + + m_doubleclickdetect[1].pos = m_pointer; + m_doubleclickdetect[1].time = porting::getTimeMs(); + } + else if (event.MouseInput.Event == EMIE_LMOUSE_LEFT_UP) { + u64 delta = porting::getDeltaMs(m_doubleclickdetect[0].time, porting::getTimeMs()); + if (delta > 400) { + return false; + } + + double squaredistance = + m_doubleclickdetect[0].pos + .getDistanceFromSQ(m_doubleclickdetect[1].pos); + + if (squaredistance > (30*30)) { + return false; + } + + SEvent* translated = new SEvent(); + assert(translated != 0); + //translate doubleclick to escape + memset(translated, 0, sizeof(SEvent)); + translated->EventType = irr::EET_KEY_INPUT_EVENT; + translated->KeyInput.Key = KEY_ESCAPE; + translated->KeyInput.Control = false; + translated->KeyInput.Shift = false; + translated->KeyInput.PressedDown = true; + translated->KeyInput.Char = 0; + OnEvent(*translated); + + // no need to send the key up event as we're already deleted + // and no one else did notice this event + delete translated; + return true; + } + + return false; +} + +void GUIFormSpecMenu::tryClose() +{ + if (m_allowclose) { + doPause = false; + acceptInput(quit_mode_cancel); + quitMenu(); + } else { + m_text_dst->gotText(L"MenuQuit"); + } +} + +bool GUIFormSpecMenu::OnEvent(const SEvent& event) +{ + if (event.EventType==EET_KEY_INPUT_EVENT) { + KeyPress kp(event.KeyInput); + if (event.KeyInput.PressedDown && ( + (kp == EscapeKey) || (kp == CancelKey) || + ((m_client != NULL) && (kp == getKeySetting("keymap_inventory"))))) { + tryClose(); + return true; + } + + if (m_client != NULL && event.KeyInput.PressedDown && + (kp == getKeySetting("keymap_screenshot"))) { + m_client->makeScreenshot(); + } + if (event.KeyInput.PressedDown && + (event.KeyInput.Key==KEY_RETURN || + event.KeyInput.Key==KEY_UP || + event.KeyInput.Key==KEY_DOWN) + ) { + switch (event.KeyInput.Key) { + case KEY_RETURN: + current_keys_pending.key_enter = true; + break; + case KEY_UP: + current_keys_pending.key_up = true; + break; + case KEY_DOWN: + current_keys_pending.key_down = true; + break; + break; + default: + //can't happen at all! + FATAL_ERROR("Reached a source line that can't ever been reached"); + break; + } + if (current_keys_pending.key_enter && m_allowclose) { + acceptInput(quit_mode_accept); + quitMenu(); + } else { + acceptInput(); + } + return true; + } + + } + + /* Mouse event other than movement, or crossing the border of inventory + field while holding right mouse button + */ + if (event.EventType == EET_MOUSE_INPUT_EVENT && + (event.MouseInput.Event != EMIE_MOUSE_MOVED || + (event.MouseInput.Event == EMIE_MOUSE_MOVED && + event.MouseInput.isRightPressed() && + getItemAtPos(m_pointer).i != getItemAtPos(m_old_pointer).i))) { + + // Get selected item and hovered/clicked item (s) + + m_old_tooltip_id = -1; + updateSelectedItem(); + ItemSpec s = getItemAtPos(m_pointer); + + Inventory *inv_selected = NULL; + Inventory *inv_s = NULL; + InventoryList *list_s = NULL; + + if (m_selected_item) { + inv_selected = m_invmgr->getInventory(m_selected_item->inventoryloc); + sanity_check(inv_selected); + sanity_check(inv_selected->getList(m_selected_item->listname) != NULL); + } + + u32 s_count = 0; + + if (s.isValid()) + do { // breakable + inv_s = m_invmgr->getInventory(s.inventoryloc); + + if (!inv_s) { + errorstream << "InventoryMenu: The selected inventory location " + << "\"" << s.inventoryloc.dump() << "\" doesn't exist" + << std::endl; + s.i = -1; // make it invalid again + break; + } + + list_s = inv_s->getList(s.listname); + if (list_s == NULL) { + verbosestream << "InventoryMenu: The selected inventory list \"" + << s.listname << "\" does not exist" << std::endl; + s.i = -1; // make it invalid again + break; + } + + if ((u32)s.i >= list_s->getSize()) { + infostream << "InventoryMenu: The selected inventory list \"" + << s.listname << "\" is too small (i=" << s.i << ", size=" + << list_s->getSize() << ")" << std::endl; + s.i = -1; // make it invalid again + break; + } + + s_count = list_s->getItem(s.i).count; + } while(0); + + bool identical = (m_selected_item != NULL) && s.isValid() && + (inv_selected == inv_s) && + (m_selected_item->listname == s.listname) && + (m_selected_item->i == s.i); + + // buttons: 0 = left, 1 = right, 2 = middle + // up/down: 0 = down (press), 1 = up (release), 2 = unknown event, -1 movement + int button = 0; + int updown = 2; + if (event.MouseInput.Event == EMIE_LMOUSE_PRESSED_DOWN) + { button = 0; updown = 0; } + else if (event.MouseInput.Event == EMIE_RMOUSE_PRESSED_DOWN) + { button = 1; updown = 0; } + else if (event.MouseInput.Event == EMIE_MMOUSE_PRESSED_DOWN) + { button = 2; updown = 0; } + else if (event.MouseInput.Event == EMIE_LMOUSE_LEFT_UP) + { button = 0; updown = 1; } + else if (event.MouseInput.Event == EMIE_RMOUSE_LEFT_UP) + { button = 1; updown = 1; } + else if (event.MouseInput.Event == EMIE_MMOUSE_LEFT_UP) + { button = 2; updown = 1; } + else if (event.MouseInput.Event == EMIE_MOUSE_MOVED) + { updown = -1;} + + // Set this number to a positive value to generate a move action + // from m_selected_item to s. + u32 move_amount = 0; + + // Set this number to a positive value to generate a move action + // from s to the next inventory ring. + u32 shift_move_amount = 0; + + // Set this number to a positive value to generate a drop action + // from m_selected_item. + u32 drop_amount = 0; + + // Set this number to a positive value to generate a craft action at s. + u32 craft_amount = 0; + + if (updown == 0) { + // Some mouse button has been pressed + + //infostream<<"Mouse button "<= 1); + + if (s.isValid()) { + // Clicked a slot: move + if (button == 1) // right + move_amount = 1; + else if (button == 2) // middle + move_amount = MYMIN(m_selected_amount, 10); + else // left + move_amount = m_selected_amount; + + if (identical) { + if (move_amount >= m_selected_amount) + m_selected_amount = 0; + else + m_selected_amount -= move_amount; + move_amount = 0; + } + } + else if (!getAbsoluteClippingRect().isPointInside(m_pointer)) { + // Clicked outside of the window: drop + if (button == 1) // right + drop_amount = 1; + else if (button == 2) // middle + drop_amount = MYMIN(m_selected_amount, 10); + else // left + drop_amount = m_selected_amount; + } + } + } + else if (updown == 1) { + // Some mouse button has been released + + //infostream<<"Mouse button "<getList(m_selected_item->listname); + InventoryList *list_to = list_s; + assert(list_from && list_to); + ItemStack stack_from = list_from->getItem(m_selected_item->i); + ItemStack stack_to = list_to->getItem(s.i); + if (stack_to.empty() || stack_to.name == stack_from.name) + move_amount = 1; + } + } + } + + // Possibly send inventory action to server + if (move_amount > 0) { + // Send IAction::Move + + assert(m_selected_item && m_selected_item->isValid()); + assert(s.isValid()); + + assert(inv_selected && inv_s); + InventoryList *list_from = inv_selected->getList(m_selected_item->listname); + InventoryList *list_to = list_s; + assert(list_from && list_to); + ItemStack stack_from = list_from->getItem(m_selected_item->i); + ItemStack stack_to = list_to->getItem(s.i); + + // Check how many items can be moved + move_amount = stack_from.count = MYMIN(move_amount, stack_from.count); + ItemStack leftover = stack_to.addItem(stack_from, m_client->idef()); + // If source stack cannot be added to destination stack at all, + // they are swapped + if ((leftover.count == stack_from.count) && + (leftover.name == stack_from.name)) { + m_selected_amount = stack_to.count; + // In case the server doesn't directly swap them but instead + // moves stack_to somewhere else, set this + m_selected_content_guess = stack_to; + m_selected_content_guess_inventory = s.inventoryloc; + } + // Source stack goes fully into destination stack + else if (leftover.empty()) { + m_selected_amount -= move_amount; + m_selected_content_guess = ItemStack(); // Clear + } + // Source stack goes partly into destination stack + else { + move_amount -= leftover.count; + m_selected_amount -= move_amount; + m_selected_content_guess = ItemStack(); // Clear + } + + infostream << "Handing IAction::Move to manager" << std::endl; + IMoveAction *a = new IMoveAction(); + a->count = move_amount; + a->from_inv = m_selected_item->inventoryloc; + a->from_list = m_selected_item->listname; + a->from_i = m_selected_item->i; + a->to_inv = s.inventoryloc; + a->to_list = s.listname; + a->to_i = s.i; + m_invmgr->inventoryAction(a); + } else if (shift_move_amount > 0) { + u32 mis = m_inventory_rings.size(); + u32 i = 0; + for (; i < mis; i++) { + const ListRingSpec &sp = m_inventory_rings[i]; + if (sp.inventoryloc == s.inventoryloc + && sp.listname == s.listname) + break; + } + do { + if (i >= mis) // if not found + break; + u32 to_inv_ind = (i + 1) % mis; + const ListRingSpec &to_inv_sp = m_inventory_rings[to_inv_ind]; + InventoryList *list_from = list_s; + if (!s.isValid()) + break; + Inventory *inv_to = m_invmgr->getInventory(to_inv_sp.inventoryloc); + if (!inv_to) + break; + InventoryList *list_to = inv_to->getList(to_inv_sp.listname); + if (!list_to) + break; + ItemStack stack_from = list_from->getItem(s.i); + assert(shift_move_amount <= stack_from.count); + if (m_client->getProtoVersion() >= 25) { + infostream << "Handing IAction::Move to manager" << std::endl; + IMoveAction *a = new IMoveAction(); + a->count = shift_move_amount; + a->from_inv = s.inventoryloc; + a->from_list = s.listname; + a->from_i = s.i; + a->to_inv = to_inv_sp.inventoryloc; + a->to_list = to_inv_sp.listname; + a->move_somewhere = true; + m_invmgr->inventoryAction(a); + } else { + // find a place (or more than one) to add the new item + u32 ilt_size = list_to->getSize(); + ItemStack leftover; + for (u32 slot_to = 0; slot_to < ilt_size + && shift_move_amount > 0; slot_to++) { + list_to->itemFits(slot_to, stack_from, &leftover); + if (leftover.count < stack_from.count) { + infostream << "Handing IAction::Move to manager" << std::endl; + IMoveAction *a = new IMoveAction(); + a->count = MYMIN(shift_move_amount, + (u32) (stack_from.count - leftover.count)); + shift_move_amount -= a->count; + a->from_inv = s.inventoryloc; + a->from_list = s.listname; + a->from_i = s.i; + a->to_inv = to_inv_sp.inventoryloc; + a->to_list = to_inv_sp.listname; + a->to_i = slot_to; + m_invmgr->inventoryAction(a); + stack_from = leftover; + } + } + } + } while (0); + } else if (drop_amount > 0) { + m_selected_content_guess = ItemStack(); // Clear + + // Send IAction::Drop + + assert(m_selected_item && m_selected_item->isValid()); + assert(inv_selected); + InventoryList *list_from = inv_selected->getList(m_selected_item->listname); + assert(list_from); + ItemStack stack_from = list_from->getItem(m_selected_item->i); + + // Check how many items can be dropped + drop_amount = stack_from.count = MYMIN(drop_amount, stack_from.count); + assert(drop_amount > 0 && drop_amount <= m_selected_amount); + m_selected_amount -= drop_amount; + + infostream << "Handing IAction::Drop to manager" << std::endl; + IDropAction *a = new IDropAction(); + a->count = drop_amount; + a->from_inv = m_selected_item->inventoryloc; + a->from_list = m_selected_item->listname; + a->from_i = m_selected_item->i; + m_invmgr->inventoryAction(a); + } else if (craft_amount > 0) { + m_selected_content_guess = ItemStack(); // Clear + + // Send IAction::Craft + + assert(s.isValid()); + assert(inv_s); + + infostream << "Handing IAction::Craft to manager" << std::endl; + ICraftAction *a = new ICraftAction(); + a->count = craft_amount; + a->craft_inv = s.inventoryloc; + m_invmgr->inventoryAction(a); + } + + // If m_selected_amount has been decreased to zero, deselect + if (m_selected_amount == 0) { + delete m_selected_item; + m_selected_item = NULL; + m_selected_amount = 0; + m_selected_dragging = false; + m_selected_content_guess = ItemStack(); + } + m_old_pointer = m_pointer; + } + if (event.EventType == EET_GUI_EVENT) { + + if (event.GUIEvent.EventType == gui::EGET_TAB_CHANGED + && isVisible()) { + // find the element that was clicked + for (GUIFormSpecMenu::FieldSpec &s : m_fields) { + if ((s.ftype == f_TabHeader) && + (s.fid == event.GUIEvent.Caller->getID())) { + s.send = true; + acceptInput(); + s.send = false; + return true; + } + } + } + if (event.GUIEvent.EventType == gui::EGET_ELEMENT_FOCUS_LOST + && isVisible()) { + if (!canTakeFocus(event.GUIEvent.Element)) { + infostream<<"GUIFormSpecMenu: Not allowing focus change." + <getID(); + + if (btn_id == 257) { + if (m_allowclose) { + acceptInput(quit_mode_accept); + quitMenu(); + } else { + acceptInput(); + m_text_dst->gotText(L"ExitButton"); + } + // quitMenu deallocates menu + return true; + } + + // find the element that was clicked + for (GUIFormSpecMenu::FieldSpec &s : m_fields) { + // if its a button, set the send field so + // lua knows which button was pressed + if ((s.ftype == f_Button || s.ftype == f_CheckBox) && + s.fid == event.GUIEvent.Caller->getID()) { + s.send = true; + if (s.is_exit) { + if (m_allowclose) { + acceptInput(quit_mode_accept); + quitMenu(); + } else { + m_text_dst->gotText(L"ExitButton"); + } + return true; + } + + acceptInput(quit_mode_no); + s.send = false; + return true; + + } else if ((s.ftype == f_DropDown) && + (s.fid == event.GUIEvent.Caller->getID())) { + // only send the changed dropdown + for (GUIFormSpecMenu::FieldSpec &s2 : m_fields) { + if (s2.ftype == f_DropDown) { + s2.send = false; + } + } + s.send = true; + acceptInput(quit_mode_no); + + // revert configuration to make sure dropdowns are sent on + // regular button click + for (GUIFormSpecMenu::FieldSpec &s2 : m_fields) { + if (s2.ftype == f_DropDown) { + s2.send = true; + } + } + return true; + } else if ((s.ftype == f_ScrollBar) && + (s.fid == event.GUIEvent.Caller->getID())) { + s.fdefault = L"Changed"; + acceptInput(quit_mode_no); + s.fdefault = L""; + } + } + } + + if (event.GUIEvent.EventType == gui::EGET_EDITBOX_ENTER) { + if (event.GUIEvent.Caller->getID() > 257) { + bool close_on_enter = true; + for (GUIFormSpecMenu::FieldSpec &s : m_fields) { + if (s.ftype == f_Unknown && + s.fid == event.GUIEvent.Caller->getID()) { + current_field_enter_pending = s.fname; + std::unordered_map::const_iterator it = + field_close_on_enter.find(s.fname); + if (it != field_close_on_enter.end()) + close_on_enter = (*it).second; + + break; + } + } + + if (m_allowclose && close_on_enter) { + current_keys_pending.key_enter = true; + acceptInput(quit_mode_accept); + quitMenu(); + } else { + current_keys_pending.key_enter = true; + acceptInput(); + } + // quitMenu deallocates menu + return true; + } + } + + if (event.GUIEvent.EventType == gui::EGET_TABLE_CHANGED) { + int current_id = event.GUIEvent.Caller->getID(); + if (current_id > 257) { + // find the element that was clicked + for (GUIFormSpecMenu::FieldSpec &s : m_fields) { + // if it's a table, set the send field + // so lua knows which table was changed + if ((s.ftype == f_Table) && (s.fid == current_id)) { + s.send = true; + acceptInput(); + s.send=false; + } + } + return true; + } + } + } + + return Parent ? Parent->OnEvent(event) : false; +} + +/** + * get name of element by element id + * @param id of element + * @return name string or empty string + */ +std::string GUIFormSpecMenu::getNameByID(s32 id) +{ + for (FieldSpec &spec : m_fields) { + if (spec.fid == id) { + return spec.fname; + } + } + return ""; +} + +/** + * get label of element by id + * @param id of element + * @return label string or empty string + */ +std::wstring GUIFormSpecMenu::getLabelByID(s32 id) +{ + for (FieldSpec &spec : m_fields) { + if (spec.fid == id) { + return spec.flabel; + } + } + return L""; +} diff --git a/src/gui/guiFormSpecMenu.h b/src/gui/guiFormSpecMenu.h new file mode 100644 index 000000000..071efb37f --- /dev/null +++ b/src/gui/guiFormSpecMenu.h @@ -0,0 +1,565 @@ +/* +Minetest +Copyright (C) 2013 celeron55, Perttu Ahola + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#pragma once + +#include +#include + +#include "irrlichttypes_extrabloated.h" +#include "inventorymanager.h" +#include "modalMenu.h" +#include "guiTable.h" +#include "network/networkprotocol.h" +#include "client/joystick_controller.h" +#include "util/string.h" +#include "util/enriched_string.h" + +class InventoryManager; +class ISimpleTextureSource; +class Client; + +typedef enum { + f_Button, + f_Table, + f_TabHeader, + f_CheckBox, + f_DropDown, + f_ScrollBar, + f_Unknown +} FormspecFieldType; + +typedef enum { + quit_mode_no, + quit_mode_accept, + quit_mode_cancel +} FormspecQuitMode; + +struct TextDest +{ + virtual ~TextDest() = default; + + // This is deprecated I guess? -celeron55 + virtual void gotText(const std::wstring &text) {} + virtual void gotText(const StringMap &fields) = 0; + + std::string m_formname; +}; + +class IFormSource +{ +public: + virtual ~IFormSource() = default; + virtual const std::string &getForm() const = 0; + // Fill in variables in field text + virtual std::string resolveText(const std::string &str) { return str; } +}; + +class GUIFormSpecMenu : public GUIModalMenu +{ + struct ItemSpec + { + ItemSpec() = default; + + ItemSpec(const InventoryLocation &a_inventoryloc, + const std::string &a_listname, + s32 a_i) : + inventoryloc(a_inventoryloc), + listname(a_listname), + i(a_i) + { + } + + bool isValid() const { return i != -1; } + + InventoryLocation inventoryloc; + std::string listname; + s32 i = -1; + }; + + struct ListDrawSpec + { + ListDrawSpec() = default; + + ListDrawSpec(const InventoryLocation &a_inventoryloc, + const std::string &a_listname, + v2s32 a_pos, v2s32 a_geom, s32 a_start_item_i): + inventoryloc(a_inventoryloc), + listname(a_listname), + pos(a_pos), + geom(a_geom), + start_item_i(a_start_item_i) + { + } + + InventoryLocation inventoryloc; + std::string listname; + v2s32 pos; + v2s32 geom; + s32 start_item_i; + }; + + struct ListRingSpec + { + ListRingSpec() = default; + + ListRingSpec(const InventoryLocation &a_inventoryloc, + const std::string &a_listname): + inventoryloc(a_inventoryloc), + listname(a_listname) + { + } + + InventoryLocation inventoryloc; + std::string listname; + }; + + struct ImageDrawSpec + { + ImageDrawSpec(): + parent_button(NULL), + clip(false) + { + } + + ImageDrawSpec(const std::string &a_name, + const std::string &a_item_name, + gui::IGUIButton *a_parent_button, + const v2s32 &a_pos, const v2s32 &a_geom): + name(a_name), + item_name(a_item_name), + parent_button(a_parent_button), + pos(a_pos), + geom(a_geom), + scale(true), + clip(false) + { + } + + ImageDrawSpec(const std::string &a_name, + const std::string &a_item_name, + const v2s32 &a_pos, const v2s32 &a_geom): + name(a_name), + item_name(a_item_name), + parent_button(NULL), + pos(a_pos), + geom(a_geom), + scale(true), + clip(false) + { + } + + ImageDrawSpec(const std::string &a_name, + const v2s32 &a_pos, const v2s32 &a_geom, bool clip=false): + name(a_name), + parent_button(NULL), + pos(a_pos), + geom(a_geom), + scale(true), + clip(clip) + { + } + + ImageDrawSpec(const std::string &a_name, + const v2s32 &a_pos): + name(a_name), + parent_button(NULL), + pos(a_pos), + scale(false), + clip(false) + { + } + + std::string name; + std::string item_name; + gui::IGUIButton *parent_button; + v2s32 pos; + v2s32 geom; + bool scale; + bool clip; + }; + + struct FieldSpec + { + FieldSpec() = default; + + FieldSpec(const std::string &name, const std::wstring &label, + const std::wstring &default_text, int id) : + fname(name), + flabel(label), + fdefault(unescape_enriched(translate_string(default_text))), + fid(id), + send(false), + ftype(f_Unknown), + is_exit(false) + { + } + + std::string fname; + std::wstring flabel; + std::wstring fdefault; + int fid; + bool send; + FormspecFieldType ftype; + bool is_exit; + core::rect rect; + }; + + struct BoxDrawSpec + { + BoxDrawSpec(v2s32 a_pos, v2s32 a_geom,irr::video::SColor a_color): + pos(a_pos), + geom(a_geom), + color(a_color) + { + } + v2s32 pos; + v2s32 geom; + irr::video::SColor color; + }; + + struct TooltipSpec + { + TooltipSpec() = default; + TooltipSpec(const std::wstring &a_tooltip, irr::video::SColor a_bgcolor, + irr::video::SColor a_color): + tooltip(translate_string(a_tooltip)), + bgcolor(a_bgcolor), + color(a_color) + { + } + + std::wstring tooltip; + irr::video::SColor bgcolor; + irr::video::SColor color; + }; + + struct StaticTextSpec + { + StaticTextSpec(): + parent_button(NULL) + { + } + + StaticTextSpec(const std::wstring &a_text, + const core::rect &a_rect): + text(a_text), + rect(a_rect), + parent_button(NULL) + { + } + + StaticTextSpec(const std::wstring &a_text, + const core::rect &a_rect, + gui::IGUIButton *a_parent_button): + text(a_text), + rect(a_rect), + parent_button(a_parent_button) + { + } + + std::wstring text; + core::rect rect; + gui::IGUIButton *parent_button; + }; + +public: + GUIFormSpecMenu(JoystickController *joystick, + gui::IGUIElement* parent, s32 id, + IMenuManager *menumgr, + Client *client, + ISimpleTextureSource *tsrc, + IFormSource* fs_src, + TextDest* txt_dst, + bool remap_dbl_click = true); + + ~GUIFormSpecMenu(); + + void setFormSpec(const std::string &formspec_string, + const InventoryLocation ¤t_inventory_location) + { + m_formspec_string = formspec_string; + m_current_inventory_location = current_inventory_location; + regenerateGui(m_screensize_old); + } + + // form_src is deleted by this GUIFormSpecMenu + void setFormSource(IFormSource *form_src) + { + delete m_form_src; + m_form_src = form_src; + } + + // text_dst is deleted by this GUIFormSpecMenu + void setTextDest(TextDest *text_dst) + { + delete m_text_dst; + m_text_dst = text_dst; + } + + void allowClose(bool value) + { + m_allowclose = value; + } + + void lockSize(bool lock,v2u32 basescreensize=v2u32(0,0)) + { + m_lock = lock; + m_lockscreensize = basescreensize; + } + + void removeChildren(); + void setInitialFocus(); + + void setFocus(const std::string &elementname) + { + m_focused_element = elementname; + } + + /* + Remove and re-add (or reposition) stuff + */ + void regenerateGui(v2u32 screensize); + + ItemSpec getItemAtPos(v2s32 p) const; + void drawList(const ListDrawSpec &s, int phase, bool &item_hovered); + void drawSelectedItem(); + void drawMenu(); + void updateSelectedItem(); + ItemStack verifySelectedItem(); + + void acceptInput(FormspecQuitMode quitmode); + bool preprocessEvent(const SEvent& event); + bool OnEvent(const SEvent& event); + bool doPause; + bool pausesGame() { return doPause; } + + GUITable* getTable(const std::string &tablename); + std::vector* getDropDownValues(const std::string &name); + +#ifdef __ANDROID__ + bool getAndroidUIInput(); +#endif + +protected: + v2s32 getBasePos() const + { + return padding + offset + AbsoluteRect.UpperLeftCorner; + } + + v2s32 padding; + v2s32 spacing; + v2s32 imgsize; + v2s32 offset; + v2s32 pos_offset; + std::stack container_stack; + + InventoryManager *m_invmgr; + ISimpleTextureSource *m_tsrc; + Client *m_client; + + std::string m_formspec_string; + InventoryLocation m_current_inventory_location; + + std::vector m_inventorylists; + std::vector m_inventory_rings; + std::vector m_backgrounds; + std::vector m_images; + std::vector m_itemimages; + std::vector m_boxes; + std::unordered_map field_close_on_enter; + std::vector m_fields; + std::vector m_static_texts; + std::vector > m_tables; + std::vector > m_checkboxes; + std::map m_tooltips; + std::vector > m_scrollbars; + std::vector > > m_dropdowns; + + ItemSpec *m_selected_item = nullptr; + u32 m_selected_amount = 0; + bool m_selected_dragging = false; + + // WARNING: BLACK MAGIC + // Used to guess and keep up with some special things the server can do. + // If name is "", no guess exists. + ItemStack m_selected_content_guess; + InventoryLocation m_selected_content_guess_inventory; + + v2s32 m_pointer; + v2s32 m_old_pointer; // Mouse position after previous mouse event + gui::IGUIStaticText *m_tooltip_element = nullptr; + + u64 m_tooltip_show_delay; + bool m_tooltip_append_itemname; + u64 m_hovered_time = 0; + s32 m_old_tooltip_id = -1; + + bool m_auto_place = false; + + bool m_allowclose = true; + bool m_lock = false; + v2u32 m_lockscreensize; + + bool m_bgfullscreen; + bool m_slotborder; + video::SColor m_bgcolor; + video::SColor m_fullscreen_bgcolor; + video::SColor m_slotbg_n; + video::SColor m_slotbg_h; + video::SColor m_slotbordercolor; + video::SColor m_default_tooltip_bgcolor; + video::SColor m_default_tooltip_color; + +private: + IFormSource *m_form_src; + TextDest *m_text_dst; + u32 m_formspec_version = 0; + std::string m_focused_element = ""; + JoystickController *m_joystick; + + typedef struct { + bool explicit_size; + v2f invsize; + v2s32 size; + v2f32 offset; + v2f32 anchor; + core::rect rect; + v2s32 basepos; + v2u32 screensize; + std::string focused_fieldname; + GUITable::TableOptions table_options; + GUITable::TableColumns table_columns; + // used to restore table selection/scroll/treeview state + std::unordered_map table_dyndata; + } parserData; + + typedef struct { + bool key_up; + bool key_down; + bool key_enter; + bool key_escape; + } fs_key_pendig; + + fs_key_pendig current_keys_pending; + std::string current_field_enter_pending = ""; + + void parseElement(parserData* data, const std::string &element); + + void parseSize(parserData* data, const std::string &element); + void parseContainer(parserData* data, const std::string &element); + void parseContainerEnd(parserData* data); + void parseList(parserData* data, const std::string &element); + void parseListRing(parserData* data, const std::string &element); + void parseCheckbox(parserData* data, const std::string &element); + void parseImage(parserData* data, const std::string &element); + void parseItemImage(parserData* data, const std::string &element); + void parseButton(parserData* data, const std::string &element, + const std::string &typ); + void parseBackground(parserData* data, const std::string &element); + void parseTableOptions(parserData* data, const std::string &element); + void parseTableColumns(parserData* data, const std::string &element); + void parseTable(parserData* data, const std::string &element); + void parseTextList(parserData* data, const std::string &element); + void parseDropDown(parserData* data, const std::string &element); + void parseFieldCloseOnEnter(parserData *data, const std::string &element); + void parsePwdField(parserData* data, const std::string &element); + void parseField(parserData* data, const std::string &element, const std::string &type); + void parseSimpleField(parserData* data,std::vector &parts); + void parseTextArea(parserData* data,std::vector& parts, + const std::string &type); + void parseLabel(parserData* data, const std::string &element); + void parseVertLabel(parserData* data, const std::string &element); + void parseImageButton(parserData* data, const std::string &element, + const std::string &type); + void parseItemImageButton(parserData* data, const std::string &element); + void parseTabHeader(parserData* data, const std::string &element); + void parseBox(parserData* data, const std::string &element); + void parseBackgroundColor(parserData* data, const std::string &element); + void parseListColors(parserData* data, const std::string &element); + void parseTooltip(parserData* data, const std::string &element); + bool parseVersionDirect(const std::string &data); + bool parseSizeDirect(parserData* data, const std::string &element); + void parseScrollBar(parserData* data, const std::string &element); + bool parsePositionDirect(parserData *data, const std::string &element); + void parsePosition(parserData *data, const std::string &element); + bool parseAnchorDirect(parserData *data, const std::string &element); + void parseAnchor(parserData *data, const std::string &element); + + void tryClose(); + + void showTooltip(const std::wstring &text, const irr::video::SColor &color, + const irr::video::SColor &bgcolor); + + /** + * check if event is part of a double click + * @param event event to evaluate + * @return true/false if a doubleclick was detected + */ + bool DoubleClickDetection(const SEvent event); + + struct clickpos + { + v2s32 pos; + s64 time; + }; + clickpos m_doubleclickdetect[2]; + + int m_btn_height; + gui::IGUIFont *m_font = nullptr; + + std::wstring getLabelByID(s32 id); + std::string getNameByID(s32 id); +#ifdef __ANDROID__ + v2s32 m_down_pos; + std::string m_JavaDialogFieldName; +#endif + + /* If true, remap a double-click (or double-tap) action to ESC. This is so + * that, for example, Android users can double-tap to close a formspec. + * + * This value can (currently) only be set by the class constructor + * and the default value for the setting is true. + */ + bool m_remap_dbl_click; + +}; + +class FormspecFormSource: public IFormSource +{ +public: + FormspecFormSource(const std::string &formspec): + m_formspec(formspec) + { + } + + ~FormspecFormSource() = default; + + void setForm(const std::string &formspec) + { + m_formspec = FORMSPEC_VERSION_STRING + formspec; + } + + const std::string &getForm() const + { + return m_formspec; + } + + std::string m_formspec; +}; diff --git a/src/gui/guiKeyChangeMenu.cpp b/src/gui/guiKeyChangeMenu.cpp new file mode 100644 index 000000000..53677a57b --- /dev/null +++ b/src/gui/guiKeyChangeMenu.cpp @@ -0,0 +1,436 @@ +/* + Minetest + Copyright (C) 2010-2013 celeron55, Perttu Ahola + Copyright (C) 2013 Ciaran Gultnieks + Copyright (C) 2013 teddydestodes + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2.1 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "guiKeyChangeMenu.h" +#include "debug.h" +#include "serialization.h" +#include +#include +#include +#include +#include +#include +#include "settings.h" +#include + +#include "mainmenumanager.h" // for g_gamecallback + +#define KMaxButtonPerColumns 12 + +extern MainGameCallback *g_gamecallback; + +enum +{ + GUI_ID_BACK_BUTTON = 101, GUI_ID_ABORT_BUTTON, GUI_ID_SCROLL_BAR, + // buttons + GUI_ID_KEY_FORWARD_BUTTON, + GUI_ID_KEY_BACKWARD_BUTTON, + GUI_ID_KEY_LEFT_BUTTON, + GUI_ID_KEY_RIGHT_BUTTON, + GUI_ID_KEY_USE_BUTTON, + GUI_ID_KEY_FLY_BUTTON, + GUI_ID_KEY_FAST_BUTTON, + GUI_ID_KEY_JUMP_BUTTON, + GUI_ID_KEY_NOCLIP_BUTTON, + GUI_ID_KEY_CINEMATIC_BUTTON, + GUI_ID_KEY_CHAT_BUTTON, + GUI_ID_KEY_CMD_BUTTON, + GUI_ID_KEY_CMD_LOCAL_BUTTON, + GUI_ID_KEY_CONSOLE_BUTTON, + GUI_ID_KEY_SNEAK_BUTTON, + GUI_ID_KEY_DROP_BUTTON, + GUI_ID_KEY_INVENTORY_BUTTON, + GUI_ID_KEY_HOTBAR_PREV_BUTTON, + GUI_ID_KEY_HOTBAR_NEXT_BUTTON, + GUI_ID_KEY_MUTE_BUTTON, + GUI_ID_KEY_DEC_VOLUME_BUTTON, + GUI_ID_KEY_INC_VOLUME_BUTTON, + GUI_ID_KEY_RANGE_BUTTON, + GUI_ID_KEY_ZOOM_BUTTON, + GUI_ID_KEY_CAMERA_BUTTON, + GUI_ID_KEY_MINIMAP_BUTTON, + GUI_ID_KEY_SCREENSHOT_BUTTON, + GUI_ID_KEY_CHATLOG_BUTTON, + GUI_ID_KEY_HUD_BUTTON, + GUI_ID_KEY_FOG_BUTTON, + GUI_ID_KEY_DEC_RANGE_BUTTON, + GUI_ID_KEY_INC_RANGE_BUTTON, + GUI_ID_KEY_AUTOFWD_BUTTON, + // other + GUI_ID_CB_AUX1_DESCENDS, + GUI_ID_CB_DOUBLETAP_JUMP, +}; + +GUIKeyChangeMenu::GUIKeyChangeMenu(gui::IGUIEnvironment* env, + gui::IGUIElement* parent, s32 id, IMenuManager *menumgr) : +GUIModalMenu(env, parent, id, menumgr) +{ + init_keys(); + for (key_setting *ks : key_settings) + key_used.push_back(ks->key); +} + +GUIKeyChangeMenu::~GUIKeyChangeMenu() +{ + removeChildren(); + + for (key_setting *ks : key_settings) { + delete[] ks->button_name; + delete ks; + } + key_settings.clear(); +} + +void GUIKeyChangeMenu::removeChildren() +{ + const core::list &children = getChildren(); + core::list children_copy; + for (gui::IGUIElement*i : children) { + children_copy.push_back(i); + } + + for (gui::IGUIElement *i : children_copy) { + i->remove(); + } +} + +void GUIKeyChangeMenu::regenerateGui(v2u32 screensize) +{ + removeChildren(); + v2s32 size(745, 430); + + core::rect < s32 > rect(screensize.X / 2 - size.X / 2, + screensize.Y / 2 - size.Y / 2, screensize.X / 2 + size.X / 2, + screensize.Y / 2 + size.Y / 2); + + DesiredRect = rect; + recalculateAbsolutePosition(false); + + v2s32 topleft(0, 0); + + { + core::rect < s32 > rect(0, 0, 600, 40); + rect += topleft + v2s32(25, 3); + //gui::IGUIStaticText *t = + const wchar_t *text = wgettext("Keybindings. (If this menu screws up, remove stuff from minetest.conf)"); + Environment->addStaticText(text, + rect, false, true, this, -1); + delete[] text; + //t->setTextAlignment(gui::EGUIA_CENTER, gui::EGUIA_UPPERLEFT); + } + + // Build buttons + + v2s32 offset(25, 60); + + for(size_t i = 0; i < key_settings.size(); i++) + { + key_setting *k = key_settings.at(i); + { + core::rect < s32 > rect(0, 0, 150, 20); + rect += topleft + v2s32(offset.X, offset.Y); + Environment->addStaticText(k->button_name, rect, false, true, this, -1); + } + + { + core::rect < s32 > rect(0, 0, 100, 30); + rect += topleft + v2s32(offset.X + 120, offset.Y - 5); + const wchar_t *text = wgettext(k->key.name()); + k->button = Environment->addButton(rect, this, k->id, text); + delete[] text; + } + if ((i + 1) % KMaxButtonPerColumns == 0) { + offset.X += 230; + offset.Y = 60; + } else { + offset += v2s32(0, 25); + } + } + + { + s32 option_x = offset.X; + s32 option_y = offset.Y + 5; + u32 option_w = 180; + { + core::rect rect(0, 0, option_w, 30); + rect += topleft + v2s32(option_x, option_y); + const wchar_t *text = wgettext("\"Special\" = climb down"); + Environment->addCheckBox(g_settings->getBool("aux1_descends"), rect, this, + GUI_ID_CB_AUX1_DESCENDS, text); + delete[] text; + } + offset += v2s32(0, 25); + } + + { + s32 option_x = offset.X; + s32 option_y = offset.Y + 5; + u32 option_w = 280; + { + core::rect rect(0, 0, option_w, 30); + rect += topleft + v2s32(option_x, option_y); + const wchar_t *text = wgettext("Double tap \"jump\" to toggle fly"); + Environment->addCheckBox(g_settings->getBool("doubletap_jump"), rect, this, + GUI_ID_CB_DOUBLETAP_JUMP, text); + delete[] text; + } + offset += v2s32(0, 25); + } + + { + core::rect < s32 > rect(0, 0, 100, 30); + rect += topleft + v2s32(size.X / 2 - 105, size.Y - 40); + const wchar_t *text = wgettext("Save"); + Environment->addButton(rect, this, GUI_ID_BACK_BUTTON, + text); + delete[] text; + } + { + core::rect < s32 > rect(0, 0, 100, 30); + rect += topleft + v2s32(size.X / 2 + 5, size.Y - 40); + const wchar_t *text = wgettext("Cancel"); + Environment->addButton(rect, this, GUI_ID_ABORT_BUTTON, + text); + delete[] text; + } +} + +void GUIKeyChangeMenu::drawMenu() +{ + gui::IGUISkin* skin = Environment->getSkin(); + if (!skin) + return; + video::IVideoDriver* driver = Environment->getVideoDriver(); + + video::SColor bgcolor(140, 0, 0, 0); + + { + core::rect < s32 > rect(0, 0, 745, 620); + rect += AbsoluteRect.UpperLeftCorner; + driver->draw2DRectangle(bgcolor, rect, &AbsoluteClippingRect); + } + + gui::IGUIElement::draw(); +} + +bool GUIKeyChangeMenu::acceptInput() +{ + for (key_setting *k : key_settings) { + g_settings->set(k->setting_name, k->key.sym()); + } + + { + gui::IGUIElement *e = getElementFromId(GUI_ID_CB_AUX1_DESCENDS); + if(e != NULL && e->getType() == gui::EGUIET_CHECK_BOX) + g_settings->setBool("aux1_descends", ((gui::IGUICheckBox*)e)->isChecked()); + } + { + gui::IGUIElement *e = getElementFromId(GUI_ID_CB_DOUBLETAP_JUMP); + if(e != NULL && e->getType() == gui::EGUIET_CHECK_BOX) + g_settings->setBool("doubletap_jump", ((gui::IGUICheckBox*)e)->isChecked()); + } + + clearKeyCache(); + + g_gamecallback->signalKeyConfigChange(); + + return true; +} + +bool GUIKeyChangeMenu::resetMenu() +{ + if (activeKey >= 0) + { + for (key_setting *k : key_settings) { + if (k->id == activeKey) { + const wchar_t *text = wgettext(k->key.name()); + k->button->setText(text); + delete[] text; + break; + } + } + activeKey = -1; + return false; + } + return true; +} +bool GUIKeyChangeMenu::OnEvent(const SEvent& event) +{ + if (event.EventType == EET_KEY_INPUT_EVENT && activeKey >= 0 + && event.KeyInput.PressedDown) { + + bool prefer_character = shift_down; + KeyPress kp(event.KeyInput, prefer_character); + + bool shift_went_down = false; + if(!shift_down && + (event.KeyInput.Key == irr::KEY_SHIFT || + event.KeyInput.Key == irr::KEY_LSHIFT || + event.KeyInput.Key == irr::KEY_RSHIFT)) + shift_went_down = true; + + // Remove Key already in use message + if(this->key_used_text) + { + this->key_used_text->remove(); + this->key_used_text = NULL; + } + // Display Key already in use message + if (std::find(this->key_used.begin(), this->key_used.end(), kp) != this->key_used.end()) + { + core::rect < s32 > rect(0, 0, 600, 40); + rect += v2s32(0, 0) + v2s32(25, 30); + const wchar_t *text = wgettext("Key already in use"); + this->key_used_text = Environment->addStaticText(text, + rect, false, true, this, -1); + delete[] text; + //infostream << "Key already in use" << std::endl; + } + + // But go on + { + key_setting *k = NULL; + for (key_setting *ks : key_settings) { + if (ks->id == activeKey) { + k = ks; + break; + } + } + FATAL_ERROR_IF(k == NULL, "Key setting not found"); + k->key = kp; + const wchar_t *text = wgettext(k->key.name()); + k->button->setText(text); + delete[] text; + + this->key_used.push_back(kp); + + // Allow characters made with shift + if(shift_went_down){ + shift_down = true; + return false; + } + + activeKey = -1; + return true; + } + } else if (event.EventType == EET_KEY_INPUT_EVENT && activeKey < 0 + && event.KeyInput.PressedDown + && event.KeyInput.Key == irr::KEY_ESCAPE) { + quitMenu(); + return true; + } else if (event.EventType == EET_GUI_EVENT) { + if (event.GUIEvent.EventType == gui::EGET_ELEMENT_FOCUS_LOST + && isVisible()) + { + if (!canTakeFocus(event.GUIEvent.Element)) + { + dstream << "GUIMainMenu: Not allowing focus change." + << std::endl; + // Returning true disables focus change + return true; + } + } + if (event.GUIEvent.EventType == gui::EGET_BUTTON_CLICKED) + { + switch (event.GUIEvent.Caller->getID()) + { + case GUI_ID_BACK_BUTTON: //back + acceptInput(); + quitMenu(); + return true; + case GUI_ID_ABORT_BUTTON: //abort + quitMenu(); + return true; + default: + key_setting *k = NULL; + + for (key_setting *ks : key_settings) { + if (ks->id == event.GUIEvent.Caller->getID()) { + k = ks; + break; + } + } + FATAL_ERROR_IF(k == NULL, "Key setting not found"); + + resetMenu(); + shift_down = false; + activeKey = event.GUIEvent.Caller->getID(); + const wchar_t *text = wgettext("press key"); + k->button->setText(text); + delete[] text; + this->key_used.erase(std::remove(this->key_used.begin(), + this->key_used.end(), k->key), this->key_used.end()); + break; + } + Environment->setFocus(this); + } + } + return Parent ? Parent->OnEvent(event) : false; +} + +void GUIKeyChangeMenu::add_key(int id, const wchar_t *button_name, const std::string &setting_name) +{ + key_setting *k = new key_setting; + k->id = id; + + k->button_name = button_name; + k->setting_name = setting_name; + k->key = getKeySetting(k->setting_name.c_str()); + key_settings.push_back(k); +} + +void GUIKeyChangeMenu::init_keys() +{ + this->add_key(GUI_ID_KEY_FORWARD_BUTTON, wgettext("Forward"), "keymap_forward"); + this->add_key(GUI_ID_KEY_BACKWARD_BUTTON, wgettext("Backward"), "keymap_backward"); + this->add_key(GUI_ID_KEY_LEFT_BUTTON, wgettext("Left"), "keymap_left"); + this->add_key(GUI_ID_KEY_RIGHT_BUTTON, wgettext("Right"), "keymap_right"); + this->add_key(GUI_ID_KEY_USE_BUTTON, wgettext("Special"), "keymap_special1"); + this->add_key(GUI_ID_KEY_JUMP_BUTTON, wgettext("Jump"), "keymap_jump"); + this->add_key(GUI_ID_KEY_SNEAK_BUTTON, wgettext("Sneak"), "keymap_sneak"); + this->add_key(GUI_ID_KEY_DROP_BUTTON, wgettext("Drop"), "keymap_drop"); + this->add_key(GUI_ID_KEY_INVENTORY_BUTTON, wgettext("Inventory"), "keymap_inventory"); + this->add_key(GUI_ID_KEY_HOTBAR_PREV_BUTTON,wgettext("Prev. item"), "keymap_hotbar_previous"); + this->add_key(GUI_ID_KEY_HOTBAR_NEXT_BUTTON,wgettext("Next item"), "keymap_hotbar_next"); + this->add_key(GUI_ID_KEY_ZOOM_BUTTON, wgettext("Zoom"), "keymap_zoom"); + this->add_key(GUI_ID_KEY_CAMERA_BUTTON, wgettext("Change camera"), "keymap_camera_mode"); + this->add_key(GUI_ID_KEY_CINEMATIC_BUTTON, wgettext("Toggle Cinematic"), "keymap_cinematic"); + this->add_key(GUI_ID_KEY_MINIMAP_BUTTON, wgettext("Toggle minimap"), "keymap_minimap"); + this->add_key(GUI_ID_KEY_FLY_BUTTON, wgettext("Toggle fly"), "keymap_freemove"); + this->add_key(GUI_ID_KEY_FAST_BUTTON, wgettext("Toggle fast"), "keymap_fastmove"); + this->add_key(GUI_ID_KEY_NOCLIP_BUTTON, wgettext("Toggle noclip"), "keymap_noclip"); + this->add_key(GUI_ID_KEY_MUTE_BUTTON, wgettext("Mute"), "keymap_mute"); + this->add_key(GUI_ID_KEY_DEC_VOLUME_BUTTON,wgettext("Dec. volume"), "keymap_decrease_volume"); + this->add_key(GUI_ID_KEY_INC_VOLUME_BUTTON,wgettext("Inc. volume"), "keymap_increase_volume"); + this->add_key(GUI_ID_KEY_AUTOFWD_BUTTON, wgettext("Autoforward"), "keymap_autoforward"); + this->add_key(GUI_ID_KEY_CHAT_BUTTON, wgettext("Chat"), "keymap_chat"); + this->add_key(GUI_ID_KEY_SCREENSHOT_BUTTON,wgettext("Screenshot"), "keymap_screenshot"); + this->add_key(GUI_ID_KEY_RANGE_BUTTON, wgettext("Range select"), "keymap_rangeselect"); + this->add_key(GUI_ID_KEY_DEC_RANGE_BUTTON, wgettext("Dec. range"), "keymap_decrease_viewing_range_min"); + this->add_key(GUI_ID_KEY_INC_RANGE_BUTTON, wgettext("Inc. range"), "keymap_increase_viewing_range_min"); + this->add_key(GUI_ID_KEY_CONSOLE_BUTTON, wgettext("Console"), "keymap_console"); + this->add_key(GUI_ID_KEY_CMD_BUTTON, wgettext("Command"), "keymap_cmd"); + this->add_key(GUI_ID_KEY_CMD_LOCAL_BUTTON, wgettext("Local command"), "keymap_cmd_local"); + this->add_key(GUI_ID_KEY_HUD_BUTTON, wgettext("Toggle HUD"), "keymap_toggle_hud"); + this->add_key(GUI_ID_KEY_CHATLOG_BUTTON, wgettext("Toggle chat log"), "keymap_toggle_chat"); + this->add_key(GUI_ID_KEY_FOG_BUTTON, wgettext("Toggle fog"), "keymap_toggle_force_fog_off"); +} + diff --git a/src/gui/guiKeyChangeMenu.h b/src/gui/guiKeyChangeMenu.h new file mode 100644 index 000000000..7cf11d3f9 --- /dev/null +++ b/src/gui/guiKeyChangeMenu.h @@ -0,0 +1,74 @@ +/* + Minetest + Copyright (C) 2010-2013 celeron55, Perttu Ahola + Copyright (C) 2013 Ciaran Gultnieks + Copyright (C) 2013 teddydestodes + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2.1 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include "irrlichttypes_extrabloated.h" +#include "modalMenu.h" +#include "gettext.h" +#include "keycode.h" +#include +#include + +struct key_setting +{ + int id; + const wchar_t *button_name; + KeyPress key; + std::string setting_name; + gui::IGUIButton *button; +}; + +class GUIKeyChangeMenu : public GUIModalMenu +{ +public: + GUIKeyChangeMenu(gui::IGUIEnvironment *env, gui::IGUIElement *parent, s32 id, + IMenuManager *menumgr); + ~GUIKeyChangeMenu(); + + void removeChildren(); + /* + Remove and re-add (or reposition) stuff + */ + void regenerateGui(v2u32 screensize); + + void drawMenu(); + + bool acceptInput(); + + bool OnEvent(const SEvent &event); + + bool pausesGame() { return true; } + +private: + void init_keys(); + + bool resetMenu(); + + void add_key(int id, const wchar_t *button_name, const std::string &setting_name); + + bool shift_down = false; + s32 activeKey = -1; + + std::vector key_used; + gui::IGUIStaticText *key_used_text = nullptr; + std::vector key_settings; +}; diff --git a/src/gui/guiMainMenu.h b/src/gui/guiMainMenu.h new file mode 100644 index 000000000..43a3b1a33 --- /dev/null +++ b/src/gui/guiMainMenu.h @@ -0,0 +1,55 @@ +/* +Minetest +Copyright (C) 2010-2013 celeron55, Perttu Ahola + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#pragma once + +#include "irrlichttypes_extrabloated.h" +#include "modalMenu.h" +#include +#include + +struct MainMenuDataForScript { + + MainMenuDataForScript() = default; + + // Whether the server has requested a reconnect + bool reconnect_requested = false; + std::string errormessage = ""; +}; + +struct MainMenuData { + // Client options + std::string servername; + std::string serverdescription; + std::string address; + std::string port; + std::string name; + std::string password; + // Whether to reconnect + bool do_reconnect = false; + + // Server options + int selected_world = 0; + bool simple_singleplayer_mode = false; + + // Data to be passed to the script + MainMenuDataForScript script_data; + + MainMenuData() = default; +}; diff --git a/src/gui/guiPasswordChange.cpp b/src/gui/guiPasswordChange.cpp new file mode 100644 index 000000000..46de2026c --- /dev/null +++ b/src/gui/guiPasswordChange.cpp @@ -0,0 +1,261 @@ +/* +Part of Minetest +Copyright (C) 2013 celeron55, Perttu Ahola +Copyright (C) 2013 Ciaran Gultnieks + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ + +#include "guiPasswordChange.h" +#include "client.h" +#include +#include +#include +#include +#include + +#include "gettext.h" + +const int ID_oldPassword = 256; +const int ID_newPassword1 = 257; +const int ID_newPassword2 = 258; +const int ID_change = 259; +const int ID_message = 260; +const int ID_cancel = 261; + +GUIPasswordChange::GUIPasswordChange(gui::IGUIEnvironment* env, + gui::IGUIElement* parent, s32 id, + IMenuManager *menumgr, + Client* client +): + GUIModalMenu(env, parent, id, menumgr), + m_client(client) +{ +} + +GUIPasswordChange::~GUIPasswordChange() +{ + removeChildren(); +} + +void GUIPasswordChange::removeChildren() +{ + const core::list &children = getChildren(); + core::list children_copy; + for (gui::IGUIElement *i : children) { + children_copy.push_back(i); + } + + for (gui::IGUIElement *i : children_copy) { + i->remove(); + } +} +void GUIPasswordChange::regenerateGui(v2u32 screensize) +{ + /* + save current input + */ + acceptInput(); + + /* + Remove stuff + */ + removeChildren(); + + /* + Calculate new sizes and positions + */ + core::rect rect( + screensize.X/2 - 580/2, + screensize.Y/2 - 300/2, + screensize.X/2 + 580/2, + screensize.Y/2 + 300/2 + ); + + DesiredRect = rect; + recalculateAbsolutePosition(false); + + v2s32 size = rect.getSize(); + v2s32 topleft_client(40, 0); + + const wchar_t *text; + + /* + Add stuff + */ + s32 ypos = 50; + { + core::rect rect(0, 0, 150, 20); + rect += topleft_client + v2s32(25, ypos + 6); + text = wgettext("Old Password"); + Environment->addStaticText(text, rect, false, true, this, -1); + delete[] text; + } + { + core::rect rect(0, 0, 230, 30); + rect += topleft_client + v2s32(160, ypos); + gui::IGUIEditBox *e = Environment->addEditBox( + m_oldpass.c_str(), rect, true, this, ID_oldPassword); + Environment->setFocus(e); + e->setPasswordBox(true); + } + ypos += 50; + { + core::rect rect(0, 0, 150, 20); + rect += topleft_client + v2s32(25, ypos + 6); + text = wgettext("New Password"); + Environment->addStaticText(text, rect, false, true, this, -1); + delete[] text; + } + { + core::rect rect(0, 0, 230, 30); + rect += topleft_client + v2s32(160, ypos); + gui::IGUIEditBox *e = Environment->addEditBox( + m_newpass.c_str(), rect, true, this, ID_newPassword1); + e->setPasswordBox(true); + } + ypos += 50; + { + core::rect rect(0, 0, 150, 20); + rect += topleft_client + v2s32(25, ypos + 6); + text = wgettext("Confirm Password"); + Environment->addStaticText(text, rect, false, true, this, -1); + delete[] text; + } + { + core::rect rect(0, 0, 230, 30); + rect += topleft_client + v2s32(160, ypos); + gui::IGUIEditBox *e = Environment->addEditBox( + m_newpass_confirm.c_str(), rect, true, this, ID_newPassword2); + e->setPasswordBox(true); + } + + ypos += 50; + { + core::rect rect(0, 0, 100, 30); + rect = rect + v2s32(size.X / 4 + 56, ypos); + text = wgettext("Change"); + Environment->addButton(rect, this, ID_change, text); + delete[] text; + } + { + core::rect rect(0, 0, 100, 30); + rect = rect + v2s32(size.X / 4 + 185, ypos); + text = wgettext("Cancel"); + Environment->addButton(rect, this, ID_cancel, text); + delete[] text; + } + + ypos += 50; + { + core::rect rect(0, 0, 300, 20); + rect += topleft_client + v2s32(35, ypos); + text = wgettext("Passwords do not match!"); + IGUIElement *e = + Environment->addStaticText( + text, rect, false, true, this, ID_message); + e->setVisible(false); + delete[] text; + } +} + +void GUIPasswordChange::drawMenu() +{ + gui::IGUISkin *skin = Environment->getSkin(); + if (!skin) + return; + video::IVideoDriver *driver = Environment->getVideoDriver(); + + video::SColor bgcolor(140, 0, 0, 0); + driver->draw2DRectangle(bgcolor, AbsoluteRect, &AbsoluteClippingRect); + + gui::IGUIElement::draw(); +} + +void GUIPasswordChange::acceptInput() +{ + gui::IGUIElement *e; + e = getElementFromId(ID_oldPassword); + if (e != NULL) + m_oldpass = e->getText(); + e = getElementFromId(ID_newPassword1); + if (e != NULL) + m_newpass = e->getText(); + e = getElementFromId(ID_newPassword2); + if (e != NULL) + m_newpass_confirm = e->getText(); +} + +bool GUIPasswordChange::processInput() +{ + if (m_newpass != m_newpass_confirm) { + gui::IGUIElement *e = getElementFromId(ID_message); + if (e != NULL) + e->setVisible(true); + return false; + } + m_client->sendChangePassword(wide_to_utf8(m_oldpass), wide_to_utf8(m_newpass)); + return true; +} + +bool GUIPasswordChange::OnEvent(const SEvent &event) +{ + if (event.EventType == EET_KEY_INPUT_EVENT) { + if (event.KeyInput.Key == KEY_ESCAPE && event.KeyInput.PressedDown) { + quitMenu(); + return true; + } + if (event.KeyInput.Key == KEY_RETURN && event.KeyInput.PressedDown) { + acceptInput(); + if (processInput()) + quitMenu(); + return true; + } + } + if (event.EventType == EET_GUI_EVENT) { + if (event.GUIEvent.EventType == gui::EGET_ELEMENT_FOCUS_LOST && + isVisible()) { + if (!canTakeFocus(event.GUIEvent.Element)) { + dstream << "GUIPasswordChange: Not allowing focus change." + << std::endl; + // Returning true disables focus change + return true; + } + } + if (event.GUIEvent.EventType == gui::EGET_BUTTON_CLICKED) { + switch (event.GUIEvent.Caller->getID()) { + case ID_change: + acceptInput(); + if (processInput()) + quitMenu(); + return true; + case ID_cancel: + quitMenu(); + return true; + } + } + if (event.GUIEvent.EventType == gui::EGET_EDITBOX_ENTER) { + switch (event.GUIEvent.Caller->getID()) { + case ID_oldPassword: + case ID_newPassword1: + case ID_newPassword2: + acceptInput(); + if (processInput()) + quitMenu(); + return true; + } + } + } + + return Parent ? Parent->OnEvent(event) : false; +} diff --git a/src/gui/guiPasswordChange.h b/src/gui/guiPasswordChange.h new file mode 100644 index 000000000..59f3513b2 --- /dev/null +++ b/src/gui/guiPasswordChange.h @@ -0,0 +1,53 @@ +/* +Part of Minetest +Copyright (C) 2010-2013 celeron55, Perttu Ahola +Copyright (C) 2013 Ciaran Gultnieks + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ + +#pragma once + +#include "irrlichttypes_extrabloated.h" +#include "modalMenu.h" +#include + +class Client; + +class GUIPasswordChange : public GUIModalMenu +{ +public: + GUIPasswordChange(gui::IGUIEnvironment *env, gui::IGUIElement *parent, s32 id, + IMenuManager *menumgr, Client *client); + ~GUIPasswordChange(); + + void removeChildren(); + /* + Remove and re-add (or reposition) stuff + */ + void regenerateGui(v2u32 screensize); + + void drawMenu(); + + void acceptInput(); + + bool processInput(); + + bool OnEvent(const SEvent &event); + +private: + Client *m_client; + std::wstring m_oldpass = L""; + std::wstring m_newpass = L""; + std::wstring m_newpass_confirm = L""; +}; diff --git a/src/gui/guiPathSelectMenu.cpp b/src/gui/guiPathSelectMenu.cpp new file mode 100644 index 000000000..b999f0a68 --- /dev/null +++ b/src/gui/guiPathSelectMenu.cpp @@ -0,0 +1,113 @@ +/* + Minetest + Copyright (C) 2013 sapier + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2.1 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "guiPathSelectMenu.h" + +GUIFileSelectMenu::GUIFileSelectMenu(gui::IGUIEnvironment* env, + gui::IGUIElement* parent, s32 id, IMenuManager *menumgr, + const std::string &title, const std::string &formname, + bool is_file_select) : + GUIModalMenu(env, parent, id, menumgr), + m_title(utf8_to_wide(title)), + m_formname(formname), + m_file_select_dialog(is_file_select) +{ +} + +GUIFileSelectMenu::~GUIFileSelectMenu() +{ + removeChildren(); + setlocale(LC_NUMERIC, "C"); +} + +void GUIFileSelectMenu::regenerateGui(v2u32 screensize) +{ + removeChildren(); + m_fileOpenDialog = 0; + + core::dimension2du size(600, 400); + core::rect rect(0, 0, screensize.X, screensize.Y); + + DesiredRect = rect; + recalculateAbsolutePosition(false); + + m_fileOpenDialog = + Environment->addFileOpenDialog(m_title.c_str(), false, this, -1); + + core::position2di pos = core::position2di(screensize.X / 2 - size.Width / 2, + screensize.Y / 2 - size.Height / 2); + m_fileOpenDialog->setRelativePosition(pos); + m_fileOpenDialog->setMinSize(size); +} + +void GUIFileSelectMenu::drawMenu() +{ + gui::IGUISkin *skin = Environment->getSkin(); + if (!skin) + return; + + gui::IGUIElement::draw(); +} + +void GUIFileSelectMenu::acceptInput() +{ + if (m_text_dst && !m_formname.empty()) { + StringMap fields; + if (m_accepted) { + std::string path; + if (!m_file_select_dialog) { + core::string string = + m_fileOpenDialog->getDirectoryName(); + path = std::string(string.c_str()); + } else { + path = wide_to_utf8(m_fileOpenDialog->getFileName()); + } + fields[m_formname + "_accepted"] = path; + } else { + fields[m_formname + "_canceled"] = m_formname; + } + m_text_dst->gotText(fields); + } + quitMenu(); +} + +bool GUIFileSelectMenu::OnEvent(const SEvent &event) +{ + if (event.EventType == irr::EET_GUI_EVENT) { + switch (event.GUIEvent.EventType) { + case gui::EGET_ELEMENT_CLOSED: + case gui::EGET_FILE_CHOOSE_DIALOG_CANCELLED: + m_accepted = false; + acceptInput(); + return true; + case gui::EGET_DIRECTORY_SELECTED: + m_accepted = !m_file_select_dialog; + acceptInput(); + return true; + case gui::EGET_FILE_SELECTED: + m_accepted = m_file_select_dialog; + acceptInput(); + return true; + default: + // ignore this event + break; + } + } + return Parent ? Parent->OnEvent(event) : false; +} diff --git a/src/gui/guiPathSelectMenu.h b/src/gui/guiPathSelectMenu.h new file mode 100644 index 000000000..f69d0acd7 --- /dev/null +++ b/src/gui/guiPathSelectMenu.h @@ -0,0 +1,59 @@ +/* + Minetest + Copyright (C) 2013 sapier + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2.1 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include + +#include "modalMenu.h" +#include "IGUIFileOpenDialog.h" +#include "guiFormSpecMenu.h" //required because of TextDest only !!! + +class GUIFileSelectMenu : public GUIModalMenu +{ +public: + GUIFileSelectMenu(gui::IGUIEnvironment *env, gui::IGUIElement *parent, s32 id, + IMenuManager *menumgr, const std::string &title, + const std::string &formid, bool is_file_select); + ~GUIFileSelectMenu(); + + /* + Remove and re-add (or reposition) stuff + */ + void regenerateGui(v2u32 screensize); + + void drawMenu(); + + bool OnEvent(const SEvent &event); + + void setTextDest(TextDest *dest) { m_text_dst = dest; } + +private: + void acceptInput(); + + std::wstring m_title; + bool m_accepted = false; + + gui::IGUIFileOpenDialog *m_fileOpenDialog = nullptr; + + TextDest *m_text_dst = nullptr; + + std::string m_formname; + bool m_file_select_dialog; +}; diff --git a/src/gui/guiTable.cpp b/src/gui/guiTable.cpp new file mode 100644 index 000000000..a2738afa9 --- /dev/null +++ b/src/gui/guiTable.cpp @@ -0,0 +1,1261 @@ +/* +Minetest +Copyright (C) 2013 celeron55, Perttu Ahola + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + + +#include "guiTable.h" +#include +#include +#include +#include +#include +#include +#include +#include "client/renderingengine.h" +#include "debug.h" +#include "log.h" +#include "client/tile.h" +#include "gettime.h" +#include "util/string.h" +#include "util/numeric.h" +#include "util/string.h" // for parseColorString() +#include "settings.h" // for settings +#include "porting.h" // for dpi +#include "guiscalingfilter.h" + +/* + GUITable +*/ + +GUITable::GUITable(gui::IGUIEnvironment *env, + gui::IGUIElement* parent, s32 id, + core::rect rectangle, + ISimpleTextureSource *tsrc +): + gui::IGUIElement(gui::EGUIET_ELEMENT, env, parent, id, rectangle), + m_tsrc(tsrc) +{ + assert(tsrc != NULL); + + gui::IGUISkin* skin = Environment->getSkin(); + + m_font = skin->getFont(); + if (m_font) { + m_font->grab(); + m_rowheight = m_font->getDimension(L"A").Height + 4; + m_rowheight = MYMAX(m_rowheight, 1); + } + + const s32 s = skin->getSize(gui::EGDS_SCROLLBAR_SIZE); + m_scrollbar = Environment->addScrollBar(false, + core::rect(RelativeRect.getWidth() - s, + 0, + RelativeRect.getWidth(), + RelativeRect.getHeight()), + this, -1); + m_scrollbar->setSubElement(true); + m_scrollbar->setTabStop(false); + m_scrollbar->setAlignment(gui::EGUIA_LOWERRIGHT, gui::EGUIA_LOWERRIGHT, + gui::EGUIA_UPPERLEFT, gui::EGUIA_LOWERRIGHT); + m_scrollbar->setVisible(false); + m_scrollbar->setPos(0); + + setTabStop(true); + setTabOrder(-1); + updateAbsolutePosition(); + + core::rect relative_rect = m_scrollbar->getRelativePosition(); + s32 width = (relative_rect.getWidth()/(2.0/3.0)) * + RenderingEngine::getDisplayDensity() * + g_settings->getFloat("gui_scaling"); + m_scrollbar->setRelativePosition(core::rect( + relative_rect.LowerRightCorner.X-width,relative_rect.UpperLeftCorner.Y, + relative_rect.LowerRightCorner.X,relative_rect.LowerRightCorner.Y + )); +} + +GUITable::~GUITable() +{ + for (GUITable::Row &row : m_rows) + delete[] row.cells; + + if (m_font) + m_font->drop(); + + m_scrollbar->remove(); +} + +GUITable::Option GUITable::splitOption(const std::string &str) +{ + size_t equal_pos = str.find('='); + if (equal_pos == std::string::npos) + return GUITable::Option(str, ""); + + return GUITable::Option(str.substr(0, equal_pos), + str.substr(equal_pos + 1)); +} + +void GUITable::setTextList(const std::vector &content, + bool transparent) +{ + clear(); + + if (transparent) { + m_background.setAlpha(0); + m_border = false; + } + + m_is_textlist = true; + + s32 empty_string_index = allocString(""); + + m_rows.resize(content.size()); + for (s32 i = 0; i < (s32) content.size(); ++i) { + Row *row = &m_rows[i]; + row->cells = new Cell[1]; + row->cellcount = 1; + row->indent = 0; + row->visible_index = i; + m_visible_rows.push_back(i); + + Cell *cell = row->cells; + cell->xmin = 0; + cell->xmax = 0x7fff; // something large enough + cell->xpos = 6; + cell->content_type = COLUMN_TYPE_TEXT; + cell->content_index = empty_string_index; + cell->tooltip_index = empty_string_index; + cell->color.set(255, 255, 255, 255); + cell->color_defined = false; + cell->reported_column = 1; + + // parse row content (color) + const std::string &s = content[i]; + if (s[0] == '#' && s[1] == '#') { + // double # to escape + cell->content_index = allocString(s.substr(2)); + } + else if (s[0] == '#' && s.size() >= 7 && + parseColorString( + s.substr(0,7), cell->color, false)) { + // single # for color + cell->color_defined = true; + cell->content_index = allocString(s.substr(7)); + } + else { + // no #, just text + cell->content_index = allocString(s); + } + + } + + allocationComplete(); + + // Clamp scroll bar position + updateScrollBar(); +} + +void GUITable::setTable(const TableOptions &options, + const TableColumns &columns, + std::vector &content) +{ + clear(); + + // Naming conventions: + // i is always a row index, 0-based + // j is always a column index, 0-based + // k is another index, for example an option index + + // Handle a stupid error case... (issue #1187) + if (columns.empty()) { + TableColumn text_column; + text_column.type = "text"; + TableColumns new_columns; + new_columns.push_back(text_column); + setTable(options, new_columns, content); + return; + } + + // Handle table options + video::SColor default_color(255, 255, 255, 255); + s32 opendepth = 0; + for (const Option &option : options) { + const std::string &name = option.name; + const std::string &value = option.value; + if (name == "color") + parseColorString(value, m_color, false); + else if (name == "background") + parseColorString(value, m_background, false); + else if (name == "border") + m_border = is_yes(value); + else if (name == "highlight") + parseColorString(value, m_highlight, false); + else if (name == "highlight_text") + parseColorString(value, m_highlight_text, false); + else if (name == "opendepth") + opendepth = stoi(value); + else + errorstream<<"Invalid table option: \""<= 1); + // rowcount = ceil(cellcount / colcount) but use integer arithmetic + s32 rowcount = (content.size() + colcount - 1) / colcount; + assert(rowcount >= 0); + // Append empty strings to content if there is an incomplete row + s32 cellcount = rowcount * colcount; + while (content.size() < (u32) cellcount) + content.emplace_back(""); + + // Create temporary rows (for processing columns) + struct TempRow { + // Current horizontal position (may different between rows due + // to indent/tree columns, or text/image columns with width<0) + s32 x; + // Tree indentation level + s32 indent; + // Next cell: Index into m_strings or m_images + s32 content_index; + // Next cell: Width in pixels + s32 content_width; + // Vector of completed cells in this row + std::vector cells; + // Stores colors and how long they last (maximum column index) + std::vector > colors; + + TempRow(): x(0), indent(0), content_index(0), content_width(0) {} + }; + TempRow *rows = new TempRow[rowcount]; + + // Get em width. Pedantically speaking, the width of "M" is not + // necessarily the same as the em width, but whatever, close enough. + s32 em = 6; + if (m_font) + em = m_font->getDimension(L"M").Width; + + s32 default_tooltip_index = allocString(""); + + std::map active_image_indices; + + // Process content in column-major order + for (s32 j = 0; j < colcount; ++j) { + // Check column type + ColumnType columntype = COLUMN_TYPE_TEXT; + if (columns[j].type == "text") + columntype = COLUMN_TYPE_TEXT; + else if (columns[j].type == "image") + columntype = COLUMN_TYPE_IMAGE; + else if (columns[j].type == "color") + columntype = COLUMN_TYPE_COLOR; + else if (columns[j].type == "indent") + columntype = COLUMN_TYPE_INDENT; + else if (columns[j].type == "tree") + columntype = COLUMN_TYPE_TREE; + else + errorstream<<"Invalid table column type: \"" + <colors.empty() && row->colors.back().second < j) + row->colors.pop_back(); + } + } + + // Make template for new cells + Cell newcell; + memset(&newcell, 0, sizeof newcell); + newcell.content_type = columntype; + newcell.tooltip_index = tooltip_index; + newcell.reported_column = j+1; + + if (columntype == COLUMN_TYPE_TEXT) { + // Find right edge of column + s32 xmax = 0; + for (s32 i = 0; i < rowcount; ++i) { + TempRow *row = &rows[i]; + row->content_index = allocString(content[i * colcount + j]); + const core::stringw &text = m_strings[row->content_index]; + row->content_width = m_font ? + m_font->getDimension(text.c_str()).Width : 0; + row->content_width = MYMAX(row->content_width, width); + s32 row_xmax = row->x + padding + row->content_width; + xmax = MYMAX(xmax, row_xmax); + } + // Add a new cell (of text type) to each row + for (s32 i = 0; i < rowcount; ++i) { + newcell.xmin = rows[i].x + padding; + alignContent(&newcell, xmax, rows[i].content_width, align); + newcell.content_index = rows[i].content_index; + newcell.color_defined = !rows[i].colors.empty(); + if (newcell.color_defined) + newcell.color = rows[i].colors.back().first; + rows[i].cells.push_back(newcell); + rows[i].x = newcell.xmax; + } + } + else if (columntype == COLUMN_TYPE_IMAGE) { + // Find right edge of column + s32 xmax = 0; + for (s32 i = 0; i < rowcount; ++i) { + TempRow *row = &rows[i]; + row->content_index = -1; + + // Find content_index. Image indices are defined in + // column options so check active_image_indices. + s32 image_index = stoi(content[i * colcount + j]); + std::map::iterator image_iter = + active_image_indices.find(image_index); + if (image_iter != active_image_indices.end()) + row->content_index = image_iter->second; + + // Get texture object (might be NULL) + video::ITexture *image = NULL; + if (row->content_index >= 0) + image = m_images[row->content_index]; + + // Get content width and update xmax + row->content_width = image ? image->getOriginalSize().Width : 0; + row->content_width = MYMAX(row->content_width, width); + s32 row_xmax = row->x + padding + row->content_width; + xmax = MYMAX(xmax, row_xmax); + } + // Add a new cell (of image type) to each row + for (s32 i = 0; i < rowcount; ++i) { + newcell.xmin = rows[i].x + padding; + alignContent(&newcell, xmax, rows[i].content_width, align); + newcell.content_index = rows[i].content_index; + rows[i].cells.push_back(newcell); + rows[i].x = newcell.xmax; + } + active_image_indices.clear(); + } + else if (columntype == COLUMN_TYPE_COLOR) { + for (s32 i = 0; i < rowcount; ++i) { + video::SColor cellcolor(255, 255, 255, 255); + if (parseColorString(content[i * colcount + j], cellcolor, true)) + rows[i].colors.emplace_back(cellcolor, j+span); + } + } + else if (columntype == COLUMN_TYPE_INDENT || + columntype == COLUMN_TYPE_TREE) { + // For column type "tree", reserve additional space for +/- + // Also enable special processing for treeview-type tables + s32 content_width = 0; + if (columntype == COLUMN_TYPE_TREE) { + content_width = m_font ? m_font->getDimension(L"+").Width : 0; + m_has_tree_column = true; + } + // Add a new cell (of indent or tree type) to each row + for (s32 i = 0; i < rowcount; ++i) { + TempRow *row = &rows[i]; + + s32 indentlevel = stoi(content[i * colcount + j]); + indentlevel = MYMAX(indentlevel, 0); + if (columntype == COLUMN_TYPE_TREE) + row->indent = indentlevel; + + newcell.xmin = row->x + padding; + newcell.xpos = newcell.xmin + indentlevel * width; + newcell.xmax = newcell.xpos + content_width; + newcell.content_index = 0; + newcell.color_defined = !rows[i].colors.empty(); + if (newcell.color_defined) + newcell.color = rows[i].colors.back().first; + row->cells.push_back(newcell); + row->x = newcell.xmax; + } + } + } + + // Copy temporary rows to not so temporary rows + if (rowcount >= 1) { + m_rows.resize(rowcount); + for (s32 i = 0; i < rowcount; ++i) { + Row *row = &m_rows[i]; + row->cellcount = rows[i].cells.size(); + row->cells = new Cell[row->cellcount]; + memcpy((void*) row->cells, (void*) &rows[i].cells[0], + row->cellcount * sizeof(Cell)); + row->indent = rows[i].indent; + row->visible_index = i; + m_visible_rows.push_back(i); + } + } + + if (m_has_tree_column) { + // Treeview: convert tree to indent cells on leaf rows + for (s32 i = 0; i < rowcount; ++i) { + if (i == rowcount-1 || m_rows[i].indent >= m_rows[i+1].indent) + for (s32 j = 0; j < m_rows[i].cellcount; ++j) + if (m_rows[i].cells[j].content_type == COLUMN_TYPE_TREE) + m_rows[i].cells[j].content_type = COLUMN_TYPE_INDENT; + } + + // Treeview: close rows according to opendepth option + std::set opened_trees; + for (s32 i = 0; i < rowcount; ++i) + if (m_rows[i].indent < opendepth) + opened_trees.insert(i); + setOpenedTrees(opened_trees); + } + + // Delete temporary information used only during setTable() + delete[] rows; + allocationComplete(); + + // Clamp scroll bar position + updateScrollBar(); +} + +void GUITable::clear() +{ + // Clean up cells and rows + for (GUITable::Row &row : m_rows) + delete[] row.cells; + m_rows.clear(); + m_visible_rows.clear(); + + // Get colors from skin + gui::IGUISkin *skin = Environment->getSkin(); + m_color = skin->getColor(gui::EGDC_BUTTON_TEXT); + m_background = skin->getColor(gui::EGDC_3D_HIGH_LIGHT); + m_highlight = skin->getColor(gui::EGDC_HIGH_LIGHT); + m_highlight_text = skin->getColor(gui::EGDC_HIGH_LIGHT_TEXT); + + // Reset members + m_is_textlist = false; + m_has_tree_column = false; + m_selected = -1; + m_sel_column = 0; + m_sel_doubleclick = false; + m_keynav_time = 0; + m_keynav_buffer = L""; + m_border = true; + m_strings.clear(); + m_images.clear(); + m_alloc_strings.clear(); + m_alloc_images.clear(); +} + +std::string GUITable::checkEvent() +{ + s32 sel = getSelected(); + assert(sel >= 0); + + if (sel == 0) { + return "INV"; + } + + std::ostringstream os(std::ios::binary); + if (m_sel_doubleclick) { + os<<"DCL:"; + m_sel_doubleclick = false; + } + else { + os<<"CHG:"; + } + os<= 0 && m_selected < (s32) m_visible_rows.size()); + return m_visible_rows[m_selected] + 1; +} + +void GUITable::setSelected(s32 index) +{ + s32 old_selected = m_selected; + + m_selected = -1; + m_sel_column = 0; + m_sel_doubleclick = false; + + --index; // Switch from 1-based indexing to 0-based indexing + + s32 rowcount = m_rows.size(); + if (rowcount == 0 || index < 0) { + return; + } + + if (index >= rowcount) { + index = rowcount - 1; + } + + // If the selected row is not visible, open its ancestors to make it visible + bool selection_invisible = m_rows[index].visible_index < 0; + if (selection_invisible) { + std::set opened_trees; + getOpenedTrees(opened_trees); + s32 indent = m_rows[index].indent; + for (s32 j = index - 1; j >= 0; --j) { + if (m_rows[j].indent < indent) { + opened_trees.insert(j); + indent = m_rows[j].indent; + } + } + setOpenedTrees(opened_trees); + } + + if (index >= 0) { + m_selected = m_rows[index].visible_index; + assert(m_selected >= 0 && m_selected < (s32) m_visible_rows.size()); + } + + if (m_selected != old_selected || selection_invisible) { + autoScroll(); + } +} + +GUITable::DynamicData GUITable::getDynamicData() const +{ + DynamicData dyndata; + dyndata.selected = getSelected(); + dyndata.scrollpos = m_scrollbar->getPos(); + dyndata.keynav_time = m_keynav_time; + dyndata.keynav_buffer = m_keynav_buffer; + if (m_has_tree_column) + getOpenedTrees(dyndata.opened_trees); + return dyndata; +} + +void GUITable::setDynamicData(const DynamicData &dyndata) +{ + if (m_has_tree_column) + setOpenedTrees(dyndata.opened_trees); + + m_keynav_time = dyndata.keynav_time; + m_keynav_buffer = dyndata.keynav_buffer; + + setSelected(dyndata.selected); + m_sel_column = 0; + m_sel_doubleclick = false; + + m_scrollbar->setPos(dyndata.scrollpos); +} + +const c8* GUITable::getTypeName() const +{ + return "GUITable"; +} + +void GUITable::updateAbsolutePosition() +{ + IGUIElement::updateAbsolutePosition(); + updateScrollBar(); +} + +void GUITable::draw() +{ + if (!IsVisible) + return; + + gui::IGUISkin *skin = Environment->getSkin(); + + // draw background + + bool draw_background = m_background.getAlpha() > 0; + if (m_border) + skin->draw3DSunkenPane(this, m_background, + true, draw_background, + AbsoluteRect, &AbsoluteClippingRect); + else if (draw_background) + skin->draw2DRectangle(this, m_background, + AbsoluteRect, &AbsoluteClippingRect); + + // get clipping rect + + core::rect client_clip(AbsoluteRect); + client_clip.UpperLeftCorner.Y += 1; + client_clip.UpperLeftCorner.X += 1; + client_clip.LowerRightCorner.Y -= 1; + client_clip.LowerRightCorner.X -= 1; + if (m_scrollbar->isVisible()) { + client_clip.LowerRightCorner.X = + m_scrollbar->getAbsolutePosition().UpperLeftCorner.X; + } + client_clip.clipAgainst(AbsoluteClippingRect); + + // draw visible rows + + s32 scrollpos = m_scrollbar->getPos(); + s32 row_min = scrollpos / m_rowheight; + s32 row_max = (scrollpos + AbsoluteRect.getHeight() - 1) + / m_rowheight + 1; + row_max = MYMIN(row_max, (s32) m_visible_rows.size()); + + core::rect row_rect(AbsoluteRect); + if (m_scrollbar->isVisible()) + row_rect.LowerRightCorner.X -= + skin->getSize(gui::EGDS_SCROLLBAR_SIZE); + row_rect.UpperLeftCorner.Y += row_min * m_rowheight - scrollpos; + row_rect.LowerRightCorner.Y = row_rect.UpperLeftCorner.Y + m_rowheight; + + for (s32 i = row_min; i < row_max; ++i) { + Row *row = &m_rows[m_visible_rows[i]]; + bool is_sel = i == m_selected; + video::SColor color = m_color; + + if (is_sel) { + skin->draw2DRectangle(this, m_highlight, row_rect, &client_clip); + color = m_highlight_text; + } + + for (s32 j = 0; j < row->cellcount; ++j) + drawCell(&row->cells[j], color, row_rect, client_clip); + + row_rect.UpperLeftCorner.Y += m_rowheight; + row_rect.LowerRightCorner.Y += m_rowheight; + } + + // Draw children + IGUIElement::draw(); +} + +void GUITable::drawCell(const Cell *cell, video::SColor color, + const core::rect &row_rect, + const core::rect &client_clip) +{ + if ((cell->content_type == COLUMN_TYPE_TEXT) + || (cell->content_type == COLUMN_TYPE_TREE)) { + + core::rect text_rect = row_rect; + text_rect.UpperLeftCorner.X = row_rect.UpperLeftCorner.X + + cell->xpos; + text_rect.LowerRightCorner.X = row_rect.UpperLeftCorner.X + + cell->xmax; + + if (cell->color_defined) + color = cell->color; + + if (m_font) { + if (cell->content_type == COLUMN_TYPE_TEXT) + m_font->draw(m_strings[cell->content_index], + text_rect, color, + false, true, &client_clip); + else // tree + m_font->draw(cell->content_index ? L"+" : L"-", + text_rect, color, + false, true, &client_clip); + } + } + else if (cell->content_type == COLUMN_TYPE_IMAGE) { + + if (cell->content_index < 0) + return; + + video::IVideoDriver *driver = Environment->getVideoDriver(); + video::ITexture *image = m_images[cell->content_index]; + + if (image) { + core::position2d dest_pos = + row_rect.UpperLeftCorner; + dest_pos.X += cell->xpos; + core::rect source_rect( + core::position2d(0, 0), + image->getOriginalSize()); + s32 imgh = source_rect.LowerRightCorner.Y; + s32 rowh = row_rect.getHeight(); + if (imgh < rowh) + dest_pos.Y += (rowh - imgh) / 2; + else + source_rect.LowerRightCorner.Y = rowh; + + video::SColor color(255, 255, 255, 255); + + driver->draw2DImage(image, dest_pos, source_rect, + &client_clip, color, true); + } + } +} + +bool GUITable::OnEvent(const SEvent &event) +{ + if (!isEnabled()) + return IGUIElement::OnEvent(event); + + if (event.EventType == EET_KEY_INPUT_EVENT) { + if (event.KeyInput.PressedDown && ( + event.KeyInput.Key == KEY_DOWN || + event.KeyInput.Key == KEY_UP || + event.KeyInput.Key == KEY_HOME || + event.KeyInput.Key == KEY_END || + event.KeyInput.Key == KEY_NEXT || + event.KeyInput.Key == KEY_PRIOR)) { + s32 offset = 0; + switch (event.KeyInput.Key) { + case KEY_DOWN: + offset = 1; + break; + case KEY_UP: + offset = -1; + break; + case KEY_HOME: + offset = - (s32) m_visible_rows.size(); + break; + case KEY_END: + offset = m_visible_rows.size(); + break; + case KEY_NEXT: + offset = AbsoluteRect.getHeight() / m_rowheight; + break; + case KEY_PRIOR: + offset = - (s32) (AbsoluteRect.getHeight() / m_rowheight); + break; + default: + break; + } + s32 old_selected = m_selected; + s32 rowcount = m_visible_rows.size(); + if (rowcount != 0) { + m_selected = rangelim(m_selected + offset, 0, rowcount-1); + autoScroll(); + } + + if (m_selected != old_selected) + sendTableEvent(0, false); + + return true; + } + + if (event.KeyInput.PressedDown && ( + event.KeyInput.Key == KEY_LEFT || + event.KeyInput.Key == KEY_RIGHT)) { + // Open/close subtree via keyboard + if (m_selected >= 0) { + int dir = event.KeyInput.Key == KEY_LEFT ? -1 : 1; + toggleVisibleTree(m_selected, dir, true); + } + return true; + } + else if (!event.KeyInput.PressedDown && ( + event.KeyInput.Key == KEY_RETURN || + event.KeyInput.Key == KEY_SPACE)) { + sendTableEvent(0, true); + return true; + } + else if (event.KeyInput.Key == KEY_ESCAPE || + event.KeyInput.Key == KEY_SPACE) { + // pass to parent + } + else if (event.KeyInput.PressedDown && event.KeyInput.Char) { + // change selection based on text as it is typed + u64 now = porting::getTimeMs(); + if (now - m_keynav_time >= 500) + m_keynav_buffer = L""; + m_keynav_time = now; + + // add to key buffer if not a key repeat + if (!(m_keynav_buffer.size() == 1 && + m_keynav_buffer[0] == event.KeyInput.Char)) { + m_keynav_buffer.append(event.KeyInput.Char); + } + + // find the selected item, starting at the current selection + // don't change selection if the key buffer matches the current item + s32 old_selected = m_selected; + s32 start = MYMAX(m_selected, 0); + s32 rowcount = m_visible_rows.size(); + for (s32 k = 1; k < rowcount; ++k) { + s32 current = start + k; + if (current >= rowcount) + current -= rowcount; + if (doesRowStartWith(getRow(current), m_keynav_buffer)) { + m_selected = current; + break; + } + } + autoScroll(); + if (m_selected != old_selected) + sendTableEvent(0, false); + + return true; + } + } + if (event.EventType == EET_MOUSE_INPUT_EVENT) { + core::position2d p(event.MouseInput.X, event.MouseInput.Y); + + if (event.MouseInput.Event == EMIE_MOUSE_WHEEL) { + m_scrollbar->setPos(m_scrollbar->getPos() + + (event.MouseInput.Wheel < 0 ? -3 : 3) * + - (s32) m_rowheight / 2); + return true; + } + + // Find hovered row and cell + bool really_hovering = false; + s32 row_i = getRowAt(p.Y, really_hovering); + const Cell *cell = NULL; + if (really_hovering) { + s32 cell_j = getCellAt(p.X, row_i); + if (cell_j >= 0) + cell = &(getRow(row_i)->cells[cell_j]); + } + + // Update tooltip + setToolTipText(cell ? m_strings[cell->tooltip_index].c_str() : L""); + + // Fix for #1567/#1806: + // IGUIScrollBar passes double click events to its parent, + // which we don't want. Detect this case and discard the event + if (event.MouseInput.Event != EMIE_MOUSE_MOVED && + m_scrollbar->isVisible() && + m_scrollbar->isPointInside(p)) + return true; + + if (event.MouseInput.isLeftPressed() && + (isPointInside(p) || + event.MouseInput.Event == EMIE_MOUSE_MOVED)) { + s32 sel_column = 0; + bool sel_doubleclick = (event.MouseInput.Event + == EMIE_LMOUSE_DOUBLE_CLICK); + bool plusminus_clicked = false; + + // For certain events (left click), report column + // Also open/close subtrees when the +/- is clicked + if (cell && ( + event.MouseInput.Event == EMIE_LMOUSE_PRESSED_DOWN || + event.MouseInput.Event == EMIE_LMOUSE_DOUBLE_CLICK || + event.MouseInput.Event == EMIE_LMOUSE_TRIPLE_CLICK)) { + sel_column = cell->reported_column; + if (cell->content_type == COLUMN_TYPE_TREE) + plusminus_clicked = true; + } + + if (plusminus_clicked) { + if (event.MouseInput.Event == EMIE_LMOUSE_PRESSED_DOWN) { + toggleVisibleTree(row_i, 0, false); + } + } + else { + // Normal selection + s32 old_selected = m_selected; + m_selected = row_i; + autoScroll(); + + if (m_selected != old_selected || + sel_column >= 1 || + sel_doubleclick) { + sendTableEvent(sel_column, sel_doubleclick); + } + + // Treeview: double click opens/closes trees + if (m_has_tree_column && sel_doubleclick) { + toggleVisibleTree(m_selected, 0, false); + } + } + } + return true; + } + if (event.EventType == EET_GUI_EVENT && + event.GUIEvent.EventType == gui::EGET_SCROLL_BAR_CHANGED && + event.GUIEvent.Caller == m_scrollbar) { + // Don't pass events from our scrollbar to the parent + return true; + } + + return IGUIElement::OnEvent(event); +} + +/******************************************************************************/ +/* GUITable helper functions */ +/******************************************************************************/ + +s32 GUITable::allocString(const std::string &text) +{ + std::map::iterator it = m_alloc_strings.find(text); + if (it == m_alloc_strings.end()) { + s32 id = m_strings.size(); + std::wstring wtext = utf8_to_wide(text); + m_strings.emplace_back(wtext.c_str()); + m_alloc_strings.insert(std::make_pair(text, id)); + return id; + } + + return it->second; +} + +s32 GUITable::allocImage(const std::string &imagename) +{ + std::map::iterator it = m_alloc_images.find(imagename); + if (it == m_alloc_images.end()) { + s32 id = m_images.size(); + m_images.push_back(m_tsrc->getTexture(imagename)); + m_alloc_images.insert(std::make_pair(imagename, id)); + return id; + } + + return it->second; +} + +void GUITable::allocationComplete() +{ + // Called when done with creating rows and cells from table data, + // i.e. when allocString and allocImage won't be called anymore + m_alloc_strings.clear(); + m_alloc_images.clear(); +} + +const GUITable::Row* GUITable::getRow(s32 i) const +{ + if (i >= 0 && i < (s32) m_visible_rows.size()) + return &m_rows[m_visible_rows[i]]; + + return NULL; +} + +bool GUITable::doesRowStartWith(const Row *row, const core::stringw &str) const +{ + if (row == NULL) + return false; + + for (s32 j = 0; j < row->cellcount; ++j) { + Cell *cell = &row->cells[j]; + if (cell->content_type == COLUMN_TYPE_TEXT) { + const core::stringw &cellstr = m_strings[cell->content_index]; + if (cellstr.size() >= str.size() && + str.equals_ignore_case(cellstr.subString(0, str.size()))) + return true; + } + } + return false; +} + +s32 GUITable::getRowAt(s32 y, bool &really_hovering) const +{ + really_hovering = false; + + s32 rowcount = m_visible_rows.size(); + if (rowcount == 0) + return -1; + + // Use arithmetic to find row + s32 rel_y = y - AbsoluteRect.UpperLeftCorner.Y - 1; + s32 i = (rel_y + m_scrollbar->getPos()) / m_rowheight; + + if (i >= 0 && i < rowcount) { + really_hovering = true; + return i; + } + if (i < 0) + return 0; + + return rowcount - 1; +} + +s32 GUITable::getCellAt(s32 x, s32 row_i) const +{ + const Row *row = getRow(row_i); + if (row == NULL) + return -1; + + // Use binary search to find cell in row + s32 rel_x = x - AbsoluteRect.UpperLeftCorner.X - 1; + s32 jmin = 0; + s32 jmax = row->cellcount - 1; + while (jmin < jmax) { + s32 pivot = jmin + (jmax - jmin) / 2; + assert(pivot >= 0 && pivot < row->cellcount); + const Cell *cell = &row->cells[pivot]; + + if (rel_x >= cell->xmin && rel_x <= cell->xmax) + return pivot; + + if (rel_x < cell->xmin) + jmax = pivot - 1; + else + jmin = pivot + 1; + } + + if (jmin >= 0 && jmin < row->cellcount && + rel_x >= row->cells[jmin].xmin && + rel_x <= row->cells[jmin].xmax) + return jmin; + + return -1; +} + +void GUITable::autoScroll() +{ + if (m_selected >= 0) { + s32 pos = m_scrollbar->getPos(); + s32 maxpos = m_selected * m_rowheight; + s32 minpos = maxpos - (AbsoluteRect.getHeight() - m_rowheight); + if (pos > maxpos) + m_scrollbar->setPos(maxpos); + else if (pos < minpos) + m_scrollbar->setPos(minpos); + } +} + +void GUITable::updateScrollBar() +{ + s32 totalheight = m_rowheight * m_visible_rows.size(); + s32 scrollmax = MYMAX(0, totalheight - AbsoluteRect.getHeight()); + m_scrollbar->setVisible(scrollmax > 0); + m_scrollbar->setMax(scrollmax); + m_scrollbar->setSmallStep(m_rowheight); + m_scrollbar->setLargeStep(2 * m_rowheight); +} + +void GUITable::sendTableEvent(s32 column, bool doubleclick) +{ + m_sel_column = column; + m_sel_doubleclick = doubleclick; + if (Parent) { + SEvent e; + memset(&e, 0, sizeof e); + e.EventType = EET_GUI_EVENT; + e.GUIEvent.Caller = this; + e.GUIEvent.Element = 0; + e.GUIEvent.EventType = gui::EGET_TABLE_CHANGED; + Parent->OnEvent(e); + } +} + +void GUITable::getOpenedTrees(std::set &opened_trees) const +{ + opened_trees.clear(); + s32 rowcount = m_rows.size(); + for (s32 i = 0; i < rowcount - 1; ++i) { + if (m_rows[i].indent < m_rows[i+1].indent && + m_rows[i+1].visible_index != -2) + opened_trees.insert(i); + } +} + +void GUITable::setOpenedTrees(const std::set &opened_trees) +{ + s32 old_selected = -1; + if (m_selected >= 0) + old_selected = m_visible_rows[m_selected]; + + std::vector parents; + std::vector closed_parents; + + m_visible_rows.clear(); + + for (size_t i = 0; i < m_rows.size(); ++i) { + Row *row = &m_rows[i]; + + // Update list of ancestors + while (!parents.empty() && m_rows[parents.back()].indent >= row->indent) + parents.pop_back(); + while (!closed_parents.empty() && + m_rows[closed_parents.back()].indent >= row->indent) + closed_parents.pop_back(); + + assert(closed_parents.size() <= parents.size()); + + if (closed_parents.empty()) { + // Visible row + row->visible_index = m_visible_rows.size(); + m_visible_rows.push_back(i); + } + else if (parents.back() == closed_parents.back()) { + // Invisible row, direct parent is closed + row->visible_index = -2; + } + else { + // Invisible row, direct parent is open, some ancestor is closed + row->visible_index = -1; + } + + // If not a leaf, add to parents list + if (i < m_rows.size()-1 && row->indent < m_rows[i+1].indent) { + parents.push_back(i); + + s32 content_index = 0; // "-", open + if (opened_trees.count(i) == 0) { + closed_parents.push_back(i); + content_index = 1; // "+", closed + } + + // Update all cells of type "tree" + for (s32 j = 0; j < row->cellcount; ++j) + if (row->cells[j].content_type == COLUMN_TYPE_TREE) + row->cells[j].content_index = content_index; + } + } + + updateScrollBar(); + + // m_selected must be updated since it is a visible row index + if (old_selected >= 0) + m_selected = m_rows[old_selected].visible_index; +} + +void GUITable::openTree(s32 to_open) +{ + std::set opened_trees; + getOpenedTrees(opened_trees); + opened_trees.insert(to_open); + setOpenedTrees(opened_trees); +} + +void GUITable::closeTree(s32 to_close) +{ + std::set opened_trees; + getOpenedTrees(opened_trees); + opened_trees.erase(to_close); + setOpenedTrees(opened_trees); +} + +// The following function takes a visible row index (hidden rows skipped) +// dir: -1 = left (close), 0 = auto (toggle), 1 = right (open) +void GUITable::toggleVisibleTree(s32 row_i, int dir, bool move_selection) +{ + // Check if the chosen tree is currently open + const Row *row = getRow(row_i); + if (row == NULL) + return; + + bool was_open = false; + for (s32 j = 0; j < row->cellcount; ++j) { + if (row->cells[j].content_type == COLUMN_TYPE_TREE) { + was_open = row->cells[j].content_index == 0; + break; + } + } + + // Check if the chosen tree should be opened + bool do_open = !was_open; + if (dir < 0) + do_open = false; + else if (dir > 0) + do_open = true; + + // Close or open the tree; the heavy lifting is done by setOpenedTrees + if (was_open && !do_open) + closeTree(m_visible_rows[row_i]); + else if (!was_open && do_open) + openTree(m_visible_rows[row_i]); + + // Change selected row if requested by caller, + // this is useful for keyboard navigation + if (move_selection) { + s32 sel = row_i; + if (was_open && do_open) { + // Move selection to first child + const Row *maybe_child = getRow(sel + 1); + if (maybe_child && maybe_child->indent > row->indent) + sel++; + } + else if (!was_open && !do_open) { + // Move selection to parent + assert(getRow(sel) != NULL); + while (sel > 0 && getRow(sel - 1)->indent >= row->indent) + sel--; + sel--; + if (sel < 0) // was root already selected? + sel = row_i; + } + if (sel != m_selected) { + m_selected = sel; + autoScroll(); + sendTableEvent(0, false); + } + } +} + +void GUITable::alignContent(Cell *cell, s32 xmax, s32 content_width, s32 align) +{ + // requires that cell.xmin, cell.xmax are properly set + // align = 0: left aligned, 1: centered, 2: right aligned, 3: inline + if (align == 0) { + cell->xpos = cell->xmin; + cell->xmax = xmax; + } + else if (align == 1) { + cell->xpos = (cell->xmin + xmax - content_width) / 2; + cell->xmax = xmax; + } + else if (align == 2) { + cell->xpos = xmax - content_width; + cell->xmax = xmax; + } + else { + // inline alignment: the cells of the column don't have an aligned + // right border, the right border of each cell depends on the content + cell->xpos = cell->xmin; + cell->xmax = cell->xmin + content_width; + } +} diff --git a/src/gui/guiTable.h b/src/gui/guiTable.h new file mode 100644 index 000000000..f9337ff6d --- /dev/null +++ b/src/gui/guiTable.h @@ -0,0 +1,256 @@ +/* +Minetest +Copyright (C) 2013 celeron55, Perttu Ahola + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#pragma once + +#include +#include +#include +#include +#include + +#include "irrlichttypes_extrabloated.h" + +class ISimpleTextureSource; + +/* + A table GUI element for GUIFormSpecMenu. + + Sends a EGET_TABLE_CHANGED event to the parent when + an item is selected or double-clicked. + Call checkEvent() to get info. + + Credits: The interface and implementation of this class are (very) + loosely based on the Irrlicht classes CGUITable and CGUIListBox. + CGUITable and CGUIListBox are licensed under the Irrlicht license; + they are Copyright (C) 2002-2012 Nikolaus Gebhardt +*/ +class GUITable : public gui::IGUIElement +{ +public: + /* + Stores dynamic data that should be preserved + when updating a formspec + */ + struct DynamicData + { + s32 selected = 0; + s32 scrollpos = 0; + s32 keynav_time = 0; + core::stringw keynav_buffer; + std::set opened_trees; + }; + + /* + An option of the form = + */ + struct Option + { + std::string name; + std::string value; + + Option(const std::string &name_, const std::string &value_) : + name(name_), + value(value_) + {} + }; + + /* + A list of options that concern the entire table + */ + typedef std::vector