From ffd16e3feca90c356c55898de2b9f3f5c6bc5c98 Mon Sep 17 00:00:00 2001 From: RealBadAngel Date: Mon, 22 Jun 2015 04:34:56 +0200 Subject: [PATCH] Add minimap feature --- build/android/jni/Android.mk | 1 + .../minimap_shader/opengl_fragment.glsl | 32 ++ .../shaders/minimap_shader/opengl_vertex.glsl | 11 + src/CMakeLists.txt | 1 + src/client.cpp | 35 +- src/client.h | 6 + src/client/tile.cpp | 40 ++ src/client/tile.h | 2 + src/defaultsettings.cpp | 4 + src/drawscene.cpp | 11 +- src/drawscene.h | 16 +- src/game.cpp | 70 ++- src/mapblock_mesh.cpp | 28 ++ src/mapblock_mesh.h | 7 + src/minimap.cpp | 472 ++++++++++++++++++ src/minimap.h | 168 +++++++ src/nodedef.cpp | 7 +- src/nodedef.h | 2 + textures/base/pack/minimap_mask_round.png | Bin 0 -> 4081 bytes textures/base/pack/minimap_mask_square.png | Bin 0 -> 1951 bytes textures/base/pack/minimap_overlay_round.png | Bin 0 -> 20630 bytes textures/base/pack/minimap_overlay_square.png | Bin 0 -> 2889 bytes textures/base/pack/player_marker.png | Bin 0 -> 3885 bytes 23 files changed, 883 insertions(+), 30 deletions(-) create mode 100644 client/shaders/minimap_shader/opengl_fragment.glsl create mode 100644 client/shaders/minimap_shader/opengl_vertex.glsl create mode 100644 src/minimap.cpp create mode 100644 src/minimap.h create mode 100644 textures/base/pack/minimap_mask_round.png create mode 100644 textures/base/pack/minimap_mask_square.png create mode 100644 textures/base/pack/minimap_overlay_round.png create mode 100644 textures/base/pack/minimap_overlay_square.png create mode 100644 textures/base/pack/player_marker.png diff --git a/build/android/jni/Android.mk b/build/android/jni/Android.mk index d62698ce4..e606ccfc9 100644 --- a/build/android/jni/Android.mk +++ b/build/android/jni/Android.mk @@ -183,6 +183,7 @@ LOCAL_SRC_FILES := \ jni/src/mg_decoration.cpp \ jni/src/mg_ore.cpp \ jni/src/mg_schematic.cpp \ + jni/src/minimap.cpp \ jni/src/mods.cpp \ jni/src/nameidmapping.cpp \ jni/src/nodedef.cpp \ diff --git a/client/shaders/minimap_shader/opengl_fragment.glsl b/client/shaders/minimap_shader/opengl_fragment.glsl new file mode 100644 index 000000000..fa4f9cb1a --- /dev/null +++ b/client/shaders/minimap_shader/opengl_fragment.glsl @@ -0,0 +1,32 @@ +uniform sampler2D baseTexture; +uniform sampler2D normalTexture; +uniform vec3 yawVec; + +void main (void) +{ + vec2 uv = gl_TexCoord[0].st; + + //texture sampling rate + const float step = 1.0 / 256.0; + float tl = texture2D(normalTexture, vec2(uv.x - step, uv.y + step)).r; + float t = texture2D(normalTexture, vec2(uv.x - step, uv.y - step)).r; + float tr = texture2D(normalTexture, vec2(uv.x + step, uv.y + step)).r; + float r = texture2D(normalTexture, vec2(uv.x + step, uv.y)).r; + float br = texture2D(normalTexture, vec2(uv.x + step, uv.y - step)).r; + float b = texture2D(normalTexture, vec2(uv.x, uv.y - step)).r; + float bl = texture2D(normalTexture, vec2(uv.x - step, uv.y - step)).r; + float l = texture2D(normalTexture, vec2(uv.x - step, uv.y)).r; + float dX = (tr + 2.0 * r + br) - (tl + 2.0 * l + bl); + float dY = (bl + 2.0 * b + br) - (tl + 2.0 * t + tr); + vec4 bump = vec4 (normalize(vec3 (dX, dY, 0.1)),1.0); + float height = 2.0 * texture2D(normalTexture, vec2(uv.x, uv.y)).r - 1.0; + vec4 base = texture2D(baseTexture, uv).rgba; + vec3 L = normalize(vec3(0.0, 0.75, 1.0)); + float specular = pow(clamp(dot(reflect(L, bump.xyz), yawVec), 0.0, 1.0), 1.0); + float diffuse = dot(yawVec, bump.xyz); + + vec3 color = (1.1 * diffuse + 0.05 * height + 0.5 * specular) * base.rgb; + vec4 col = vec4(color.rgb, base.a); + col *= gl_Color; + gl_FragColor = vec4(col.rgb, base.a); +} diff --git a/client/shaders/minimap_shader/opengl_vertex.glsl b/client/shaders/minimap_shader/opengl_vertex.glsl new file mode 100644 index 000000000..06df5a3cf --- /dev/null +++ b/client/shaders/minimap_shader/opengl_vertex.glsl @@ -0,0 +1,11 @@ +uniform mat4 mWorldViewProj; +uniform mat4 mInvWorld; +uniform mat4 mTransWorld; +uniform mat4 mWorld; + +void main(void) +{ + gl_TexCoord[0] = gl_MultiTexCoord0; + gl_Position = mWorldViewProj * gl_Vertex; + gl_FrontColor = gl_BackColor = gl_Color; +} diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ef508d9b8..e2f555860 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -423,6 +423,7 @@ set(client_SRCS main.cpp mapblock_mesh.cpp mesh.cpp + minimap.cpp particles.cpp shader.cpp sky.cpp diff --git a/src/client.cpp b/src/client.cpp index d2ec7017e..ce48df953 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -34,6 +34,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "porting.h" #include "mapblock_mesh.h" #include "mapblock.h" +#include "minimap.h" #include "settings.h" #include "profiler.h" #include "gettext.h" @@ -185,11 +186,6 @@ void * MeshUpdateThread::Thread() ScopeProfiler sp(g_profiler, "Client: Mesh making"); MapBlockMesh *mesh_new = new MapBlockMesh(q->data, m_camera_offset); - if(mesh_new->getMesh()->getMeshBufferCount() == 0) - { - delete mesh_new; - mesh_new = NULL; - } MeshUpdateResult r; r.p = q->p; @@ -274,6 +270,7 @@ Client::Client( // Add local player m_env.addPlayer(new LocalPlayer(this, playername)); + m_mapper = new Mapper(device, this); m_cache_save_interval = g_settings->getU16("server_map_save_interval"); m_cache_smooth_lighting = g_settings->getBool("smooth_lighting"); @@ -537,27 +534,37 @@ void Client::step(float dtime) */ { int num_processed_meshes = 0; - while(!m_mesh_update_thread.m_queue_out.empty()) + while (!m_mesh_update_thread.m_queue_out.empty()) { num_processed_meshes++; MeshUpdateResult r = m_mesh_update_thread.m_queue_out.pop_frontNoEx(); MapBlock *block = m_env.getMap().getBlockNoCreateNoEx(r.p); - if(block) { + MinimapMapblock *minimap_mapblock = NULL; + if (block) { // Delete the old mesh - if(block->mesh != NULL) - { - // TODO: Remove hardware buffers of meshbuffers of block->mesh + if (block->mesh != NULL) { delete block->mesh; block->mesh = NULL; } - // Replace with the new mesh - block->mesh = r.mesh; + if (r.mesh) + minimap_mapblock = r.mesh->getMinimapMapblock(); + + if (r.mesh && r.mesh->getMesh()->getMeshBufferCount() == 0) { + delete r.mesh; + block->mesh = NULL; + } else { + // Replace with the new mesh + block->mesh = r.mesh; + } } else { delete r.mesh; + minimap_mapblock = NULL; } - if(r.ack_block_to_server) { + m_mapper->addBlock(r.p, minimap_mapblock); + + if (r.ack_block_to_server) { /* Acknowledge block [0] u8 count @@ -567,7 +574,7 @@ void Client::step(float dtime) } } - if(num_processed_meshes > 0) + if (num_processed_meshes > 0) g_profiler->graphAdd("num_processed_meshes", num_processed_meshes); } diff --git a/src/client.h b/src/client.h index daeef3985..474daf3bc 100644 --- a/src/client.h +++ b/src/client.h @@ -48,6 +48,8 @@ struct MapDrawControl; class MtEventManager; struct PointedThing; class Database; +class Mapper; +struct MinimapMapblock; struct QueuedMeshUpdate { @@ -504,6 +506,9 @@ public: float getCurRate(void); float getAvgRate(void); + Mapper* getMapper () + { return m_mapper; } + // IGameDef interface virtual IItemDefManager* getItemDefManager(); virtual INodeDefManager* getNodeDefManager(); @@ -583,6 +588,7 @@ private: ParticleManager m_particle_manager; con::Connection m_con; IrrlichtDevice *m_device; + Mapper *m_mapper; // Server serialization version u8 m_server_ser_ver; // Used version of the protocol with server diff --git a/src/client/tile.cpp b/src/client/tile.cpp index eba52033a..cf8061982 100644 --- a/src/client/tile.cpp +++ b/src/client/tile.cpp @@ -382,6 +382,8 @@ public: video::IImage* generateImage(const std::string &name); video::ITexture* getNormalTexture(const std::string &name); + video::SColor getTextureAverageColor(const std::string &name); + private: // The id of the thread that is allowed to use irrlicht directly @@ -2008,3 +2010,41 @@ video::ITexture* TextureSource::getNormalTexture(const std::string &name) } return NULL; } + +video::SColor TextureSource::getTextureAverageColor(const std::string &name) +{ + video::IVideoDriver *driver = m_device->getVideoDriver(); + video::SColor c(0, 0, 0, 0); + u32 id; + video::ITexture *texture = getTexture(name, &id); + video::IImage *image = driver->createImage(texture, + core::position2d(0, 0), + texture->getOriginalSize()); + u32 total = 0; + u32 tR = 0; + u32 tG = 0; + u32 tB = 0; + core::dimension2d dim = image->getDimension(); + u16 step = 1; + if (dim.Width > 16) + step = dim.Width / 16; + for (u16 x = 0; x < dim.Width; x += step) { + for (u16 y = 0; y < dim.Width; y += step) { + c = image->getPixel(x,y); + if (c.getAlpha() > 0) { + total++; + tR += c.getRed(); + tG += c.getGreen(); + tB += c.getBlue(); + } + } + } + image->drop(); + if (total > 0) { + c.setRed(tR / total); + c.setGreen(tG / total); + c.setBlue(tB / total); + } + c.setAlpha(255); + return c; +} diff --git a/src/client/tile.h b/src/client/tile.h index 38f8bb623..674da66f2 100644 --- a/src/client/tile.h +++ b/src/client/tile.h @@ -110,6 +110,7 @@ public: virtual video::ITexture* generateTextureFromMesh( const TextureFromMeshParams ¶ms)=0; virtual video::ITexture* getNormalTexture(const std::string &name)=0; + virtual video::SColor getTextureAverageColor(const std::string &name)=0; }; class IWritableTextureSource : public ITextureSource @@ -131,6 +132,7 @@ public: virtual void insertSourceImage(const std::string &name, video::IImage *img)=0; virtual void rebuildImagesAndTextures()=0; virtual video::ITexture* getNormalTexture(const std::string &name)=0; + virtual video::SColor getTextureAverageColor(const std::string &name)=0; }; IWritableTextureSource* createTextureSource(IrrlichtDevice *device); diff --git a/src/defaultsettings.cpp b/src/defaultsettings.cpp index eab81009f..73e36da98 100644 --- a/src/defaultsettings.cpp +++ b/src/defaultsettings.cpp @@ -43,6 +43,7 @@ void set_default_settings(Settings *settings) settings->setDefault("keymap_special1", "KEY_KEY_E"); settings->setDefault("keymap_chat", "KEY_KEY_T"); settings->setDefault("keymap_cmd", "/"); + settings->setDefault("keymap_minimap", "KEY_F9"); settings->setDefault("keymap_console", "KEY_F10"); settings->setDefault("keymap_rangeselect", "KEY_KEY_R"); settings->setDefault("keymap_freemove", "KEY_KEY_K"); @@ -176,6 +177,9 @@ void set_default_settings(Settings *settings) settings->setDefault("enable_particles", "true"); settings->setDefault("enable_mesh_cache", "true"); + settings->setDefault("enable_minimap", "true"); + settings->setDefault("minimap_shape_round", "true"); + settings->setDefault("curl_timeout", "5000"); settings->setDefault("curl_parallel_limit", "8"); settings->setDefault("curl_file_download_timeout", "300000"); diff --git a/src/drawscene.cpp b/src/drawscene.cpp index f7cfdd262..509f341d5 100644 --- a/src/drawscene.cpp +++ b/src/drawscene.cpp @@ -416,10 +416,11 @@ void draw_plain(Camera& camera, bool show_hud, Hud& hud, camera.drawWieldedTool(); } -void draw_scene(video::IVideoDriver* driver, scene::ISceneManager* smgr, - Camera& camera, Client& client, LocalPlayer* player, Hud& hud, - gui::IGUIEnvironment* guienv, std::vector hilightboxes, - const v2u32& screensize, video::SColor skycolor, bool show_hud) +void draw_scene(video::IVideoDriver *driver, scene::ISceneManager *smgr, + Camera &camera, Client& client, LocalPlayer *player, Hud &hud, + Mapper &mapper, gui::IGUIEnvironment *guienv, + std::vector hilightboxes, const v2u32 &screensize, + video::SColor skycolor, bool show_hud, bool show_minimap) { TimeTaker timer("smgr"); @@ -484,6 +485,8 @@ void draw_scene(video::IVideoDriver* driver, scene::ISceneManager* smgr, hud.drawCrosshair(); hud.drawHotbar(client.getPlayerItem()); hud.drawLuaElements(camera.getOffset()); + if (show_minimap) + mapper.drawMinimap(); } guienv->drawAll(); diff --git a/src/drawscene.h b/src/drawscene.h index 3268bcbf2..0630f2970 100644 --- a/src/drawscene.h +++ b/src/drawscene.h @@ -22,16 +22,18 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "camera.h" #include "hud.h" +#include "minimap.h" #include "irrlichttypes_extrabloated.h" -void draw_load_screen(const std::wstring &text, IrrlichtDevice* device, - gui::IGUIEnvironment* guienv, float dtime=0, int percent=0, - bool clouds=true); +void draw_load_screen(const std::wstring &text, IrrlichtDevice *device, + gui::IGUIEnvironment *guienv, float dtime = 0, int percent = 0, + bool clouds = true); -void draw_scene(video::IVideoDriver* driver, scene::ISceneManager* smgr, - Camera& camera, Client& client, LocalPlayer* player, Hud& hud, - gui::IGUIEnvironment* guienv, std::vector hilightboxes, - const v2u32& screensize, video::SColor skycolor, bool show_hud); +void draw_scene(video::IVideoDriver *driver, scene::ISceneManager *smgr, + Camera &camera, Client &client, LocalPlayer *player, Hud &hud, + Mapper &mapper, gui::IGUIEnvironment *guienv, + std::vector hilightboxes, const v2u32 &screensize, + video::SColor skycolor, bool show_hud, bool show_minimap); #endif /* DRAWSCENE_H_ */ diff --git a/src/game.cpp b/src/game.cpp index be4897d75..94fb41852 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -57,6 +57,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "util/directiontables.h" #include "util/pointedthing.h" #include "version.h" +#include "minimap.h" #include "sound.h" @@ -866,6 +867,9 @@ public: services->setPixelShaderConstant("eyePosition", (irr::f32 *)&eye_position, 3); services->setVertexShaderConstant("eyePosition", (irr::f32 *)&eye_position, 3); + v3f minimap_yaw_vec = m_client->getMapper()->getYawVec(); + services->setPixelShaderConstant("yawVec", (irr::f32 *)&minimap_yaw_vec, 3); + // Uniform sampler layers int layer0 = 0; int layer1 = 1; @@ -1238,6 +1242,7 @@ struct KeyCache { KEYMAP_ID_CHAT, KEYMAP_ID_CMD, KEYMAP_ID_CONSOLE, + KEYMAP_ID_MINIMAP, KEYMAP_ID_FREEMOVE, KEYMAP_ID_FASTMOVE, KEYMAP_ID_NOCLIP, @@ -1287,6 +1292,7 @@ void KeyCache::populate() key[KEYMAP_ID_CHAT] = getKeySetting("keymap_chat"); key[KEYMAP_ID_CMD] = getKeySetting("keymap_cmd"); key[KEYMAP_ID_CONSOLE] = getKeySetting("keymap_console"); + key[KEYMAP_ID_MINIMAP] = getKeySetting("keymap_minimap"); key[KEYMAP_ID_FREEMOVE] = getKeySetting("keymap_freemove"); key[KEYMAP_ID_FASTMOVE] = getKeySetting("keymap_fastmove"); key[KEYMAP_ID_NOCLIP] = getKeySetting("keymap_noclip"); @@ -1392,6 +1398,7 @@ struct VolatileRunFlags { bool invert_mouse; bool show_chat; bool show_hud; + bool show_minimap; bool force_fog_off; bool show_debug; bool show_profiler_graph; @@ -1490,6 +1497,8 @@ protected: void toggleChat(float *statustext_time, bool *flag); void toggleHud(float *statustext_time, bool *flag); + void toggleMinimap(float *statustext_time, bool *flag1, bool *flag2, + bool shift_pressed); void toggleFog(float *statustext_time, bool *flag); void toggleDebug(float *statustext_time, bool *show_debug, bool *show_profiler_graph); @@ -1568,6 +1577,7 @@ private: Sky *sky; // Free using ->Drop() Inventory *local_inventory; Hud *hud; + Mapper *mapper; /* 'cache' This class does take ownership/responsibily for cleaning up etc of any of @@ -1648,7 +1658,8 @@ Game::Game() : clouds(NULL), sky(NULL), local_inventory(NULL), - hud(NULL) + hud(NULL), + mapper(NULL) { m_cache_doubletap_jump = g_settings->getBool("doubletap_jump"); m_cache_enable_node_highlighting = g_settings->getBool("enable_node_highlighting"); @@ -1750,6 +1761,7 @@ void Game::run() flags.show_chat = true; flags.show_hud = true; + flags.show_minimap = g_settings->getBool("enable_minimap"); flags.show_debug = g_settings->getBool("show_debug"); flags.invert_mouse = g_settings->getBool("invert_mouse"); flags.first_loop_after_window_activation = true; @@ -2065,6 +2077,9 @@ bool Game::createClient(const std::string &playername, return false; } + mapper = client->getMapper(); + mapper->setMinimapMode(MINIMAP_MODE_OFF); + return true; } @@ -2599,6 +2614,9 @@ void Game::processKeyboardInput(VolatileRunFlags *flags, client->makeScreenshot(device); } else if (input->wasKeyDown(keycache.key[KeyCache::KEYMAP_ID_TOGGLE_HUD])) { toggleHud(statustext_time, &flags->show_hud); + } else if (input->wasKeyDown(keycache.key[KeyCache::KEYMAP_ID_MINIMAP])) { + toggleMinimap(statustext_time, &flags->show_minimap, &flags->show_hud, + input->isKeyDown(keycache.key[KeyCache::KEYMAP_ID_SNEAK])); } else if (input->wasKeyDown(keycache.key[KeyCache::KEYMAP_ID_TOGGLE_CHAT])) { toggleChat(statustext_time, &flags->show_chat); } else if (input->wasKeyDown(keycache.key[KeyCache::KEYMAP_ID_TOGGLE_FORCE_FOG_OFF])) { @@ -2819,6 +2837,44 @@ void Game::toggleHud(float *statustext_time, bool *flag) client->setHighlighted(client->getHighlighted(), *flag); } +void Game::toggleMinimap(float *statustext_time, bool *flag, bool *show_hud, bool shift_pressed) +{ + if (*show_hud && g_settings->getBool("enable_minimap")) { + if (shift_pressed) { + mapper->toggleMinimapShape(); + return; + } + MinimapMode mode = mapper->getMinimapMode(); + mode = (MinimapMode)((int)(mode) + 1); + *flag = true; + switch (mode) { + case MINIMAP_MODE_SURFACEx1: + statustext = L"Minimap in surface mode, Zoom x1"; + break; + case MINIMAP_MODE_SURFACEx2: + statustext = L"Minimap in surface mode, Zoom x2"; + break; + case MINIMAP_MODE_SURFACEx4: + statustext = L"Minimap in surface mode, Zoom x4"; + break; + case MINIMAP_MODE_RADARx1: + statustext = L"Minimap in radar mode, Zoom x1"; + break; + case MINIMAP_MODE_RADARx2: + statustext = L"Minimap in radar mode, Zoom x2"; + break; + case MINIMAP_MODE_RADARx4: + statustext = L"Minimap in radar mode, Zoom x4"; + break; + default: + mode = MINIMAP_MODE_OFF; + *flag = false; + statustext = L"Minimap hidden"; + } + *statustext_time = 0; + mapper->setMinimapMode(mode); + } +} void Game::toggleFog(float *statustext_time, bool *flag) { @@ -3953,8 +4009,9 @@ void Game::updateFrame(std::vector &highlight_boxes, stats->beginscenetime = timer.stop(true); } - draw_scene(driver, smgr, *camera, *client, player, *hud, guienv, - highlight_boxes, screensize, skycolor, flags.show_hud); + draw_scene(driver, smgr, *camera, *client, player, *hud, *mapper, + guienv, highlight_boxes, screensize, skycolor, flags.show_hud, + flags.show_minimap); /* Profiler graph @@ -3987,6 +4044,13 @@ void Game::updateFrame(std::vector &highlight_boxes, player->hurt_tilt_strength = 0; } + /* + Update minimap pos + */ + if (flags.show_minimap && flags.show_hud) { + mapper->setPos(floatToInt(player->getPosition(), BS)); + } + /* End scene */ diff --git a/src/mapblock_mesh.cpp b/src/mapblock_mesh.cpp index 79e3e81ba..0e4831166 100644 --- a/src/mapblock_mesh.cpp +++ b/src/mapblock_mesh.cpp @@ -25,6 +25,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "nodedef.h" #include "gamedef.h" #include "mesh.h" +#include "minimap.h" #include "content_mapblock.h" #include "noise.h" #include "shader.h" @@ -1028,6 +1029,7 @@ static void updateAllFastFaceRows(MeshMakeData *data, MapBlockMesh::MapBlockMesh(MeshMakeData *data, v3s16 camera_offset): m_mesh(new scene::SMesh()), + m_minimap_mapblock(new MinimapMapblock), m_gamedef(data->m_gamedef), m_tsrc(m_gamedef->getTextureSource()), m_shdrsrc(m_gamedef->getShaderSource()), @@ -1041,6 +1043,32 @@ MapBlockMesh::MapBlockMesh(MeshMakeData *data, v3s16 camera_offset): m_enable_shaders = data->m_use_shaders; m_enable_highlighting = g_settings->getBool("enable_node_highlighting"); + if (g_settings->getBool("enable_minimap")) { + v3s16 blockpos_nodes = data->m_blockpos * MAP_BLOCKSIZE; + for(s16 x = 0; x < MAP_BLOCKSIZE; x++) { + for(s16 z = 0; z < MAP_BLOCKSIZE; z++) { + s16 air_count = 0; + bool surface_found = false; + MinimapPixel* minimap_pixel = &m_minimap_mapblock->data[x + z * MAP_BLOCKSIZE]; + for(s16 y = MAP_BLOCKSIZE -1; y > -1; y--) { + v3s16 p(x, y, z); + MapNode n = data->m_vmanip.getNodeNoEx(blockpos_nodes + p); + if (!surface_found && n.getContent() != CONTENT_AIR) { + minimap_pixel->height = y; + minimap_pixel->id = n.getContent(); + surface_found = true; + } else if (n.getContent() == CONTENT_AIR) { + air_count++; + } + } + if (!surface_found) { + minimap_pixel->id = CONTENT_AIR; + } + minimap_pixel->air_count = air_count; + } + } + } + // 4-21ms for MAP_BLOCKSIZE=16 (NOTE: probably outdated) // 24-155ms for MAP_BLOCKSIZE=32 (NOTE: probably outdated) //TimeTaker timer1("MapBlockMesh()"); diff --git a/src/mapblock_mesh.h b/src/mapblock_mesh.h index 72c90c3e5..283006331 100644 --- a/src/mapblock_mesh.h +++ b/src/mapblock_mesh.h @@ -34,6 +34,7 @@ class IShaderSource; class MapBlock; +struct MinimapMapblock; struct MeshMakeData { @@ -108,6 +109,11 @@ public: return m_mesh; } + MinimapMapblock* getMinimapMapblock() + { + return m_minimap_mapblock; + } + bool isAnimationForced() const { return m_animation_force_timer == 0; @@ -123,6 +129,7 @@ public: private: scene::SMesh *m_mesh; + MinimapMapblock *m_minimap_mapblock; IGameDef *m_gamedef; ITextureSource *m_tsrc; IShaderSource *m_shdrsrc; diff --git a/src/minimap.cpp b/src/minimap.cpp new file mode 100644 index 000000000..61960ca1e --- /dev/null +++ b/src/minimap.cpp @@ -0,0 +1,472 @@ +/* +Minetest +Copyright (C) 2010-2015 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 "minimap.h" +#include "logoutputbuffer.h" +#include "jthread/jmutexautolock.h" +#include "clientmap.h" +#include "settings.h" +#include "nodedef.h" +#include "porting.h" +#include "util/numeric.h" +#include "util/string.h" +#include + +QueuedMinimapUpdate::QueuedMinimapUpdate(): + pos(-1337,-1337,-1337), + data(NULL) +{ +} + +QueuedMinimapUpdate::~QueuedMinimapUpdate() +{ + delete data; +} + +MinimapUpdateQueue::MinimapUpdateQueue() +{ +} + +MinimapUpdateQueue::~MinimapUpdateQueue() +{ + JMutexAutoLock lock(m_mutex); + + for (std::vector::iterator + i = m_queue.begin(); + i != m_queue.end(); i++) + { + QueuedMinimapUpdate *q = *i; + delete q; + } +} + +void MinimapUpdateQueue::addBlock(v3s16 pos, MinimapMapblock *data) +{ + DSTACK(__FUNCTION_NAME); + + JMutexAutoLock lock(m_mutex); + + /* + Find if block is already in queue. + If it is, update the data and quit. + */ + for (std::vector::iterator + i = m_queue.begin(); + i != m_queue.end(); i++) + { + QueuedMinimapUpdate *q = *i; + if (q->pos == pos) { + delete q->data; + q->data = data; + return; + } + } + + /* + Add the block + */ + QueuedMinimapUpdate *q = new QueuedMinimapUpdate; + q->pos = pos; + q->data = data; + m_queue.push_back(q); +} + +QueuedMinimapUpdate * MinimapUpdateQueue::pop() +{ + JMutexAutoLock lock(m_mutex); + + for (std::vector::iterator + i = m_queue.begin(); + i != m_queue.end(); i++) + { + QueuedMinimapUpdate *q = *i; + m_queue.erase(i); + return q; + } + return NULL; +} + +/* + Minimap update thread +*/ + +void *MinimapUpdateThread::Thread() +{ + ThreadStarted(); + + log_register_thread("MinimapUpdateThread"); + + DSTACK(__FUNCTION_NAME); + + BEGIN_DEBUG_EXCEPTION_HANDLER + + porting::setThreadName("MinimapUpdateThread"); + + while (!StopRequested()) { + + while (m_queue.size()) { + QueuedMinimapUpdate *q = m_queue.pop(); + std::map::iterator it; + it = m_blocks_cache.find(q->pos); + if (q->data) { + m_blocks_cache[q->pos] = q->data; + } else if (it != m_blocks_cache.end()) { + delete it->second; + m_blocks_cache.erase(it); + } + } + + if (data->map_invalidated) { + if (data->mode != MINIMAP_MODE_OFF) { + getMap(data->pos, data->map_size, data->scan_height, data->radar); + data->map_invalidated = false; + } + } + // sleep_ms(10); + } + END_DEBUG_EXCEPTION_HANDLER(errorstream) + + return NULL; +} + +MinimapPixel *MinimapUpdateThread::getMinimapPixel (v3s16 pos, s16 height, s16 &pixel_height) +{ + pixel_height = height - MAP_BLOCKSIZE; + v3s16 blockpos_max, blockpos_min, relpos; + getNodeBlockPosWithOffset(v3s16(pos.X, pos.Y - height / 2, pos.Z), blockpos_min, relpos); + getNodeBlockPosWithOffset(v3s16(pos.X, pos.Y + height / 2, pos.Z), blockpos_max, relpos); + std::map::iterator it; + for (s16 i = blockpos_max.Y; i > blockpos_min.Y - 1; i--) { + it = m_blocks_cache.find(v3s16(blockpos_max.X, i, blockpos_max.Z)); + if (it != m_blocks_cache.end()) { + MinimapPixel *pixel = &it->second->data[relpos.X + relpos.Z * MAP_BLOCKSIZE]; + if (pixel->id != CONTENT_AIR) { + pixel_height += pixel->height; + return pixel; + } + } + pixel_height -= MAP_BLOCKSIZE; + } + return NULL; +} + +s16 MinimapUpdateThread::getAirCount (v3s16 pos, s16 height) +{ + s16 air_count = 0; + v3s16 blockpos_max, blockpos_min, relpos; + getNodeBlockPosWithOffset(v3s16(pos.X, pos.Y - height / 2, pos.Z), blockpos_min, relpos); + getNodeBlockPosWithOffset(v3s16(pos.X, pos.Y + height / 2, pos.Z), blockpos_max, relpos); + std::map::iterator it; + for (s16 i = blockpos_max.Y; i > blockpos_min.Y - 1; i--) { + it = m_blocks_cache.find(v3s16(blockpos_max.X, i, blockpos_max.Z)); + if (it != m_blocks_cache.end()) { + MinimapPixel *pixel = &it->second->data[relpos.X + relpos.Z * MAP_BLOCKSIZE]; + air_count += pixel->air_count; + } + } + return air_count; +} + +void MinimapUpdateThread::getMap (v3s16 pos, s16 size, s16 height, bool radar) +{ + v3s16 p = v3s16 (pos.X - size / 2, pos.Y, pos.Z - size / 2); + + for (s16 x = 0; x < size; x++) { + for (s16 z = 0; z < size; z++){ + u16 id = CONTENT_AIR; + MinimapPixel* minimap_pixel = &data->minimap_scan[x + z * size]; + if (!radar) { + s16 pixel_height = 0; + MinimapPixel* cached_pixel = + getMinimapPixel(v3s16(p.X + x, p.Y, p.Z + z), height, pixel_height); + if (cached_pixel) { + id = cached_pixel->id; + minimap_pixel->height = pixel_height; + } + } else { + minimap_pixel->air_count = getAirCount (v3s16(p.X + x, p.Y, p.Z + z), height); + } + minimap_pixel->id = id; + } + } +} + +Mapper::Mapper(IrrlichtDevice *device, Client *client) +{ + this->device = device; + this->client = client; + this->driver = device->getVideoDriver(); + this->tsrc = client->getTextureSource(); + this->player = client->getEnv().getLocalPlayer(); + this->shdrsrc = client->getShaderSource(); + + m_enable_shaders = g_settings->getBool("enable_shaders"); + data = new MinimapData; + data->mode = MINIMAP_MODE_OFF; + data->radar = false; + data->map_invalidated = true; + data->heightmap_image = NULL; + data->minimap_image = NULL; + data->texture = NULL; + data->minimap_shape_round = g_settings->getBool("minimap_shape_round"); + std::string fname1 = "minimap_mask_round.png"; + std::string fname2 = "minimap_overlay_round.png"; + data->minimap_mask_round = driver->createImage (tsrc->getTexture(fname1), + core::position2d(0,0), core::dimension2d(512,512)); + data->minimap_overlay_round = tsrc->getTexture(fname2); + fname1 = "minimap_mask_square.png"; + fname2 = "minimap_overlay_square.png"; + data->minimap_mask_square = driver->createImage (tsrc->getTexture(fname1), + core::position2d(0,0), core::dimension2d(512,512)); + data->minimap_overlay_square = tsrc->getTexture(fname2); + data->player_marker = tsrc->getTexture("player_marker.png"); + m_meshbuffer = getMinimapMeshBuffer(); + m_minimap_update_thread = new MinimapUpdateThread(device, client); + m_minimap_update_thread->data = data; + m_minimap_update_thread->Start(); +} + +Mapper::~Mapper() +{ + m_minimap_update_thread->Stop(); + m_minimap_update_thread->Wait(); + + for (std::map::iterator + it = m_minimap_update_thread->m_blocks_cache.begin(); + it != m_minimap_update_thread->m_blocks_cache.end(); it++){ + delete it->second; + } + + m_meshbuffer->drop(); + data->minimap_mask_round->drop(); + data->minimap_mask_square->drop(); + driver->removeTexture(data->texture); + driver->removeTexture(data->heightmap_texture); + driver->removeTexture(data->minimap_overlay_round); + driver->removeTexture(data->minimap_overlay_square); + delete data; + delete m_minimap_update_thread; +} + +void Mapper::addBlock (v3s16 pos, MinimapMapblock *data) +{ + m_minimap_update_thread->m_queue.addBlock(pos, data); +} + +MinimapMode Mapper::getMinimapMode() +{ + return data->mode; +} + +void Mapper::toggleMinimapShape() +{ + data->minimap_shape_round = !data->minimap_shape_round; + g_settings->setBool(("minimap_shape_round"), data->minimap_shape_round); +} + +void Mapper::setMinimapMode(MinimapMode mode) +{ + static const u16 modeDefs[7 * 3] = { + 0, 0, 0, + 0, 256, 256, + 0, 256, 128, + 0, 256, 64, + 1, 32, 128, + 1, 32, 64, + 1, 32, 32}; + + JMutexAutoLock lock(m_mutex); + data->radar = (bool)modeDefs[(int)mode * 3]; + data->scan_height = modeDefs[(int)mode * 3 + 1]; + data->map_size = modeDefs[(int)mode * 3 + 2]; + data->mode = mode; +} + +void Mapper::setPos(v3s16 pos) +{ + JMutexAutoLock lock(m_mutex); + data->pos = pos; +} + +video::ITexture *Mapper::getMinimapTexture() +{ + // update minimap textures when new scan is ready + if (!data->map_invalidated) { + // create minimap and heightmap image + core::dimension2d dim(data->map_size,data->map_size); + video::IImage *map_image = driver->createImage(video::ECF_A8R8G8B8, dim); + video::IImage *heightmap_image = driver->createImage(video::ECF_A8R8G8B8, dim); + video::IImage *minimap_image = driver->createImage(video::ECF_A8R8G8B8, + core::dimension2d(512, 512)); + + video::SColor c; + if (!data->radar) { + // surface mode + for (s16 x = 0; x < data->map_size; x++) { + for (s16 z = 0; z < data->map_size; z++) { + MinimapPixel* minimap_pixel = &data->minimap_scan[x + z * data->map_size]; + const ContentFeatures &f = client->getNodeDefManager()->get(minimap_pixel->id); + c = f.minimap_color; + c.setAlpha(240); + map_image->setPixel(x, data->map_size - z -1, c); + u32 h = minimap_pixel->height; + heightmap_image->setPixel(x,data->map_size -z -1, + video::SColor(255, h, h, h)); + } + } + } else { + // radar mode + c = video::SColor (240, 0 , 0, 0); + for (s16 x = 0; x < data->map_size; x++) { + for (s16 z = 0; z < data->map_size; z++) { + MinimapPixel* minimap_pixel = &data->minimap_scan[x + z * data->map_size]; + if (minimap_pixel->air_count > 0) { + c.setGreen(core::clamp(core::round32(32 + minimap_pixel->air_count * 8), 0, 255)); + } else { + c.setGreen(0); + } + map_image->setPixel(x, data->map_size - z -1, c); + } + } + } + + map_image->copyToScaling(minimap_image); + map_image->drop(); + + video::IImage *minimap_mask; + if (data->minimap_shape_round) { + minimap_mask = data->minimap_mask_round; + } else { + minimap_mask = data->minimap_mask_square; + } + for (s16 x = 0; x < 512; x++) { + for (s16 y = 0; y < 512; y++) { + video::SColor mask_col = minimap_mask->getPixel(x, y); + if (!mask_col.getAlpha()) { + minimap_image->setPixel(x, y, video::SColor(0,0,0,0)); + } + } + } + + if (data->texture) { + driver->removeTexture(data->texture); + } + if (data->heightmap_texture) { + driver->removeTexture(data->heightmap_texture); + } + data->texture = driver->addTexture("minimap__", minimap_image); + data->heightmap_texture = driver->addTexture("minimap_heightmap__", heightmap_image); + minimap_image->drop(); + heightmap_image->drop(); + + data->map_invalidated = true; + } + return data->texture; +} + +v3f Mapper::getYawVec() +{ + if (data->minimap_shape_round) { + return v3f(cos(player->getYaw()* core::DEGTORAD), + sin(player->getYaw()* core::DEGTORAD), 1.0); + } else { + return v3f(1.0, 0.0, 1.0); + } +} + +scene::SMeshBuffer *Mapper::getMinimapMeshBuffer() +{ + scene::SMeshBuffer *buf = new scene::SMeshBuffer(); + buf->Vertices.set_used(4); + buf->Indices .set_used(6); + video::SColor c(255, 255, 255, 255); + + buf->Vertices[0] = video::S3DVertex(-1, -1, 0, 0, 0, 1, c, 0, 1); + buf->Vertices[1] = video::S3DVertex(-1, 1, 0, 0, 0, 1, c, 0, 0); + buf->Vertices[2] = video::S3DVertex( 1, 1, 0, 0, 0, 1, c, 1, 0); + buf->Vertices[3] = video::S3DVertex( 1, -1, 0, 0, 0, 1, c, 1, 1); + + buf->Indices[0] = 0; + buf->Indices[1] = 1; + buf->Indices[2] = 2; + buf->Indices[3] = 2; + buf->Indices[4] = 3; + buf->Indices[5] = 0; + + return buf; +} + +void Mapper::drawMinimap() +{ + v2u32 screensize = porting::getWindowSize(); + u32 size = 0.25 * screensize.Y; + video::ITexture* minimap_texture = getMinimapTexture(); + core::matrix4 matrix; + + core::rect oldViewPort = driver->getViewPort(); + driver->setViewPort(core::rect(screensize.X - size - 10, 10, + screensize.X - 10, size + 10)); + core::matrix4 oldProjMat = driver->getTransform(video::ETS_PROJECTION); + driver->setTransform(video::ETS_PROJECTION, core::matrix4()); + core::matrix4 oldViewMat = driver->getTransform(video::ETS_VIEW); + driver->setTransform(video::ETS_VIEW, core::matrix4()); + matrix.makeIdentity(); + + if (minimap_texture) { + video::SMaterial& material = m_meshbuffer->getMaterial(); + material.setFlag(video::EMF_TRILINEAR_FILTER, true); + material.Lighting = false; + material.TextureLayer[0].Texture = minimap_texture; + material.TextureLayer[1].Texture = data->heightmap_texture; + if (m_enable_shaders && !data->radar) { + u16 sid = shdrsrc->getShader("minimap_shader", 1, 1); + material.MaterialType = shdrsrc->getShaderInfo(sid).material; + } else { + material.MaterialType = video::EMT_TRANSPARENT_ALPHA_CHANNEL; + } + + if (data->minimap_shape_round) + matrix.setRotationDegrees(core::vector3df(0, 0, 360 - player->getYaw())); + driver->setTransform(video::ETS_WORLD, matrix); + driver->setMaterial(material); + driver->drawMeshBuffer(m_meshbuffer); + video::ITexture *minimap_overlay; + if (data->minimap_shape_round) { + minimap_overlay = data->minimap_overlay_round; + } else { + minimap_overlay = data->minimap_overlay_square; + } + material.TextureLayer[0].Texture = minimap_overlay; + material.MaterialType = video::EMT_TRANSPARENT_ALPHA_CHANNEL; + driver->setMaterial(material); + driver->drawMeshBuffer(m_meshbuffer); + + if (!data->minimap_shape_round) { + matrix.setRotationDegrees(core::vector3df(0, 0, player->getYaw())); + driver->setTransform(video::ETS_WORLD, matrix); + material.TextureLayer[0].Texture = data->player_marker; + driver->setMaterial(material); + driver->drawMeshBuffer(m_meshbuffer); + } + } + + driver->setTransform(video::ETS_VIEW, oldViewMat); + driver->setTransform(video::ETS_PROJECTION, oldProjMat); + driver->setViewPort(oldViewPort); +} diff --git a/src/minimap.h b/src/minimap.h new file mode 100644 index 000000000..ebb74c4fb --- /dev/null +++ b/src/minimap.h @@ -0,0 +1,168 @@ +/* +Minetest +Copyright (C) 2010-2015 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. +*/ + +#ifndef MINIMAP_HEADER +#define MINIMAP_HEADER + +#include "irrlichttypes_extrabloated.h" +#include "client.h" +#include "voxel.h" +#include "jthread/jmutex.h" +#include +#include +#include + +enum MinimapMode { + MINIMAP_MODE_OFF, + MINIMAP_MODE_SURFACEx1, + MINIMAP_MODE_SURFACEx2, + MINIMAP_MODE_SURFACEx4, + MINIMAP_MODE_RADARx1, + MINIMAP_MODE_RADARx2, + MINIMAP_MODE_RADARx4 +}; + +struct MinimapPixel +{ + u16 id; + u16 height; + u16 air_count; + u16 light; +}; + +struct MinimapMapblock +{ + MinimapPixel data[MAP_BLOCKSIZE * MAP_BLOCKSIZE]; +}; + +struct MinimapData +{ + bool radar; + MinimapMode mode; + v3s16 pos; + v3s16 old_pos; + u16 scan_height; + u16 map_size; + MinimapPixel minimap_scan[512 * 512]; + bool map_invalidated; + bool minimap_shape_round; + video::IImage *minimap_image; + video::IImage *heightmap_image; + video::IImage *minimap_mask_round; + video::IImage *minimap_mask_square; + video::ITexture *texture; + video::ITexture *heightmap_texture; + video::ITexture *minimap_overlay_round; + video::ITexture *minimap_overlay_square; + video::ITexture *player_marker; +}; + +struct QueuedMinimapUpdate +{ + v3s16 pos; + MinimapMapblock *data; + + QueuedMinimapUpdate(); + ~QueuedMinimapUpdate(); +}; + +/* + A thread-safe queue of minimap mapblocks update tasks +*/ + +class MinimapUpdateQueue +{ +public: + MinimapUpdateQueue(); + + ~MinimapUpdateQueue(); + + void addBlock(v3s16 pos, MinimapMapblock *data); + + QueuedMinimapUpdate *pop(); + + u32 size() + { + JMutexAutoLock lock(m_mutex); + return m_queue.size(); + } + +private: + std::vector m_queue; + JMutex m_mutex; +}; + +class MinimapUpdateThread : public JThread +{ +private: + +public: + MinimapUpdateThread(IrrlichtDevice *device, Client *client) + { + this->device = device; + this->client = client; + this->driver = device->getVideoDriver(); + this->tsrc = client->getTextureSource(); + } + void getMap (v3s16 pos, s16 size, s16 height, bool radar); + MinimapPixel *getMinimapPixel (v3s16 pos, s16 height, s16 &pixel_height); + s16 getAirCount (v3s16 pos, s16 height); + video::SColor getColorFromId(u16 id); + IrrlichtDevice *device; + Client *client; + video::IVideoDriver *driver; + ITextureSource *tsrc; + void *Thread(); + MinimapData *data; + MinimapUpdateQueue m_queue; + std::map m_blocks_cache; +}; + +class Mapper +{ +private: + MinimapUpdateThread *m_minimap_update_thread; + video::ITexture *minimap_texture; + scene::SMeshBuffer *m_meshbuffer; + bool m_enable_shaders; + JMutex m_mutex; + +public: + Mapper(IrrlichtDevice *device, Client *client); + ~Mapper(); + + void addBlock(v3s16 pos, MinimapMapblock *data); + void setPos(v3s16 pos); + video::ITexture* getMinimapTexture(); + v3f getYawVec(); + MinimapMode getMinimapMode(); + void setMinimapMode(MinimapMode mode); + void toggleMinimapShape(); + scene::SMeshBuffer *getMinimapMeshBuffer(); + void drawMinimap(); + IrrlichtDevice *device; + Client *client; + video::IVideoDriver *driver; + LocalPlayer *player; + ITextureSource *tsrc; + IShaderSource *shdrsrc; + MinimapData *data; +}; + +#endif diff --git a/src/nodedef.cpp b/src/nodedef.cpp index 6c2e96c4f..ba1ca2c65 100644 --- a/src/nodedef.cpp +++ b/src/nodedef.cpp @@ -202,6 +202,7 @@ void ContentFeatures::reset() #ifndef SERVER for(u32 i = 0; i < 24; i++) mesh_ptr[i] = NULL; + minimap_color = video::SColor(0, 0, 0, 0); #endif visual_scale = 1.0; for(u32 i = 0; i < 6; i++) @@ -766,7 +767,6 @@ void CNodeDefManager::applyTextureOverrides(const std::string &override_filepath } } - void CNodeDefManager::updateTextures(IGameDef *gamedef, void (*progress_callback)(void *progress_args, u32 progress, u32 max_progress), void *progress_callback_args) @@ -774,7 +774,6 @@ void CNodeDefManager::updateTextures(IGameDef *gamedef, #ifndef SERVER infostream << "CNodeDefManager::updateTextures(): Updating " "textures in node definitions" << std::endl; - ITextureSource *tsrc = gamedef->tsrc(); IShaderSource *shdsrc = gamedef->getShaderSource(); scene::ISceneManager* smgr = gamedef->getSceneManager(); @@ -797,6 +796,10 @@ void CNodeDefManager::updateTextures(IGameDef *gamedef, for (u32 i = 0; i < size; i++) { ContentFeatures *f = &m_content_features[i]; + // minimap pixel color - the average color of a texture + if (f->tiledef[0].name != "") + f->minimap_color = tsrc->getTextureAverageColor(f->tiledef[0].name); + // Figure out the actual tiles to use TileDef tiledef[6]; for (u32 j = 0; j < 6; j++) { diff --git a/src/nodedef.h b/src/nodedef.h index 3a5e5228d..bbce6ba3e 100644 --- a/src/nodedef.h +++ b/src/nodedef.h @@ -194,6 +194,7 @@ struct ContentFeatures std::string mesh; #ifndef SERVER scene::IMesh *mesh_ptr[24]; + video::SColor minimap_color; #endif float visual_scale; // Misc. scale parameter TileDef tiledef[6]; @@ -202,6 +203,7 @@ struct ContentFeatures // Post effect color, drawn when the camera is inside the node. video::SColor post_effect_color; + // Type of MapNode::param1 ContentParamType param_type; // Type of MapNode::param2 diff --git a/textures/base/pack/minimap_mask_round.png b/textures/base/pack/minimap_mask_round.png new file mode 100644 index 0000000000000000000000000000000000000000..797c780c7fec7a09a527f21543269931b51b973b GIT binary patch literal 4081 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 Iv}x&o0g+IHRR910 literal 0 HcmV?d00001 diff --git a/textures/base/pack/minimap_mask_square.png b/textures/base/pack/minimap_mask_square.png new file mode 100644 index 0000000000000000000000000000000000000000..5dd23f7d8630f4890456b3f0fa2a45b53e65c19c GIT binary patch literal 1951 zcmeAS@N?(olHy`uVBq!ia0y~yU;;9k7&zE~)R&4Yzkn25lDE4H!vqjijof}4D8gCb z5n0T@z%2~Ij105pNB{-dOFVsD+3&MTv8pKj+8tK`6p}1)jVN)>&&^HED`9XhN=+G#9$ zzMXbp_)+M2ij{#u0HOjY#lgVvAmKC%gMoACP6mbpDM0@s^Z_LufF=ONpc?Q?k*FN% zIG`?~Oh+h3xCxtB0|OCWKo~)ka!?>5e1|_26oB@lC`b4XVFW@9k8*?*iOL;-PJl!j z!U%*|15lQr7f4i&>SKbYqbf)Ej=TWc!?a-4y6?;kJAfV|HmxcE)k0zkVLL+10Vqq* zBodW30L_F1j02EG&~$`yL|`>A7%(EGLlRw1qH=^Q6@c!=k)rWSA&h7M8bLy;AW=C| zXfh@+4W;6Q0*@k>NTft9(*YI+1z>axRE#z>uh}iHrdiC`z|DZ^5TL_=;KwE5Q&If; Snc^dwKwM8(KbLh*2~7aY>y)qn literal 0 HcmV?d00001 diff --git a/textures/base/pack/minimap_overlay_round.png b/textures/base/pack/minimap_overlay_round.png new file mode 100644 index 0000000000000000000000000000000000000000..1a6b3cc416ec6916f9e6ff16c36e8e7c440e5d24 GIT binary patch literal 20630 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