aboutsummaryrefslogtreecommitdiff
path: root/src/client
diff options
context:
space:
mode:
authorQuentin Bazin <quent42340@gmail.com>2018-11-28 20:01:49 +0100
committerSmallJoker <SmallJoker@users.noreply.github.com>2018-11-28 20:01:49 +0100
commit5f1cd555cd9d1c64426e173b30b5b792d211c835 (patch)
tree2c8508467d3bf28d549cce2d25144fa8ef42beae /src/client
parentddd9317b733857630499972179caebc236b4d991 (diff)
downloadminetest-5f1cd555cd9d1c64426e173b30b5b792d211c835.tar.gz
minetest-5f1cd555cd9d1c64426e173b30b5b792d211c835.tar.bz2
minetest-5f1cd555cd9d1c64426e173b30b5b792d211c835.zip
Move client-specific files to 'src/client' (#7902)
Update Android.mk Remove 'src/client' from include_directories
Diffstat (limited to 'src/client')
-rw-r--r--src/client/CMakeLists.txt31
-rw-r--r--src/client/camera.cpp658
-rw-r--r--src/client/camera.h231
-rw-r--r--src/client/client.cpp1970
-rw-r--r--src/client/client.h608
-rw-r--r--src/client/clientenvironment.cpp540
-rw-r--r--src/client/clientenvironment.h151
-rw-r--r--src/client/clientmap.cpp671
-rw-r--r--src/client/clientmap.h138
-rw-r--r--src/client/clientmedia.cpp639
-rw-r--r--src/client/clientmedia.h148
-rw-r--r--src/client/clientobject.cpp66
-rw-r--r--src/client/clientobject.h108
-rw-r--r--src/client/clouds.cpp386
-rw-r--r--src/client/clouds.h144
-rw-r--r--src/client/content_cao.cpp1611
-rw-r--r--src/client/content_cao.h236
-rw-r--r--src/client/content_cso.cpp77
-rw-r--r--src/client/content_cso.h26
-rw-r--r--src/client/content_mapblock.cpp1430
-rw-r--r--src/client/content_mapblock.h178
-rw-r--r--src/client/filecache.cpp89
-rw-r--r--src/client/filecache.h42
-rw-r--r--src/client/fontengine.cpp505
-rw-r--r--src/client/fontengine.h128
-rw-r--r--src/client/game.cpp4218
-rw-r--r--src/client/game.h56
-rw-r--r--src/client/guiscalingfilter.cpp169
-rw-r--r--src/client/guiscalingfilter.h50
-rw-r--r--src/client/imagefilters.cpp172
-rw-r--r--src/client/imagefilters.h43
-rw-r--r--src/client/keycode.cpp384
-rw-r--r--src/client/keycode.h67
-rw-r--r--src/client/localplayer.cpp1158
-rw-r--r--src/client/localplayer.h198
-rw-r--r--src/client/mapblock_mesh.cpp1389
-rw-r--r--src/client/mapblock_mesh.h228
-rw-r--r--src/client/mesh.cpp1135
-rw-r--r--src/client/mesh.h129
-rw-r--r--src/client/mesh_generator_thread.cpp308
-rw-r--r--src/client/mesh_generator_thread.h131
-rw-r--r--src/client/meshgen/collector.cpp2
-rw-r--r--src/client/minimap.cpp624
-rw-r--r--src/client/minimap.h163
-rw-r--r--src/client/particles.cpp684
-rw-r--r--src/client/particles.h223
-rw-r--r--src/client/render/core.cpp10
-rw-r--r--src/client/render/interlaced.cpp4
-rw-r--r--src/client/render/stereo.cpp2
-rw-r--r--src/client/shader.cpp873
-rw-r--r--src/client/shader.h156
-rw-r--r--src/client/sky.cpp755
-rw-r--r--src/client/sky.h148
-rw-r--r--src/client/wieldmesh.cpp685
-rw-r--r--src/client/wieldmesh.h140
55 files changed, 25103 insertions, 12 deletions
diff --git a/src/client/CMakeLists.txt b/src/client/CMakeLists.txt
index 1cabe1b11..e24b73e80 100644
--- a/src/client/CMakeLists.txt
+++ b/src/client/CMakeLists.txt
@@ -25,12 +25,37 @@ set(client_SRCS
${CMAKE_CURRENT_SOURCE_DIR}/render/plain.cpp
${CMAKE_CURRENT_SOURCE_DIR}/render/sidebyside.cpp
${CMAKE_CURRENT_SOURCE_DIR}/render/stereo.cpp
- ${CMAKE_CURRENT_SOURCE_DIR}/renderingengine.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/camera.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/client.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/clientenvironment.cpp
${CMAKE_CURRENT_SOURCE_DIR}/clientlauncher.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/clientmap.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/clientmedia.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/clientobject.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/clouds.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/content_cao.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/content_cso.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/content_mapblock.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/filecache.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/fontengine.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/game.cpp
${CMAKE_CURRENT_SOURCE_DIR}/gameui.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/guiscalingfilter.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/hud.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/imagefilters.cpp
${CMAKE_CURRENT_SOURCE_DIR}/inputhandler.cpp
- ${CMAKE_CURRENT_SOURCE_DIR}/tile.cpp
${CMAKE_CURRENT_SOURCE_DIR}/joystick_controller.cpp
- ${CMAKE_CURRENT_SOURCE_DIR}/hud.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/keycode.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/localplayer.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/mapblock_mesh.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/mesh.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/mesh_generator_thread.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/minimap.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/particles.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/renderingengine.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/shader.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/sky.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/tile.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/wieldmesh.cpp
PARENT_SCOPE
)
diff --git a/src/client/camera.cpp b/src/client/camera.cpp
new file mode 100644
index 000000000..1bbdb56ea
--- /dev/null
+++ b/src/client/camera.cpp
@@ -0,0 +1,658 @@
+/*
+Minetest
+Copyright (C) 2010-2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+
+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 "camera.h"
+#include "debug.h"
+#include "client.h"
+#include "map.h"
+#include "clientmap.h" // MapDrawControl
+#include "player.h"
+#include <cmath>
+#include "client/renderingengine.h"
+#include "settings.h"
+#include "wieldmesh.h"
+#include "noise.h" // easeCurve
+#include "sound.h"
+#include "event.h"
+#include "nodedef.h"
+#include "util/numeric.h"
+#include "constants.h"
+#include "fontengine.h"
+#include "script/scripting_client.h"
+
+#define CAMERA_OFFSET_STEP 200
+#define WIELDMESH_OFFSET_X 55.0f
+#define WIELDMESH_OFFSET_Y -35.0f
+
+Camera::Camera(MapDrawControl &draw_control, Client *client):
+ m_draw_control(draw_control),
+ m_client(client)
+{
+ scene::ISceneManager *smgr = RenderingEngine::get_scene_manager();
+ // note: making the camera node a child of the player node
+ // would lead to unexpected behaviour, so we don't do that.
+ m_playernode = smgr->addEmptySceneNode(smgr->getRootSceneNode());
+ m_headnode = smgr->addEmptySceneNode(m_playernode);
+ m_cameranode = smgr->addCameraSceneNode(smgr->getRootSceneNode());
+ m_cameranode->bindTargetAndRotation(true);
+
+ // This needs to be in its own scene manager. It is drawn after
+ // all other 3D scene nodes and before the GUI.
+ m_wieldmgr = smgr->createNewSceneManager();
+ m_wieldmgr->addCameraSceneNode();
+ m_wieldnode = new WieldMeshSceneNode(m_wieldmgr, -1, false);
+ m_wieldnode->setItem(ItemStack(), m_client);
+ m_wieldnode->drop(); // m_wieldmgr grabbed it
+
+ /* TODO: Add a callback function so these can be updated when a setting
+ * changes. At this point in time it doesn't matter (e.g. /set
+ * is documented to change server settings only)
+ *
+ * TODO: Local caching of settings is not optimal and should at some stage
+ * be updated to use a global settings object for getting thse values
+ * (as opposed to the this local caching). This can be addressed in
+ * a later release.
+ */
+ m_cache_fall_bobbing_amount = g_settings->getFloat("fall_bobbing_amount");
+ m_cache_view_bobbing_amount = g_settings->getFloat("view_bobbing_amount");
+ // 45 degrees is the lowest FOV that doesn't cause the server to treat this
+ // as a zoom FOV and load world beyond the set server limits.
+ m_cache_fov = std::fmax(g_settings->getFloat("fov"), 45.0f);
+ m_arm_inertia = g_settings->getBool("arm_inertia");
+ m_nametags.clear();
+}
+
+Camera::~Camera()
+{
+ m_wieldmgr->drop();
+}
+
+bool Camera::successfullyCreated(std::string &error_message)
+{
+ if (!m_playernode) {
+ error_message = "Failed to create the player scene node";
+ } else if (!m_headnode) {
+ error_message = "Failed to create the head scene node";
+ } else if (!m_cameranode) {
+ error_message = "Failed to create the camera scene node";
+ } else if (!m_wieldmgr) {
+ error_message = "Failed to create the wielded item scene manager";
+ } else if (!m_wieldnode) {
+ error_message = "Failed to create the wielded item scene node";
+ } else {
+ error_message.clear();
+ }
+
+ if (g_settings->getBool("enable_client_modding")) {
+ m_client->getScript()->on_camera_ready(this);
+ }
+ return error_message.empty();
+}
+
+// Returns the fractional part of x
+inline f32 my_modf(f32 x)
+{
+ double dummy;
+ return modf(x, &dummy);
+}
+
+void Camera::step(f32 dtime)
+{
+ if(m_view_bobbing_fall > 0)
+ {
+ m_view_bobbing_fall -= 3 * dtime;
+ if(m_view_bobbing_fall <= 0)
+ m_view_bobbing_fall = -1; // Mark the effect as finished
+ }
+
+ bool was_under_zero = m_wield_change_timer < 0;
+ m_wield_change_timer = MYMIN(m_wield_change_timer + dtime, 0.125);
+
+ if (m_wield_change_timer >= 0 && was_under_zero)
+ m_wieldnode->setItem(m_wield_item_next, m_client);
+
+ if (m_view_bobbing_state != 0)
+ {
+ //f32 offset = dtime * m_view_bobbing_speed * 0.035;
+ f32 offset = dtime * m_view_bobbing_speed * 0.030;
+ if (m_view_bobbing_state == 2) {
+ // Animation is getting turned off
+ if (m_view_bobbing_anim < 0.25) {
+ m_view_bobbing_anim -= offset;
+ } else if (m_view_bobbing_anim > 0.75) {
+ m_view_bobbing_anim += offset;
+ }
+
+ if (m_view_bobbing_anim < 0.5) {
+ m_view_bobbing_anim += offset;
+ if (m_view_bobbing_anim > 0.5)
+ m_view_bobbing_anim = 0.5;
+ } else {
+ m_view_bobbing_anim -= offset;
+ if (m_view_bobbing_anim < 0.5)
+ m_view_bobbing_anim = 0.5;
+ }
+
+ if (m_view_bobbing_anim <= 0 || m_view_bobbing_anim >= 1 ||
+ fabs(m_view_bobbing_anim - 0.5) < 0.01) {
+ m_view_bobbing_anim = 0;
+ m_view_bobbing_state = 0;
+ }
+ }
+ else {
+ float was = m_view_bobbing_anim;
+ m_view_bobbing_anim = my_modf(m_view_bobbing_anim + offset);
+ bool step = (was == 0 ||
+ (was < 0.5f && m_view_bobbing_anim >= 0.5f) ||
+ (was > 0.5f && m_view_bobbing_anim <= 0.5f));
+ if(step) {
+ m_client->getEventManager()->put(new SimpleTriggerEvent(MtEvent::VIEW_BOBBING_STEP));
+ }
+ }
+ }
+
+ if (m_digging_button != -1) {
+ f32 offset = dtime * 3.5f;
+ float m_digging_anim_was = m_digging_anim;
+ m_digging_anim += offset;
+ if (m_digging_anim >= 1)
+ {
+ m_digging_anim = 0;
+ m_digging_button = -1;
+ }
+ float lim = 0.15;
+ if(m_digging_anim_was < lim && m_digging_anim >= lim)
+ {
+ if (m_digging_button == 0) {
+ m_client->getEventManager()->put(new SimpleTriggerEvent(MtEvent::CAMERA_PUNCH_LEFT));
+ } else if(m_digging_button == 1) {
+ m_client->getEventManager()->put(new SimpleTriggerEvent(MtEvent::CAMERA_PUNCH_RIGHT));
+ }
+ }
+ }
+}
+
+static inline v2f dir(const v2f &pos_dist)
+{
+ f32 x = pos_dist.X - WIELDMESH_OFFSET_X;
+ f32 y = pos_dist.Y - WIELDMESH_OFFSET_Y;
+
+ f32 x_abs = std::fabs(x);
+ f32 y_abs = std::fabs(y);
+
+ if (x_abs >= y_abs) {
+ y *= (1.0f / x_abs);
+ x /= x_abs;
+ }
+
+ if (y_abs >= x_abs) {
+ x *= (1.0f / y_abs);
+ y /= y_abs;
+ }
+
+ return v2f(std::fabs(x), std::fabs(y));
+}
+
+void Camera::addArmInertia(f32 player_yaw)
+{
+ m_cam_vel.X = std::fabs(rangelim(m_last_cam_pos.X - player_yaw,
+ -100.0f, 100.0f) / 0.016f) * 0.01f;
+ m_cam_vel.Y = std::fabs((m_last_cam_pos.Y - m_camera_direction.Y) / 0.016f);
+ f32 gap_X = std::fabs(WIELDMESH_OFFSET_X - m_wieldmesh_offset.X);
+ f32 gap_Y = std::fabs(WIELDMESH_OFFSET_Y - m_wieldmesh_offset.Y);
+
+ if (m_cam_vel.X > 1.0f || m_cam_vel.Y > 1.0f) {
+ /*
+ The arm moves relative to the camera speed,
+ with an acceleration factor.
+ */
+
+ if (m_cam_vel.X > 1.0f) {
+ if (m_cam_vel.X > m_cam_vel_old.X)
+ m_cam_vel_old.X = m_cam_vel.X;
+
+ f32 acc_X = 0.12f * (m_cam_vel.X - (gap_X * 0.1f));
+ m_wieldmesh_offset.X += m_last_cam_pos.X < player_yaw ? acc_X : -acc_X;
+
+ if (m_last_cam_pos.X != player_yaw)
+ m_last_cam_pos.X = player_yaw;
+
+ m_wieldmesh_offset.X = rangelim(m_wieldmesh_offset.X,
+ WIELDMESH_OFFSET_X - 7.0f, WIELDMESH_OFFSET_X + 7.0f);
+ }
+
+ if (m_cam_vel.Y > 1.0f) {
+ if (m_cam_vel.Y > m_cam_vel_old.Y)
+ m_cam_vel_old.Y = m_cam_vel.Y;
+
+ f32 acc_Y = 0.12f * (m_cam_vel.Y - (gap_Y * 0.1f));
+ m_wieldmesh_offset.Y +=
+ m_last_cam_pos.Y > m_camera_direction.Y ? acc_Y : -acc_Y;
+
+ if (m_last_cam_pos.Y != m_camera_direction.Y)
+ m_last_cam_pos.Y = m_camera_direction.Y;
+
+ m_wieldmesh_offset.Y = rangelim(m_wieldmesh_offset.Y,
+ WIELDMESH_OFFSET_Y - 10.0f, WIELDMESH_OFFSET_Y + 5.0f);
+ }
+
+ m_arm_dir = dir(m_wieldmesh_offset);
+ } else {
+ /*
+ Now the arm gets back to its default position when the camera stops,
+ following a vector, with a smooth deceleration factor.
+ */
+
+ f32 dec_X = 0.12f * (m_cam_vel_old.X * (1.0f +
+ (1.0f - m_arm_dir.X))) * (gap_X / 20.0f);
+
+ f32 dec_Y = 0.06f * (m_cam_vel_old.Y * (1.0f +
+ (1.0f - m_arm_dir.Y))) * (gap_Y / 15.0f);
+
+ if (gap_X < 0.1f)
+ m_cam_vel_old.X = 0.0f;
+
+ m_wieldmesh_offset.X -=
+ m_wieldmesh_offset.X > WIELDMESH_OFFSET_X ? dec_X : -dec_X;
+
+ if (gap_Y < 0.1f)
+ m_cam_vel_old.Y = 0.0f;
+
+ m_wieldmesh_offset.Y -=
+ m_wieldmesh_offset.Y > WIELDMESH_OFFSET_Y ? dec_Y : -dec_Y;
+ }
+}
+
+void Camera::update(LocalPlayer* player, f32 frametime, f32 busytime, f32 tool_reload_ratio)
+{
+ // Get player position
+ // Smooth the movement when walking up stairs
+ v3f old_player_position = m_playernode->getPosition();
+ v3f player_position = player->getPosition();
+ if (player->isAttached && player->parent)
+ player_position = player->parent->getPosition();
+ //if(player->touching_ground && player_position.Y > old_player_position.Y)
+ if(player->touching_ground &&
+ player_position.Y > old_player_position.Y)
+ {
+ f32 oldy = old_player_position.Y;
+ f32 newy = player_position.Y;
+ f32 t = std::exp(-23 * frametime);
+ player_position.Y = oldy * t + newy * (1-t);
+ }
+
+ // Set player node transformation
+ m_playernode->setPosition(player_position);
+ m_playernode->setRotation(v3f(0, -1 * player->getYaw(), 0));
+ m_playernode->updateAbsolutePosition();
+
+ // Get camera tilt timer (hurt animation)
+ float cameratilt = fabs(fabs(player->hurt_tilt_timer-0.75)-0.75);
+
+ // Fall bobbing animation
+ float fall_bobbing = 0;
+ if(player->camera_impact >= 1 && m_camera_mode < CAMERA_MODE_THIRD)
+ {
+ if(m_view_bobbing_fall == -1) // Effect took place and has finished
+ player->camera_impact = m_view_bobbing_fall = 0;
+ else if(m_view_bobbing_fall == 0) // Initialize effect
+ m_view_bobbing_fall = 1;
+
+ // Convert 0 -> 1 to 0 -> 1 -> 0
+ fall_bobbing = m_view_bobbing_fall < 0.5 ? m_view_bobbing_fall * 2 : -(m_view_bobbing_fall - 0.5) * 2 + 1;
+ // Smoothen and invert the above
+ fall_bobbing = sin(fall_bobbing * 0.5 * M_PI) * -1;
+ // Amplify according to the intensity of the impact
+ fall_bobbing *= (1 - rangelim(50 / player->camera_impact, 0, 1)) * 5;
+
+ fall_bobbing *= m_cache_fall_bobbing_amount;
+ }
+
+ // Calculate players eye offset for different camera modes
+ v3f PlayerEyeOffset = player->getEyeOffset();
+ if (m_camera_mode == CAMERA_MODE_FIRST)
+ PlayerEyeOffset += player->eye_offset_first;
+ else
+ PlayerEyeOffset += player->eye_offset_third;
+
+ // Set head node transformation
+ m_headnode->setPosition(PlayerEyeOffset+v3f(0,cameratilt*-player->hurt_tilt_strength+fall_bobbing,0));
+ m_headnode->setRotation(v3f(player->getPitch(), 0, cameratilt*player->hurt_tilt_strength));
+ m_headnode->updateAbsolutePosition();
+
+ // Compute relative camera position and target
+ v3f rel_cam_pos = v3f(0,0,0);
+ v3f rel_cam_target = v3f(0,0,1);
+ v3f rel_cam_up = v3f(0,1,0);
+
+ if (m_cache_view_bobbing_amount != 0.0f && m_view_bobbing_anim != 0.0f &&
+ m_camera_mode < CAMERA_MODE_THIRD) {
+ f32 bobfrac = my_modf(m_view_bobbing_anim * 2);
+ f32 bobdir = (m_view_bobbing_anim < 0.5) ? 1.0 : -1.0;
+
+ #if 1
+ f32 bobknob = 1.2;
+ f32 bobtmp = sin(pow(bobfrac, bobknob) * M_PI);
+ //f32 bobtmp2 = cos(pow(bobfrac, bobknob) * M_PI);
+
+ v3f bobvec = v3f(
+ 0.3 * bobdir * sin(bobfrac * M_PI),
+ -0.28 * bobtmp * bobtmp,
+ 0.);
+
+ //rel_cam_pos += 0.2 * bobvec;
+ //rel_cam_target += 0.03 * bobvec;
+ //rel_cam_up.rotateXYBy(0.02 * bobdir * bobtmp * M_PI);
+ float f = 1.0;
+ f *= m_cache_view_bobbing_amount;
+ rel_cam_pos += bobvec * f;
+ //rel_cam_target += 0.995 * bobvec * f;
+ rel_cam_target += bobvec * f;
+ rel_cam_target.Z -= 0.005 * bobvec.Z * f;
+ //rel_cam_target.X -= 0.005 * bobvec.X * f;
+ //rel_cam_target.Y -= 0.005 * bobvec.Y * f;
+ rel_cam_up.rotateXYBy(-0.03 * bobdir * bobtmp * M_PI * f);
+ #else
+ f32 angle_deg = 1 * bobdir * sin(bobfrac * M_PI);
+ f32 angle_rad = angle_deg * M_PI / 180;
+ f32 r = 0.05;
+ v3f off = v3f(
+ r * sin(angle_rad),
+ r * (cos(angle_rad) - 1),
+ 0);
+ rel_cam_pos += off;
+ //rel_cam_target += off;
+ rel_cam_up.rotateXYBy(angle_deg);
+ #endif
+
+ }
+
+ // Compute absolute camera position and target
+ m_headnode->getAbsoluteTransformation().transformVect(m_camera_position, rel_cam_pos);
+ m_headnode->getAbsoluteTransformation().rotateVect(m_camera_direction, rel_cam_target - rel_cam_pos);
+
+ v3f abs_cam_up;
+ m_headnode->getAbsoluteTransformation().rotateVect(abs_cam_up, rel_cam_up);
+
+ // Seperate camera position for calculation
+ v3f my_cp = m_camera_position;
+
+ // Reposition the camera for third person view
+ if (m_camera_mode > CAMERA_MODE_FIRST)
+ {
+ if (m_camera_mode == CAMERA_MODE_THIRD_FRONT)
+ m_camera_direction *= -1;
+
+ my_cp.Y += 2;
+
+ // Calculate new position
+ bool abort = false;
+ for (int i = BS; i <= BS * 2.75; i++) {
+ my_cp.X = m_camera_position.X + m_camera_direction.X * -i;
+ my_cp.Z = m_camera_position.Z + m_camera_direction.Z * -i;
+ if (i > 12)
+ my_cp.Y = m_camera_position.Y + (m_camera_direction.Y * -i);
+
+ // Prevent camera positioned inside nodes
+ const NodeDefManager *nodemgr = m_client->ndef();
+ MapNode n = m_client->getEnv().getClientMap()
+ .getNodeNoEx(floatToInt(my_cp, BS));
+
+ const ContentFeatures& features = nodemgr->get(n);
+ if (features.walkable) {
+ my_cp.X += m_camera_direction.X*-1*-BS/2;
+ my_cp.Z += m_camera_direction.Z*-1*-BS/2;
+ my_cp.Y += m_camera_direction.Y*-1*-BS/2;
+ abort = true;
+ break;
+ }
+ }
+
+ // If node blocks camera position don't move y to heigh
+ if (abort && my_cp.Y > player_position.Y+BS*2)
+ my_cp.Y = player_position.Y+BS*2;
+ }
+
+ // Update offset if too far away from the center of the map
+ m_camera_offset.X += CAMERA_OFFSET_STEP*
+ (((s16)(my_cp.X/BS) - m_camera_offset.X)/CAMERA_OFFSET_STEP);
+ m_camera_offset.Y += CAMERA_OFFSET_STEP*
+ (((s16)(my_cp.Y/BS) - m_camera_offset.Y)/CAMERA_OFFSET_STEP);
+ m_camera_offset.Z += CAMERA_OFFSET_STEP*
+ (((s16)(my_cp.Z/BS) - m_camera_offset.Z)/CAMERA_OFFSET_STEP);
+
+ // Set camera node transformation
+ m_cameranode->setPosition(my_cp-intToFloat(m_camera_offset, BS));
+ m_cameranode->setUpVector(abs_cam_up);
+ // *100.0 helps in large map coordinates
+ m_cameranode->setTarget(my_cp-intToFloat(m_camera_offset, BS) + 100 * m_camera_direction);
+
+ // update the camera position in third-person mode to render blocks behind player
+ // and correctly apply liquid post FX.
+ if (m_camera_mode != CAMERA_MODE_FIRST)
+ m_camera_position = my_cp;
+
+ // Get FOV
+ f32 fov_degrees;
+ // Disable zoom with zoom FOV = 0
+ if (player->getPlayerControl().zoom && player->getZoomFOV() > 0.001f) {
+ fov_degrees = player->getZoomFOV();
+ } else {
+ fov_degrees = m_cache_fov;
+ }
+ fov_degrees = rangelim(fov_degrees, 1.0f, 160.0f);
+
+ // FOV and aspect ratio
+ const v2u32 &window_size = RenderingEngine::get_instance()->getWindowSize();
+ m_aspect = (f32) window_size.X / (f32) window_size.Y;
+ m_fov_y = fov_degrees * M_PI / 180.0;
+ // Increase vertical FOV on lower aspect ratios (<16:10)
+ m_fov_y *= MYMAX(1.0, MYMIN(1.4, sqrt(16./10. / m_aspect)));
+ m_fov_x = 2 * atan(m_aspect * tan(0.5 * m_fov_y));
+ m_cameranode->setAspectRatio(m_aspect);
+ m_cameranode->setFOV(m_fov_y);
+
+ if (m_arm_inertia)
+ addArmInertia(player->getYaw());
+
+ // Position the wielded item
+ //v3f wield_position = v3f(45, -35, 65);
+ v3f wield_position = v3f(m_wieldmesh_offset.X, m_wieldmesh_offset.Y, 65);
+ //v3f wield_rotation = v3f(-100, 120, -100);
+ v3f wield_rotation = v3f(-100, 120, -100);
+ wield_position.Y += fabs(m_wield_change_timer)*320 - 40;
+ if(m_digging_anim < 0.05 || m_digging_anim > 0.5)
+ {
+ f32 frac = 1.0;
+ if(m_digging_anim > 0.5)
+ frac = 2.0 * (m_digging_anim - 0.5);
+ // This value starts from 1 and settles to 0
+ f32 ratiothing = std::pow((1.0f - tool_reload_ratio), 0.5f);
+ //f32 ratiothing2 = pow(ratiothing, 0.5f);
+ f32 ratiothing2 = (easeCurve(ratiothing*0.5))*2.0;
+ wield_position.Y -= frac * 25.0 * pow(ratiothing2, 1.7f);
+ //wield_position.Z += frac * 5.0 * ratiothing2;
+ wield_position.X -= frac * 35.0 * pow(ratiothing2, 1.1f);
+ wield_rotation.Y += frac * 70.0 * pow(ratiothing2, 1.4f);
+ //wield_rotation.X -= frac * 15.0 * pow(ratiothing2, 1.4f);
+ //wield_rotation.Z += frac * 15.0 * pow(ratiothing2, 1.0f);
+ }
+ if (m_digging_button != -1)
+ {
+ f32 digfrac = m_digging_anim;
+ wield_position.X -= 50 * sin(pow(digfrac, 0.8f) * M_PI);
+ wield_position.Y += 24 * sin(digfrac * 1.8 * M_PI);
+ wield_position.Z += 25 * 0.5;
+
+ // Euler angles are PURE EVIL, so why not use quaternions?
+ core::quaternion quat_begin(wield_rotation * core::DEGTORAD);
+ core::quaternion quat_end(v3f(80, 30, 100) * core::DEGTORAD);
+ core::quaternion quat_slerp;
+ quat_slerp.slerp(quat_begin, quat_end, sin(digfrac * M_PI));
+ quat_slerp.toEuler(wield_rotation);
+ wield_rotation *= core::RADTODEG;
+ } else {
+ f32 bobfrac = my_modf(m_view_bobbing_anim);
+ wield_position.X -= sin(bobfrac*M_PI*2.0) * 3.0;
+ wield_position.Y += sin(my_modf(bobfrac*2.0)*M_PI) * 3.0;
+ }
+ m_wieldnode->setPosition(wield_position);
+ m_wieldnode->setRotation(wield_rotation);
+
+ m_wieldnode->setColor(player->light_color);
+
+ // Set render distance
+ updateViewingRange();
+
+ // If the player is walking, swimming, or climbing,
+ // view bobbing is enabled and free_move is off,
+ // start (or continue) the view bobbing animation.
+ const v3f &speed = player->getSpeed();
+ const bool movement_XZ = hypot(speed.X, speed.Z) > BS;
+ const bool movement_Y = fabs(speed.Y) > BS;
+
+ const bool walking = movement_XZ && player->touching_ground;
+ const bool swimming = (movement_XZ || player->swimming_vertical) && player->in_liquid;
+ const bool climbing = movement_Y && player->is_climbing;
+ if ((walking || swimming || climbing) &&
+ (!g_settings->getBool("free_move") || !m_client->checkLocalPrivilege("fly"))) {
+ // Start animation
+ m_view_bobbing_state = 1;
+ m_view_bobbing_speed = MYMIN(speed.getLength(), 70);
+ }
+ else if (m_view_bobbing_state == 1)
+ {
+ // Stop animation
+ m_view_bobbing_state = 2;
+ m_view_bobbing_speed = 60;
+ }
+}
+
+void Camera::updateViewingRange()
+{
+ f32 viewing_range = g_settings->getFloat("viewing_range");
+ f32 near_plane = g_settings->getFloat("near_plane");
+
+ m_draw_control.wanted_range = std::fmin(adjustDist(viewing_range, getFovMax()), 4000);
+ m_cameranode->setNearValue(rangelim(near_plane, 0.0f, 0.5f) * BS);
+ if (m_draw_control.range_all) {
+ m_cameranode->setFarValue(100000.0);
+ return;
+ }
+ m_cameranode->setFarValue((viewing_range < 2000) ? 2000 * BS : viewing_range * BS);
+}
+
+void Camera::setDigging(s32 button)
+{
+ if (m_digging_button == -1)
+ m_digging_button = button;
+}
+
+void Camera::wield(const ItemStack &item)
+{
+ if (item.name != m_wield_item_next.name ||
+ item.metadata != m_wield_item_next.metadata) {
+ m_wield_item_next = item;
+ if (m_wield_change_timer > 0)
+ m_wield_change_timer = -m_wield_change_timer;
+ else if (m_wield_change_timer == 0)
+ m_wield_change_timer = -0.001;
+ }
+}
+
+void Camera::drawWieldedTool(irr::core::matrix4* translation)
+{
+ // Clear Z buffer so that the wielded tool stay in front of world geometry
+ m_wieldmgr->getVideoDriver()->clearZBuffer();
+
+ // Draw the wielded node (in a separate scene manager)
+ scene::ICameraSceneNode* cam = m_wieldmgr->getActiveCamera();
+ cam->setAspectRatio(m_cameranode->getAspectRatio());
+ cam->setFOV(72.0*M_PI/180.0);
+ cam->setNearValue(10);
+ cam->setFarValue(1000);
+ if (translation != NULL)
+ {
+ irr::core::matrix4 startMatrix = cam->getAbsoluteTransformation();
+ irr::core::vector3df focusPoint = (cam->getTarget()
+ - cam->getAbsolutePosition()).setLength(1)
+ + cam->getAbsolutePosition();
+
+ irr::core::vector3df camera_pos =
+ (startMatrix * *translation).getTranslation();
+ cam->setPosition(camera_pos);
+ cam->setTarget(focusPoint);
+ }
+ m_wieldmgr->drawAll();
+}
+
+void Camera::drawNametags()
+{
+ core::matrix4 trans = m_cameranode->getProjectionMatrix();
+ trans *= m_cameranode->getViewMatrix();
+
+ for (std::list<Nametag *>::const_iterator
+ i = m_nametags.begin();
+ i != m_nametags.end(); ++i) {
+ Nametag *nametag = *i;
+ if (nametag->nametag_color.getAlpha() == 0) {
+ // Enforce hiding nametag,
+ // because if freetype is enabled, a grey
+ // shadow can remain.
+ continue;
+ }
+ v3f pos = nametag->parent_node->getAbsolutePosition() + nametag->nametag_pos * BS;
+ f32 transformed_pos[4] = { pos.X, pos.Y, pos.Z, 1.0f };
+ trans.multiplyWith1x4Matrix(transformed_pos);
+ if (transformed_pos[3] > 0) {
+ std::wstring nametag_colorless =
+ unescape_translate(utf8_to_wide(nametag->nametag_text));
+ core::dimension2d<u32> textsize =
+ g_fontengine->getFont()->getDimension(
+ nametag_colorless.c_str());
+ f32 zDiv = transformed_pos[3] == 0.0f ? 1.0f :
+ core::reciprocal(transformed_pos[3]);
+ v2u32 screensize = RenderingEngine::get_video_driver()->getScreenSize();
+ v2s32 screen_pos;
+ screen_pos.X = screensize.X *
+ (0.5 * transformed_pos[0] * zDiv + 0.5) - textsize.Width / 2;
+ screen_pos.Y = screensize.Y *
+ (0.5 - transformed_pos[1] * zDiv * 0.5) - textsize.Height / 2;
+ core::rect<s32> size(0, 0, textsize.Width, textsize.Height);
+ g_fontengine->getFont()->draw(
+ translate_string(utf8_to_wide(nametag->nametag_text)).c_str(),
+ size + screen_pos, nametag->nametag_color);
+ }
+ }
+}
+
+Nametag *Camera::addNametag(scene::ISceneNode *parent_node,
+ const std::string &nametag_text, video::SColor nametag_color,
+ const v3f &pos)
+{
+ Nametag *nametag = new Nametag(parent_node, nametag_text, nametag_color, pos);
+ m_nametags.push_back(nametag);
+ return nametag;
+}
+
+void Camera::removeNametag(Nametag *nametag)
+{
+ m_nametags.remove(nametag);
+ delete nametag;
+}
diff --git a/src/client/camera.h b/src/client/camera.h
new file mode 100644
index 000000000..88de3570a
--- /dev/null
+++ b/src/client/camera.h
@@ -0,0 +1,231 @@
+/*
+Minetest
+Copyright (C) 2010-2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+
+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 "inventory.h"
+#include "client/tile.h"
+#include <ICameraSceneNode.h>
+#include <ISceneNode.h>
+#include <list>
+
+class LocalPlayer;
+struct MapDrawControl;
+class Client;
+class WieldMeshSceneNode;
+
+struct Nametag {
+ Nametag(scene::ISceneNode *a_parent_node,
+ const std::string &a_nametag_text,
+ const video::SColor &a_nametag_color,
+ const v3f &a_nametag_pos):
+ parent_node(a_parent_node),
+ nametag_text(a_nametag_text),
+ nametag_color(a_nametag_color),
+ nametag_pos(a_nametag_pos)
+ {
+ }
+ scene::ISceneNode *parent_node;
+ std::string nametag_text;
+ video::SColor nametag_color;
+ v3f nametag_pos;
+};
+
+enum CameraMode {CAMERA_MODE_FIRST, CAMERA_MODE_THIRD, CAMERA_MODE_THIRD_FRONT};
+
+/*
+ Client camera class, manages the player and camera scene nodes, the viewing distance
+ and performs view bobbing etc. It also displays the wielded tool in front of the
+ first-person camera.
+*/
+class Camera
+{
+public:
+ Camera(MapDrawControl &draw_control, Client *client);
+ ~Camera();
+
+ // Get camera scene node.
+ // It has the eye transformation, pitch and view bobbing applied.
+ inline scene::ICameraSceneNode* getCameraNode() const
+ {
+ return m_cameranode;
+ }
+
+ // Get the camera position (in absolute scene coordinates).
+ // This has view bobbing applied.
+ inline v3f getPosition() const
+ {
+ return m_camera_position;
+ }
+
+ // Get the camera direction (in absolute camera coordinates).
+ // This has view bobbing applied.
+ inline v3f getDirection() const
+ {
+ return m_camera_direction;
+ }
+
+ // Get the camera offset
+ inline v3s16 getOffset() const
+ {
+ return m_camera_offset;
+ }
+
+ // Horizontal field of view
+ inline f32 getFovX() const
+ {
+ return m_fov_x;
+ }
+
+ // Vertical field of view
+ inline f32 getFovY() const
+ {
+ return m_fov_y;
+ }
+
+ // Get maximum of getFovX() and getFovY()
+ inline f32 getFovMax() const
+ {
+ return MYMAX(m_fov_x, m_fov_y);
+ }
+
+ // Checks if the constructor was able to create the scene nodes
+ bool successfullyCreated(std::string &error_message);
+
+ // Step the camera: updates the viewing range and view bobbing.
+ void step(f32 dtime);
+
+ // Update the camera from the local player's position.
+ // busytime is used to adjust the viewing range.
+ void update(LocalPlayer* player, f32 frametime, f32 busytime,
+ f32 tool_reload_ratio);
+
+ // Update render distance
+ void updateViewingRange();
+
+ // Start digging animation
+ // Pass 0 for left click, 1 for right click
+ void setDigging(s32 button);
+
+ // Replace the wielded item mesh
+ void wield(const ItemStack &item);
+
+ // Draw the wielded tool.
+ // This has to happen *after* the main scene is drawn.
+ // Warning: This clears the Z buffer.
+ void drawWieldedTool(irr::core::matrix4* translation=NULL);
+
+ // Toggle the current camera mode
+ void toggleCameraMode() {
+ if (m_camera_mode == CAMERA_MODE_FIRST)
+ m_camera_mode = CAMERA_MODE_THIRD;
+ else if (m_camera_mode == CAMERA_MODE_THIRD)
+ m_camera_mode = CAMERA_MODE_THIRD_FRONT;
+ else
+ m_camera_mode = CAMERA_MODE_FIRST;
+ }
+
+ // Set the current camera mode
+ inline void setCameraMode(CameraMode mode)
+ {
+ m_camera_mode = mode;
+ }
+
+ //read the current camera mode
+ inline CameraMode getCameraMode()
+ {
+ return m_camera_mode;
+ }
+
+ Nametag *addNametag(scene::ISceneNode *parent_node,
+ const std::string &nametag_text, video::SColor nametag_color,
+ const v3f &pos);
+
+ void removeNametag(Nametag *nametag);
+
+ const std::list<Nametag *> &getNametags() { return m_nametags; }
+
+ void drawNametags();
+
+ inline void addArmInertia(f32 player_yaw);
+
+private:
+ // Nodes
+ scene::ISceneNode *m_playernode = nullptr;
+ scene::ISceneNode *m_headnode = nullptr;
+ scene::ICameraSceneNode *m_cameranode = nullptr;
+
+ scene::ISceneManager *m_wieldmgr = nullptr;
+ WieldMeshSceneNode *m_wieldnode = nullptr;
+
+ // draw control
+ MapDrawControl& m_draw_control;
+
+ Client *m_client;
+
+ // Absolute camera position
+ v3f m_camera_position;
+ // Absolute camera direction
+ v3f m_camera_direction;
+ // Camera offset
+ v3s16 m_camera_offset;
+
+ v2f m_wieldmesh_offset = v2f(55.0f, -35.0f);
+ v2f m_arm_dir;
+ v2f m_cam_vel;
+ v2f m_cam_vel_old;
+ v2f m_last_cam_pos;
+
+ // Field of view and aspect ratio stuff
+ f32 m_aspect = 1.0f;
+ f32 m_fov_x = 1.0f;
+ f32 m_fov_y = 1.0f;
+
+ // View bobbing animation frame (0 <= m_view_bobbing_anim < 1)
+ f32 m_view_bobbing_anim = 0.0f;
+ // If 0, view bobbing is off (e.g. player is standing).
+ // If 1, view bobbing is on (player is walking).
+ // If 2, view bobbing is getting switched off.
+ s32 m_view_bobbing_state = 0;
+ // Speed of view bobbing animation
+ f32 m_view_bobbing_speed = 0.0f;
+ // Fall view bobbing
+ f32 m_view_bobbing_fall = 0.0f;
+
+ // Digging animation frame (0 <= m_digging_anim < 1)
+ f32 m_digging_anim = 0.0f;
+ // If -1, no digging animation
+ // If 0, left-click digging animation
+ // If 1, right-click digging animation
+ s32 m_digging_button = -1;
+
+ // Animation when changing wielded item
+ f32 m_wield_change_timer = 0.125f;
+ ItemStack m_wield_item_next;
+
+ CameraMode m_camera_mode = CAMERA_MODE_FIRST;
+
+ f32 m_cache_fall_bobbing_amount;
+ f32 m_cache_view_bobbing_amount;
+ f32 m_cache_fov;
+ bool m_arm_inertia;
+
+ std::list<Nametag *> m_nametags;
+};
diff --git a/src/client/client.cpp b/src/client/client.cpp
new file mode 100644
index 000000000..17ee3a10a
--- /dev/null
+++ b/src/client/client.cpp
@@ -0,0 +1,1970 @@
+/*
+Minetest
+Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+
+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 <iostream>
+#include <algorithm>
+#include <sstream>
+#include <cmath>
+#include <IFileSystem.h>
+#include "client.h"
+#include "network/clientopcodes.h"
+#include "network/connection.h"
+#include "network/networkpacket.h"
+#include "threading/mutex_auto_lock.h"
+#include "client/clientevent.h"
+#include "client/gameui.h"
+#include "client/renderingengine.h"
+#include "client/sound.h"
+#include "client/tile.h"
+#include "util/auth.h"
+#include "util/directiontables.h"
+#include "util/pointedthing.h"
+#include "util/serialize.h"
+#include "util/string.h"
+#include "util/srp.h"
+#include "filesys.h"
+#include "mapblock_mesh.h"
+#include "mapblock.h"
+#include "minimap.h"
+#include "modchannels.h"
+#include "content/mods.h"
+#include "profiler.h"
+#include "shader.h"
+#include "gettext.h"
+#include "clientmap.h"
+#include "clientmedia.h"
+#include "version.h"
+#include "database/database-sqlite3.h"
+#include "serialization.h"
+#include "guiscalingfilter.h"
+#include "script/scripting_client.h"
+#include "game.h"
+#include "chatmessage.h"
+#include "translation.h"
+
+extern gui::IGUIEnvironment* guienv;
+
+/*
+ Client
+*/
+
+Client::Client(
+ const char *playername,
+ const std::string &password,
+ const std::string &address_name,
+ MapDrawControl &control,
+ IWritableTextureSource *tsrc,
+ IWritableShaderSource *shsrc,
+ IWritableItemDefManager *itemdef,
+ NodeDefManager *nodedef,
+ ISoundManager *sound,
+ MtEventManager *event,
+ bool ipv6,
+ GameUI *game_ui
+):
+ m_tsrc(tsrc),
+ m_shsrc(shsrc),
+ m_itemdef(itemdef),
+ m_nodedef(nodedef),
+ m_sound(sound),
+ m_event(event),
+ m_mesh_update_thread(this),
+ m_env(
+ new ClientMap(this, control, 666),
+ tsrc, this
+ ),
+ m_particle_manager(&m_env),
+ m_con(new con::Connection(PROTOCOL_ID, 512, CONNECTION_TIMEOUT, ipv6, this)),
+ m_address_name(address_name),
+ m_server_ser_ver(SER_FMT_VER_INVALID),
+ m_last_chat_message_sent(time(NULL)),
+ m_password(password),
+ m_chosen_auth_mech(AUTH_MECHANISM_NONE),
+ m_media_downloader(new ClientMediaDownloader()),
+ m_state(LC_Created),
+ m_game_ui(game_ui),
+ m_modchannel_mgr(new ModChannelMgr())
+{
+ // Add local player
+ m_env.setLocalPlayer(new LocalPlayer(this, playername));
+
+ if (g_settings->getBool("enable_minimap")) {
+ m_minimap = new Minimap(this);
+ }
+ m_cache_save_interval = g_settings->getU16("server_map_save_interval");
+
+ m_modding_enabled = g_settings->getBool("enable_client_modding");
+ // Only create the client script environment if client modding is enabled
+ if (m_modding_enabled) {
+ m_script = new ClientScripting(this);
+ m_env.setScript(m_script);
+ m_script->setEnv(&m_env);
+ }
+}
+
+void Client::loadMods()
+{
+ // Don't load mods twice
+ if (m_mods_loaded) {
+ return;
+ }
+
+ // If client modding is not enabled, don't load client-provided CSM mods or
+ // builtin.
+ if (!m_modding_enabled) {
+ warningstream << "Client side mods are disabled by configuration." << std::endl;
+ return;
+ }
+
+ // Load builtin
+ scanModIntoMemory(BUILTIN_MOD_NAME, getBuiltinLuaPath());
+ m_script->loadModFromMemory(BUILTIN_MOD_NAME);
+
+ // If the server has disabled client-provided CSM mod loading, don't load
+ // client-provided CSM mods. Builtin is loaded so needs verfying.
+ if (checkCSMRestrictionFlag(CSMRestrictionFlags::CSM_RF_LOAD_CLIENT_MODS)) {
+ warningstream << "Client side mods are disabled by server." << std::endl;
+ // If builtin integrity is wrong, disconnect user
+ if (!checkBuiltinIntegrity()) {
+ // @TODO disconnect user
+ }
+ return;
+ }
+
+ ClientModConfiguration modconf(getClientModsLuaPath());
+ m_mods = modconf.getMods();
+ // complain about mods with unsatisfied dependencies
+ if (!modconf.isConsistent()) {
+ modconf.printUnsatisfiedModsError();
+ }
+
+ // Print mods
+ infostream << "Client Loading mods: ";
+ for (const ModSpec &mod : m_mods)
+ infostream << mod.name << " ";
+ infostream << std::endl;
+
+ // Load and run "mod" scripts
+ for (const ModSpec &mod : m_mods) {
+ if (!string_allowed(mod.name, MODNAME_ALLOWED_CHARS)) {
+ throw ModError("Error loading mod \"" + mod.name +
+ "\": Mod name does not follow naming conventions: "
+ "Only characters [a-z0-9_] are allowed.");
+ }
+ scanModIntoMemory(mod.name, mod.path);
+ }
+
+ // Load and run "mod" scripts
+ for (const ModSpec &mod : m_mods)
+ m_script->loadModFromMemory(mod.name);
+
+ // Run a callback when mods are loaded
+ m_script->on_mods_loaded();
+ m_mods_loaded = true;
+}
+
+bool Client::checkBuiltinIntegrity()
+{
+ // @TODO
+ return true;
+}
+
+void Client::scanModSubfolder(const std::string &mod_name, const std::string &mod_path,
+ std::string mod_subpath)
+{
+ std::string full_path = mod_path + DIR_DELIM + mod_subpath;
+ std::vector<fs::DirListNode> mod = fs::GetDirListing(full_path);
+ for (const fs::DirListNode &j : mod) {
+ std::string filename = j.name;
+ if (j.dir) {
+ scanModSubfolder(mod_name, mod_path, mod_subpath
+ + filename + DIR_DELIM);
+ continue;
+ }
+ std::replace( mod_subpath.begin(), mod_subpath.end(), DIR_DELIM_CHAR, '/');
+ m_mod_files[mod_name + ":" + mod_subpath + filename] = full_path + filename;
+ }
+}
+
+const std::string &Client::getBuiltinLuaPath()
+{
+ static const std::string builtin_dir = porting::path_share + DIR_DELIM + "builtin";
+ return builtin_dir;
+}
+
+const std::string &Client::getClientModsLuaPath()
+{
+ static const std::string clientmods_dir = porting::path_share + DIR_DELIM + "clientmods";
+ return clientmods_dir;
+}
+
+const std::vector<ModSpec>& Client::getMods() const
+{
+ static std::vector<ModSpec> client_modspec_temp;
+ return client_modspec_temp;
+}
+
+const ModSpec* Client::getModSpec(const std::string &modname) const
+{
+ return NULL;
+}
+
+void Client::Stop()
+{
+ m_shutdown = true;
+ if (m_modding_enabled)
+ m_script->on_shutdown();
+ //request all client managed threads to stop
+ m_mesh_update_thread.stop();
+ // Save local server map
+ if (m_localdb) {
+ infostream << "Local map saving ended." << std::endl;
+ m_localdb->endSave();
+ }
+
+ if (m_modding_enabled)
+ delete m_script;
+}
+
+bool Client::isShutdown()
+{
+ return m_shutdown || !m_mesh_update_thread.isRunning();
+}
+
+Client::~Client()
+{
+ m_shutdown = true;
+ m_con->Disconnect();
+
+ deleteAuthData();
+
+ m_mesh_update_thread.stop();
+ m_mesh_update_thread.wait();
+ while (!m_mesh_update_thread.m_queue_out.empty()) {
+ MeshUpdateResult r = m_mesh_update_thread.m_queue_out.pop_frontNoEx();
+ delete r.mesh;
+ }
+
+
+ delete m_inventory_from_server;
+
+ // Delete detached inventories
+ for (auto &m_detached_inventorie : m_detached_inventories) {
+ delete m_detached_inventorie.second;
+ }
+
+ // cleanup 3d model meshes on client shutdown
+ while (RenderingEngine::get_mesh_cache()->getMeshCount() != 0) {
+ scene::IAnimatedMesh *mesh = RenderingEngine::get_mesh_cache()->getMeshByIndex(0);
+
+ if (mesh)
+ RenderingEngine::get_mesh_cache()->removeMesh(mesh);
+ }
+
+ delete m_minimap;
+ delete m_media_downloader;
+}
+
+void Client::connect(Address address, bool is_local_server)
+{
+ initLocalMapSaving(address, m_address_name, is_local_server);
+
+ m_con->SetTimeoutMs(0);
+ m_con->Connect(address);
+}
+
+void Client::step(float dtime)
+{
+ // Limit a bit
+ if (dtime > 2.0)
+ dtime = 2.0;
+
+ m_animation_time += dtime;
+ if(m_animation_time > 60.0)
+ m_animation_time -= 60.0;
+
+ m_time_of_day_update_timer += dtime;
+
+ ReceiveAll();
+
+ /*
+ Packet counter
+ */
+ {
+ float &counter = m_packetcounter_timer;
+ counter -= dtime;
+ if(counter <= 0.0)
+ {
+ counter = 20.0;
+
+ infostream << "Client packetcounter (" << m_packetcounter_timer
+ << "):"<<std::endl;
+ m_packetcounter.print(infostream);
+ m_packetcounter.clear();
+ }
+ }
+
+ // UGLY hack to fix 2 second startup delay caused by non existent
+ // server client startup synchronization in local server or singleplayer mode
+ static bool initial_step = true;
+ if (initial_step) {
+ initial_step = false;
+ }
+ else if(m_state == LC_Created) {
+ if (m_is_registration_confirmation_state) {
+ // Waiting confirmation
+ return;
+ }
+ float &counter = m_connection_reinit_timer;
+ counter -= dtime;
+ if(counter <= 0.0) {
+ counter = 2.0;
+
+ LocalPlayer *myplayer = m_env.getLocalPlayer();
+ FATAL_ERROR_IF(myplayer == NULL, "Local player not found in environment.");
+
+ sendInit(myplayer->getName());
+ }
+
+ // Not connected, return
+ return;
+ }
+
+ /*
+ Do stuff if connected
+ */
+
+ /*
+ Run Map's timers and unload unused data
+ */
+ const float map_timer_and_unload_dtime = 5.25;
+ if(m_map_timer_and_unload_interval.step(dtime, map_timer_and_unload_dtime)) {
+ ScopeProfiler sp(g_profiler, "Client: map timer and unload");
+ std::vector<v3s16> deleted_blocks;
+ m_env.getMap().timerUpdate(map_timer_and_unload_dtime,
+ g_settings->getFloat("client_unload_unused_data_timeout"),
+ g_settings->getS32("client_mapblock_limit"),
+ &deleted_blocks);
+
+ /*
+ Send info to server
+ NOTE: This loop is intentionally iterated the way it is.
+ */
+
+ std::vector<v3s16>::iterator i = deleted_blocks.begin();
+ std::vector<v3s16> sendlist;
+ for(;;) {
+ if(sendlist.size() == 255 || i == deleted_blocks.end()) {
+ if(sendlist.empty())
+ break;
+ /*
+ [0] u16 command
+ [2] u8 count
+ [3] v3s16 pos_0
+ [3+6] v3s16 pos_1
+ ...
+ */
+
+ sendDeletedBlocks(sendlist);
+
+ if(i == deleted_blocks.end())
+ break;
+
+ sendlist.clear();
+ }
+
+ sendlist.push_back(*i);
+ ++i;
+ }
+ }
+
+ /*
+ Send pending messages on out chat queue
+ */
+ if (!m_out_chat_queue.empty() && canSendChatMessage()) {
+ sendChatMessage(m_out_chat_queue.front());
+ m_out_chat_queue.pop();
+ }
+
+ /*
+ Handle environment
+ */
+ // Control local player (0ms)
+ LocalPlayer *player = m_env.getLocalPlayer();
+ assert(player);
+ player->applyControl(dtime, &m_env);
+
+ // Step environment
+ m_env.step(dtime);
+ m_sound->step(dtime);
+
+ /*
+ Get events
+ */
+ while (m_env.hasClientEnvEvents()) {
+ ClientEnvEvent envEvent = m_env.getClientEnvEvent();
+
+ if (envEvent.type == CEE_PLAYER_DAMAGE) {
+ u8 damage = envEvent.player_damage.amount;
+
+ if (envEvent.player_damage.send_to_server)
+ sendDamage(damage);
+
+ // Add to ClientEvent queue
+ ClientEvent *event = new ClientEvent();
+ event->type = CE_PLAYER_DAMAGE;
+ event->player_damage.amount = damage;
+ m_client_event_queue.push(event);
+ }
+ }
+
+ /*
+ Print some info
+ */
+ float &counter = m_avg_rtt_timer;
+ counter += dtime;
+ if(counter >= 10) {
+ counter = 0.0;
+ // connectedAndInitialized() is true, peer exists.
+ float avg_rtt = getRTT();
+ infostream << "Client: average rtt: " << avg_rtt << std::endl;
+ }
+
+ /*
+ Send player position to server
+ */
+ {
+ float &counter = m_playerpos_send_timer;
+ counter += dtime;
+ if((m_state == LC_Ready) && (counter >= m_recommended_send_interval))
+ {
+ counter = 0.0;
+ sendPlayerPos();
+ }
+ }
+
+ /*
+ Replace updated meshes
+ */
+ {
+ int num_processed_meshes = 0;
+ while (!m_mesh_update_thread.m_queue_out.empty())
+ {
+ num_processed_meshes++;
+
+ MinimapMapblock *minimap_mapblock = NULL;
+ bool do_mapper_update = true;
+
+ MeshUpdateResult r = m_mesh_update_thread.m_queue_out.pop_frontNoEx();
+ MapBlock *block = m_env.getMap().getBlockNoCreateNoEx(r.p);
+ if (block) {
+ // Delete the old mesh
+ delete block->mesh;
+ block->mesh = nullptr;
+
+ if (r.mesh) {
+ minimap_mapblock = r.mesh->moveMinimapMapblock();
+ if (minimap_mapblock == NULL)
+ do_mapper_update = false;
+
+ bool is_empty = true;
+ for (int l = 0; l < MAX_TILE_LAYERS; l++)
+ if (r.mesh->getMesh(l)->getMeshBufferCount() != 0)
+ is_empty = false;
+
+ if (is_empty)
+ delete r.mesh;
+ else
+ // Replace with the new mesh
+ block->mesh = r.mesh;
+ }
+ } else {
+ delete r.mesh;
+ }
+
+ if (m_minimap && do_mapper_update)
+ m_minimap->addBlock(r.p, minimap_mapblock);
+
+ if (r.ack_block_to_server) {
+ /*
+ Acknowledge block
+ [0] u8 count
+ [1] v3s16 pos_0
+ */
+ sendGotBlocks(r.p);
+ }
+ }
+
+ if (num_processed_meshes > 0)
+ g_profiler->graphAdd("num_processed_meshes", num_processed_meshes);
+ }
+
+ /*
+ Load fetched media
+ */
+ if (m_media_downloader && m_media_downloader->isStarted()) {
+ m_media_downloader->step(this);
+ if (m_media_downloader->isDone()) {
+ delete m_media_downloader;
+ m_media_downloader = NULL;
+ }
+ }
+
+ /*
+ If the server didn't update the inventory in a while, revert
+ the local inventory (so the player notices the lag problem
+ and knows something is wrong).
+ */
+ if (m_inventory_from_server) {
+ float interval = 10.0f;
+ float count_before = std::floor(m_inventory_from_server_age / interval);
+
+ m_inventory_from_server_age += dtime;
+
+ float count_after = std::floor(m_inventory_from_server_age / interval);
+
+ if (count_after != count_before) {
+ // Do this every <interval> seconds after TOCLIENT_INVENTORY
+ // Reset the locally changed inventory to the authoritative inventory
+ m_env.getLocalPlayer()->inventory = *m_inventory_from_server;
+ m_inventory_updated = true;
+ }
+ }
+
+ /*
+ Update positions of sounds attached to objects
+ */
+ {
+ for (auto &m_sounds_to_object : m_sounds_to_objects) {
+ int client_id = m_sounds_to_object.first;
+ u16 object_id = m_sounds_to_object.second;
+ ClientActiveObject *cao = m_env.getActiveObject(object_id);
+ if (!cao)
+ continue;
+ m_sound->updateSoundPosition(client_id, cao->getPosition());
+ }
+ }
+
+ /*
+ Handle removed remotely initiated sounds
+ */
+ m_removed_sounds_check_timer += dtime;
+ if(m_removed_sounds_check_timer >= 2.32) {
+ m_removed_sounds_check_timer = 0;
+ // Find removed sounds and clear references to them
+ std::vector<s32> removed_server_ids;
+ for (std::unordered_map<s32, int>::iterator i = m_sounds_server_to_client.begin();
+ i != m_sounds_server_to_client.end();) {
+ s32 server_id = i->first;
+ int client_id = i->second;
+ ++i;
+ if(!m_sound->soundExists(client_id)) {
+ m_sounds_server_to_client.erase(server_id);
+ m_sounds_client_to_server.erase(client_id);
+ m_sounds_to_objects.erase(client_id);
+ removed_server_ids.push_back(server_id);
+ }
+ }
+
+ // Sync to server
+ if(!removed_server_ids.empty()) {
+ sendRemovedSounds(removed_server_ids);
+ }
+ }
+
+ m_mod_storage_save_timer -= dtime;
+ if (m_mod_storage_save_timer <= 0.0f) {
+ verbosestream << "Saving registered mod storages." << std::endl;
+ m_mod_storage_save_timer = g_settings->getFloat("server_map_save_interval");
+ for (std::unordered_map<std::string, ModMetadata *>::const_iterator
+ it = m_mod_storages.begin(); it != m_mod_storages.end(); ++it) {
+ if (it->second->isModified()) {
+ it->second->save(getModStoragePath());
+ }
+ }
+ }
+
+ // Write server map
+ if (m_localdb && m_localdb_save_interval.step(dtime,
+ m_cache_save_interval)) {
+ m_localdb->endSave();
+ m_localdb->beginSave();
+ }
+}
+
+bool Client::loadMedia(const std::string &data, const std::string &filename)
+{
+ // Silly irrlicht's const-incorrectness
+ Buffer<char> data_rw(data.c_str(), data.size());
+
+ std::string name;
+
+ const char *image_ext[] = {
+ ".png", ".jpg", ".bmp", ".tga",
+ ".pcx", ".ppm", ".psd", ".wal", ".rgb",
+ NULL
+ };
+ name = removeStringEnd(filename, image_ext);
+ if (!name.empty()) {
+ verbosestream<<"Client: Attempting to load image "
+ <<"file \""<<filename<<"\""<<std::endl;
+
+ io::IFileSystem *irrfs = RenderingEngine::get_filesystem();
+ video::IVideoDriver *vdrv = RenderingEngine::get_video_driver();
+
+ // Create an irrlicht memory file
+ io::IReadFile *rfile = irrfs->createMemoryReadFile(
+ *data_rw, data_rw.getSize(), "_tempreadfile");
+
+ FATAL_ERROR_IF(!rfile, "Could not create irrlicht memory file.");
+
+ // Read image
+ video::IImage *img = vdrv->createImageFromFile(rfile);
+ if (!img) {
+ errorstream<<"Client: Cannot create image from data of "
+ <<"file \""<<filename<<"\""<<std::endl;
+ rfile->drop();
+ return false;
+ }
+
+ m_tsrc->insertSourceImage(filename, img);
+ img->drop();
+ rfile->drop();
+ return true;
+ }
+
+ const char *sound_ext[] = {
+ ".0.ogg", ".1.ogg", ".2.ogg", ".3.ogg", ".4.ogg",
+ ".5.ogg", ".6.ogg", ".7.ogg", ".8.ogg", ".9.ogg",
+ ".ogg", NULL
+ };
+ name = removeStringEnd(filename, sound_ext);
+ if (!name.empty()) {
+ verbosestream<<"Client: Attempting to load sound "
+ <<"file \""<<filename<<"\""<<std::endl;
+ m_sound->loadSoundData(name, data);
+ return true;
+ }
+
+ const char *model_ext[] = {
+ ".x", ".b3d", ".md2", ".obj",
+ NULL
+ };
+
+ name = removeStringEnd(filename, model_ext);
+ if (!name.empty()) {
+ verbosestream<<"Client: Storing model into memory: "
+ <<"\""<<filename<<"\""<<std::endl;
+ if(m_mesh_data.count(filename))
+ errorstream<<"Multiple models with name \""<<filename.c_str()
+ <<"\" found; replacing previous model"<<std::endl;
+ m_mesh_data[filename] = data;
+ return true;
+ }
+
+ const char *translate_ext[] = {
+ ".tr", NULL
+ };
+ name = removeStringEnd(filename, translate_ext);
+ if (!name.empty()) {
+ verbosestream << "Client: Loading translation: "
+ << "\"" << filename << "\"" << std::endl;
+ g_translations->loadTranslation(data);
+ return true;
+ }
+
+ errorstream << "Client: Don't know how to load file \""
+ << filename << "\"" << std::endl;
+ return false;
+}
+
+// Virtual methods from con::PeerHandler
+void Client::peerAdded(con::Peer *peer)
+{
+ infostream << "Client::peerAdded(): peer->id="
+ << peer->id << std::endl;
+}
+void Client::deletingPeer(con::Peer *peer, bool timeout)
+{
+ infostream << "Client::deletingPeer(): "
+ "Server Peer is getting deleted "
+ << "(timeout=" << timeout << ")" << std::endl;
+
+ if (timeout) {
+ m_access_denied = true;
+ m_access_denied_reason = gettext("Connection timed out.");
+ }
+}
+
+/*
+ u16 command
+ u16 number of files requested
+ for each file {
+ u16 length of name
+ string name
+ }
+*/
+void Client::request_media(const std::vector<std::string> &file_requests)
+{
+ std::ostringstream os(std::ios_base::binary);
+ writeU16(os, TOSERVER_REQUEST_MEDIA);
+ size_t file_requests_size = file_requests.size();
+
+ FATAL_ERROR_IF(file_requests_size > 0xFFFF, "Unsupported number of file requests");
+
+ // Packet dynamicly resized
+ NetworkPacket pkt(TOSERVER_REQUEST_MEDIA, 2 + 0);
+
+ pkt << (u16) (file_requests_size & 0xFFFF);
+
+ for (const std::string &file_request : file_requests) {
+ pkt << file_request;
+ }
+
+ Send(&pkt);
+
+ infostream << "Client: Sending media request list to server ("
+ << file_requests.size() << " files. packet size)" << std::endl;
+}
+
+void Client::initLocalMapSaving(const Address &address,
+ const std::string &hostname,
+ bool is_local_server)
+{
+ if (!g_settings->getBool("enable_local_map_saving") || is_local_server) {
+ return;
+ }
+
+ const std::string world_path = porting::path_user
+ + DIR_DELIM + "worlds"
+ + DIR_DELIM + "server_"
+ + hostname + "_" + std::to_string(address.getPort());
+
+ fs::CreateAllDirs(world_path);
+
+ m_localdb = new MapDatabaseSQLite3(world_path);
+ m_localdb->beginSave();
+ actionstream << "Local map saving started, map will be saved at '" << world_path << "'" << std::endl;
+}
+
+void Client::ReceiveAll()
+{
+ u64 start_ms = porting::getTimeMs();
+ for(;;)
+ {
+ // Limit time even if there would be huge amounts of data to
+ // process
+ if(porting::getTimeMs() > start_ms + 100)
+ break;
+
+ try {
+ Receive();
+ g_profiler->graphAdd("client_received_packets", 1);
+ }
+ catch(con::NoIncomingDataException &e) {
+ break;
+ }
+ catch(con::InvalidIncomingDataException &e) {
+ infostream<<"Client::ReceiveAll(): "
+ "InvalidIncomingDataException: what()="
+ <<e.what()<<std::endl;
+ }
+ }
+}
+
+void Client::Receive()
+{
+ NetworkPacket pkt;
+ m_con->Receive(&pkt);
+ ProcessData(&pkt);
+}
+
+inline void Client::handleCommand(NetworkPacket* pkt)
+{
+ const ToClientCommandHandler& opHandle = toClientCommandTable[pkt->getCommand()];
+ (this->*opHandle.handler)(pkt);
+}
+
+/*
+ sender_peer_id given to this shall be quaranteed to be a valid peer
+*/
+void Client::ProcessData(NetworkPacket *pkt)
+{
+ ToClientCommand command = (ToClientCommand) pkt->getCommand();
+ u32 sender_peer_id = pkt->getPeerId();
+
+ //infostream<<"Client: received command="<<command<<std::endl;
+ m_packetcounter.add((u16)command);
+
+ /*
+ If this check is removed, be sure to change the queue
+ system to know the ids
+ */
+ if(sender_peer_id != PEER_ID_SERVER) {
+ infostream << "Client::ProcessData(): Discarding data not "
+ "coming from server: peer_id=" << sender_peer_id
+ << std::endl;
+ return;
+ }
+
+ // Command must be handled into ToClientCommandHandler
+ if (command >= TOCLIENT_NUM_MSG_TYPES) {
+ infostream << "Client: Ignoring unknown command "
+ << command << std::endl;
+ return;
+ }
+
+ /*
+ * Those packets are handled before m_server_ser_ver is set, it's normal
+ * But we must use the new ToClientConnectionState in the future,
+ * as a byte mask
+ */
+ if(toClientCommandTable[command].state == TOCLIENT_STATE_NOT_CONNECTED) {
+ handleCommand(pkt);
+ return;
+ }
+
+ if(m_server_ser_ver == SER_FMT_VER_INVALID) {
+ infostream << "Client: Server serialization"
+ " format invalid or not initialized."
+ " Skipping incoming command=" << command << std::endl;
+ return;
+ }
+
+ /*
+ Handle runtime commands
+ */
+
+ handleCommand(pkt);
+}
+
+void Client::Send(NetworkPacket* pkt)
+{
+ m_con->Send(PEER_ID_SERVER,
+ serverCommandFactoryTable[pkt->getCommand()].channel,
+ pkt,
+ serverCommandFactoryTable[pkt->getCommand()].reliable);
+}
+
+// Will fill up 12 + 12 + 4 + 4 + 4 bytes
+void writePlayerPos(LocalPlayer *myplayer, ClientMap *clientMap, NetworkPacket *pkt)
+{
+ v3f pf = myplayer->getPosition() * 100;
+ v3f sf = myplayer->getSpeed() * 100;
+ s32 pitch = myplayer->getPitch() * 100;
+ s32 yaw = myplayer->getYaw() * 100;
+ u32 keyPressed = myplayer->keyPressed;
+ // scaled by 80, so that pi can fit into a u8
+ u8 fov = clientMap->getCameraFov() * 80;
+ u8 wanted_range = MYMIN(255,
+ std::ceil(clientMap->getControl().wanted_range / MAP_BLOCKSIZE));
+
+ v3s32 position(pf.X, pf.Y, pf.Z);
+ v3s32 speed(sf.X, sf.Y, sf.Z);
+
+ /*
+ Format:
+ [0] v3s32 position*100
+ [12] v3s32 speed*100
+ [12+12] s32 pitch*100
+ [12+12+4] s32 yaw*100
+ [12+12+4+4] u32 keyPressed
+ [12+12+4+4+4] u8 fov*80
+ [12+12+4+4+4+1] u8 ceil(wanted_range / MAP_BLOCKSIZE)
+ */
+ *pkt << position << speed << pitch << yaw << keyPressed;
+ *pkt << fov << wanted_range;
+}
+
+void Client::interact(u8 action, const PointedThing& pointed)
+{
+ if(m_state != LC_Ready) {
+ errorstream << "Client::interact() "
+ "Canceled (not connected)"
+ << std::endl;
+ return;
+ }
+
+ LocalPlayer *myplayer = m_env.getLocalPlayer();
+ if (myplayer == NULL)
+ return;
+
+ /*
+ [0] u16 command
+ [2] u8 action
+ [3] u16 item
+ [5] u32 length of the next item (plen)
+ [9] serialized PointedThing
+ [9 + plen] player position information
+ actions:
+ 0: start digging (from undersurface) or use
+ 1: stop digging (all parameters ignored)
+ 2: digging completed
+ 3: place block or item (to abovesurface)
+ 4: use item
+ 5: perform secondary action of item
+ */
+
+ NetworkPacket pkt(TOSERVER_INTERACT, 1 + 2 + 0);
+
+ pkt << action;
+ pkt << (u16)getPlayerItem();
+
+ std::ostringstream tmp_os(std::ios::binary);
+ pointed.serialize(tmp_os);
+
+ pkt.putLongString(tmp_os.str());
+
+ writePlayerPos(myplayer, &m_env.getClientMap(), &pkt);
+
+ Send(&pkt);
+}
+
+void Client::deleteAuthData()
+{
+ if (!m_auth_data)
+ return;
+
+ switch (m_chosen_auth_mech) {
+ case AUTH_MECHANISM_FIRST_SRP:
+ break;
+ case AUTH_MECHANISM_SRP:
+ case AUTH_MECHANISM_LEGACY_PASSWORD:
+ srp_user_delete((SRPUser *) m_auth_data);
+ m_auth_data = NULL;
+ break;
+ case AUTH_MECHANISM_NONE:
+ break;
+ }
+ m_chosen_auth_mech = AUTH_MECHANISM_NONE;
+}
+
+
+AuthMechanism Client::choseAuthMech(const u32 mechs)
+{
+ if (mechs & AUTH_MECHANISM_SRP)
+ return AUTH_MECHANISM_SRP;
+
+ if (mechs & AUTH_MECHANISM_FIRST_SRP)
+ return AUTH_MECHANISM_FIRST_SRP;
+
+ if (mechs & AUTH_MECHANISM_LEGACY_PASSWORD)
+ return AUTH_MECHANISM_LEGACY_PASSWORD;
+
+ return AUTH_MECHANISM_NONE;
+}
+
+void Client::sendInit(const std::string &playerName)
+{
+ NetworkPacket pkt(TOSERVER_INIT, 1 + 2 + 2 + (1 + playerName.size()));
+
+ // we don't support network compression yet
+ u16 supp_comp_modes = NETPROTO_COMPRESSION_NONE;
+
+ pkt << (u8) SER_FMT_VER_HIGHEST_READ << (u16) supp_comp_modes;
+ pkt << (u16) CLIENT_PROTOCOL_VERSION_MIN << (u16) CLIENT_PROTOCOL_VERSION_MAX;
+ pkt << playerName;
+
+ Send(&pkt);
+}
+
+void Client::promptConfirmRegistration(AuthMechanism chosen_auth_mechanism)
+{
+ m_chosen_auth_mech = chosen_auth_mechanism;
+ m_is_registration_confirmation_state = true;
+}
+
+void Client::confirmRegistration()
+{
+ m_is_registration_confirmation_state = false;
+ startAuth(m_chosen_auth_mech);
+}
+
+void Client::startAuth(AuthMechanism chosen_auth_mechanism)
+{
+ m_chosen_auth_mech = chosen_auth_mechanism;
+
+ switch (chosen_auth_mechanism) {
+ case AUTH_MECHANISM_FIRST_SRP: {
+ // send srp verifier to server
+ std::string verifier;
+ std::string salt;
+ generate_srp_verifier_and_salt(getPlayerName(), m_password,
+ &verifier, &salt);
+
+ NetworkPacket resp_pkt(TOSERVER_FIRST_SRP, 0);
+ resp_pkt << salt << verifier << (u8)((m_password.empty()) ? 1 : 0);
+
+ Send(&resp_pkt);
+ break;
+ }
+ case AUTH_MECHANISM_SRP:
+ case AUTH_MECHANISM_LEGACY_PASSWORD: {
+ u8 based_on = 1;
+
+ if (chosen_auth_mechanism == AUTH_MECHANISM_LEGACY_PASSWORD) {
+ m_password = translate_password(getPlayerName(), m_password);
+ based_on = 0;
+ }
+
+ std::string playername_u = lowercase(getPlayerName());
+ m_auth_data = srp_user_new(SRP_SHA256, SRP_NG_2048,
+ getPlayerName().c_str(), playername_u.c_str(),
+ (const unsigned char *) m_password.c_str(),
+ m_password.length(), NULL, NULL);
+ char *bytes_A = 0;
+ size_t len_A = 0;
+ SRP_Result res = srp_user_start_authentication(
+ (struct SRPUser *) m_auth_data, NULL, NULL, 0,
+ (unsigned char **) &bytes_A, &len_A);
+ FATAL_ERROR_IF(res != SRP_OK, "Creating local SRP user failed.");
+
+ NetworkPacket resp_pkt(TOSERVER_SRP_BYTES_A, 0);
+ resp_pkt << std::string(bytes_A, len_A) << based_on;
+ Send(&resp_pkt);
+ break;
+ }
+ case AUTH_MECHANISM_NONE:
+ break; // not handled in this method
+ }
+}
+
+void Client::sendDeletedBlocks(std::vector<v3s16> &blocks)
+{
+ NetworkPacket pkt(TOSERVER_DELETEDBLOCKS, 1 + sizeof(v3s16) * blocks.size());
+
+ pkt << (u8) blocks.size();
+
+ for (const v3s16 &block : blocks) {
+ pkt << block;
+ }
+
+ Send(&pkt);
+}
+
+void Client::sendGotBlocks(v3s16 block)
+{
+ NetworkPacket pkt(TOSERVER_GOTBLOCKS, 1 + 6);
+ pkt << (u8) 1 << block;
+ Send(&pkt);
+}
+
+void Client::sendRemovedSounds(std::vector<s32> &soundList)
+{
+ size_t server_ids = soundList.size();
+ assert(server_ids <= 0xFFFF);
+
+ NetworkPacket pkt(TOSERVER_REMOVED_SOUNDS, 2 + server_ids * 4);
+
+ pkt << (u16) (server_ids & 0xFFFF);
+
+ for (int sound_id : soundList)
+ pkt << sound_id;
+
+ Send(&pkt);
+}
+
+void Client::sendNodemetaFields(v3s16 p, const std::string &formname,
+ const StringMap &fields)
+{
+ size_t fields_size = fields.size();
+
+ FATAL_ERROR_IF(fields_size > 0xFFFF, "Unsupported number of nodemeta fields");
+
+ NetworkPacket pkt(TOSERVER_NODEMETA_FIELDS, 0);
+
+ pkt << p << formname << (u16) (fields_size & 0xFFFF);
+
+ StringMap::const_iterator it;
+ for (it = fields.begin(); it != fields.end(); ++it) {
+ const std::string &name = it->first;
+ const std::string &value = it->second;
+ pkt << name;
+ pkt.putLongString(value);
+ }
+
+ Send(&pkt);
+}
+
+void Client::sendInventoryFields(const std::string &formname,
+ const StringMap &fields)
+{
+ size_t fields_size = fields.size();
+ FATAL_ERROR_IF(fields_size > 0xFFFF, "Unsupported number of inventory fields");
+
+ NetworkPacket pkt(TOSERVER_INVENTORY_FIELDS, 0);
+ pkt << formname << (u16) (fields_size & 0xFFFF);
+
+ StringMap::const_iterator it;
+ for (it = fields.begin(); it != fields.end(); ++it) {
+ const std::string &name = it->first;
+ const std::string &value = it->second;
+ pkt << name;
+ pkt.putLongString(value);
+ }
+
+ Send(&pkt);
+}
+
+void Client::sendInventoryAction(InventoryAction *a)
+{
+ std::ostringstream os(std::ios_base::binary);
+
+ a->serialize(os);
+
+ // Make data buffer
+ std::string s = os.str();
+
+ NetworkPacket pkt(TOSERVER_INVENTORY_ACTION, s.size());
+ pkt.putRawString(s.c_str(),s.size());
+
+ Send(&pkt);
+}
+
+bool Client::canSendChatMessage() const
+{
+ u32 now = time(NULL);
+ float time_passed = now - m_last_chat_message_sent;
+
+ float virt_chat_message_allowance = m_chat_message_allowance + time_passed *
+ (CLIENT_CHAT_MESSAGE_LIMIT_PER_10S / 8.0f);
+
+ if (virt_chat_message_allowance < 1.0f)
+ return false;
+
+ return true;
+}
+
+void Client::sendChatMessage(const std::wstring &message)
+{
+ const s16 max_queue_size = g_settings->getS16("max_out_chat_queue_size");
+ if (canSendChatMessage()) {
+ u32 now = time(NULL);
+ float time_passed = now - m_last_chat_message_sent;
+ m_last_chat_message_sent = time(NULL);
+
+ m_chat_message_allowance += time_passed * (CLIENT_CHAT_MESSAGE_LIMIT_PER_10S / 8.0f);
+ if (m_chat_message_allowance > CLIENT_CHAT_MESSAGE_LIMIT_PER_10S)
+ m_chat_message_allowance = CLIENT_CHAT_MESSAGE_LIMIT_PER_10S;
+
+ m_chat_message_allowance -= 1.0f;
+
+ NetworkPacket pkt(TOSERVER_CHAT_MESSAGE, 2 + message.size() * sizeof(u16));
+
+ pkt << message;
+
+ Send(&pkt);
+ } else if (m_out_chat_queue.size() < (u16) max_queue_size || max_queue_size == -1) {
+ m_out_chat_queue.push(message);
+ } else {
+ infostream << "Could not queue chat message because maximum out chat queue size ("
+ << max_queue_size << ") is reached." << std::endl;
+ }
+}
+
+void Client::clearOutChatQueue()
+{
+ m_out_chat_queue = std::queue<std::wstring>();
+}
+
+void Client::sendChangePassword(const std::string &oldpassword,
+ const std::string &newpassword)
+{
+ LocalPlayer *player = m_env.getLocalPlayer();
+ if (player == NULL)
+ return;
+
+ // get into sudo mode and then send new password to server
+ m_password = oldpassword;
+ m_new_password = newpassword;
+ startAuth(choseAuthMech(m_sudo_auth_methods));
+}
+
+
+void Client::sendDamage(u8 damage)
+{
+ NetworkPacket pkt(TOSERVER_DAMAGE, sizeof(u8));
+ pkt << damage;
+ Send(&pkt);
+}
+
+void Client::sendRespawn()
+{
+ NetworkPacket pkt(TOSERVER_RESPAWN, 0);
+ Send(&pkt);
+}
+
+void Client::sendReady()
+{
+ NetworkPacket pkt(TOSERVER_CLIENT_READY,
+ 1 + 1 + 1 + 1 + 2 + sizeof(char) * strlen(g_version_hash));
+
+ pkt << (u8) VERSION_MAJOR << (u8) VERSION_MINOR << (u8) VERSION_PATCH
+ << (u8) 0 << (u16) strlen(g_version_hash);
+
+ pkt.putRawString(g_version_hash, (u16) strlen(g_version_hash));
+ Send(&pkt);
+}
+
+void Client::sendPlayerPos()
+{
+ LocalPlayer *myplayer = m_env.getLocalPlayer();
+ if (!myplayer)
+ return;
+
+ ClientMap &map = m_env.getClientMap();
+
+ u8 camera_fov = map.getCameraFov();
+ u8 wanted_range = map.getControl().wanted_range;
+
+ // Save bandwidth by only updating position when something changed
+ if(myplayer->last_position == myplayer->getPosition() &&
+ myplayer->last_speed == myplayer->getSpeed() &&
+ myplayer->last_pitch == myplayer->getPitch() &&
+ myplayer->last_yaw == myplayer->getYaw() &&
+ myplayer->last_keyPressed == myplayer->keyPressed &&
+ myplayer->last_camera_fov == camera_fov &&
+ myplayer->last_wanted_range == wanted_range)
+ return;
+
+ myplayer->last_position = myplayer->getPosition();
+ myplayer->last_speed = myplayer->getSpeed();
+ myplayer->last_pitch = myplayer->getPitch();
+ myplayer->last_yaw = myplayer->getYaw();
+ myplayer->last_keyPressed = myplayer->keyPressed;
+ myplayer->last_camera_fov = camera_fov;
+ myplayer->last_wanted_range = wanted_range;
+
+ NetworkPacket pkt(TOSERVER_PLAYERPOS, 12 + 12 + 4 + 4 + 4 + 1 + 1);
+
+ writePlayerPos(myplayer, &map, &pkt);
+
+ Send(&pkt);
+}
+
+void Client::sendPlayerItem(u16 item)
+{
+ LocalPlayer *myplayer = m_env.getLocalPlayer();
+ if (!myplayer)
+ return;
+
+ NetworkPacket pkt(TOSERVER_PLAYERITEM, 2);
+
+ pkt << item;
+
+ Send(&pkt);
+}
+
+void Client::removeNode(v3s16 p)
+{
+ std::map<v3s16, MapBlock*> modified_blocks;
+
+ try {
+ m_env.getMap().removeNodeAndUpdate(p, modified_blocks);
+ }
+ catch(InvalidPositionException &e) {
+ }
+
+ for (const auto &modified_block : modified_blocks) {
+ addUpdateMeshTaskWithEdge(modified_block.first, false, true);
+ }
+}
+
+/**
+ * Helper function for Client Side Modding
+ * CSM restrictions are applied there, this should not be used for core engine
+ * @param p
+ * @param is_valid_position
+ * @return
+ */
+MapNode Client::getNode(v3s16 p, bool *is_valid_position)
+{
+ if (checkCSMRestrictionFlag(CSMRestrictionFlags::CSM_RF_LOOKUP_NODES)) {
+ v3s16 ppos = floatToInt(m_env.getLocalPlayer()->getPosition(), BS);
+ if ((u32) ppos.getDistanceFrom(p) > m_csm_restriction_noderange) {
+ *is_valid_position = false;
+ return {};
+ }
+ }
+ return m_env.getMap().getNodeNoEx(p, is_valid_position);
+}
+
+void Client::addNode(v3s16 p, MapNode n, bool remove_metadata)
+{
+ //TimeTaker timer1("Client::addNode()");
+
+ std::map<v3s16, MapBlock*> modified_blocks;
+
+ try {
+ //TimeTaker timer3("Client::addNode(): addNodeAndUpdate");
+ m_env.getMap().addNodeAndUpdate(p, n, modified_blocks, remove_metadata);
+ }
+ catch(InvalidPositionException &e) {
+ }
+
+ for (const auto &modified_block : modified_blocks) {
+ addUpdateMeshTaskWithEdge(modified_block.first, false, true);
+ }
+}
+
+void Client::setPlayerControl(PlayerControl &control)
+{
+ LocalPlayer *player = m_env.getLocalPlayer();
+ assert(player);
+ player->control = control;
+}
+
+void Client::selectPlayerItem(u16 item)
+{
+ m_playeritem = item;
+ m_inventory_updated = true;
+ sendPlayerItem(item);
+}
+
+// Returns true if the inventory of the local player has been
+// updated from the server. If it is true, it is set to false.
+bool Client::getLocalInventoryUpdated()
+{
+ bool updated = m_inventory_updated;
+ m_inventory_updated = false;
+ return updated;
+}
+
+// Copies the inventory of the local player to parameter
+void Client::getLocalInventory(Inventory &dst)
+{
+ LocalPlayer *player = m_env.getLocalPlayer();
+ assert(player);
+ dst = player->inventory;
+}
+
+Inventory* Client::getInventory(const InventoryLocation &loc)
+{
+ switch(loc.type){
+ case InventoryLocation::UNDEFINED:
+ {}
+ break;
+ case InventoryLocation::CURRENT_PLAYER:
+ {
+ LocalPlayer *player = m_env.getLocalPlayer();
+ assert(player);
+ return &player->inventory;
+ }
+ break;
+ case InventoryLocation::PLAYER:
+ {
+ // Check if we are working with local player inventory
+ LocalPlayer *player = m_env.getLocalPlayer();
+ if (!player || strcmp(player->getName(), loc.name.c_str()) != 0)
+ return NULL;
+ return &player->inventory;
+ }
+ break;
+ case InventoryLocation::NODEMETA:
+ {
+ NodeMetadata *meta = m_env.getMap().getNodeMetadata(loc.p);
+ if(!meta)
+ return NULL;
+ return meta->getInventory();
+ }
+ break;
+ case InventoryLocation::DETACHED:
+ {
+ if (m_detached_inventories.count(loc.name) == 0)
+ return NULL;
+ return m_detached_inventories[loc.name];
+ }
+ break;
+ default:
+ FATAL_ERROR("Invalid inventory location type.");
+ break;
+ }
+ return NULL;
+}
+
+void Client::inventoryAction(InventoryAction *a)
+{
+ /*
+ Send it to the server
+ */
+ sendInventoryAction(a);
+
+ /*
+ Predict some local inventory changes
+ */
+ a->clientApply(this, this);
+
+ // Remove it
+ delete a;
+}
+
+float Client::getAnimationTime()
+{
+ return m_animation_time;
+}
+
+int Client::getCrackLevel()
+{
+ return m_crack_level;
+}
+
+v3s16 Client::getCrackPos()
+{
+ return m_crack_pos;
+}
+
+void Client::setCrack(int level, v3s16 pos)
+{
+ int old_crack_level = m_crack_level;
+ v3s16 old_crack_pos = m_crack_pos;
+
+ m_crack_level = level;
+ m_crack_pos = pos;
+
+ if(old_crack_level >= 0 && (level < 0 || pos != old_crack_pos))
+ {
+ // remove old crack
+ addUpdateMeshTaskForNode(old_crack_pos, false, true);
+ }
+ if(level >= 0 && (old_crack_level < 0 || pos != old_crack_pos))
+ {
+ // add new crack
+ addUpdateMeshTaskForNode(pos, false, true);
+ }
+}
+
+u16 Client::getHP()
+{
+ LocalPlayer *player = m_env.getLocalPlayer();
+ assert(player);
+ return player->hp;
+}
+
+bool Client::getChatMessage(std::wstring &res)
+{
+ if (m_chat_queue.empty())
+ return false;
+
+ ChatMessage *chatMessage = m_chat_queue.front();
+ m_chat_queue.pop();
+
+ res = L"";
+
+ switch (chatMessage->type) {
+ case CHATMESSAGE_TYPE_RAW:
+ case CHATMESSAGE_TYPE_ANNOUNCE:
+ case CHATMESSAGE_TYPE_SYSTEM:
+ res = chatMessage->message;
+ break;
+ case CHATMESSAGE_TYPE_NORMAL: {
+ if (!chatMessage->sender.empty())
+ res = L"<" + chatMessage->sender + L"> " + chatMessage->message;
+ else
+ res = chatMessage->message;
+ break;
+ }
+ default:
+ break;
+ }
+
+ delete chatMessage;
+ return true;
+}
+
+void Client::typeChatMessage(const std::wstring &message)
+{
+ // Discard empty line
+ if (message.empty())
+ return;
+
+ // If message was consumed by script API, don't send it to server
+ if (m_modding_enabled && m_script->on_sending_message(wide_to_utf8(message)))
+ return;
+
+ // Send to others
+ sendChatMessage(message);
+
+ // Show locally
+ if (message[0] != L'/') {
+ // compatibility code
+ if (m_proto_ver < 29) {
+ LocalPlayer *player = m_env.getLocalPlayer();
+ assert(player);
+ std::wstring name = narrow_to_wide(player->getName());
+ pushToChatQueue(new ChatMessage(CHATMESSAGE_TYPE_NORMAL, message, name));
+ }
+ }
+}
+
+void Client::addUpdateMeshTask(v3s16 p, bool ack_to_server, bool urgent)
+{
+ // Check if the block exists to begin with. In the case when a non-existing
+ // neighbor is automatically added, it may not. In that case we don't want
+ // to tell the mesh update thread about it.
+ MapBlock *b = m_env.getMap().getBlockNoCreateNoEx(p);
+ if (b == NULL)
+ return;
+
+ m_mesh_update_thread.updateBlock(&m_env.getMap(), p, ack_to_server, urgent);
+}
+
+void Client::addUpdateMeshTaskWithEdge(v3s16 blockpos, bool ack_to_server, bool urgent)
+{
+ try{
+ addUpdateMeshTask(blockpos, ack_to_server, urgent);
+ }
+ catch(InvalidPositionException &e){}
+
+ // Leading edge
+ for (int i=0;i<6;i++)
+ {
+ try{
+ v3s16 p = blockpos + g_6dirs[i];
+ addUpdateMeshTask(p, false, urgent);
+ }
+ catch(InvalidPositionException &e){}
+ }
+}
+
+void Client::addUpdateMeshTaskForNode(v3s16 nodepos, bool ack_to_server, bool urgent)
+{
+ {
+ v3s16 p = nodepos;
+ infostream<<"Client::addUpdateMeshTaskForNode(): "
+ <<"("<<p.X<<","<<p.Y<<","<<p.Z<<")"
+ <<std::endl;
+ }
+
+ v3s16 blockpos = getNodeBlockPos(nodepos);
+ v3s16 blockpos_relative = blockpos * MAP_BLOCKSIZE;
+
+ try{
+ addUpdateMeshTask(blockpos, ack_to_server, urgent);
+ }
+ catch(InvalidPositionException &e) {}
+
+ // Leading edge
+ if(nodepos.X == blockpos_relative.X){
+ try{
+ v3s16 p = blockpos + v3s16(-1,0,0);
+ addUpdateMeshTask(p, false, urgent);
+ }
+ catch(InvalidPositionException &e){}
+ }
+
+ if(nodepos.Y == blockpos_relative.Y){
+ try{
+ v3s16 p = blockpos + v3s16(0,-1,0);
+ addUpdateMeshTask(p, false, urgent);
+ }
+ catch(InvalidPositionException &e){}
+ }
+
+ if(nodepos.Z == blockpos_relative.Z){
+ try{
+ v3s16 p = blockpos + v3s16(0,0,-1);
+ addUpdateMeshTask(p, false, urgent);
+ }
+ catch(InvalidPositionException &e){}
+ }
+}
+
+ClientEvent *Client::getClientEvent()
+{
+ FATAL_ERROR_IF(m_client_event_queue.empty(),
+ "Cannot getClientEvent, queue is empty.");
+
+ ClientEvent *event = m_client_event_queue.front();
+ m_client_event_queue.pop();
+ return event;
+}
+
+bool Client::connectedToServer()
+{
+ return m_con->Connected();
+}
+
+const Address Client::getServerAddress()
+{
+ return m_con->GetPeerAddress(PEER_ID_SERVER);
+}
+
+float Client::mediaReceiveProgress()
+{
+ if (m_media_downloader)
+ return m_media_downloader->getProgress();
+
+ return 1.0; // downloader only exists when not yet done
+}
+
+typedef struct TextureUpdateArgs {
+ gui::IGUIEnvironment *guienv;
+ u64 last_time_ms;
+ u16 last_percent;
+ const wchar_t* text_base;
+ ITextureSource *tsrc;
+} TextureUpdateArgs;
+
+void texture_update_progress(void *args, u32 progress, u32 max_progress)
+{
+ TextureUpdateArgs* targs = (TextureUpdateArgs*) args;
+ u16 cur_percent = ceil(progress / (double) max_progress * 100.);
+
+ // update the loading menu -- if neccessary
+ bool do_draw = false;
+ u64 time_ms = targs->last_time_ms;
+ if (cur_percent != targs->last_percent) {
+ targs->last_percent = cur_percent;
+ time_ms = porting::getTimeMs();
+ // only draw when the user will notice something:
+ do_draw = (time_ms - targs->last_time_ms > 100);
+ }
+
+ if (do_draw) {
+ targs->last_time_ms = time_ms;
+ std::basic_stringstream<wchar_t> strm;
+ strm << targs->text_base << " " << targs->last_percent << "%...";
+ RenderingEngine::draw_load_screen(strm.str(), targs->guienv, targs->tsrc, 0,
+ 72 + (u16) ((18. / 100.) * (double) targs->last_percent), true);
+ }
+}
+
+void Client::afterContentReceived()
+{
+ infostream<<"Client::afterContentReceived() started"<<std::endl;
+ assert(m_itemdef_received); // pre-condition
+ assert(m_nodedef_received); // pre-condition
+ assert(mediaReceived()); // pre-condition
+
+ const wchar_t* text = wgettext("Loading textures...");
+
+ // Clear cached pre-scaled 2D GUI images, as this cache
+ // might have images with the same name but different
+ // content from previous sessions.
+ guiScalingCacheClear();
+
+ // Rebuild inherited images and recreate textures
+ infostream<<"- Rebuilding images and textures"<<std::endl;
+ RenderingEngine::draw_load_screen(text, guienv, m_tsrc, 0, 70);
+ m_tsrc->rebuildImagesAndTextures();
+ delete[] text;
+
+ // Rebuild shaders
+ infostream<<"- Rebuilding shaders"<<std::endl;
+ text = wgettext("Rebuilding shaders...");
+ RenderingEngine::draw_load_screen(text, guienv, m_tsrc, 0, 71);
+ m_shsrc->rebuildShaders();
+ delete[] text;
+
+ // Update node aliases
+ infostream<<"- Updating node aliases"<<std::endl;
+ text = wgettext("Initializing nodes...");
+ RenderingEngine::draw_load_screen(text, guienv, m_tsrc, 0, 72);
+ m_nodedef->updateAliases(m_itemdef);
+ for (const auto &path : getTextureDirs())
+ m_nodedef->applyTextureOverrides(path + DIR_DELIM + "override.txt");
+ m_nodedef->setNodeRegistrationStatus(true);
+ m_nodedef->runNodeResolveCallbacks();
+ delete[] text;
+
+ // Update node textures and assign shaders to each tile
+ infostream<<"- Updating node textures"<<std::endl;
+ TextureUpdateArgs tu_args;
+ tu_args.guienv = guienv;
+ tu_args.last_time_ms = porting::getTimeMs();
+ tu_args.last_percent = 0;
+ tu_args.text_base = wgettext("Initializing nodes");
+ tu_args.tsrc = m_tsrc;
+ m_nodedef->updateTextures(this, texture_update_progress, &tu_args);
+ delete[] tu_args.text_base;
+
+ // Start mesh update thread after setting up content definitions
+ infostream<<"- Starting mesh update thread"<<std::endl;
+ m_mesh_update_thread.start();
+
+ m_state = LC_Ready;
+ sendReady();
+
+ if (g_settings->getBool("enable_client_modding")) {
+ m_script->on_client_ready(m_env.getLocalPlayer());
+ }
+
+ text = wgettext("Done!");
+ RenderingEngine::draw_load_screen(text, guienv, m_tsrc, 0, 100);
+ infostream<<"Client::afterContentReceived() done"<<std::endl;
+ delete[] text;
+}
+
+// returns the Round Trip Time
+// if the RTT did not become updated within 2 seconds, e.g. before timing out,
+// it returns the expired time instead
+float Client::getRTT()
+{
+ float avg_rtt = m_con->getPeerStat(PEER_ID_SERVER, con::AVG_RTT);
+ float time_from_last_rtt =
+ m_con->getPeerStat(PEER_ID_SERVER, con::TIMEOUT_COUNTER);
+ if (avg_rtt + 2.0f > time_from_last_rtt)
+ return avg_rtt;
+ return time_from_last_rtt;
+}
+
+float Client::getCurRate()
+{
+ return (m_con->getLocalStat(con::CUR_INC_RATE) +
+ m_con->getLocalStat(con::CUR_DL_RATE));
+}
+
+void Client::makeScreenshot()
+{
+ irr::video::IVideoDriver *driver = RenderingEngine::get_video_driver();
+ irr::video::IImage* const raw_image = driver->createScreenShot();
+
+ if (!raw_image)
+ return;
+
+ time_t t = time(NULL);
+ struct tm *tm = localtime(&t);
+
+ char timetstamp_c[64];
+ strftime(timetstamp_c, sizeof(timetstamp_c), "%Y%m%d_%H%M%S", tm);
+
+ std::string filename_base = g_settings->get("screenshot_path")
+ + DIR_DELIM
+ + std::string("screenshot_")
+ + std::string(timetstamp_c);
+ std::string filename_ext = "." + g_settings->get("screenshot_format");
+ std::string filename;
+
+ u32 quality = (u32)g_settings->getS32("screenshot_quality");
+ quality = MYMIN(MYMAX(quality, 0), 100) / 100.0 * 255;
+
+ // Try to find a unique filename
+ unsigned serial = 0;
+
+ while (serial < SCREENSHOT_MAX_SERIAL_TRIES) {
+ filename = filename_base + (serial > 0 ? ("_" + itos(serial)) : "") + filename_ext;
+ std::ifstream tmp(filename.c_str());
+ if (!tmp.good())
+ break; // File did not apparently exist, we'll go with it
+ serial++;
+ }
+
+ if (serial == SCREENSHOT_MAX_SERIAL_TRIES) {
+ infostream << "Could not find suitable filename for screenshot" << std::endl;
+ } else {
+ irr::video::IImage* const image =
+ driver->createImage(video::ECF_R8G8B8, raw_image->getDimension());
+
+ if (image) {
+ raw_image->copyTo(image);
+
+ std::ostringstream sstr;
+ if (driver->writeImageToFile(image, filename.c_str(), quality)) {
+ sstr << "Saved screenshot to '" << filename << "'";
+ } else {
+ sstr << "Failed to save screenshot '" << filename << "'";
+ }
+ pushToChatQueue(new ChatMessage(CHATMESSAGE_TYPE_SYSTEM,
+ narrow_to_wide(sstr.str())));
+ infostream << sstr.str() << std::endl;
+ image->drop();
+ }
+ }
+
+ raw_image->drop();
+}
+
+bool Client::shouldShowMinimap() const
+{
+ return !m_minimap_disabled_by_server;
+}
+
+void Client::pushToEventQueue(ClientEvent *event)
+{
+ m_client_event_queue.push(event);
+}
+
+void Client::showMinimap(const bool show)
+{
+ m_game_ui->showMinimap(show);
+}
+
+// IGameDef interface
+// Under envlock
+IItemDefManager* Client::getItemDefManager()
+{
+ return m_itemdef;
+}
+const NodeDefManager* Client::getNodeDefManager()
+{
+ return m_nodedef;
+}
+ICraftDefManager* Client::getCraftDefManager()
+{
+ return NULL;
+ //return m_craftdef;
+}
+ITextureSource* Client::getTextureSource()
+{
+ return m_tsrc;
+}
+IShaderSource* Client::getShaderSource()
+{
+ return m_shsrc;
+}
+
+u16 Client::allocateUnknownNodeId(const std::string &name)
+{
+ errorstream << "Client::allocateUnknownNodeId(): "
+ << "Client cannot allocate node IDs" << std::endl;
+ FATAL_ERROR("Client allocated unknown node");
+
+ return CONTENT_IGNORE;
+}
+ISoundManager* Client::getSoundManager()
+{
+ return m_sound;
+}
+MtEventManager* Client::getEventManager()
+{
+ return m_event;
+}
+
+ParticleManager* Client::getParticleManager()
+{
+ return &m_particle_manager;
+}
+
+scene::IAnimatedMesh* Client::getMesh(const std::string &filename, bool cache)
+{
+ StringMap::const_iterator it = m_mesh_data.find(filename);
+ if (it == m_mesh_data.end()) {
+ errorstream << "Client::getMesh(): Mesh not found: \"" << filename
+ << "\"" << std::endl;
+ return NULL;
+ }
+ const std::string &data = it->second;
+
+ // Create the mesh, remove it from cache and return it
+ // This allows unique vertex colors and other properties for each instance
+ Buffer<char> data_rw(data.c_str(), data.size()); // Const-incorrect Irrlicht
+ io::IReadFile *rfile = RenderingEngine::get_filesystem()->createMemoryReadFile(
+ *data_rw, data_rw.getSize(), filename.c_str());
+ FATAL_ERROR_IF(!rfile, "Could not create/open RAM file");
+
+ scene::IAnimatedMesh *mesh = RenderingEngine::get_scene_manager()->getMesh(rfile);
+ rfile->drop();
+ mesh->grab();
+ if (!cache)
+ RenderingEngine::get_mesh_cache()->removeMesh(mesh);
+ return mesh;
+}
+
+const std::string* Client::getModFile(const std::string &filename)
+{
+ StringMap::const_iterator it = m_mod_files.find(filename);
+ if (it == m_mod_files.end()) {
+ errorstream << "Client::getModFile(): File not found: \"" << filename
+ << "\"" << std::endl;
+ return NULL;
+ }
+ return &it->second;
+}
+
+bool Client::registerModStorage(ModMetadata *storage)
+{
+ if (m_mod_storages.find(storage->getModName()) != m_mod_storages.end()) {
+ errorstream << "Unable to register same mod storage twice. Storage name: "
+ << storage->getModName() << std::endl;
+ return false;
+ }
+
+ m_mod_storages[storage->getModName()] = storage;
+ return true;
+}
+
+void Client::unregisterModStorage(const std::string &name)
+{
+ std::unordered_map<std::string, ModMetadata *>::const_iterator it =
+ m_mod_storages.find(name);
+ if (it != m_mod_storages.end()) {
+ // Save unconditionaly on unregistration
+ it->second->save(getModStoragePath());
+ m_mod_storages.erase(name);
+ }
+}
+
+std::string Client::getModStoragePath() const
+{
+ return porting::path_user + DIR_DELIM + "client" + DIR_DELIM + "mod_storage";
+}
+
+/*
+ * Mod channels
+ */
+
+bool Client::joinModChannel(const std::string &channel)
+{
+ if (m_modchannel_mgr->channelRegistered(channel))
+ return false;
+
+ NetworkPacket pkt(TOSERVER_MODCHANNEL_JOIN, 2 + channel.size());
+ pkt << channel;
+ Send(&pkt);
+
+ m_modchannel_mgr->joinChannel(channel, 0);
+ return true;
+}
+
+bool Client::leaveModChannel(const std::string &channel)
+{
+ if (!m_modchannel_mgr->channelRegistered(channel))
+ return false;
+
+ NetworkPacket pkt(TOSERVER_MODCHANNEL_LEAVE, 2 + channel.size());
+ pkt << channel;
+ Send(&pkt);
+
+ m_modchannel_mgr->leaveChannel(channel, 0);
+ return true;
+}
+
+bool Client::sendModChannelMessage(const std::string &channel, const std::string &message)
+{
+ if (!m_modchannel_mgr->canWriteOnChannel(channel))
+ return false;
+
+ if (message.size() > STRING_MAX_LEN) {
+ warningstream << "ModChannel message too long, dropping before sending "
+ << " (" << message.size() << " > " << STRING_MAX_LEN << ", channel: "
+ << channel << ")" << std::endl;
+ return false;
+ }
+
+ // @TODO: do some client rate limiting
+ NetworkPacket pkt(TOSERVER_MODCHANNEL_MSG, 2 + channel.size() + 2 + message.size());
+ pkt << channel << message;
+ Send(&pkt);
+ return true;
+}
+
+ModChannel* Client::getModChannel(const std::string &channel)
+{
+ return m_modchannel_mgr->getModChannel(channel);
+}
diff --git a/src/client/client.h b/src/client/client.h
new file mode 100644
index 000000000..68a832d8e
--- /dev/null
+++ b/src/client/client.h
@@ -0,0 +1,608 @@
+/*
+Minetest
+Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+
+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 "clientenvironment.h"
+#include "irrlichttypes_extrabloated.h"
+#include <ostream>
+#include <map>
+#include <set>
+#include <vector>
+#include <unordered_set>
+#include "clientobject.h"
+#include "gamedef.h"
+#include "inventorymanager.h"
+#include "localplayer.h"
+#include "client/hud.h"
+#include "particles.h"
+#include "mapnode.h"
+#include "tileanimation.h"
+#include "mesh_generator_thread.h"
+#include "network/address.h"
+#include "network/peerhandler.h"
+#include <fstream>
+
+#define CLIENT_CHAT_MESSAGE_LIMIT_PER_10S 10.0f
+
+struct ClientEvent;
+struct MeshMakeData;
+struct ChatMessage;
+class MapBlockMesh;
+class IWritableTextureSource;
+class IWritableShaderSource;
+class IWritableItemDefManager;
+class ISoundManager;
+class NodeDefManager;
+//class IWritableCraftDefManager;
+class ClientMediaDownloader;
+struct MapDrawControl;
+class ModChannelMgr;
+class MtEventManager;
+struct PointedThing;
+class MapDatabase;
+class Minimap;
+struct MinimapMapblock;
+class Camera;
+class NetworkPacket;
+namespace con {
+class Connection;
+}
+
+enum LocalClientState {
+ LC_Created,
+ LC_Init,
+ LC_Ready
+};
+
+/*
+ Packet counter
+*/
+
+class PacketCounter
+{
+public:
+ PacketCounter() = default;
+
+ void add(u16 command)
+ {
+ std::map<u16, u16>::iterator n = m_packets.find(command);
+ if(n == m_packets.end())
+ {
+ m_packets[command] = 1;
+ }
+ else
+ {
+ n->second++;
+ }
+ }
+
+ void clear()
+ {
+ for (auto &m_packet : m_packets) {
+ m_packet.second = 0;
+ }
+ }
+
+ void print(std::ostream &o)
+ {
+ for (const auto &m_packet : m_packets) {
+ o << "cmd "<< m_packet.first <<" count "<< m_packet.second << std::endl;
+ }
+ }
+
+private:
+ // command, count
+ std::map<u16, u16> m_packets;
+};
+
+class ClientScripting;
+class GameUI;
+
+class Client : public con::PeerHandler, public InventoryManager, public IGameDef
+{
+public:
+ /*
+ NOTE: Nothing is thread-safe here.
+ */
+
+ Client(
+ const char *playername,
+ const std::string &password,
+ const std::string &address_name,
+ MapDrawControl &control,
+ IWritableTextureSource *tsrc,
+ IWritableShaderSource *shsrc,
+ IWritableItemDefManager *itemdef,
+ NodeDefManager *nodedef,
+ ISoundManager *sound,
+ MtEventManager *event,
+ bool ipv6,
+ GameUI *game_ui
+ );
+
+ ~Client();
+ DISABLE_CLASS_COPY(Client);
+
+ // Load local mods into memory
+ void scanModSubfolder(const std::string &mod_name, const std::string &mod_path,
+ std::string mod_subpath);
+ inline void scanModIntoMemory(const std::string &mod_name, const std::string &mod_path)
+ {
+ scanModSubfolder(mod_name, mod_path, "");
+ }
+
+ /*
+ request all threads managed by client to be stopped
+ */
+ void Stop();
+
+
+ bool isShutdown();
+
+ /*
+ The name of the local player should already be set when
+ calling this, as it is sent in the initialization.
+ */
+ void connect(Address address, bool is_local_server);
+
+ /*
+ Stuff that references the environment is valid only as
+ long as this is not called. (eg. Players)
+ If this throws a PeerNotFoundException, the connection has
+ timed out.
+ */
+ void step(float dtime);
+
+ /*
+ * Command Handlers
+ */
+
+ void handleCommand(NetworkPacket* pkt);
+
+ void handleCommand_Null(NetworkPacket* pkt) {};
+ void handleCommand_Deprecated(NetworkPacket* pkt);
+ void handleCommand_Hello(NetworkPacket* pkt);
+ void handleCommand_AuthAccept(NetworkPacket* pkt);
+ void handleCommand_AcceptSudoMode(NetworkPacket* pkt);
+ void handleCommand_DenySudoMode(NetworkPacket* pkt);
+ void handleCommand_AccessDenied(NetworkPacket* pkt);
+ void handleCommand_RemoveNode(NetworkPacket* pkt);
+ void handleCommand_AddNode(NetworkPacket* pkt);
+ void handleCommand_BlockData(NetworkPacket* pkt);
+ void handleCommand_Inventory(NetworkPacket* pkt);
+ void handleCommand_TimeOfDay(NetworkPacket* pkt);
+ void handleCommand_ChatMessage(NetworkPacket *pkt);
+ void handleCommand_ActiveObjectRemoveAdd(NetworkPacket* pkt);
+ void handleCommand_ActiveObjectMessages(NetworkPacket* pkt);
+ void handleCommand_Movement(NetworkPacket* pkt);
+ void handleCommand_HP(NetworkPacket* pkt);
+ void handleCommand_Breath(NetworkPacket* pkt);
+ void handleCommand_MovePlayer(NetworkPacket* pkt);
+ void handleCommand_DeathScreen(NetworkPacket* pkt);
+ void handleCommand_AnnounceMedia(NetworkPacket* pkt);
+ void handleCommand_Media(NetworkPacket* pkt);
+ void handleCommand_NodeDef(NetworkPacket* pkt);
+ void handleCommand_ItemDef(NetworkPacket* pkt);
+ void handleCommand_PlaySound(NetworkPacket* pkt);
+ void handleCommand_StopSound(NetworkPacket* pkt);
+ void handleCommand_FadeSound(NetworkPacket *pkt);
+ void handleCommand_Privileges(NetworkPacket* pkt);
+ void handleCommand_InventoryFormSpec(NetworkPacket* pkt);
+ void handleCommand_DetachedInventory(NetworkPacket* pkt);
+ void handleCommand_ShowFormSpec(NetworkPacket* pkt);
+ void handleCommand_SpawnParticle(NetworkPacket* pkt);
+ void handleCommand_AddParticleSpawner(NetworkPacket* pkt);
+ void handleCommand_DeleteParticleSpawner(NetworkPacket* pkt);
+ void handleCommand_HudAdd(NetworkPacket* pkt);
+ void handleCommand_HudRemove(NetworkPacket* pkt);
+ void handleCommand_HudChange(NetworkPacket* pkt);
+ void handleCommand_HudSetFlags(NetworkPacket* pkt);
+ void handleCommand_HudSetParam(NetworkPacket* pkt);
+ void handleCommand_HudSetSky(NetworkPacket* pkt);
+ void handleCommand_CloudParams(NetworkPacket* pkt);
+ void handleCommand_OverrideDayNightRatio(NetworkPacket* pkt);
+ void handleCommand_LocalPlayerAnimations(NetworkPacket* pkt);
+ void handleCommand_EyeOffset(NetworkPacket* pkt);
+ void handleCommand_UpdatePlayerList(NetworkPacket* pkt);
+ void handleCommand_ModChannelMsg(NetworkPacket *pkt);
+ void handleCommand_ModChannelSignal(NetworkPacket *pkt);
+ void handleCommand_SrpBytesSandB(NetworkPacket *pkt);
+ void handleCommand_FormspecPrepend(NetworkPacket *pkt);
+ void handleCommand_CSMRestrictionFlags(NetworkPacket *pkt);
+
+ void ProcessData(NetworkPacket *pkt);
+
+ void Send(NetworkPacket* pkt);
+
+ void interact(u8 action, const PointedThing& pointed);
+
+ void sendNodemetaFields(v3s16 p, const std::string &formname,
+ const StringMap &fields);
+ void sendInventoryFields(const std::string &formname,
+ const StringMap &fields);
+ void sendInventoryAction(InventoryAction *a);
+ void sendChatMessage(const std::wstring &message);
+ void clearOutChatQueue();
+ void sendChangePassword(const std::string &oldpassword,
+ const std::string &newpassword);
+ void sendDamage(u8 damage);
+ void sendRespawn();
+ void sendReady();
+
+ ClientEnvironment& getEnv() { return m_env; }
+ ITextureSource *tsrc() { return getTextureSource(); }
+ ISoundManager *sound() { return getSoundManager(); }
+ static const std::string &getBuiltinLuaPath();
+ static const std::string &getClientModsLuaPath();
+
+ const std::vector<ModSpec> &getMods() const override;
+ const ModSpec* getModSpec(const std::string &modname) const override;
+
+ // Causes urgent mesh updates (unlike Map::add/removeNodeWithEvent)
+ void removeNode(v3s16 p);
+
+ /**
+ * Helper function for Client Side Modding
+ * CSM restrictions are applied there, this should not be used for core engine
+ * @param p
+ * @param is_valid_position
+ * @return
+ */
+ MapNode getNode(v3s16 p, bool *is_valid_position);
+ void addNode(v3s16 p, MapNode n, bool remove_metadata = true);
+
+ void setPlayerControl(PlayerControl &control);
+
+ void selectPlayerItem(u16 item);
+ u16 getPlayerItem() const
+ { return m_playeritem; }
+
+ // Returns true if the inventory of the local player has been
+ // updated from the server. If it is true, it is set to false.
+ bool getLocalInventoryUpdated();
+ // Copies the inventory of the local player to parameter
+ void getLocalInventory(Inventory &dst);
+
+ /* InventoryManager interface */
+ Inventory* getInventory(const InventoryLocation &loc) override;
+ void inventoryAction(InventoryAction *a) override;
+
+ const std::list<std::string> &getConnectedPlayerNames()
+ {
+ return m_env.getPlayerNames();
+ }
+
+ float getAnimationTime();
+
+ int getCrackLevel();
+ v3s16 getCrackPos();
+ void setCrack(int level, v3s16 pos);
+
+ u16 getHP();
+
+ bool checkPrivilege(const std::string &priv) const
+ { return (m_privileges.count(priv) != 0); }
+
+ const std::unordered_set<std::string> &getPrivilegeList() const
+ { return m_privileges; }
+
+ bool getChatMessage(std::wstring &message);
+ void typeChatMessage(const std::wstring& message);
+
+ u64 getMapSeed(){ return m_map_seed; }
+
+ void addUpdateMeshTask(v3s16 blockpos, bool ack_to_server=false, bool urgent=false);
+ // Including blocks at appropriate edges
+ void addUpdateMeshTaskWithEdge(v3s16 blockpos, bool ack_to_server=false, bool urgent=false);
+ void addUpdateMeshTaskForNode(v3s16 nodepos, bool ack_to_server=false, bool urgent=false);
+
+ void updateCameraOffset(v3s16 camera_offset)
+ { m_mesh_update_thread.m_camera_offset = camera_offset; }
+
+ bool hasClientEvents() const { return !m_client_event_queue.empty(); }
+ // Get event from queue. If queue is empty, it triggers an assertion failure.
+ ClientEvent * getClientEvent();
+
+ bool accessDenied() const { return m_access_denied; }
+
+ bool reconnectRequested() const { return m_access_denied_reconnect; }
+
+ void setFatalError(const std::string &reason)
+ {
+ m_access_denied = true;
+ m_access_denied_reason = reason;
+ }
+
+ // Renaming accessDeniedReason to better name could be good as it's used to
+ // disconnect client when CSM failed.
+ const std::string &accessDeniedReason() const { return m_access_denied_reason; }
+
+ bool itemdefReceived()
+ { return m_itemdef_received; }
+ bool nodedefReceived()
+ { return m_nodedef_received; }
+ bool mediaReceived()
+ { return !m_media_downloader; }
+
+ u8 getProtoVersion()
+ { return m_proto_ver; }
+
+ bool connectedToServer();
+ void confirmRegistration();
+ bool m_is_registration_confirmation_state = false;
+ bool m_simple_singleplayer_mode;
+
+ float mediaReceiveProgress();
+
+ void afterContentReceived();
+
+ float getRTT();
+ float getCurRate();
+
+ Minimap* getMinimap() { return m_minimap; }
+ void setCamera(Camera* camera) { m_camera = camera; }
+
+ Camera* getCamera () { return m_camera; }
+
+ bool shouldShowMinimap() const;
+
+ // IGameDef interface
+ IItemDefManager* getItemDefManager() override;
+ const NodeDefManager* getNodeDefManager() override;
+ ICraftDefManager* getCraftDefManager() override;
+ ITextureSource* getTextureSource();
+ virtual IShaderSource* getShaderSource();
+ u16 allocateUnknownNodeId(const std::string &name) override;
+ virtual ISoundManager* getSoundManager();
+ MtEventManager* getEventManager();
+ virtual ParticleManager* getParticleManager();
+ bool checkLocalPrivilege(const std::string &priv)
+ { return checkPrivilege(priv); }
+ virtual scene::IAnimatedMesh* getMesh(const std::string &filename, bool cache = false);
+ const std::string* getModFile(const std::string &filename);
+
+ std::string getModStoragePath() const override;
+ bool registerModStorage(ModMetadata *meta) override;
+ void unregisterModStorage(const std::string &name) override;
+
+ // The following set of functions is used by ClientMediaDownloader
+ // Insert a media file appropriately into the appropriate manager
+ bool loadMedia(const std::string &data, const std::string &filename);
+ // Send a request for conventional media transfer
+ void request_media(const std::vector<std::string> &file_requests);
+
+ LocalClientState getState() { return m_state; }
+
+ void makeScreenshot();
+
+ inline void pushToChatQueue(ChatMessage *cec)
+ {
+ m_chat_queue.push(cec);
+ }
+
+ ClientScripting *getScript() { return m_script; }
+ const bool moddingEnabled() const { return m_modding_enabled; }
+ const bool modsLoaded() const { return m_mods_loaded; }
+
+ void pushToEventQueue(ClientEvent *event);
+
+ void showMinimap(bool show = true);
+
+ const Address getServerAddress();
+
+ const std::string &getAddressName() const
+ {
+ return m_address_name;
+ }
+
+ inline bool checkCSMRestrictionFlag(CSMRestrictionFlags flag) const
+ {
+ return m_csm_restriction_flags & flag;
+ }
+
+ u32 getCSMNodeRangeLimit() const
+ {
+ return m_csm_restriction_noderange;
+ }
+
+ inline std::unordered_map<u32, u32> &getHUDTranslationMap()
+ {
+ return m_hud_server_to_client;
+ }
+
+ bool joinModChannel(const std::string &channel) override;
+ bool leaveModChannel(const std::string &channel) override;
+ bool sendModChannelMessage(const std::string &channel,
+ const std::string &message) override;
+ ModChannel *getModChannel(const std::string &channel) override;
+
+ const std::string &getFormspecPrepend() const
+ {
+ return m_env.getLocalPlayer()->formspec_prepend;
+ }
+private:
+ void loadMods();
+ bool checkBuiltinIntegrity();
+
+ // Virtual methods from con::PeerHandler
+ void peerAdded(con::Peer *peer) override;
+ void deletingPeer(con::Peer *peer, bool timeout) override;
+
+ void initLocalMapSaving(const Address &address,
+ const std::string &hostname,
+ bool is_local_server);
+
+ void ReceiveAll();
+ void Receive();
+
+ void sendPlayerPos();
+ // Send the item number 'item' as player item to the server
+ void sendPlayerItem(u16 item);
+
+ void deleteAuthData();
+ // helper method shared with clientpackethandler
+ static AuthMechanism choseAuthMech(const u32 mechs);
+
+ void sendInit(const std::string &playerName);
+ void promptConfirmRegistration(AuthMechanism chosen_auth_mechanism);
+ void startAuth(AuthMechanism chosen_auth_mechanism);
+ void sendDeletedBlocks(std::vector<v3s16> &blocks);
+ void sendGotBlocks(v3s16 block);
+ void sendRemovedSounds(std::vector<s32> &soundList);
+
+ // Helper function
+ inline std::string getPlayerName()
+ { return m_env.getLocalPlayer()->getName(); }
+
+ bool canSendChatMessage() const;
+
+ float m_packetcounter_timer = 0.0f;
+ float m_connection_reinit_timer = 0.1f;
+ float m_avg_rtt_timer = 0.0f;
+ float m_playerpos_send_timer = 0.0f;
+ IntervalLimiter m_map_timer_and_unload_interval;
+
+ IWritableTextureSource *m_tsrc;
+ IWritableShaderSource *m_shsrc;
+ IWritableItemDefManager *m_itemdef;
+ NodeDefManager *m_nodedef;
+ ISoundManager *m_sound;
+ MtEventManager *m_event;
+
+
+ MeshUpdateThread m_mesh_update_thread;
+ ClientEnvironment m_env;
+ ParticleManager m_particle_manager;
+ std::unique_ptr<con::Connection> m_con;
+ std::string m_address_name;
+ Camera *m_camera = nullptr;
+ Minimap *m_minimap = nullptr;
+ bool m_minimap_disabled_by_server = false;
+ // Server serialization version
+ u8 m_server_ser_ver;
+
+ // Used version of the protocol with server
+ // Values smaller than 25 only mean they are smaller than 25,
+ // and aren't accurate. We simply just don't know, because
+ // the server didn't send the version back then.
+ // If 0, server init hasn't been received yet.
+ u8 m_proto_ver = 0;
+
+ u16 m_playeritem = 0;
+ bool m_inventory_updated = false;
+ Inventory *m_inventory_from_server = nullptr;
+ float m_inventory_from_server_age = 0.0f;
+ PacketCounter m_packetcounter;
+ // Block mesh animation parameters
+ float m_animation_time = 0.0f;
+ int m_crack_level = -1;
+ v3s16 m_crack_pos;
+ // 0 <= m_daynight_i < DAYNIGHT_CACHE_COUNT
+ //s32 m_daynight_i;
+ //u32 m_daynight_ratio;
+ std::queue<std::wstring> m_out_chat_queue;
+ u32 m_last_chat_message_sent;
+ float m_chat_message_allowance = 5.0f;
+ std::queue<ChatMessage *> m_chat_queue;
+
+ // The authentication methods we can use to enter sudo mode (=change password)
+ u32 m_sudo_auth_methods;
+
+ // The seed returned by the server in TOCLIENT_INIT is stored here
+ u64 m_map_seed = 0;
+
+ // Auth data
+ std::string m_playername;
+ std::string m_password;
+ // If set, this will be sent (and cleared) upon a TOCLIENT_ACCEPT_SUDO_MODE
+ std::string m_new_password;
+ // Usable by auth mechanisms.
+ AuthMechanism m_chosen_auth_mech;
+ void *m_auth_data = nullptr;
+
+
+ bool m_access_denied = false;
+ bool m_access_denied_reconnect = false;
+ std::string m_access_denied_reason = "";
+ std::queue<ClientEvent *> m_client_event_queue;
+ bool m_itemdef_received = false;
+ bool m_nodedef_received = false;
+ bool m_mods_loaded = false;
+ ClientMediaDownloader *m_media_downloader;
+
+ // time_of_day speed approximation for old protocol
+ bool m_time_of_day_set = false;
+ float m_last_time_of_day_f = -1.0f;
+ float m_time_of_day_update_timer = 0.0f;
+
+ // An interval for generally sending object positions and stuff
+ float m_recommended_send_interval = 0.1f;
+
+ // Sounds
+ float m_removed_sounds_check_timer = 0.0f;
+ // Mapping from server sound ids to our sound ids
+ std::unordered_map<s32, int> m_sounds_server_to_client;
+ // And the other way!
+ std::unordered_map<int, s32> m_sounds_client_to_server;
+ // And relations to objects
+ std::unordered_map<int, u16> m_sounds_to_objects;
+
+ // CSM/client IDs to SSM/server IDs Mapping
+ // Map server particle spawner IDs to client IDs
+ std::unordered_map<u32, u32> m_particles_server_to_client;
+ // Map server hud ids to client hud ids
+ std::unordered_map<u32, u32> m_hud_server_to_client;
+
+ // Privileges
+ std::unordered_set<std::string> m_privileges;
+
+ // Detached inventories
+ // key = name
+ std::unordered_map<std::string, Inventory*> m_detached_inventories;
+
+ // Storage for mesh data for creating multiple instances of the same mesh
+ StringMap m_mesh_data;
+
+ StringMap m_mod_files;
+
+ // own state
+ LocalClientState m_state;
+
+ GameUI *m_game_ui;
+
+ // Used for saving server map to disk client-side
+ MapDatabase *m_localdb = nullptr;
+ IntervalLimiter m_localdb_save_interval;
+ u16 m_cache_save_interval;
+
+ ClientScripting *m_script = nullptr;
+ bool m_modding_enabled;
+ std::unordered_map<std::string, ModMetadata *> m_mod_storages;
+ float m_mod_storage_save_timer = 10.0f;
+ std::vector<ModSpec> m_mods;
+
+ bool m_shutdown = false;
+
+ // CSM restrictions byteflag
+ u64 m_csm_restriction_flags = CSMRestrictionFlags::CSM_RF_NONE;
+ u32 m_csm_restriction_noderange = 8;
+
+ std::unique_ptr<ModChannelMgr> m_modchannel_mgr;
+};
diff --git a/src/client/clientenvironment.cpp b/src/client/clientenvironment.cpp
new file mode 100644
index 000000000..e2f24aaa3
--- /dev/null
+++ b/src/client/clientenvironment.cpp
@@ -0,0 +1,540 @@
+/*
+Minetest
+Copyright (C) 2010-2017 celeron55, Perttu Ahola <celeron55@gmail.com>
+
+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/serialize.h"
+#include "util/pointedthing.h"
+#include "client.h"
+#include "clientenvironment.h"
+#include "clientsimpleobject.h"
+#include "clientmap.h"
+#include "scripting_client.h"
+#include "mapblock_mesh.h"
+#include "event.h"
+#include "collision.h"
+#include "nodedef.h"
+#include "profiler.h"
+#include "raycast.h"
+#include "voxelalgorithms.h"
+#include "settings.h"
+#include "content_cao.h"
+#include <algorithm>
+#include "client/renderingengine.h"
+
+/*
+ ClientEnvironment
+*/
+
+ClientEnvironment::ClientEnvironment(ClientMap *map,
+ ITextureSource *texturesource, Client *client):
+ Environment(client),
+ m_map(map),
+ m_texturesource(texturesource),
+ m_client(client)
+{
+ char zero = 0;
+ memset(attachement_parent_ids, zero, sizeof(attachement_parent_ids));
+}
+
+ClientEnvironment::~ClientEnvironment()
+{
+ // delete active objects
+ for (auto &active_object : m_active_objects) {
+ delete active_object.second;
+ }
+
+ for (auto &simple_object : m_simple_objects) {
+ delete simple_object;
+ }
+
+ // Drop/delete map
+ m_map->drop();
+
+ delete m_local_player;
+}
+
+Map & ClientEnvironment::getMap()
+{
+ return *m_map;
+}
+
+ClientMap & ClientEnvironment::getClientMap()
+{
+ return *m_map;
+}
+
+void ClientEnvironment::setLocalPlayer(LocalPlayer *player)
+{
+ /*
+ It is a failure if already is a local player
+ */
+ FATAL_ERROR_IF(m_local_player != NULL,
+ "Local player already allocated");
+
+ m_local_player = player;
+}
+
+void ClientEnvironment::step(float dtime)
+{
+ /* Step time of day */
+ stepTimeOfDay(dtime);
+
+ // Get some settings
+ bool fly_allowed = m_client->checkLocalPrivilege("fly");
+ bool free_move = fly_allowed && g_settings->getBool("free_move");
+
+ // Get local player
+ LocalPlayer *lplayer = getLocalPlayer();
+ assert(lplayer);
+ // collision info queue
+ std::vector<CollisionInfo> player_collisions;
+
+ /*
+ Get the speed the player is going
+ */
+ bool is_climbing = lplayer->is_climbing;
+
+ f32 player_speed = lplayer->getSpeed().getLength();
+
+ /*
+ Maximum position increment
+ */
+ //f32 position_max_increment = 0.05*BS;
+ f32 position_max_increment = 0.1*BS;
+
+ // Maximum time increment (for collision detection etc)
+ // time = distance / speed
+ f32 dtime_max_increment = 1;
+ if(player_speed > 0.001)
+ dtime_max_increment = position_max_increment / player_speed;
+
+ // Maximum time increment is 10ms or lower
+ if(dtime_max_increment > 0.01)
+ dtime_max_increment = 0.01;
+
+ // Don't allow overly huge dtime
+ if(dtime > 0.5)
+ dtime = 0.5;
+
+ f32 dtime_downcount = dtime;
+
+ /*
+ Stuff that has a maximum time increment
+ */
+
+ u32 loopcount = 0;
+ do
+ {
+ loopcount++;
+
+ f32 dtime_part;
+ if(dtime_downcount > dtime_max_increment)
+ {
+ dtime_part = dtime_max_increment;
+ dtime_downcount -= dtime_part;
+ }
+ else
+ {
+ dtime_part = dtime_downcount;
+ /*
+ Setting this to 0 (no -=dtime_part) disables an infinite loop
+ when dtime_part is so small that dtime_downcount -= dtime_part
+ does nothing
+ */
+ dtime_downcount = 0;
+ }
+
+ /*
+ Handle local player
+ */
+
+ {
+ // Apply physics
+ if (!free_move && !is_climbing) {
+ // Gravity
+ v3f speed = lplayer->getSpeed();
+ if (!lplayer->in_liquid)
+ speed.Y -= lplayer->movement_gravity *
+ lplayer->physics_override_gravity * dtime_part * 2.0f;
+
+ // Liquid floating / sinking
+ if (lplayer->in_liquid && !lplayer->swimming_vertical)
+ speed.Y -= lplayer->movement_liquid_sink * dtime_part * 2.0f;
+
+ // Liquid resistance
+ if (lplayer->in_liquid_stable || lplayer->in_liquid) {
+ // How much the node's viscosity blocks movement, ranges
+ // between 0 and 1. Should match the scale at which viscosity
+ // increase affects other liquid attributes.
+ static const f32 viscosity_factor = 0.3f;
+
+ v3f d_wanted = -speed / lplayer->movement_liquid_fluidity;
+ f32 dl = d_wanted.getLength();
+ if (dl > lplayer->movement_liquid_fluidity_smooth)
+ dl = lplayer->movement_liquid_fluidity_smooth;
+
+ dl *= (lplayer->liquid_viscosity * viscosity_factor) +
+ (1 - viscosity_factor);
+ v3f d = d_wanted.normalize() * (dl * dtime_part * 100.0f);
+ speed += d;
+ }
+
+ lplayer->setSpeed(speed);
+ }
+
+ /*
+ Move the lplayer.
+ This also does collision detection.
+ */
+ lplayer->move(dtime_part, this, position_max_increment,
+ &player_collisions);
+ }
+ } while (dtime_downcount > 0.001);
+
+ bool player_immortal = lplayer->getCAO() && lplayer->getCAO()->isImmortal();
+
+ for (const CollisionInfo &info : player_collisions) {
+ v3f speed_diff = info.new_speed - info.old_speed;;
+ // Handle only fall damage
+ // (because otherwise walking against something in fast_move kills you)
+ if (speed_diff.Y < 0 || info.old_speed.Y >= 0)
+ continue;
+ // Get rid of other components
+ speed_diff.X = 0;
+ speed_diff.Z = 0;
+ f32 pre_factor = 1; // 1 hp per node/s
+ f32 tolerance = BS*14; // 5 without damage
+ f32 post_factor = 1; // 1 hp per node/s
+ if (info.type == COLLISION_NODE) {
+ const ContentFeatures &f = m_client->ndef()->
+ get(m_map->getNodeNoEx(info.node_p));
+ // Determine fall damage multiplier
+ int addp = itemgroup_get(f.groups, "fall_damage_add_percent");
+ pre_factor = 1.0f + (float)addp / 100.0f;
+ }
+ float speed = pre_factor * speed_diff.getLength();
+ if (speed > tolerance && !player_immortal) {
+ f32 damage_f = (speed - tolerance) / BS * post_factor;
+ u8 damage = (u8)MYMIN(damage_f + 0.5, 255);
+ if (damage != 0) {
+ damageLocalPlayer(damage, true);
+ m_client->getEventManager()->put(
+ new SimpleTriggerEvent(MtEvent::PLAYER_FALLING_DAMAGE));
+ }
+ }
+ }
+
+ if (m_client->modsLoaded())
+ m_script->environment_step(dtime);
+
+ // Update lighting on local player (used for wield item)
+ u32 day_night_ratio = getDayNightRatio();
+ {
+ // Get node at head
+
+ // On InvalidPositionException, use this as default
+ // (day: LIGHT_SUN, night: 0)
+ MapNode node_at_lplayer(CONTENT_AIR, 0x0f, 0);
+
+ v3s16 p = lplayer->getLightPosition();
+ node_at_lplayer = m_map->getNodeNoEx(p);
+
+ u16 light = getInteriorLight(node_at_lplayer, 0, m_client->ndef());
+ final_color_blend(&lplayer->light_color, light, day_night_ratio);
+ }
+
+ /*
+ Step active objects and update lighting of them
+ */
+
+ g_profiler->avg("CEnv: num of objects", m_active_objects.size());
+ bool update_lighting = m_active_object_light_update_interval.step(dtime, 0.21);
+ for (auto &ao_it : m_active_objects) {
+ ClientActiveObject* obj = ao_it.second;
+ // Step object
+ obj->step(dtime, this);
+
+ if (update_lighting) {
+ // Update lighting
+ u8 light = 0;
+ bool pos_ok;
+
+ // Get node at head
+ v3s16 p = obj->getLightPosition();
+ MapNode n = m_map->getNodeNoEx(p, &pos_ok);
+ if (pos_ok)
+ light = n.getLightBlend(day_night_ratio, m_client->ndef());
+ else
+ light = blend_light(day_night_ratio, LIGHT_SUN, 0);
+
+ obj->updateLight(light);
+ }
+ }
+
+ /*
+ Step and handle simple objects
+ */
+ g_profiler->avg("CEnv: num of simple objects", m_simple_objects.size());
+ for (auto i = m_simple_objects.begin(); i != m_simple_objects.end();) {
+ auto cur = i;
+ ClientSimpleObject *simple = *cur;
+
+ simple->step(dtime);
+ if(simple->m_to_be_removed) {
+ delete simple;
+ i = m_simple_objects.erase(cur);
+ }
+ else {
+ ++i;
+ }
+ }
+}
+
+void ClientEnvironment::addSimpleObject(ClientSimpleObject *simple)
+{
+ m_simple_objects.push_back(simple);
+}
+
+GenericCAO* ClientEnvironment::getGenericCAO(u16 id)
+{
+ ClientActiveObject *obj = getActiveObject(id);
+ if (obj && obj->getType() == ACTIVEOBJECT_TYPE_GENERIC)
+ return (GenericCAO*) obj;
+
+ return NULL;
+}
+
+ClientActiveObject* ClientEnvironment::getActiveObject(u16 id)
+{
+ auto n = m_active_objects.find(id);
+ if (n == m_active_objects.end())
+ return NULL;
+ return n->second;
+}
+
+bool isFreeClientActiveObjectId(const u16 id,
+ ClientActiveObjectMap &objects)
+{
+ return id != 0 && objects.find(id) == objects.end();
+
+}
+
+u16 getFreeClientActiveObjectId(ClientActiveObjectMap &objects)
+{
+ //try to reuse id's as late as possible
+ static u16 last_used_id = 0;
+ u16 startid = last_used_id;
+ for(;;) {
+ last_used_id ++;
+ if (isFreeClientActiveObjectId(last_used_id, objects))
+ return last_used_id;
+
+ if (last_used_id == startid)
+ return 0;
+ }
+}
+
+u16 ClientEnvironment::addActiveObject(ClientActiveObject *object)
+{
+ assert(object); // Pre-condition
+ if(object->getId() == 0)
+ {
+ u16 new_id = getFreeClientActiveObjectId(m_active_objects);
+ if(new_id == 0)
+ {
+ infostream<<"ClientEnvironment::addActiveObject(): "
+ <<"no free ids available"<<std::endl;
+ delete object;
+ return 0;
+ }
+ object->setId(new_id);
+ }
+ if (!isFreeClientActiveObjectId(object->getId(), m_active_objects)) {
+ infostream<<"ClientEnvironment::addActiveObject(): "
+ <<"id is not free ("<<object->getId()<<")"<<std::endl;
+ delete object;
+ return 0;
+ }
+ infostream<<"ClientEnvironment::addActiveObject(): "
+ <<"added (id="<<object->getId()<<")"<<std::endl;
+ m_active_objects[object->getId()] = object;
+ object->addToScene(m_texturesource);
+ { // Update lighting immediately
+ u8 light = 0;
+ bool pos_ok;
+
+ // Get node at head
+ v3s16 p = object->getLightPosition();
+ MapNode n = m_map->getNodeNoEx(p, &pos_ok);
+ if (pos_ok)
+ light = n.getLightBlend(getDayNightRatio(), m_client->ndef());
+ else
+ light = blend_light(getDayNightRatio(), LIGHT_SUN, 0);
+
+ object->updateLight(light);
+ }
+ return object->getId();
+}
+
+void ClientEnvironment::addActiveObject(u16 id, u8 type,
+ const std::string &init_data)
+{
+ ClientActiveObject* obj =
+ ClientActiveObject::create((ActiveObjectType) type, m_client, this);
+ if(obj == NULL)
+ {
+ infostream<<"ClientEnvironment::addActiveObject(): "
+ <<"id="<<id<<" type="<<type<<": Couldn't create object"
+ <<std::endl;
+ return;
+ }
+
+ obj->setId(id);
+
+ try
+ {
+ obj->initialize(init_data);
+ }
+ catch(SerializationError &e)
+ {
+ errorstream<<"ClientEnvironment::addActiveObject():"
+ <<" id="<<id<<" type="<<type
+ <<": SerializationError in initialize(): "
+ <<e.what()
+ <<": init_data="<<serializeJsonString(init_data)
+ <<std::endl;
+ }
+
+ addActiveObject(obj);
+}
+
+void ClientEnvironment::removeActiveObject(u16 id)
+{
+ verbosestream<<"ClientEnvironment::removeActiveObject(): "
+ <<"id="<<id<<std::endl;
+ ClientActiveObject* obj = getActiveObject(id);
+ if (obj == NULL) {
+ infostream<<"ClientEnvironment::removeActiveObject(): "
+ <<"id="<<id<<" not found"<<std::endl;
+ return;
+ }
+ obj->removeFromScene(true);
+ delete obj;
+ m_active_objects.erase(id);
+}
+
+void ClientEnvironment::processActiveObjectMessage(u16 id, const std::string &data)
+{
+ ClientActiveObject *obj = getActiveObject(id);
+ if (obj == NULL) {
+ infostream << "ClientEnvironment::processActiveObjectMessage():"
+ << " got message for id=" << id << ", which doesn't exist."
+ << std::endl;
+ return;
+ }
+
+ try {
+ obj->processMessage(data);
+ } catch (SerializationError &e) {
+ errorstream<<"ClientEnvironment::processActiveObjectMessage():"
+ << " id=" << id << " type=" << obj->getType()
+ << " SerializationError in processMessage(): " << e.what()
+ << std::endl;
+ }
+}
+
+/*
+ Callbacks for activeobjects
+*/
+
+void ClientEnvironment::damageLocalPlayer(u8 damage, bool handle_hp)
+{
+ LocalPlayer *lplayer = getLocalPlayer();
+ assert(lplayer);
+
+ if (handle_hp) {
+ if (lplayer->hp > damage)
+ lplayer->hp -= damage;
+ else
+ lplayer->hp = 0;
+ }
+
+ ClientEnvEvent event;
+ event.type = CEE_PLAYER_DAMAGE;
+ event.player_damage.amount = damage;
+ event.player_damage.send_to_server = handle_hp;
+ m_client_event_queue.push(event);
+}
+
+/*
+ Client likes to call these
+*/
+
+void ClientEnvironment::getActiveObjects(v3f origin, f32 max_d,
+ std::vector<DistanceSortedActiveObject> &dest)
+{
+ for (auto &ao_it : m_active_objects) {
+ ClientActiveObject* obj = ao_it.second;
+
+ f32 d = (obj->getPosition() - origin).getLength();
+
+ if (d > max_d)
+ continue;
+
+ dest.emplace_back(obj, d);
+ }
+}
+
+ClientEnvEvent ClientEnvironment::getClientEnvEvent()
+{
+ FATAL_ERROR_IF(m_client_event_queue.empty(),
+ "ClientEnvironment::getClientEnvEvent(): queue is empty");
+
+ ClientEnvEvent event = m_client_event_queue.front();
+ m_client_event_queue.pop();
+ return event;
+}
+
+void ClientEnvironment::getSelectedActiveObjects(
+ const core::line3d<f32> &shootline_on_map,
+ std::vector<PointedThing> &objects)
+{
+ std::vector<DistanceSortedActiveObject> allObjects;
+ getActiveObjects(shootline_on_map.start,
+ shootline_on_map.getLength() + 10.0f, allObjects);
+ const v3f line_vector = shootline_on_map.getVector();
+
+ for (const auto &allObject : allObjects) {
+ ClientActiveObject *obj = allObject.obj;
+ aabb3f selection_box;
+ if (!obj->getSelectionBox(&selection_box))
+ continue;
+
+ const v3f &pos = obj->getPosition();
+ aabb3f offsetted_box(selection_box.MinEdge + pos,
+ selection_box.MaxEdge + pos);
+
+ v3f current_intersection;
+ v3s16 current_normal;
+ if (boxLineCollision(offsetted_box, shootline_on_map.start, line_vector,
+ &current_intersection, &current_normal)) {
+ objects.emplace_back((s16) obj->getId(), current_intersection, current_normal,
+ (current_intersection - shootline_on_map.start).getLengthSQ());
+ }
+ }
+}
diff --git a/src/client/clientenvironment.h b/src/client/clientenvironment.h
new file mode 100644
index 000000000..606070e3a
--- /dev/null
+++ b/src/client/clientenvironment.h
@@ -0,0 +1,151 @@
+/*
+Minetest
+Copyright (C) 2010-2017 celeron55, Perttu Ahola <celeron55@gmail.com>
+
+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 "environment.h"
+#include <ISceneManager.h>
+#include "clientobject.h"
+#include "util/numeric.h"
+
+class ClientSimpleObject;
+class ClientMap;
+class ClientScripting;
+class ClientActiveObject;
+class GenericCAO;
+class LocalPlayer;
+
+/*
+ The client-side environment.
+
+ This is not thread-safe.
+ Must be called from main (irrlicht) thread (uses the SceneManager)
+ Client uses an environment mutex.
+*/
+
+enum ClientEnvEventType
+{
+ CEE_NONE,
+ CEE_PLAYER_DAMAGE
+};
+
+struct ClientEnvEvent
+{
+ ClientEnvEventType type;
+ union {
+ //struct{
+ //} none;
+ struct{
+ u8 amount;
+ bool send_to_server;
+ } player_damage;
+ };
+};
+
+typedef std::unordered_map<u16, ClientActiveObject*> ClientActiveObjectMap;
+class ClientEnvironment : public Environment
+{
+public:
+ ClientEnvironment(ClientMap *map, ITextureSource *texturesource, Client *client);
+ ~ClientEnvironment();
+
+ Map & getMap();
+ ClientMap & getClientMap();
+
+ Client *getGameDef() { return m_client; }
+ void setScript(ClientScripting *script) { m_script = script; }
+
+ void step(f32 dtime);
+
+ virtual void setLocalPlayer(LocalPlayer *player);
+ LocalPlayer *getLocalPlayer() const { return m_local_player; }
+
+ /*
+ ClientSimpleObjects
+ */
+
+ void addSimpleObject(ClientSimpleObject *simple);
+
+ /*
+ ActiveObjects
+ */
+
+ GenericCAO* getGenericCAO(u16 id);
+ ClientActiveObject* getActiveObject(u16 id);
+
+ /*
+ Adds an active object to the environment.
+ Environment handles deletion of object.
+ Object may be deleted by environment immediately.
+ If id of object is 0, assigns a free id to it.
+ Returns the id of the object.
+ Returns 0 if not added and thus deleted.
+ */
+ u16 addActiveObject(ClientActiveObject *object);
+
+ void addActiveObject(u16 id, u8 type, const std::string &init_data);
+ void removeActiveObject(u16 id);
+
+ void processActiveObjectMessage(u16 id, const std::string &data);
+
+ /*
+ Callbacks for activeobjects
+ */
+
+ void damageLocalPlayer(u8 damage, bool handle_hp=true);
+
+ /*
+ Client likes to call these
+ */
+
+ // Get all nearby objects
+ void getActiveObjects(v3f origin, f32 max_d,
+ std::vector<DistanceSortedActiveObject> &dest);
+
+ bool hasClientEnvEvents() const { return !m_client_event_queue.empty(); }
+
+ // Get event from queue. If queue is empty, it triggers an assertion failure.
+ ClientEnvEvent getClientEnvEvent();
+
+ virtual void getSelectedActiveObjects(
+ const core::line3d<f32> &shootline_on_map,
+ std::vector<PointedThing> &objects
+ );
+
+ u16 attachement_parent_ids[USHRT_MAX + 1];
+
+ const std::list<std::string> &getPlayerNames() { return m_player_names; }
+ void addPlayerName(const std::string &name) { m_player_names.push_back(name); }
+ void removePlayerName(const std::string &name) { m_player_names.remove(name); }
+ void updateCameraOffset(const v3s16 &camera_offset)
+ { m_camera_offset = camera_offset; }
+ v3s16 getCameraOffset() const { return m_camera_offset; }
+private:
+ ClientMap *m_map;
+ LocalPlayer *m_local_player = nullptr;
+ ITextureSource *m_texturesource;
+ Client *m_client;
+ ClientScripting *m_script = nullptr;
+ ClientActiveObjectMap m_active_objects;
+ std::vector<ClientSimpleObject*> m_simple_objects;
+ std::queue<ClientEnvEvent> m_client_event_queue;
+ IntervalLimiter m_active_object_light_update_interval;
+ std::list<std::string> m_player_names;
+ v3s16 m_camera_offset;
+};
diff --git a/src/client/clientmap.cpp b/src/client/clientmap.cpp
new file mode 100644
index 000000000..969c55539
--- /dev/null
+++ b/src/client/clientmap.cpp
@@ -0,0 +1,671 @@
+/*
+Minetest
+Copyright (C) 2010-2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+
+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 "clientmap.h"
+#include "client.h"
+#include "mapblock_mesh.h"
+#include <IMaterialRenderer.h>
+#include <matrix4.h>
+#include "mapsector.h"
+#include "mapblock.h"
+#include "profiler.h"
+#include "settings.h"
+#include "camera.h" // CameraModes
+#include "util/basic_macros.h"
+#include <algorithm>
+#include "client/renderingengine.h"
+
+ClientMap::ClientMap(
+ Client *client,
+ MapDrawControl &control,
+ s32 id
+):
+ Map(dout_client, client),
+ scene::ISceneNode(RenderingEngine::get_scene_manager()->getRootSceneNode(),
+ RenderingEngine::get_scene_manager(), id),
+ m_client(client),
+ m_control(control)
+{
+ m_box = aabb3f(-BS*1000000,-BS*1000000,-BS*1000000,
+ BS*1000000,BS*1000000,BS*1000000);
+
+ /* TODO: Add a callback function so these can be updated when a setting
+ * changes. At this point in time it doesn't matter (e.g. /set
+ * is documented to change server settings only)
+ *
+ * TODO: Local caching of settings is not optimal and should at some stage
+ * be updated to use a global settings object for getting thse values
+ * (as opposed to the this local caching). This can be addressed in
+ * a later release.
+ */
+ m_cache_trilinear_filter = g_settings->getBool("trilinear_filter");
+ m_cache_bilinear_filter = g_settings->getBool("bilinear_filter");
+ m_cache_anistropic_filter = g_settings->getBool("anisotropic_filter");
+
+}
+
+MapSector * ClientMap::emergeSector(v2s16 p2d)
+{
+ // Check that it doesn't exist already
+ try {
+ return getSectorNoGenerate(p2d);
+ } catch(InvalidPositionException &e) {
+ }
+
+ // Create a sector
+ MapSector *sector = new MapSector(this, p2d, m_gamedef);
+ m_sectors[p2d] = sector;
+
+ return sector;
+}
+
+void ClientMap::OnRegisterSceneNode()
+{
+ if(IsVisible)
+ {
+ SceneManager->registerNodeForRendering(this, scene::ESNRP_SOLID);
+ SceneManager->registerNodeForRendering(this, scene::ESNRP_TRANSPARENT);
+ }
+
+ ISceneNode::OnRegisterSceneNode();
+}
+
+void ClientMap::getBlocksInViewRange(v3s16 cam_pos_nodes,
+ v3s16 *p_blocks_min, v3s16 *p_blocks_max)
+{
+ v3s16 box_nodes_d = m_control.wanted_range * v3s16(1, 1, 1);
+ // Define p_nodes_min/max as v3s32 because 'cam_pos_nodes -/+ box_nodes_d'
+ // can exceed the range of v3s16 when a large view range is used near the
+ // world edges.
+ v3s32 p_nodes_min(
+ cam_pos_nodes.X - box_nodes_d.X,
+ cam_pos_nodes.Y - box_nodes_d.Y,
+ cam_pos_nodes.Z - box_nodes_d.Z);
+ v3s32 p_nodes_max(
+ cam_pos_nodes.X + box_nodes_d.X,
+ cam_pos_nodes.Y + box_nodes_d.Y,
+ cam_pos_nodes.Z + box_nodes_d.Z);
+ // Take a fair amount as we will be dropping more out later
+ // Umm... these additions are a bit strange but they are needed.
+ *p_blocks_min = v3s16(
+ p_nodes_min.X / MAP_BLOCKSIZE - 3,
+ p_nodes_min.Y / MAP_BLOCKSIZE - 3,
+ p_nodes_min.Z / MAP_BLOCKSIZE - 3);
+ *p_blocks_max = v3s16(
+ p_nodes_max.X / MAP_BLOCKSIZE + 1,
+ p_nodes_max.Y / MAP_BLOCKSIZE + 1,
+ p_nodes_max.Z / MAP_BLOCKSIZE + 1);
+}
+
+void ClientMap::updateDrawList()
+{
+ ScopeProfiler sp(g_profiler, "CM::updateDrawList()", SPT_AVG);
+ g_profiler->add("CM::updateDrawList() count", 1);
+
+ for (auto &i : m_drawlist) {
+ MapBlock *block = i.second;
+ block->refDrop();
+ }
+ m_drawlist.clear();
+
+ v3f camera_position = m_camera_position;
+ v3f camera_direction = m_camera_direction;
+ f32 camera_fov = m_camera_fov;
+
+ // Use a higher fov to accomodate faster camera movements.
+ // Blocks are cropped better when they are drawn.
+ // Or maybe they aren't? Well whatever.
+ camera_fov *= 1.2;
+
+ v3s16 cam_pos_nodes = floatToInt(camera_position, BS);
+ v3s16 p_blocks_min;
+ v3s16 p_blocks_max;
+ getBlocksInViewRange(cam_pos_nodes, &p_blocks_min, &p_blocks_max);
+
+ // Number of blocks in rendering range
+ u32 blocks_in_range = 0;
+ // Number of blocks occlusion culled
+ u32 blocks_occlusion_culled = 0;
+ // Number of blocks in rendering range but don't have a mesh
+ u32 blocks_in_range_without_mesh = 0;
+ // Blocks that had mesh that would have been drawn according to
+ // rendering range (if max blocks limit didn't kick in)
+ u32 blocks_would_have_drawn = 0;
+ // Blocks that were drawn and had a mesh
+ u32 blocks_drawn = 0;
+ // Blocks which had a corresponding meshbuffer for this pass
+ //u32 blocks_had_pass_meshbuf = 0;
+ // Blocks from which stuff was actually drawn
+ //u32 blocks_without_stuff = 0;
+ // Distance to farthest drawn block
+ float farthest_drawn = 0;
+
+ // No occlusion culling when free_move is on and camera is
+ // inside ground
+ bool occlusion_culling_enabled = true;
+ if (g_settings->getBool("free_move")) {
+ MapNode n = getNodeNoEx(cam_pos_nodes);
+ if (n.getContent() == CONTENT_IGNORE ||
+ m_nodedef->get(n).solidness == 2)
+ occlusion_culling_enabled = false;
+ }
+
+ for (const auto &sector_it : m_sectors) {
+ MapSector *sector = sector_it.second;
+ v2s16 sp = sector->getPos();
+
+ if (!m_control.range_all) {
+ if (sp.X < p_blocks_min.X || sp.X > p_blocks_max.X ||
+ sp.Y < p_blocks_min.Z || sp.Y > p_blocks_max.Z)
+ continue;
+ }
+
+ MapBlockVect sectorblocks;
+ sector->getBlocks(sectorblocks);
+
+ /*
+ Loop through blocks in sector
+ */
+
+ u32 sector_blocks_drawn = 0;
+
+ for (auto block : sectorblocks) {
+ /*
+ Compare block position to camera position, skip
+ if not seen on display
+ */
+
+ if (block->mesh)
+ block->mesh->updateCameraOffset(m_camera_offset);
+
+ float range = 100000 * BS;
+ if (!m_control.range_all)
+ range = m_control.wanted_range * BS;
+
+ float d = 0.0;
+ if (!isBlockInSight(block->getPos(), camera_position,
+ camera_direction, camera_fov, range, &d))
+ continue;
+
+ blocks_in_range++;
+
+ /*
+ Ignore if mesh doesn't exist
+ */
+ if (!block->mesh) {
+ blocks_in_range_without_mesh++;
+ continue;
+ }
+
+ /*
+ Occlusion culling
+ */
+ if (occlusion_culling_enabled && isBlockOccluded(block, cam_pos_nodes)) {
+ blocks_occlusion_culled++;
+ continue;
+ }
+
+ // This block is in range. Reset usage timer.
+ block->resetUsageTimer();
+
+ // Limit block count in case of a sudden increase
+ blocks_would_have_drawn++;
+ if (blocks_drawn >= m_control.wanted_max_blocks &&
+ !m_control.range_all &&
+ d > m_control.wanted_range * BS)
+ continue;
+
+ // Add to set
+ block->refGrab();
+ m_drawlist[block->getPos()] = block;
+
+ sector_blocks_drawn++;
+ blocks_drawn++;
+ if (d / BS > farthest_drawn)
+ farthest_drawn = d / BS;
+
+ } // foreach sectorblocks
+
+ if (sector_blocks_drawn != 0)
+ m_last_drawn_sectors.insert(sp);
+ }
+
+ g_profiler->avg("CM: blocks in range", blocks_in_range);
+ g_profiler->avg("CM: blocks occlusion culled", blocks_occlusion_culled);
+ if (blocks_in_range != 0)
+ g_profiler->avg("CM: blocks in range without mesh (frac)",
+ (float)blocks_in_range_without_mesh / blocks_in_range);
+ g_profiler->avg("CM: blocks drawn", blocks_drawn);
+ g_profiler->avg("CM: farthest drawn", farthest_drawn);
+ g_profiler->avg("CM: wanted max blocks", m_control.wanted_max_blocks);
+}
+
+struct MeshBufList
+{
+ video::SMaterial m;
+ std::vector<scene::IMeshBuffer*> bufs;
+};
+
+struct MeshBufListList
+{
+ /*!
+ * Stores the mesh buffers of the world.
+ * The array index is the material's layer.
+ * The vector part groups vertices by material.
+ */
+ std::vector<MeshBufList> lists[MAX_TILE_LAYERS];
+
+ void clear()
+ {
+ for (auto &list : lists)
+ list.clear();
+ }
+
+ void add(scene::IMeshBuffer *buf, u8 layer)
+ {
+ // Append to the correct layer
+ std::vector<MeshBufList> &list = lists[layer];
+ const video::SMaterial &m = buf->getMaterial();
+ for (MeshBufList &l : list) {
+ // comparing a full material is quite expensive so we don't do it if
+ // not even first texture is equal
+ if (l.m.TextureLayer[0].Texture != m.TextureLayer[0].Texture)
+ continue;
+
+ if (l.m == m) {
+ l.bufs.push_back(buf);
+ return;
+ }
+ }
+ MeshBufList l;
+ l.m = m;
+ l.bufs.push_back(buf);
+ list.push_back(l);
+ }
+};
+
+void ClientMap::renderMap(video::IVideoDriver* driver, s32 pass)
+{
+ bool is_transparent_pass = pass == scene::ESNRP_TRANSPARENT;
+
+ std::string prefix;
+ if (pass == scene::ESNRP_SOLID)
+ prefix = "CM: solid: ";
+ else
+ prefix = "CM: transparent: ";
+
+ /*
+ This is called two times per frame, reset on the non-transparent one
+ */
+ if (pass == scene::ESNRP_SOLID)
+ m_last_drawn_sectors.clear();
+
+ /*
+ Get time for measuring timeout.
+
+ Measuring time is very useful for long delays when the
+ machine is swapping a lot.
+ */
+ std::time_t time1 = time(0);
+
+ /*
+ Get animation parameters
+ */
+ float animation_time = m_client->getAnimationTime();
+ int crack = m_client->getCrackLevel();
+ u32 daynight_ratio = m_client->getEnv().getDayNightRatio();
+
+ v3f camera_position = m_camera_position;
+ v3f camera_direction = m_camera_direction;
+ f32 camera_fov = m_camera_fov;
+
+ /*
+ Get all blocks and draw all visible ones
+ */
+
+ u32 vertex_count = 0;
+ u32 meshbuffer_count = 0;
+
+ // For limiting number of mesh animations per frame
+ u32 mesh_animate_count = 0;
+ u32 mesh_animate_count_far = 0;
+
+ // Blocks that were drawn and had a mesh
+ u32 blocks_drawn = 0;
+ // Blocks which had a corresponding meshbuffer for this pass
+ u32 blocks_had_pass_meshbuf = 0;
+ // Blocks from which stuff was actually drawn
+ u32 blocks_without_stuff = 0;
+
+ /*
+ Draw the selected MapBlocks
+ */
+
+ {
+ ScopeProfiler sp(g_profiler, prefix + "drawing blocks", SPT_AVG);
+
+ MeshBufListList drawbufs;
+
+ for (auto &i : m_drawlist) {
+ MapBlock *block = i.second;
+
+ // If the mesh of the block happened to get deleted, ignore it
+ if (!block->mesh)
+ continue;
+
+ float d = 0.0;
+ if (!isBlockInSight(block->getPos(), camera_position,
+ camera_direction, camera_fov, 100000 * BS, &d))
+ continue;
+
+ // Mesh animation
+ if (pass == scene::ESNRP_SOLID) {
+ //MutexAutoLock lock(block->mesh_mutex);
+ MapBlockMesh *mapBlockMesh = block->mesh;
+ assert(mapBlockMesh);
+ // Pretty random but this should work somewhat nicely
+ bool faraway = d >= BS * 50;
+ //bool faraway = d >= m_control.wanted_range * BS;
+ if (mapBlockMesh->isAnimationForced() || !faraway ||
+ mesh_animate_count_far < (m_control.range_all ? 200 : 50)) {
+ bool animated = mapBlockMesh->animate(faraway, animation_time,
+ crack, daynight_ratio);
+ if (animated)
+ mesh_animate_count++;
+ if (animated && faraway)
+ mesh_animate_count_far++;
+ } else {
+ mapBlockMesh->decreaseAnimationForceTimer();
+ }
+ }
+
+ /*
+ Get the meshbuffers of the block
+ */
+ {
+ //MutexAutoLock lock(block->mesh_mutex);
+
+ MapBlockMesh *mapBlockMesh = block->mesh;
+ assert(mapBlockMesh);
+
+ for (int layer = 0; layer < MAX_TILE_LAYERS; layer++) {
+ scene::IMesh *mesh = mapBlockMesh->getMesh(layer);
+ assert(mesh);
+
+ u32 c = mesh->getMeshBufferCount();
+ for (u32 i = 0; i < c; i++) {
+ scene::IMeshBuffer *buf = mesh->getMeshBuffer(i);
+
+ video::SMaterial& material = buf->getMaterial();
+ video::IMaterialRenderer* rnd =
+ driver->getMaterialRenderer(material.MaterialType);
+ bool transparent = (rnd && rnd->isTransparent());
+ if (transparent == is_transparent_pass) {
+ if (buf->getVertexCount() == 0)
+ errorstream << "Block [" << analyze_block(block)
+ << "] contains an empty meshbuf" << std::endl;
+
+ material.setFlag(video::EMF_TRILINEAR_FILTER,
+ m_cache_trilinear_filter);
+ material.setFlag(video::EMF_BILINEAR_FILTER,
+ m_cache_bilinear_filter);
+ material.setFlag(video::EMF_ANISOTROPIC_FILTER,
+ m_cache_anistropic_filter);
+ material.setFlag(video::EMF_WIREFRAME,
+ m_control.show_wireframe);
+
+ drawbufs.add(buf, layer);
+ }
+ }
+ }
+ }
+ }
+
+ // Render all layers in order
+ for (auto &lists : drawbufs.lists) {
+ int timecheck_counter = 0;
+ for (MeshBufList &list : lists) {
+ timecheck_counter++;
+ if (timecheck_counter > 50) {
+ timecheck_counter = 0;
+ std::time_t time2 = time(0);
+ if (time2 > time1 + 4) {
+ infostream << "ClientMap::renderMap(): "
+ "Rendering takes ages, returning."
+ << std::endl;
+ return;
+ }
+ }
+
+ driver->setMaterial(list.m);
+
+ for (scene::IMeshBuffer *buf : list.bufs) {
+ driver->drawMeshBuffer(buf);
+ vertex_count += buf->getVertexCount();
+ meshbuffer_count++;
+ }
+ }
+ }
+ } // ScopeProfiler
+
+ // Log only on solid pass because values are the same
+ if (pass == scene::ESNRP_SOLID) {
+ g_profiler->avg("CM: animated meshes", mesh_animate_count);
+ g_profiler->avg("CM: animated meshes (far)", mesh_animate_count_far);
+ }
+
+ g_profiler->avg(prefix + "vertices drawn", vertex_count);
+ if (blocks_had_pass_meshbuf != 0)
+ g_profiler->avg(prefix + "meshbuffers per block",
+ (float)meshbuffer_count / (float)blocks_had_pass_meshbuf);
+ if (blocks_drawn != 0)
+ g_profiler->avg(prefix + "empty blocks (frac)",
+ (float)blocks_without_stuff / blocks_drawn);
+}
+
+static bool getVisibleBrightness(Map *map, const v3f &p0, v3f dir, float step,
+ float step_multiplier, float start_distance, float end_distance,
+ const NodeDefManager *ndef, u32 daylight_factor, float sunlight_min_d,
+ int *result, bool *sunlight_seen)
+{
+ int brightness_sum = 0;
+ int brightness_count = 0;
+ float distance = start_distance;
+ dir.normalize();
+ v3f pf = p0;
+ pf += dir * distance;
+ int noncount = 0;
+ bool nonlight_seen = false;
+ bool allow_allowing_non_sunlight_propagates = false;
+ bool allow_non_sunlight_propagates = false;
+ // Check content nearly at camera position
+ {
+ v3s16 p = floatToInt(p0 /*+ dir * 3*BS*/, BS);
+ MapNode n = map->getNodeNoEx(p);
+ if(ndef->get(n).param_type == CPT_LIGHT &&
+ !ndef->get(n).sunlight_propagates)
+ allow_allowing_non_sunlight_propagates = true;
+ }
+ // If would start at CONTENT_IGNORE, start closer
+ {
+ v3s16 p = floatToInt(pf, BS);
+ MapNode n = map->getNodeNoEx(p);
+ if(n.getContent() == CONTENT_IGNORE){
+ float newd = 2*BS;
+ pf = p0 + dir * 2*newd;
+ distance = newd;
+ sunlight_min_d = 0;
+ }
+ }
+ for (int i=0; distance < end_distance; i++) {
+ pf += dir * step;
+ distance += step;
+ step *= step_multiplier;
+
+ v3s16 p = floatToInt(pf, BS);
+ MapNode n = map->getNodeNoEx(p);
+ if (allow_allowing_non_sunlight_propagates && i == 0 &&
+ ndef->get(n).param_type == CPT_LIGHT &&
+ !ndef->get(n).sunlight_propagates) {
+ allow_non_sunlight_propagates = true;
+ }
+
+ if (ndef->get(n).param_type != CPT_LIGHT ||
+ (!ndef->get(n).sunlight_propagates &&
+ !allow_non_sunlight_propagates)){
+ nonlight_seen = true;
+ noncount++;
+ if(noncount >= 4)
+ break;
+ continue;
+ }
+
+ if (distance >= sunlight_min_d && !*sunlight_seen && !nonlight_seen)
+ if (n.getLight(LIGHTBANK_DAY, ndef) == LIGHT_SUN)
+ *sunlight_seen = true;
+ noncount = 0;
+ brightness_sum += decode_light(n.getLightBlend(daylight_factor, ndef));
+ brightness_count++;
+ }
+ *result = 0;
+ if(brightness_count == 0)
+ return false;
+ *result = brightness_sum / brightness_count;
+ /*std::cerr<<"Sampled "<<brightness_count<<" points; result="
+ <<(*result)<<std::endl;*/
+ return true;
+}
+
+int ClientMap::getBackgroundBrightness(float max_d, u32 daylight_factor,
+ int oldvalue, bool *sunlight_seen_result)
+{
+ static v3f z_directions[50] = {
+ v3f(-100, 0, 0)
+ };
+ static f32 z_offsets[sizeof(z_directions)/sizeof(*z_directions)] = {
+ -1000,
+ };
+
+ if(z_directions[0].X < -99){
+ for(u32 i=0; i<sizeof(z_directions)/sizeof(*z_directions); i++){
+ // Assumes FOV of 72 and 16/9 aspect ratio
+ z_directions[i] = v3f(
+ 0.02 * myrand_range(-100, 100),
+ 1.0,
+ 0.01 * myrand_range(-100, 100)
+ ).normalize();
+ z_offsets[i] = 0.01 * myrand_range(0,100);
+ }
+ }
+
+ int sunlight_seen_count = 0;
+ float sunlight_min_d = max_d*0.8;
+ if(sunlight_min_d > 35*BS)
+ sunlight_min_d = 35*BS;
+ std::vector<int> values;
+ for(u32 i=0; i<sizeof(z_directions)/sizeof(*z_directions); i++){
+ v3f z_dir = z_directions[i];
+ core::CMatrix4<f32> a;
+ a.buildRotateFromTo(v3f(0,1,0), z_dir);
+ v3f dir = m_camera_direction;
+ a.rotateVect(dir);
+ int br = 0;
+ float step = BS*1.5;
+ if(max_d > 35*BS)
+ step = max_d / 35 * 1.5;
+ float off = step * z_offsets[i];
+ bool sunlight_seen_now = false;
+ bool ok = getVisibleBrightness(this, m_camera_position, dir,
+ step, 1.0, max_d*0.6+off, max_d, m_nodedef, daylight_factor,
+ sunlight_min_d,
+ &br, &sunlight_seen_now);
+ if(sunlight_seen_now)
+ sunlight_seen_count++;
+ if(!ok)
+ continue;
+ values.push_back(br);
+ // Don't try too much if being in the sun is clear
+ if(sunlight_seen_count >= 20)
+ break;
+ }
+ int brightness_sum = 0;
+ int brightness_count = 0;
+ std::sort(values.begin(), values.end());
+ u32 num_values_to_use = values.size();
+ if(num_values_to_use >= 10)
+ num_values_to_use -= num_values_to_use/2;
+ else if(num_values_to_use >= 7)
+ num_values_to_use -= num_values_to_use/3;
+ u32 first_value_i = (values.size() - num_values_to_use) / 2;
+
+ for (u32 i=first_value_i; i < first_value_i + num_values_to_use; i++) {
+ brightness_sum += values[i];
+ brightness_count++;
+ }
+
+ int ret = 0;
+ if(brightness_count == 0){
+ MapNode n = getNodeNoEx(floatToInt(m_camera_position, BS));
+ if(m_nodedef->get(n).param_type == CPT_LIGHT){
+ ret = decode_light(n.getLightBlend(daylight_factor, m_nodedef));
+ } else {
+ ret = oldvalue;
+ }
+ } else {
+ ret = brightness_sum / brightness_count;
+ }
+
+ *sunlight_seen_result = (sunlight_seen_count > 0);
+ return ret;
+}
+
+void ClientMap::renderPostFx(CameraMode cam_mode)
+{
+ // Sadly ISceneManager has no "post effects" render pass, in that case we
+ // could just register for that and handle it in renderMap().
+
+ MapNode n = getNodeNoEx(floatToInt(m_camera_position, BS));
+
+ // - If the player is in a solid node, make everything black.
+ // - If the player is in liquid, draw a semi-transparent overlay.
+ // - Do not if player is in third person mode
+ const ContentFeatures& features = m_nodedef->get(n);
+ video::SColor post_effect_color = features.post_effect_color;
+ if(features.solidness == 2 && !(g_settings->getBool("noclip") &&
+ m_client->checkLocalPrivilege("noclip")) &&
+ cam_mode == CAMERA_MODE_FIRST)
+ {
+ post_effect_color = video::SColor(255, 0, 0, 0);
+ }
+ if (post_effect_color.getAlpha() != 0)
+ {
+ // Draw a full-screen rectangle
+ video::IVideoDriver* driver = SceneManager->getVideoDriver();
+ v2u32 ss = driver->getScreenSize();
+ core::rect<s32> rect(0,0, ss.X, ss.Y);
+ driver->draw2DRectangle(post_effect_color, rect);
+ }
+}
+
+void ClientMap::PrintInfo(std::ostream &out)
+{
+ out<<"ClientMap: ";
+}
+
+
diff --git a/src/client/clientmap.h b/src/client/clientmap.h
new file mode 100644
index 000000000..8402bb00d
--- /dev/null
+++ b/src/client/clientmap.h
@@ -0,0 +1,138 @@
+/*
+Minetest
+Copyright (C) 2010-2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+
+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 "map.h"
+#include "camera.h"
+#include <set>
+#include <map>
+
+struct MapDrawControl
+{
+ // Overrides limits by drawing everything
+ bool range_all = false;
+ // Wanted drawing range
+ float wanted_range = 0.0f;
+ // Maximum number of blocks to draw
+ u32 wanted_max_blocks = 0;
+ // show a wire frame for debugging
+ bool show_wireframe = false;
+};
+
+class Client;
+class ITextureSource;
+
+/*
+ ClientMap
+
+ This is the only map class that is able to render itself on screen.
+*/
+
+class ClientMap : public Map, public scene::ISceneNode
+{
+public:
+ ClientMap(
+ Client *client,
+ MapDrawControl &control,
+ s32 id
+ );
+
+ virtual ~ClientMap() = default;
+
+ s32 mapType() const
+ {
+ return MAPTYPE_CLIENT;
+ }
+
+ void drop()
+ {
+ ISceneNode::drop();
+ }
+
+ void updateCamera(const v3f &pos, const v3f &dir, f32 fov, const v3s16 &offset)
+ {
+ m_camera_position = pos;
+ m_camera_direction = dir;
+ m_camera_fov = fov;
+ m_camera_offset = offset;
+ }
+
+ /*
+ Forcefully get a sector from somewhere
+ */
+ MapSector * emergeSector(v2s16 p);
+
+ //void deSerializeSector(v2s16 p2d, std::istream &is);
+
+ /*
+ ISceneNode methods
+ */
+
+ virtual void OnRegisterSceneNode();
+
+ virtual void render()
+ {
+ video::IVideoDriver* driver = SceneManager->getVideoDriver();
+ driver->setTransform(video::ETS_WORLD, AbsoluteTransformation);
+ renderMap(driver, SceneManager->getSceneNodeRenderPass());
+ }
+
+ virtual const aabb3f &getBoundingBox() const
+ {
+ return m_box;
+ }
+
+ void getBlocksInViewRange(v3s16 cam_pos_nodes,
+ v3s16 *p_blocks_min, v3s16 *p_blocks_max);
+ void updateDrawList();
+ void renderMap(video::IVideoDriver* driver, s32 pass);
+
+ int getBackgroundBrightness(float max_d, u32 daylight_factor,
+ int oldvalue, bool *sunlight_seen_result);
+
+ void renderPostFx(CameraMode cam_mode);
+
+ // For debug printing
+ virtual void PrintInfo(std::ostream &out);
+
+ const MapDrawControl & getControl() const { return m_control; }
+ f32 getCameraFov() const { return m_camera_fov; }
+private:
+ Client *m_client;
+
+ aabb3f m_box = aabb3f(-BS * 1000000, -BS * 1000000, -BS * 1000000,
+ BS * 1000000, BS * 1000000, BS * 1000000);
+
+ MapDrawControl &m_control;
+
+ v3f m_camera_position = v3f(0,0,0);
+ v3f m_camera_direction = v3f(0,0,1);
+ f32 m_camera_fov = M_PI;
+ v3s16 m_camera_offset;
+
+ std::map<v3s16, MapBlock*> m_drawlist;
+
+ std::set<v2s16> m_last_drawn_sectors;
+
+ bool m_cache_trilinear_filter;
+ bool m_cache_bilinear_filter;
+ bool m_cache_anistropic_filter;
+};
diff --git a/src/client/clientmedia.cpp b/src/client/clientmedia.cpp
new file mode 100644
index 000000000..97931ee68
--- /dev/null
+++ b/src/client/clientmedia.cpp
@@ -0,0 +1,639 @@
+/*
+Minetest
+Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+
+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 "clientmedia.h"
+#include "httpfetch.h"
+#include "client.h"
+#include "filecache.h"
+#include "filesys.h"
+#include "log.h"
+#include "porting.h"
+#include "settings.h"
+#include "util/hex.h"
+#include "util/serialize.h"
+#include "util/sha1.h"
+#include "util/string.h"
+
+static std::string getMediaCacheDir()
+{
+ return porting::path_cache + DIR_DELIM + "media";
+}
+
+/*
+ ClientMediaDownloader
+*/
+
+ClientMediaDownloader::ClientMediaDownloader():
+ m_media_cache(getMediaCacheDir()),
+ m_httpfetch_caller(HTTPFETCH_DISCARD)
+{
+}
+
+ClientMediaDownloader::~ClientMediaDownloader()
+{
+ if (m_httpfetch_caller != HTTPFETCH_DISCARD)
+ httpfetch_caller_free(m_httpfetch_caller);
+
+ for (auto &file_it : m_files)
+ delete file_it.second;
+
+ for (auto &remote : m_remotes)
+ delete remote;
+}
+
+void ClientMediaDownloader::addFile(const std::string &name, const std::string &sha1)
+{
+ assert(!m_initial_step_done); // pre-condition
+
+ // if name was already announced, ignore the new announcement
+ if (m_files.count(name) != 0) {
+ errorstream << "Client: ignoring duplicate media announcement "
+ << "sent by server: \"" << name << "\""
+ << std::endl;
+ return;
+ }
+
+ // if name is empty or contains illegal characters, ignore the file
+ if (name.empty() || !string_allowed(name, TEXTURENAME_ALLOWED_CHARS)) {
+ errorstream << "Client: ignoring illegal file name "
+ << "sent by server: \"" << name << "\""
+ << std::endl;
+ return;
+ }
+
+ // length of sha1 must be exactly 20 (160 bits), else ignore the file
+ if (sha1.size() != 20) {
+ errorstream << "Client: ignoring illegal SHA1 sent by server: "
+ << hex_encode(sha1) << " \"" << name << "\""
+ << std::endl;
+ return;
+ }
+
+ FileStatus *filestatus = new FileStatus();
+ filestatus->received = false;
+ filestatus->sha1 = sha1;
+ filestatus->current_remote = -1;
+ m_files.insert(std::make_pair(name, filestatus));
+}
+
+void ClientMediaDownloader::addRemoteServer(const std::string &baseurl)
+{
+ assert(!m_initial_step_done); // pre-condition
+
+ #ifdef USE_CURL
+
+ if (g_settings->getBool("enable_remote_media_server")) {
+ infostream << "Client: Adding remote server \""
+ << baseurl << "\" for media download" << std::endl;
+
+ RemoteServerStatus *remote = new RemoteServerStatus();
+ remote->baseurl = baseurl;
+ remote->active_count = 0;
+ remote->request_by_filename = false;
+ m_remotes.push_back(remote);
+ }
+
+ #else
+
+ infostream << "Client: Ignoring remote server \""
+ << baseurl << "\" because cURL support is not compiled in"
+ << std::endl;
+
+ #endif
+}
+
+void ClientMediaDownloader::step(Client *client)
+{
+ if (!m_initial_step_done) {
+ initialStep(client);
+ m_initial_step_done = true;
+ }
+
+ // Remote media: check for completion of fetches
+ if (m_httpfetch_active) {
+ bool fetched_something = false;
+ HTTPFetchResult fetch_result;
+
+ while (httpfetch_async_get(m_httpfetch_caller, fetch_result)) {
+ m_httpfetch_active--;
+ fetched_something = true;
+
+ // Is this a hashset (index.mth) or a media file?
+ if (fetch_result.request_id < m_remotes.size())
+ remoteHashSetReceived(fetch_result);
+ else
+ remoteMediaReceived(fetch_result, client);
+ }
+
+ if (fetched_something)
+ startRemoteMediaTransfers();
+
+ // Did all remote transfers end and no new ones can be started?
+ // If so, request still missing files from the minetest server
+ // (Or report that we have all files.)
+ if (m_httpfetch_active == 0) {
+ if (m_uncached_received_count < m_uncached_count) {
+ infostream << "Client: Failed to remote-fetch "
+ << (m_uncached_count-m_uncached_received_count)
+ << " files. Requesting them"
+ << " the usual way." << std::endl;
+ }
+ startConventionalTransfers(client);
+ }
+ }
+}
+
+void ClientMediaDownloader::initialStep(Client *client)
+{
+ // Check media cache
+ m_uncached_count = m_files.size();
+ for (auto &file_it : m_files) {
+ std::string name = file_it.first;
+ FileStatus *filestatus = file_it.second;
+ const std::string &sha1 = filestatus->sha1;
+
+ std::ostringstream tmp_os(std::ios_base::binary);
+ bool found_in_cache = m_media_cache.load(hex_encode(sha1), tmp_os);
+
+ // If found in cache, try to load it from there
+ if (found_in_cache) {
+ bool success = checkAndLoad(name, sha1,
+ tmp_os.str(), true, client);
+ if (success) {
+ filestatus->received = true;
+ m_uncached_count--;
+ }
+ }
+ }
+
+ assert(m_uncached_received_count == 0);
+
+ // Create the media cache dir if we are likely to write to it
+ if (m_uncached_count != 0) {
+ bool did = fs::CreateAllDirs(getMediaCacheDir());
+ if (!did) {
+ errorstream << "Client: "
+ << "Could not create media cache directory: "
+ << getMediaCacheDir()
+ << std::endl;
+ }
+ }
+
+ // If we found all files in the cache, report this fact to the server.
+ // If the server reported no remote servers, immediately start
+ // conventional transfers. Note: if cURL support is not compiled in,
+ // m_remotes is always empty, so "!USE_CURL" is redundant but may
+ // reduce the size of the compiled code
+ if (!USE_CURL || m_uncached_count == 0 || m_remotes.empty()) {
+ startConventionalTransfers(client);
+ }
+ else {
+ // Otherwise start off by requesting each server's sha1 set
+
+ // This is the first time we use httpfetch, so alloc a caller ID
+ m_httpfetch_caller = httpfetch_caller_alloc();
+ m_httpfetch_timeout = g_settings->getS32("curl_timeout");
+
+ // Set the active fetch limit to curl_parallel_limit or 84,
+ // whichever is greater. This gives us some leeway so that
+ // inefficiencies in communicating with the httpfetch thread
+ // don't slow down fetches too much. (We still want some limit
+ // so that when the first remote server returns its hash set,
+ // not all files are requested from that server immediately.)
+ // One such inefficiency is that ClientMediaDownloader::step()
+ // is only called a couple times per second, while httpfetch
+ // might return responses much faster than that.
+ // Note that httpfetch strictly enforces curl_parallel_limit
+ // but at no inter-thread communication cost. This however
+ // doesn't help with the aforementioned inefficiencies.
+ // The signifance of 84 is that it is 2*6*9 in base 13.
+ m_httpfetch_active_limit = g_settings->getS32("curl_parallel_limit");
+ m_httpfetch_active_limit = MYMAX(m_httpfetch_active_limit, 84);
+
+ // Write a list of hashes that we need. This will be POSTed
+ // to the server using Content-Type: application/octet-stream
+ std::string required_hash_set = serializeRequiredHashSet();
+
+ // minor fixme: this loop ignores m_httpfetch_active_limit
+
+ // another minor fixme, unlikely to matter in normal usage:
+ // these index.mth fetches do (however) count against
+ // m_httpfetch_active_limit when starting actual media file
+ // requests, so if there are lots of remote servers that are
+ // not responding, those will stall new media file transfers.
+
+ for (u32 i = 0; i < m_remotes.size(); ++i) {
+ assert(m_httpfetch_next_id == i);
+
+ RemoteServerStatus *remote = m_remotes[i];
+ actionstream << "Client: Contacting remote server \""
+ << remote->baseurl << "\"" << std::endl;
+
+ HTTPFetchRequest fetch_request;
+ fetch_request.url =
+ remote->baseurl + MTHASHSET_FILE_NAME;
+ fetch_request.caller = m_httpfetch_caller;
+ fetch_request.request_id = m_httpfetch_next_id; // == i
+ fetch_request.timeout = m_httpfetch_timeout;
+ fetch_request.connect_timeout = m_httpfetch_timeout;
+ fetch_request.post_data = required_hash_set;
+ fetch_request.extra_headers.emplace_back(
+ "Content-Type: application/octet-stream");
+ httpfetch_async(fetch_request);
+
+ m_httpfetch_active++;
+ m_httpfetch_next_id++;
+ m_outstanding_hash_sets++;
+ }
+ }
+}
+
+void ClientMediaDownloader::remoteHashSetReceived(
+ const HTTPFetchResult &fetch_result)
+{
+ u32 remote_id = fetch_result.request_id;
+ assert(remote_id < m_remotes.size());
+ RemoteServerStatus *remote = m_remotes[remote_id];
+
+ m_outstanding_hash_sets--;
+
+ if (fetch_result.succeeded) {
+ try {
+ // Server sent a list of file hashes that are
+ // available on it, try to parse the list
+
+ std::set<std::string> sha1_set;
+ deSerializeHashSet(fetch_result.data, sha1_set);
+
+ // Parsing succeeded: For every file that is
+ // available on this server, add this server
+ // to the available_remotes array
+
+ for(std::map<std::string, FileStatus*>::iterator
+ it = m_files.upper_bound(m_name_bound);
+ it != m_files.end(); ++it) {
+ FileStatus *f = it->second;
+ if (!f->received && sha1_set.count(f->sha1))
+ f->available_remotes.push_back(remote_id);
+ }
+ }
+ catch (SerializationError &e) {
+ infostream << "Client: Remote server \""
+ << remote->baseurl << "\" sent invalid hash set: "
+ << e.what() << std::endl;
+ }
+ }
+
+ // For compatibility: If index.mth is not found, assume that the
+ // server contains files named like the original files (not their sha1)
+
+ // Do NOT check for any particular response code (e.g. 404) here,
+ // because different servers respond differently
+
+ if (!fetch_result.succeeded && !fetch_result.timeout) {
+ infostream << "Client: Enabling compatibility mode for remote "
+ << "server \"" << remote->baseurl << "\"" << std::endl;
+ remote->request_by_filename = true;
+
+ // Assume every file is available on this server
+
+ for(std::map<std::string, FileStatus*>::iterator
+ it = m_files.upper_bound(m_name_bound);
+ it != m_files.end(); ++it) {
+ FileStatus *f = it->second;
+ if (!f->received)
+ f->available_remotes.push_back(remote_id);
+ }
+ }
+}
+
+void ClientMediaDownloader::remoteMediaReceived(
+ const HTTPFetchResult &fetch_result,
+ Client *client)
+{
+ // Some remote server sent us a file.
+ // -> decrement number of active fetches
+ // -> mark file as received if fetch succeeded
+ // -> try to load media
+
+ std::string name;
+ {
+ std::unordered_map<unsigned long, std::string>::iterator it =
+ m_remote_file_transfers.find(fetch_result.request_id);
+ assert(it != m_remote_file_transfers.end());
+ name = it->second;
+ m_remote_file_transfers.erase(it);
+ }
+
+ sanity_check(m_files.count(name) != 0);
+
+ FileStatus *filestatus = m_files[name];
+ sanity_check(!filestatus->received);
+ sanity_check(filestatus->current_remote >= 0);
+
+ RemoteServerStatus *remote = m_remotes[filestatus->current_remote];
+
+ filestatus->current_remote = -1;
+ remote->active_count--;
+
+ // If fetch succeeded, try to load media file
+
+ if (fetch_result.succeeded) {
+ bool success = checkAndLoad(name, filestatus->sha1,
+ fetch_result.data, false, client);
+ if (success) {
+ filestatus->received = true;
+ assert(m_uncached_received_count < m_uncached_count);
+ m_uncached_received_count++;
+ }
+ }
+}
+
+s32 ClientMediaDownloader::selectRemoteServer(FileStatus *filestatus)
+{
+ // Pre-conditions
+ assert(filestatus != NULL);
+ assert(!filestatus->received);
+ assert(filestatus->current_remote < 0);
+
+ if (filestatus->available_remotes.empty())
+ return -1;
+
+ // Of all servers that claim to provide the file (and haven't
+ // been unsuccessfully tried before), find the one with the
+ // smallest number of currently active transfers
+
+ s32 best = 0;
+ s32 best_remote_id = filestatus->available_remotes[best];
+ s32 best_active_count = m_remotes[best_remote_id]->active_count;
+
+ for (u32 i = 1; i < filestatus->available_remotes.size(); ++i) {
+ s32 remote_id = filestatus->available_remotes[i];
+ s32 active_count = m_remotes[remote_id]->active_count;
+ if (active_count < best_active_count) {
+ best = i;
+ best_remote_id = remote_id;
+ best_active_count = active_count;
+ }
+ }
+
+ filestatus->available_remotes.erase(
+ filestatus->available_remotes.begin() + best);
+
+ return best_remote_id;
+
+}
+
+void ClientMediaDownloader::startRemoteMediaTransfers()
+{
+ bool changing_name_bound = true;
+
+ for (std::map<std::string, FileStatus*>::iterator
+ files_iter = m_files.upper_bound(m_name_bound);
+ files_iter != m_files.end(); ++files_iter) {
+
+ // Abort if active fetch limit is exceeded
+ if (m_httpfetch_active >= m_httpfetch_active_limit)
+ break;
+
+ const std::string &name = files_iter->first;
+ FileStatus *filestatus = files_iter->second;
+
+ if (!filestatus->received && filestatus->current_remote < 0) {
+ // File has not been received yet and is not currently
+ // being transferred. Choose a server for it.
+ s32 remote_id = selectRemoteServer(filestatus);
+ if (remote_id >= 0) {
+ // Found a server, so start fetching
+ RemoteServerStatus *remote =
+ m_remotes[remote_id];
+
+ std::string url = remote->baseurl +
+ (remote->request_by_filename ? name :
+ hex_encode(filestatus->sha1));
+ verbosestream << "Client: "
+ << "Requesting remote media file "
+ << "\"" << name << "\" "
+ << "\"" << url << "\"" << std::endl;
+
+ HTTPFetchRequest fetch_request;
+ fetch_request.url = url;
+ fetch_request.caller = m_httpfetch_caller;
+ fetch_request.request_id = m_httpfetch_next_id;
+ fetch_request.timeout = 0; // no data timeout!
+ fetch_request.connect_timeout =
+ m_httpfetch_timeout;
+ httpfetch_async(fetch_request);
+
+ m_remote_file_transfers.insert(std::make_pair(
+ m_httpfetch_next_id,
+ name));
+
+ filestatus->current_remote = remote_id;
+ remote->active_count++;
+ m_httpfetch_active++;
+ m_httpfetch_next_id++;
+ }
+ }
+
+ if (filestatus->received ||
+ (filestatus->current_remote < 0 &&
+ !m_outstanding_hash_sets)) {
+ // If we arrive here, we conclusively know that we
+ // won't fetch this file from a remote server in the
+ // future. So update the name bound if possible.
+ if (changing_name_bound)
+ m_name_bound = name;
+ }
+ else
+ changing_name_bound = false;
+ }
+
+}
+
+void ClientMediaDownloader::startConventionalTransfers(Client *client)
+{
+ assert(m_httpfetch_active == 0); // pre-condition
+
+ if (m_uncached_received_count != m_uncached_count) {
+ // Some media files have not been received yet, use the
+ // conventional slow method (minetest protocol) to get them
+ std::vector<std::string> file_requests;
+ for (auto &file : m_files) {
+ if (!file.second->received)
+ file_requests.push_back(file.first);
+ }
+ assert((s32) file_requests.size() ==
+ m_uncached_count - m_uncached_received_count);
+ client->request_media(file_requests);
+ }
+}
+
+void ClientMediaDownloader::conventionalTransferDone(
+ const std::string &name,
+ const std::string &data,
+ Client *client)
+{
+ // Check that file was announced
+ std::map<std::string, FileStatus*>::iterator
+ file_iter = m_files.find(name);
+ if (file_iter == m_files.end()) {
+ errorstream << "Client: server sent media file that was"
+ << "not announced, ignoring it: \"" << name << "\""
+ << std::endl;
+ return;
+ }
+ FileStatus *filestatus = file_iter->second;
+ assert(filestatus != NULL);
+
+ // Check that file hasn't already been received
+ if (filestatus->received) {
+ errorstream << "Client: server sent media file that we already"
+ << "received, ignoring it: \"" << name << "\""
+ << std::endl;
+ return;
+ }
+
+ // Mark file as received, regardless of whether loading it works and
+ // whether the checksum matches (because at this point there is no
+ // other server that could send a replacement)
+ filestatus->received = true;
+ assert(m_uncached_received_count < m_uncached_count);
+ m_uncached_received_count++;
+
+ // Check that received file matches announced checksum
+ // If so, load it
+ checkAndLoad(name, filestatus->sha1, data, false, client);
+}
+
+bool ClientMediaDownloader::checkAndLoad(
+ const std::string &name, const std::string &sha1,
+ const std::string &data, bool is_from_cache, Client *client)
+{
+ const char *cached_or_received = is_from_cache ? "cached" : "received";
+ const char *cached_or_received_uc = is_from_cache ? "Cached" : "Received";
+ std::string sha1_hex = hex_encode(sha1);
+
+ // Compute actual checksum of data
+ std::string data_sha1;
+ {
+ SHA1 data_sha1_calculator;
+ data_sha1_calculator.addBytes(data.c_str(), data.size());
+ unsigned char *data_tmpdigest = data_sha1_calculator.getDigest();
+ data_sha1.assign((char*) data_tmpdigest, 20);
+ free(data_tmpdigest);
+ }
+
+ // Check that received file matches announced checksum
+ if (data_sha1 != sha1) {
+ std::string data_sha1_hex = hex_encode(data_sha1);
+ infostream << "Client: "
+ << cached_or_received_uc << " media file "
+ << sha1_hex << " \"" << name << "\" "
+ << "mismatches actual checksum " << data_sha1_hex
+ << std::endl;
+ return false;
+ }
+
+ // Checksum is ok, try loading the file
+ bool success = client->loadMedia(data, name);
+ if (!success) {
+ infostream << "Client: "
+ << "Failed to load " << cached_or_received << " media: "
+ << sha1_hex << " \"" << name << "\""
+ << std::endl;
+ return false;
+ }
+
+ verbosestream << "Client: "
+ << "Loaded " << cached_or_received << " media: "
+ << sha1_hex << " \"" << name << "\""
+ << std::endl;
+
+ // Update cache (unless we just loaded the file from the cache)
+ if (!is_from_cache)
+ m_media_cache.update(sha1_hex, data);
+
+ return true;
+}
+
+
+/*
+ Minetest Hashset File Format
+
+ All values are stored in big-endian byte order.
+ [u32] signature: 'MTHS'
+ [u16] version: 1
+ For each hash in set:
+ [u8*20] SHA1 hash
+
+ Version changes:
+ 1 - Initial version
+*/
+
+std::string ClientMediaDownloader::serializeRequiredHashSet()
+{
+ std::ostringstream os(std::ios::binary);
+
+ writeU32(os, MTHASHSET_FILE_SIGNATURE); // signature
+ writeU16(os, 1); // version
+
+ // Write list of hashes of files that have not been
+ // received (found in cache) yet
+ for (std::map<std::string, FileStatus*>::iterator
+ it = m_files.begin();
+ it != m_files.end(); ++it) {
+ if (!it->second->received) {
+ FATAL_ERROR_IF(it->second->sha1.size() != 20, "Invalid SHA1 size");
+ os << it->second->sha1;
+ }
+ }
+
+ return os.str();
+}
+
+void ClientMediaDownloader::deSerializeHashSet(const std::string &data,
+ std::set<std::string> &result)
+{
+ if (data.size() < 6 || data.size() % 20 != 6) {
+ throw SerializationError(
+ "ClientMediaDownloader::deSerializeHashSet: "
+ "invalid hash set file size");
+ }
+
+ const u8 *data_cstr = (const u8*) data.c_str();
+
+ u32 signature = readU32(&data_cstr[0]);
+ if (signature != MTHASHSET_FILE_SIGNATURE) {
+ throw SerializationError(
+ "ClientMediaDownloader::deSerializeHashSet: "
+ "invalid hash set file signature");
+ }
+
+ u16 version = readU16(&data_cstr[4]);
+ if (version != 1) {
+ throw SerializationError(
+ "ClientMediaDownloader::deSerializeHashSet: "
+ "unsupported hash set file version");
+ }
+
+ for (u32 pos = 6; pos < data.size(); pos += 20) {
+ result.insert(data.substr(pos, 20));
+ }
+}
diff --git a/src/client/clientmedia.h b/src/client/clientmedia.h
new file mode 100644
index 000000000..b08b83e4d
--- /dev/null
+++ b/src/client/clientmedia.h
@@ -0,0 +1,148 @@
+/*
+Minetest
+Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+
+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.h"
+#include "filecache.h"
+#include <ostream>
+#include <map>
+#include <set>
+#include <vector>
+#include <unordered_map>
+
+class Client;
+struct HTTPFetchResult;
+
+#define MTHASHSET_FILE_SIGNATURE 0x4d544853 // 'MTHS'
+#define MTHASHSET_FILE_NAME "index.mth"
+
+class ClientMediaDownloader
+{
+public:
+ ClientMediaDownloader();
+ ~ClientMediaDownloader();
+
+ float getProgress() const {
+ if (m_uncached_count >= 1)
+ return 1.0f * m_uncached_received_count /
+ m_uncached_count;
+
+ return 0.0f;
+ }
+
+ bool isStarted() const {
+ return m_initial_step_done;
+ }
+
+ // If this returns true, the downloader is done and can be deleted
+ bool isDone() const {
+ return m_initial_step_done &&
+ m_uncached_received_count == m_uncached_count;
+ }
+
+ // Add a file to the list of required file (but don't fetch it yet)
+ void addFile(const std::string &name, const std::string &sha1);
+
+ // Add a remote server to the list; ignored if not built with cURL
+ void addRemoteServer(const std::string &baseurl);
+
+ // Steps the media downloader:
+ // - May load media into client by calling client->loadMedia()
+ // - May check media cache for files
+ // - May add files to media cache
+ // - May start remote transfers by calling httpfetch_async
+ // - May check for completion of current remote transfers
+ // - May start conventional transfers by calling client->request_media()
+ // - May inform server that all media has been loaded
+ // by calling client->received_media()
+ // After step has been called once, don't call addFile/addRemoteServer.
+ void step(Client *client);
+
+ // Must be called for each file received through TOCLIENT_MEDIA
+ void conventionalTransferDone(
+ const std::string &name,
+ const std::string &data,
+ Client *client);
+
+private:
+ struct FileStatus {
+ bool received;
+ std::string sha1;
+ s32 current_remote;
+ std::vector<s32> available_remotes;
+ };
+
+ struct RemoteServerStatus {
+ std::string baseurl;
+ s32 active_count;
+ bool request_by_filename;
+ };
+
+ void initialStep(Client *client);
+ void remoteHashSetReceived(const HTTPFetchResult &fetch_result);
+ void remoteMediaReceived(const HTTPFetchResult &fetch_result,
+ Client *client);
+ s32 selectRemoteServer(FileStatus *filestatus);
+ void startRemoteMediaTransfers();
+ void startConventionalTransfers(Client *client);
+
+ bool checkAndLoad(const std::string &name, const std::string &sha1,
+ const std::string &data, bool is_from_cache,
+ Client *client);
+
+ std::string serializeRequiredHashSet();
+ static void deSerializeHashSet(const std::string &data,
+ std::set<std::string> &result);
+
+ // Maps filename to file status
+ std::map<std::string, FileStatus*> m_files;
+
+ // Array of remote media servers
+ std::vector<RemoteServerStatus*> m_remotes;
+
+ // Filesystem-based media cache
+ FileCache m_media_cache;
+
+ // Has an attempt been made to load media files from the file cache?
+ // Have hash sets been requested from remote servers?
+ bool m_initial_step_done = false;
+
+ // Total number of media files to load
+ s32 m_uncached_count = 0;
+
+ // Number of media files that have been received
+ s32 m_uncached_received_count = 0;
+
+ // Status of remote transfers
+ unsigned long m_httpfetch_caller;
+ unsigned long m_httpfetch_next_id = 0;
+ long m_httpfetch_timeout = 0;
+ s32 m_httpfetch_active = 0;
+ s32 m_httpfetch_active_limit = 0;
+ s32 m_outstanding_hash_sets = 0;
+ std::unordered_map<unsigned long, std::string> m_remote_file_transfers;
+
+ // All files up to this name have either been received from a
+ // remote server or failed on all remote servers, so those files
+ // don't need to be looked at again
+ // (use m_files.upper_bound(m_name_bound) to get an iterator)
+ std::string m_name_bound = "";
+
+};
diff --git a/src/client/clientobject.cpp b/src/client/clientobject.cpp
new file mode 100644
index 000000000..f4b69201b
--- /dev/null
+++ b/src/client/clientobject.cpp
@@ -0,0 +1,66 @@
+/*
+Minetest
+Copyright (C) 2010-2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+
+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 "clientobject.h"
+#include "debug.h"
+#include "porting.h"
+
+/*
+ ClientActiveObject
+*/
+
+ClientActiveObject::ClientActiveObject(u16 id, Client *client,
+ ClientEnvironment *env):
+ ActiveObject(id),
+ m_client(client),
+ m_env(env)
+{
+}
+
+ClientActiveObject::~ClientActiveObject()
+{
+ removeFromScene(true);
+}
+
+ClientActiveObject* ClientActiveObject::create(ActiveObjectType type,
+ Client *client, ClientEnvironment *env)
+{
+ // Find factory function
+ auto n = m_types.find(type);
+ if (n == m_types.end()) {
+ // If factory is not found, just return.
+ warningstream << "ClientActiveObject: No factory for type="
+ << (int)type << std::endl;
+ return NULL;
+ }
+
+ Factory f = n->second;
+ ClientActiveObject *object = (*f)(client, env);
+ return object;
+}
+
+void ClientActiveObject::registerType(u16 type, Factory f)
+{
+ auto n = m_types.find(type);
+ if (n != m_types.end())
+ return;
+ m_types[type] = f;
+}
+
+
diff --git a/src/client/clientobject.h b/src/client/clientobject.h
new file mode 100644
index 000000000..9377d1e67
--- /dev/null
+++ b/src/client/clientobject.h
@@ -0,0 +1,108 @@
+/*
+Minetest
+Copyright (C) 2010-2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+
+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 "activeobject.h"
+#include <unordered_map>
+
+class ClientEnvironment;
+class ITextureSource;
+class Client;
+class IGameDef;
+class LocalPlayer;
+struct ItemStack;
+class WieldMeshSceneNode;
+
+class ClientActiveObject : public ActiveObject
+{
+public:
+ ClientActiveObject(u16 id, Client *client, ClientEnvironment *env);
+ virtual ~ClientActiveObject();
+
+ virtual void addToScene(ITextureSource *tsrc) {};
+ virtual void removeFromScene(bool permanent) {}
+ // 0 <= light_at_pos <= LIGHT_SUN
+ virtual void updateLight(u8 light_at_pos){}
+ virtual void updateLightNoCheck(u8 light_at_pos){}
+ virtual v3s16 getLightPosition(){return v3s16(0,0,0);}
+ virtual bool getCollisionBox(aabb3f *toset) const { return false; }
+ virtual bool getSelectionBox(aabb3f *toset) const { return false; }
+ virtual bool collideWithObjects() const { return false; }
+ virtual v3f getPosition(){ return v3f(0,0,0); }
+ virtual float getYaw() const { return 0; }
+ virtual scene::ISceneNode *getSceneNode() { return NULL; }
+ virtual scene::IAnimatedMeshSceneNode *getAnimatedMeshSceneNode() { return NULL; }
+ virtual bool isLocalPlayer() const {return false;}
+ virtual ClientActiveObject *getParent() const { return nullptr; };
+ virtual void setAttachments() {}
+ virtual bool doShowSelectionBox(){return true;}
+
+ // Step object in time
+ virtual void step(float dtime, ClientEnvironment *env){}
+
+ // Process a message sent by the server side object
+ virtual void processMessage(const std::string &data){}
+
+ virtual std::string infoText() {return "";}
+ virtual std::string debugInfoText() {return "";}
+
+ /*
+ This takes the return value of
+ ServerActiveObject::getClientInitializationData
+ */
+ virtual void initialize(const std::string &data){}
+
+ // Create a certain type of ClientActiveObject
+ static ClientActiveObject* create(ActiveObjectType type, Client *client,
+ ClientEnvironment *env);
+
+ // If returns true, punch will not be sent to the server
+ virtual bool directReportPunch(v3f dir, const ItemStack *punchitem=NULL,
+ float time_from_last_punch=1000000)
+ { return false; }
+
+protected:
+ // Used for creating objects based on type
+ typedef ClientActiveObject* (*Factory)(Client *client, ClientEnvironment *env);
+ static void registerType(u16 type, Factory f);
+ Client *m_client;
+ ClientEnvironment *m_env;
+private:
+ // Used for creating objects based on type
+ static std::unordered_map<u16, Factory> m_types;
+};
+
+struct DistanceSortedActiveObject
+{
+ ClientActiveObject *obj;
+ f32 d;
+
+ DistanceSortedActiveObject(ClientActiveObject *a_obj, f32 a_d)
+ {
+ obj = a_obj;
+ d = a_d;
+ }
+
+ bool operator < (const DistanceSortedActiveObject &other) const
+ {
+ return d < other.d;
+ }
+};
diff --git a/src/client/clouds.cpp b/src/client/clouds.cpp
new file mode 100644
index 000000000..13051f32c
--- /dev/null
+++ b/src/client/clouds.cpp
@@ -0,0 +1,386 @@
+/*
+Minetest
+Copyright (C) 2010-2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+
+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 "client/renderingengine.h"
+#include "clouds.h"
+#include "noise.h"
+#include "constants.h"
+#include "debug.h"
+#include "profiler.h"
+#include "settings.h"
+#include <cmath>
+
+
+// Menu clouds are created later
+class Clouds;
+Clouds *g_menuclouds = NULL;
+irr::scene::ISceneManager *g_menucloudsmgr = NULL;
+
+// Constant for now
+static constexpr const float cloud_size = BS * 64.0f;
+
+static void cloud_3d_setting_changed(const std::string &settingname, void *data)
+{
+ ((Clouds *)data)->readSettings();
+}
+
+Clouds::Clouds(scene::ISceneManager* mgr,
+ s32 id,
+ u32 seed
+):
+ scene::ISceneNode(mgr->getRootSceneNode(), mgr, id),
+ m_seed(seed)
+{
+ m_material.setFlag(video::EMF_LIGHTING, false);
+ //m_material.setFlag(video::EMF_BACK_FACE_CULLING, false);
+ m_material.setFlag(video::EMF_BACK_FACE_CULLING, true);
+ m_material.setFlag(video::EMF_BILINEAR_FILTER, false);
+ m_material.setFlag(video::EMF_FOG_ENABLE, true);
+ m_material.setFlag(video::EMF_ANTI_ALIASING, true);
+ //m_material.MaterialType = video::EMT_TRANSPARENT_VERTEX_ALPHA;
+ m_material.MaterialType = video::EMT_TRANSPARENT_ALPHA_CHANNEL;
+
+ m_params.height = 120;
+ m_params.density = 0.4f;
+ m_params.thickness = 16.0f;
+ m_params.color_bright = video::SColor(229, 240, 240, 255);
+ m_params.color_ambient = video::SColor(255, 0, 0, 0);
+ m_params.speed = v2f(0.0f, -2.0f);
+
+ readSettings();
+ g_settings->registerChangedCallback("enable_3d_clouds",
+ &cloud_3d_setting_changed, this);
+
+ updateBox();
+}
+
+Clouds::~Clouds()
+{
+ g_settings->deregisterChangedCallback("enable_3d_clouds",
+ &cloud_3d_setting_changed, this);
+}
+
+void Clouds::OnRegisterSceneNode()
+{
+ if(IsVisible)
+ {
+ SceneManager->registerNodeForRendering(this, scene::ESNRP_TRANSPARENT);
+ //SceneManager->registerNodeForRendering(this, scene::ESNRP_SOLID);
+ }
+
+ ISceneNode::OnRegisterSceneNode();
+}
+
+void Clouds::render()
+{
+
+ if (m_params.density <= 0.0f)
+ return; // no need to do anything
+
+ video::IVideoDriver* driver = SceneManager->getVideoDriver();
+
+ if(SceneManager->getSceneNodeRenderPass() != scene::ESNRP_TRANSPARENT)
+ //if(SceneManager->getSceneNodeRenderPass() != scene::ESNRP_SOLID)
+ return;
+
+ ScopeProfiler sp(g_profiler, "Rendering of clouds, avg", SPT_AVG);
+
+ int num_faces_to_draw = m_enable_3d ? 6 : 1;
+
+ m_material.setFlag(video::EMF_BACK_FACE_CULLING, m_enable_3d);
+
+ driver->setTransform(video::ETS_WORLD, AbsoluteTransformation);
+ driver->setMaterial(m_material);
+
+ /*
+ Clouds move from Z+ towards Z-
+ */
+
+ const float cloud_full_radius = cloud_size * m_cloud_radius_i;
+
+ v2f camera_pos_2d(m_camera_pos.X, m_camera_pos.Z);
+ // Position of cloud noise origin from the camera
+ v2f cloud_origin_from_camera_f = m_origin - camera_pos_2d;
+ // The center point of drawing in the noise
+ v2f center_of_drawing_in_noise_f = -cloud_origin_from_camera_f;
+ // The integer center point of drawing in the noise
+ v2s16 center_of_drawing_in_noise_i(
+ std::floor(center_of_drawing_in_noise_f.X / cloud_size),
+ std::floor(center_of_drawing_in_noise_f.Y / cloud_size)
+ );
+
+ // The world position of the integer center point of drawing in the noise
+ v2f world_center_of_drawing_in_noise_f = v2f(
+ center_of_drawing_in_noise_i.X * cloud_size,
+ center_of_drawing_in_noise_i.Y * cloud_size
+ ) + m_origin;
+
+ /*video::SColor c_top(128,b*240,b*240,b*255);
+ video::SColor c_side_1(128,b*230,b*230,b*255);
+ video::SColor c_side_2(128,b*220,b*220,b*245);
+ video::SColor c_bottom(128,b*205,b*205,b*230);*/
+ video::SColorf c_top_f(m_color);
+ video::SColorf c_side_1_f(m_color);
+ video::SColorf c_side_2_f(m_color);
+ video::SColorf c_bottom_f(m_color);
+ c_side_1_f.r *= 0.95;
+ c_side_1_f.g *= 0.95;
+ c_side_1_f.b *= 0.95;
+ c_side_2_f.r *= 0.90;
+ c_side_2_f.g *= 0.90;
+ c_side_2_f.b *= 0.90;
+ c_bottom_f.r *= 0.80;
+ c_bottom_f.g *= 0.80;
+ c_bottom_f.b *= 0.80;
+ video::SColor c_top = c_top_f.toSColor();
+ video::SColor c_side_1 = c_side_1_f.toSColor();
+ video::SColor c_side_2 = c_side_2_f.toSColor();
+ video::SColor c_bottom = c_bottom_f.toSColor();
+
+ // Get fog parameters for setting them back later
+ video::SColor fog_color(0,0,0,0);
+ video::E_FOG_TYPE fog_type = video::EFT_FOG_LINEAR;
+ f32 fog_start = 0;
+ f32 fog_end = 0;
+ f32 fog_density = 0;
+ bool fog_pixelfog = false;
+ bool fog_rangefog = false;
+ driver->getFog(fog_color, fog_type, fog_start, fog_end, fog_density,
+ fog_pixelfog, fog_rangefog);
+
+ // Set our own fog
+ driver->setFog(fog_color, fog_type, cloud_full_radius * 0.5,
+ cloud_full_radius*1.2, fog_density, fog_pixelfog, fog_rangefog);
+
+ // Read noise
+
+ bool *grid = new bool[m_cloud_radius_i * 2 * m_cloud_radius_i * 2];
+
+
+ for(s16 zi = -m_cloud_radius_i; zi < m_cloud_radius_i; zi++) {
+ u32 si = (zi + m_cloud_radius_i) * m_cloud_radius_i * 2 + m_cloud_radius_i;
+
+ for (s16 xi = -m_cloud_radius_i; xi < m_cloud_radius_i; xi++) {
+ u32 i = si + xi;
+
+ grid[i] = gridFilled(
+ xi + center_of_drawing_in_noise_i.X,
+ zi + center_of_drawing_in_noise_i.Y
+ );
+ }
+ }
+
+#define GETINDEX(x, z, radius) (((z)+(radius))*(radius)*2 + (x)+(radius))
+#define INAREA(x, z, radius) \
+ ((x) >= -(radius) && (x) < (radius) && (z) >= -(radius) && (z) < (radius))
+
+ for (s16 zi0= -m_cloud_radius_i; zi0 < m_cloud_radius_i; zi0++)
+ for (s16 xi0= -m_cloud_radius_i; xi0 < m_cloud_radius_i; xi0++)
+ {
+ s16 zi = zi0;
+ s16 xi = xi0;
+ // Draw from front to back (needed for transparency)
+ /*if(zi <= 0)
+ zi = -m_cloud_radius_i - zi;
+ if(xi <= 0)
+ xi = -m_cloud_radius_i - xi;*/
+ // Draw from back to front
+ if(zi >= 0)
+ zi = m_cloud_radius_i - zi - 1;
+ if(xi >= 0)
+ xi = m_cloud_radius_i - xi - 1;
+
+ u32 i = GETINDEX(xi, zi, m_cloud_radius_i);
+
+ if (!grid[i])
+ continue;
+
+ v2f p0 = v2f(xi,zi)*cloud_size + world_center_of_drawing_in_noise_f;
+
+ video::S3DVertex v[4] = {
+ video::S3DVertex(0,0,0, 0,0,0, c_top, 0, 1),
+ video::S3DVertex(0,0,0, 0,0,0, c_top, 1, 1),
+ video::S3DVertex(0,0,0, 0,0,0, c_top, 1, 0),
+ video::S3DVertex(0,0,0, 0,0,0, c_top, 0, 0)
+ };
+
+ /*if(zi <= 0 && xi <= 0){
+ v[0].Color.setBlue(255);
+ v[1].Color.setBlue(255);
+ v[2].Color.setBlue(255);
+ v[3].Color.setBlue(255);
+ }*/
+
+ f32 rx = cloud_size / 2.0f;
+ // if clouds are flat, the top layer should be at the given height
+ f32 ry = m_enable_3d ? m_params.thickness * BS : 0.0f;
+ f32 rz = cloud_size / 2;
+
+ for(int i=0; i<num_faces_to_draw; i++)
+ {
+ switch(i)
+ {
+ case 0: // top
+ for (video::S3DVertex &vertex : v) {
+ vertex.Normal.set(0,1,0);
+ }
+ v[0].Pos.set(-rx, ry,-rz);
+ v[1].Pos.set(-rx, ry, rz);
+ v[2].Pos.set( rx, ry, rz);
+ v[3].Pos.set( rx, ry,-rz);
+ break;
+ case 1: // back
+ if (INAREA(xi, zi - 1, m_cloud_radius_i)) {
+ u32 j = GETINDEX(xi, zi - 1, m_cloud_radius_i);
+ if(grid[j])
+ continue;
+ }
+ for (video::S3DVertex &vertex : v) {
+ vertex.Color = c_side_1;
+ vertex.Normal.set(0,0,-1);
+ }
+ v[0].Pos.set(-rx, ry,-rz);
+ v[1].Pos.set( rx, ry,-rz);
+ v[2].Pos.set( rx, 0,-rz);
+ v[3].Pos.set(-rx, 0,-rz);
+ break;
+ case 2: //right
+ if (INAREA(xi + 1, zi, m_cloud_radius_i)) {
+ u32 j = GETINDEX(xi+1, zi, m_cloud_radius_i);
+ if(grid[j])
+ continue;
+ }
+ for (video::S3DVertex &vertex : v) {
+ vertex.Color = c_side_2;
+ vertex.Normal.set(1,0,0);
+ }
+ v[0].Pos.set( rx, ry,-rz);
+ v[1].Pos.set( rx, ry, rz);
+ v[2].Pos.set( rx, 0, rz);
+ v[3].Pos.set( rx, 0,-rz);
+ break;
+ case 3: // front
+ if (INAREA(xi, zi + 1, m_cloud_radius_i)) {
+ u32 j = GETINDEX(xi, zi + 1, m_cloud_radius_i);
+ if(grid[j])
+ continue;
+ }
+ for (video::S3DVertex &vertex : v) {
+ vertex.Color = c_side_1;
+ vertex.Normal.set(0,0,-1);
+ }
+ v[0].Pos.set( rx, ry, rz);
+ v[1].Pos.set(-rx, ry, rz);
+ v[2].Pos.set(-rx, 0, rz);
+ v[3].Pos.set( rx, 0, rz);
+ break;
+ case 4: // left
+ if (INAREA(xi-1, zi, m_cloud_radius_i)) {
+ u32 j = GETINDEX(xi-1, zi, m_cloud_radius_i);
+ if(grid[j])
+ continue;
+ }
+ for (video::S3DVertex &vertex : v) {
+ vertex.Color = c_side_2;
+ vertex.Normal.set(-1,0,0);
+ }
+ v[0].Pos.set(-rx, ry, rz);
+ v[1].Pos.set(-rx, ry,-rz);
+ v[2].Pos.set(-rx, 0,-rz);
+ v[3].Pos.set(-rx, 0, rz);
+ break;
+ case 5: // bottom
+ for (video::S3DVertex &vertex : v) {
+ vertex.Color = c_bottom;
+ vertex.Normal.set(0,-1,0);
+ }
+ v[0].Pos.set( rx, 0, rz);
+ v[1].Pos.set(-rx, 0, rz);
+ v[2].Pos.set(-rx, 0,-rz);
+ v[3].Pos.set( rx, 0,-rz);
+ break;
+ }
+
+ v3f pos(p0.X, m_params.height * BS, p0.Y);
+ pos -= intToFloat(m_camera_offset, BS);
+
+ for (video::S3DVertex &vertex : v)
+ vertex.Pos += pos;
+ u16 indices[] = {0,1,2,2,3,0};
+ driver->drawVertexPrimitiveList(v, 4, indices, 2,
+ video::EVT_STANDARD, scene::EPT_TRIANGLES, video::EIT_16BIT);
+ }
+ }
+
+ delete[] grid;
+
+ // Restore fog settings
+ driver->setFog(fog_color, fog_type, fog_start, fog_end, fog_density,
+ fog_pixelfog, fog_rangefog);
+}
+
+void Clouds::step(float dtime)
+{
+ m_origin = m_origin + dtime * BS * m_params.speed;
+}
+
+void Clouds::update(const v3f &camera_p, const video::SColorf &color_diffuse)
+{
+ m_camera_pos = camera_p;
+ m_color.r = MYMIN(MYMAX(color_diffuse.r * m_params.color_bright.getRed(),
+ m_params.color_ambient.getRed()), 255) / 255.0f;
+ m_color.g = MYMIN(MYMAX(color_diffuse.g * m_params.color_bright.getGreen(),
+ m_params.color_ambient.getGreen()), 255) / 255.0f;
+ m_color.b = MYMIN(MYMAX(color_diffuse.b * m_params.color_bright.getBlue(),
+ m_params.color_ambient.getBlue()), 255) / 255.0f;
+ m_color.a = m_params.color_bright.getAlpha() / 255.0f;
+
+ // is the camera inside the cloud mesh?
+ m_camera_inside_cloud = false; // default
+ if (m_enable_3d) {
+ float camera_height = camera_p.Y;
+ if (camera_height >= m_box.MinEdge.Y &&
+ camera_height <= m_box.MaxEdge.Y) {
+ v2f camera_in_noise;
+ camera_in_noise.X = floor((camera_p.X - m_origin.X) / cloud_size + 0.5);
+ camera_in_noise.Y = floor((camera_p.Z - m_origin.Y) / cloud_size + 0.5);
+ bool filled = gridFilled(camera_in_noise.X, camera_in_noise.Y);
+ m_camera_inside_cloud = filled;
+ }
+ }
+}
+
+void Clouds::readSettings()
+{
+ m_cloud_radius_i = g_settings->getU16("cloud_radius");
+ m_enable_3d = g_settings->getBool("enable_3d_clouds");
+}
+
+bool Clouds::gridFilled(int x, int y) const
+{
+ float cloud_size_noise = cloud_size / (BS * 200.f);
+ float noise = noise2d_perlin(
+ (float)x * cloud_size_noise,
+ (float)y * cloud_size_noise,
+ m_seed, 3, 0.5);
+ // normalize to 0..1 (given 3 octaves)
+ static constexpr const float noise_bound = 1.0f + 0.5f + 0.25f;
+ float density = noise / noise_bound * 0.5f + 0.5f;
+ return (density < m_params.density);
+}
diff --git a/src/client/clouds.h b/src/client/clouds.h
new file mode 100644
index 000000000..a4d810faa
--- /dev/null
+++ b/src/client/clouds.h
@@ -0,0 +1,144 @@
+/*
+Minetest
+Copyright (C) 2010-2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+
+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 <iostream>
+#include "constants.h"
+#include "cloudparams.h"
+
+// Menu clouds
+class Clouds;
+extern Clouds *g_menuclouds;
+
+// Scene manager used for menu clouds
+namespace irr{namespace scene{class ISceneManager;}}
+extern irr::scene::ISceneManager *g_menucloudsmgr;
+
+class Clouds : public scene::ISceneNode
+{
+public:
+ Clouds(scene::ISceneManager* mgr,
+ s32 id,
+ u32 seed
+ );
+
+ ~Clouds();
+
+ /*
+ ISceneNode methods
+ */
+
+ virtual void OnRegisterSceneNode();
+
+ virtual void render();
+
+ virtual const aabb3f &getBoundingBox() const
+ {
+ return m_box;
+ }
+
+ virtual u32 getMaterialCount() const
+ {
+ return 1;
+ }
+
+ virtual video::SMaterial& getMaterial(u32 i)
+ {
+ return m_material;
+ }
+
+ /*
+ Other stuff
+ */
+
+ void step(float dtime);
+
+ void update(const v3f &camera_p, const video::SColorf &color);
+
+ void updateCameraOffset(const v3s16 &camera_offset)
+ {
+ m_camera_offset = camera_offset;
+ updateBox();
+ }
+
+ void readSettings();
+
+ void setDensity(float density)
+ {
+ m_params.density = density;
+ // currently does not need bounding
+ }
+
+ void setColorBright(const video::SColor &color_bright)
+ {
+ m_params.color_bright = color_bright;
+ }
+
+ void setColorAmbient(const video::SColor &color_ambient)
+ {
+ m_params.color_ambient = color_ambient;
+ }
+
+ void setHeight(float height)
+ {
+ m_params.height = height; // add bounding when necessary
+ updateBox();
+ }
+
+ void setSpeed(v2f speed)
+ {
+ m_params.speed = speed;
+ }
+
+ void setThickness(float thickness)
+ {
+ m_params.thickness = thickness;
+ updateBox();
+ }
+
+ bool isCameraInsideCloud() const { return m_camera_inside_cloud; }
+
+ const video::SColor getColor() const { return m_color.toSColor(); }
+
+private:
+ void updateBox()
+ {
+ float height_bs = m_params.height * BS;
+ float thickness_bs = m_params.thickness * BS;
+ m_box = aabb3f(-BS * 1000000.0f, height_bs - BS * m_camera_offset.Y, -BS * 1000000.0f,
+ BS * 1000000.0f, height_bs + thickness_bs - BS * m_camera_offset.Y, BS * 1000000.0f);
+ }
+
+ bool gridFilled(int x, int y) const;
+
+ video::SMaterial m_material;
+ aabb3f m_box;
+ u16 m_cloud_radius_i;
+ bool m_enable_3d;
+ u32 m_seed;
+ v3f m_camera_pos;
+ v2f m_origin;
+ v3s16 m_camera_offset;
+ video::SColorf m_color = video::SColorf(1.0f, 1.0f, 1.0f, 1.0f);
+ CloudParams m_params;
+ bool m_camera_inside_cloud = false;
+
+};
diff --git a/src/client/content_cao.cpp b/src/client/content_cao.cpp
new file mode 100644
index 000000000..db59ae5c5
--- /dev/null
+++ b/src/client/content_cao.cpp
@@ -0,0 +1,1611 @@
+/*
+Minetest
+Copyright (C) 2010-2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+
+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 <ICameraSceneNode.h>
+#include <ITextSceneNode.h>
+#include <IBillboardSceneNode.h>
+#include <IMeshManipulator.h>
+#include <IAnimatedMeshSceneNode.h>
+#include "content_cao.h"
+#include "util/numeric.h" // For IntervalLimiter
+#include "util/serialize.h"
+#include "util/basic_macros.h"
+#include "client/sound.h"
+#include "client/tile.h"
+#include "environment.h"
+#include "collision.h"
+#include "settings.h"
+#include "serialization.h" // For decompressZlib
+#include "clientobject.h"
+#include "mesh.h"
+#include "itemdef.h"
+#include "tool.h"
+#include "content_cso.h"
+#include "sound.h"
+#include "nodedef.h"
+#include "localplayer.h"
+#include "map.h"
+#include "camera.h" // CameraModes
+#include "client.h"
+#include "wieldmesh.h"
+#include <algorithm>
+#include <cmath>
+#include "client/renderingengine.h"
+
+class Settings;
+struct ToolCapabilities;
+
+std::unordered_map<u16, ClientActiveObject::Factory> ClientActiveObject::m_types;
+
+template<typename T>
+void SmoothTranslator<T>::init(T current)
+{
+ val_old = current;
+ val_current = current;
+ val_target = current;
+ anim_time = 0;
+ anim_time_counter = 0;
+ aim_is_end = true;
+}
+
+template<typename T>
+void SmoothTranslator<T>::update(T new_target, bool is_end_position, float update_interval)
+{
+ aim_is_end = is_end_position;
+ val_old = val_current;
+ val_target = new_target;
+ if (update_interval > 0) {
+ anim_time = update_interval;
+ } else {
+ if (anim_time < 0.001 || anim_time > 1.0)
+ anim_time = anim_time_counter;
+ else
+ anim_time = anim_time * 0.9 + anim_time_counter * 0.1;
+ }
+ anim_time_counter = 0;
+}
+
+template<typename T>
+void SmoothTranslator<T>::translate(f32 dtime)
+{
+ anim_time_counter = anim_time_counter + dtime;
+ T val_diff = val_target - val_old;
+ f32 moveratio = 1.0;
+ if (anim_time > 0.001)
+ moveratio = anim_time_counter / anim_time;
+ f32 move_end = aim_is_end ? 1.0 : 1.5;
+
+ // Move a bit less than should, to avoid oscillation
+ moveratio = std::min(moveratio * 0.8f, move_end);
+ val_current = val_old + val_diff * moveratio;
+}
+
+void SmoothTranslatorWrapped::translate(f32 dtime)
+{
+ anim_time_counter = anim_time_counter + dtime;
+ f32 val_diff = std::abs(val_target - val_old);
+ if (val_diff > 180.f)
+ val_diff = 360.f - val_diff;
+
+ f32 moveratio = 1.0;
+ if (anim_time > 0.001)
+ moveratio = anim_time_counter / anim_time;
+ f32 move_end = aim_is_end ? 1.0 : 1.5;
+
+ // Move a bit less than should, to avoid oscillation
+ moveratio = std::min(moveratio * 0.8f, move_end);
+ wrappedApproachShortest(val_current, val_target,
+ val_diff * moveratio, 360.f);
+}
+
+void SmoothTranslatorWrappedv3f::translate(f32 dtime)
+{
+ anim_time_counter = anim_time_counter + dtime;
+
+ v3f val_diff_v3f;
+ val_diff_v3f.X = std::abs(val_target.X - val_old.X);
+ val_diff_v3f.Y = std::abs(val_target.Y - val_old.Y);
+ val_diff_v3f.Z = std::abs(val_target.Z - val_old.Z);
+
+ if (val_diff_v3f.X > 180.f)
+ val_diff_v3f.X = 360.f - val_diff_v3f.X;
+
+ if (val_diff_v3f.Y > 180.f)
+ val_diff_v3f.Y = 360.f - val_diff_v3f.Y;
+
+ if (val_diff_v3f.Z > 180.f)
+ val_diff_v3f.Z = 360.f - val_diff_v3f.Z;
+
+ f32 moveratio = 1.0;
+ if (anim_time > 0.001)
+ moveratio = anim_time_counter / anim_time;
+ f32 move_end = aim_is_end ? 1.0 : 1.5;
+
+ // Move a bit less than should, to avoid oscillation
+ moveratio = std::min(moveratio * 0.8f, move_end);
+ wrappedApproachShortest(val_current.X, val_target.X,
+ val_diff_v3f.X * moveratio, 360.f);
+
+ wrappedApproachShortest(val_current.Y, val_target.Y,
+ val_diff_v3f.Y * moveratio, 360.f);
+
+ wrappedApproachShortest(val_current.Z, val_target.Z,
+ val_diff_v3f.Z * moveratio, 360.f);
+}
+
+/*
+ Other stuff
+*/
+
+static void setBillboardTextureMatrix(scene::IBillboardSceneNode *bill,
+ float txs, float tys, int col, int row)
+{
+ video::SMaterial& material = bill->getMaterial(0);
+ core::matrix4& matrix = material.getTextureMatrix(0);
+ matrix.setTextureTranslate(txs*col, tys*row);
+ matrix.setTextureScale(txs, tys);
+}
+
+/*
+ TestCAO
+*/
+
+class TestCAO : public ClientActiveObject
+{
+public:
+ TestCAO(Client *client, ClientEnvironment *env);
+ virtual ~TestCAO() = default;
+
+ ActiveObjectType getType() const
+ {
+ return ACTIVEOBJECT_TYPE_TEST;
+ }
+
+ static ClientActiveObject* create(Client *client, ClientEnvironment *env);
+
+ void addToScene(ITextureSource *tsrc);
+ void removeFromScene(bool permanent);
+ void updateLight(u8 light_at_pos);
+ v3s16 getLightPosition();
+ void updateNodePos();
+
+ void step(float dtime, ClientEnvironment *env);
+
+ void processMessage(const std::string &data);
+
+ bool getCollisionBox(aabb3f *toset) const { return false; }
+private:
+ scene::IMeshSceneNode *m_node;
+ v3f m_position;
+};
+
+// Prototype
+TestCAO proto_TestCAO(NULL, NULL);
+
+TestCAO::TestCAO(Client *client, ClientEnvironment *env):
+ ClientActiveObject(0, client, env),
+ m_node(NULL),
+ m_position(v3f(0,10*BS,0))
+{
+ ClientActiveObject::registerType(getType(), create);
+}
+
+ClientActiveObject* TestCAO::create(Client *client, ClientEnvironment *env)
+{
+ return new TestCAO(client, env);
+}
+
+void TestCAO::addToScene(ITextureSource *tsrc)
+{
+ if(m_node != NULL)
+ return;
+
+ //video::IVideoDriver* driver = smgr->getVideoDriver();
+
+ scene::SMesh *mesh = new scene::SMesh();
+ scene::IMeshBuffer *buf = new scene::SMeshBuffer();
+ video::SColor c(255,255,255,255);
+ video::S3DVertex vertices[4] =
+ {
+ video::S3DVertex(-BS/2,-BS/4,0, 0,0,0, c, 0,1),
+ video::S3DVertex(BS/2,-BS/4,0, 0,0,0, c, 1,1),
+ video::S3DVertex(BS/2,BS/4,0, 0,0,0, c, 1,0),
+ video::S3DVertex(-BS/2,BS/4,0, 0,0,0, c, 0,0),
+ };
+ u16 indices[] = {0,1,2,2,3,0};
+ buf->append(vertices, 4, indices, 6);
+ // Set material
+ buf->getMaterial().setFlag(video::EMF_LIGHTING, false);
+ buf->getMaterial().setFlag(video::EMF_BACK_FACE_CULLING, false);
+ buf->getMaterial().setTexture(0, tsrc->getTextureForMesh("rat.png"));
+ buf->getMaterial().setFlag(video::EMF_BILINEAR_FILTER, false);
+ buf->getMaterial().setFlag(video::EMF_FOG_ENABLE, true);
+ buf->getMaterial().MaterialType = video::EMT_TRANSPARENT_ALPHA_CHANNEL;
+ // Add to mesh
+ mesh->addMeshBuffer(buf);
+ buf->drop();
+ m_node = RenderingEngine::get_scene_manager()->addMeshSceneNode(mesh, NULL);
+ mesh->drop();
+ updateNodePos();
+}
+
+void TestCAO::removeFromScene(bool permanent)
+{
+ if (!m_node)
+ return;
+
+ m_node->remove();
+ m_node = NULL;
+}
+
+void TestCAO::updateLight(u8 light_at_pos)
+{
+}
+
+v3s16 TestCAO::getLightPosition()
+{
+ return floatToInt(m_position, BS);
+}
+
+void TestCAO::updateNodePos()
+{
+ if (!m_node)
+ return;
+
+ m_node->setPosition(m_position);
+ //m_node->setRotation(v3f(0, 45, 0));
+}
+
+void TestCAO::step(float dtime, ClientEnvironment *env)
+{
+ if(m_node)
+ {
+ v3f rot = m_node->getRotation();
+ //infostream<<"dtime="<<dtime<<", rot.Y="<<rot.Y<<std::endl;
+ rot.Y += dtime * 180;
+ m_node->setRotation(rot);
+ }
+}
+
+void TestCAO::processMessage(const std::string &data)
+{
+ infostream<<"TestCAO: Got data: "<<data<<std::endl;
+ std::istringstream is(data, std::ios::binary);
+ u16 cmd;
+ is>>cmd;
+ if(cmd == 0)
+ {
+ v3f newpos;
+ is>>newpos.X;
+ is>>newpos.Y;
+ is>>newpos.Z;
+ m_position = newpos;
+ updateNodePos();
+ }
+}
+
+/*
+ GenericCAO
+*/
+
+#include "genericobject.h"
+
+GenericCAO::GenericCAO(Client *client, ClientEnvironment *env):
+ ClientActiveObject(0, client, env)
+{
+ if (client == NULL) {
+ ClientActiveObject::registerType(getType(), create);
+ } else {
+ m_client = client;
+ }
+}
+
+bool GenericCAO::getCollisionBox(aabb3f *toset) const
+{
+ if (m_prop.physical)
+ {
+ //update collision box
+ toset->MinEdge = m_prop.collisionbox.MinEdge * BS;
+ toset->MaxEdge = m_prop.collisionbox.MaxEdge * BS;
+
+ toset->MinEdge += m_position;
+ toset->MaxEdge += m_position;
+
+ return true;
+ }
+
+ return false;
+}
+
+bool GenericCAO::collideWithObjects() const
+{
+ return m_prop.collideWithObjects;
+}
+
+void GenericCAO::initialize(const std::string &data)
+{
+ infostream<<"GenericCAO: Got init data"<<std::endl;
+ processInitData(data);
+
+ if (m_is_player) {
+ // Check if it's the current player
+ LocalPlayer *player = m_env->getLocalPlayer();
+ if (player && strcmp(player->getName(), m_name.c_str()) == 0) {
+ m_is_local_player = true;
+ m_is_visible = false;
+ player->setCAO(this);
+ }
+ }
+}
+
+void GenericCAO::processInitData(const std::string &data)
+{
+ std::istringstream is(data, std::ios::binary);
+ int num_messages = 0;
+ // version
+ u8 version = readU8(is);
+ // check version
+ if (version == 1) { // In PROTOCOL_VERSION 14
+ m_name = deSerializeString(is);
+ m_is_player = readU8(is);
+ m_id = readU16(is);
+ m_position = readV3F1000(is);
+ m_rotation = readV3F1000(is);
+ m_hp = readS16(is);
+ num_messages = readU8(is);
+ } else {
+ errorstream<<"GenericCAO: Unsupported init data version"
+ <<std::endl;
+ return;
+ }
+
+ for (int i = 0; i < num_messages; i++) {
+ std::string message = deSerializeLongString(is);
+ processMessage(message);
+ }
+
+ m_rotation = wrapDegrees_0_360_v3f(m_rotation);
+ pos_translator.init(m_position);
+ rot_translator.init(m_rotation);
+ updateNodePos();
+}
+
+GenericCAO::~GenericCAO()
+{
+ removeFromScene(true);
+}
+
+bool GenericCAO::getSelectionBox(aabb3f *toset) const
+{
+ if (!m_prop.is_visible || !m_is_visible || m_is_local_player
+ || !m_prop.pointable) {
+ return false;
+ }
+ *toset = m_selection_box;
+ return true;
+}
+
+v3f GenericCAO::getPosition()
+{
+ if (getParent() != NULL) {
+ scene::ISceneNode *node = getSceneNode();
+ if (node)
+ return node->getAbsolutePosition();
+
+ return m_position;
+ }
+ return pos_translator.val_current;
+}
+
+const bool GenericCAO::isImmortal()
+{
+ return itemgroup_get(getGroups(), "immortal");
+}
+
+scene::ISceneNode* GenericCAO::getSceneNode()
+{
+ if (m_meshnode) {
+ return m_meshnode;
+ }
+
+ if (m_animated_meshnode) {
+ return m_animated_meshnode;
+ }
+
+ if (m_wield_meshnode) {
+ return m_wield_meshnode;
+ }
+
+ if (m_spritenode) {
+ return m_spritenode;
+ }
+ return NULL;
+}
+
+scene::IAnimatedMeshSceneNode* GenericCAO::getAnimatedMeshSceneNode()
+{
+ return m_animated_meshnode;
+}
+
+void GenericCAO::setChildrenVisible(bool toset)
+{
+ for (u16 cao_id : m_children) {
+ GenericCAO *obj = m_env->getGenericCAO(cao_id);
+ if (obj) {
+ obj->setVisible(toset);
+ }
+ }
+}
+
+void GenericCAO::setAttachments()
+{
+ updateAttachments();
+}
+
+ClientActiveObject* GenericCAO::getParent() const
+{
+ ClientActiveObject *obj = NULL;
+
+ u16 attached_id = m_env->attachement_parent_ids[getId()];
+
+ if ((attached_id != 0) &&
+ (attached_id != getId())) {
+ obj = m_env->getActiveObject(attached_id);
+ }
+ return obj;
+}
+
+void GenericCAO::removeFromScene(bool permanent)
+{
+ // Should be true when removing the object permanently and false when refreshing (eg: updating visuals)
+ if((m_env != NULL) && (permanent))
+ {
+ for (u16 ci : m_children) {
+ if (m_env->attachement_parent_ids[ci] == getId()) {
+ m_env->attachement_parent_ids[ci] = 0;
+ }
+ }
+ m_children.clear();
+
+ m_env->attachement_parent_ids[getId()] = 0;
+
+ LocalPlayer* player = m_env->getLocalPlayer();
+ if (this == player->parent) {
+ player->parent = NULL;
+ player->isAttached = false;
+ }
+ }
+
+ if (m_meshnode) {
+ m_meshnode->remove();
+ m_meshnode->drop();
+ m_meshnode = NULL;
+ } else if (m_animated_meshnode) {
+ m_animated_meshnode->remove();
+ m_animated_meshnode->drop();
+ m_animated_meshnode = NULL;
+ } else if (m_wield_meshnode) {
+ m_wield_meshnode->remove();
+ m_wield_meshnode->drop();
+ m_wield_meshnode = NULL;
+ } else if (m_spritenode) {
+ m_spritenode->remove();
+ m_spritenode->drop();
+ m_spritenode = NULL;
+ }
+
+ if (m_nametag) {
+ m_client->getCamera()->removeNametag(m_nametag);
+ m_nametag = NULL;
+ }
+}
+
+void GenericCAO::addToScene(ITextureSource *tsrc)
+{
+ m_smgr = RenderingEngine::get_scene_manager();
+
+ if (getSceneNode() != NULL) {
+ return;
+ }
+
+ m_visuals_expired = false;
+
+ if (!m_prop.is_visible) {
+ return;
+ }
+
+ video::E_MATERIAL_TYPE material_type = (m_prop.use_texture_alpha) ?
+ video::EMT_TRANSPARENT_ALPHA_CHANNEL : video::EMT_TRANSPARENT_ALPHA_CHANNEL_REF;
+
+ if (m_prop.visual == "sprite") {
+ infostream<<"GenericCAO::addToScene(): single_sprite"<<std::endl;
+ m_spritenode = RenderingEngine::get_scene_manager()->addBillboardSceneNode(
+ NULL, v2f(1, 1), v3f(0,0,0), -1);
+ m_spritenode->grab();
+ m_spritenode->setMaterialTexture(0,
+ tsrc->getTextureForMesh("unknown_node.png"));
+ m_spritenode->setMaterialFlag(video::EMF_LIGHTING, false);
+ m_spritenode->setMaterialFlag(video::EMF_BILINEAR_FILTER, false);
+ m_spritenode->setMaterialType(material_type);
+ m_spritenode->setMaterialFlag(video::EMF_FOG_ENABLE, true);
+ u8 li = m_last_light;
+ m_spritenode->setColor(video::SColor(255,li,li,li));
+ m_spritenode->setSize(m_prop.visual_size*BS);
+ {
+ const float txs = 1.0 / 1;
+ const float tys = 1.0 / 1;
+ setBillboardTextureMatrix(m_spritenode,
+ txs, tys, 0, 0);
+ }
+ } else if (m_prop.visual == "upright_sprite") {
+ scene::SMesh *mesh = new scene::SMesh();
+ double dx = BS * m_prop.visual_size.X / 2;
+ double dy = BS * m_prop.visual_size.Y / 2;
+ u8 li = m_last_light;
+ video::SColor c(255, li, li, li);
+
+ { // Front
+ scene::IMeshBuffer *buf = new scene::SMeshBuffer();
+ video::S3DVertex vertices[4] = {
+ video::S3DVertex(-dx, -dy, 0, 0,0,0, c, 1,1),
+ video::S3DVertex( dx, -dy, 0, 0,0,0, c, 0,1),
+ video::S3DVertex( dx, dy, 0, 0,0,0, c, 0,0),
+ video::S3DVertex(-dx, dy, 0, 0,0,0, c, 1,0),
+ };
+ if (m_is_player) {
+ // Move minimal Y position to 0 (feet position)
+ for (video::S3DVertex &vertex : vertices)
+ vertex.Pos.Y += dy;
+ }
+ u16 indices[] = {0,1,2,2,3,0};
+ buf->append(vertices, 4, indices, 6);
+ // Set material
+ buf->getMaterial().setFlag(video::EMF_LIGHTING, false);
+ buf->getMaterial().setFlag(video::EMF_BILINEAR_FILTER, false);
+ buf->getMaterial().setFlag(video::EMF_FOG_ENABLE, true);
+ buf->getMaterial().MaterialType = video::EMT_TRANSPARENT_ALPHA_CHANNEL;
+ // Add to mesh
+ mesh->addMeshBuffer(buf);
+ buf->drop();
+ }
+ { // Back
+ scene::IMeshBuffer *buf = new scene::SMeshBuffer();
+ video::S3DVertex vertices[4] = {
+ video::S3DVertex( dx,-dy, 0, 0,0,0, c, 1,1),
+ video::S3DVertex(-dx,-dy, 0, 0,0,0, c, 0,1),
+ video::S3DVertex(-dx, dy, 0, 0,0,0, c, 0,0),
+ video::S3DVertex( dx, dy, 0, 0,0,0, c, 1,0),
+ };
+ if (m_is_player) {
+ // Move minimal Y position to 0 (feet position)
+ for (video::S3DVertex &vertex : vertices)
+ vertex.Pos.Y += dy;
+ }
+ u16 indices[] = {0,1,2,2,3,0};
+ buf->append(vertices, 4, indices, 6);
+ // Set material
+ buf->getMaterial().setFlag(video::EMF_LIGHTING, false);
+ buf->getMaterial().setFlag(video::EMF_BILINEAR_FILTER, false);
+ buf->getMaterial().setFlag(video::EMF_FOG_ENABLE, true);
+ buf->getMaterial().MaterialType = video::EMT_TRANSPARENT_ALPHA_CHANNEL_REF;
+ // Add to mesh
+ mesh->addMeshBuffer(buf);
+ buf->drop();
+ }
+ m_meshnode = RenderingEngine::get_scene_manager()->addMeshSceneNode(mesh, NULL);
+ m_meshnode->grab();
+ mesh->drop();
+ // Set it to use the materials of the meshbuffers directly.
+ // This is needed for changing the texture in the future
+ m_meshnode->setReadOnlyMaterials(true);
+ }
+ else if(m_prop.visual == "cube") {
+ infostream<<"GenericCAO::addToScene(): cube"<<std::endl;
+ scene::IMesh *mesh = createCubeMesh(v3f(BS,BS,BS));
+ m_meshnode = RenderingEngine::get_scene_manager()->addMeshSceneNode(mesh, NULL);
+ m_meshnode->grab();
+ mesh->drop();
+
+ m_meshnode->setScale(v3f(m_prop.visual_size.X,
+ m_prop.visual_size.Y,
+ m_prop.visual_size.X));
+ u8 li = m_last_light;
+ setMeshColor(m_meshnode->getMesh(), video::SColor(255,li,li,li));
+
+ m_meshnode->setMaterialFlag(video::EMF_LIGHTING, false);
+ m_meshnode->setMaterialFlag(video::EMF_BILINEAR_FILTER, false);
+ m_meshnode->setMaterialType(material_type);
+ m_meshnode->setMaterialFlag(video::EMF_FOG_ENABLE, true);
+ }
+ else if(m_prop.visual == "mesh") {
+ infostream<<"GenericCAO::addToScene(): mesh"<<std::endl;
+ scene::IAnimatedMesh *mesh = m_client->getMesh(m_prop.mesh, true);
+ if(mesh)
+ {
+ m_animated_meshnode = RenderingEngine::get_scene_manager()->
+ addAnimatedMeshSceneNode(mesh, NULL);
+ m_animated_meshnode->grab();
+ mesh->drop(); // The scene node took hold of it
+ m_animated_meshnode->animateJoints(); // Needed for some animations
+ m_animated_meshnode->setScale(v3f(m_prop.visual_size.X,
+ m_prop.visual_size.Y,
+ m_prop.visual_size.X));
+ u8 li = m_last_light;
+
+ // set vertex colors to ensure alpha is set
+ setMeshColor(m_animated_meshnode->getMesh(), video::SColor(255,li,li,li));
+
+ setAnimatedMeshColor(m_animated_meshnode, video::SColor(255,li,li,li));
+
+ m_animated_meshnode->setMaterialFlag(video::EMF_LIGHTING, true);
+ m_animated_meshnode->setMaterialFlag(video::EMF_BILINEAR_FILTER, false);
+ m_animated_meshnode->setMaterialType(material_type);
+ m_animated_meshnode->setMaterialFlag(video::EMF_FOG_ENABLE, true);
+ m_animated_meshnode->setMaterialFlag(video::EMF_BACK_FACE_CULLING,
+ m_prop.backface_culling);
+ }
+ else
+ errorstream<<"GenericCAO::addToScene(): Could not load mesh "<<m_prop.mesh<<std::endl;
+ } else if (m_prop.visual == "wielditem") {
+ ItemStack item;
+ infostream << "GenericCAO::addToScene(): wielditem" << std::endl;
+ if (m_prop.wield_item.empty()) {
+ // Old format, only textures are specified.
+ infostream << "textures: " << m_prop.textures.size() << std::endl;
+ if (!m_prop.textures.empty()) {
+ infostream << "textures[0]: " << m_prop.textures[0]
+ << std::endl;
+ IItemDefManager *idef = m_client->idef();
+ item = ItemStack(m_prop.textures[0], 1, 0, idef);
+ }
+ } else {
+ infostream << "serialized form: " << m_prop.wield_item << std::endl;
+ item.deSerialize(m_prop.wield_item, m_client->idef());
+ }
+ m_wield_meshnode = new WieldMeshSceneNode(
+ RenderingEngine::get_scene_manager(), -1);
+ m_wield_meshnode->setItem(item, m_client);
+
+ m_wield_meshnode->setScale(
+ v3f(m_prop.visual_size.X / 2, m_prop.visual_size.Y / 2,
+ m_prop.visual_size.X / 2));
+ u8 li = m_last_light;
+ m_wield_meshnode->setColor(video::SColor(255, li, li, li));
+ } else {
+ infostream<<"GenericCAO::addToScene(): \""<<m_prop.visual
+ <<"\" not supported"<<std::endl;
+ }
+
+ /* don't update while punch texture modifier is active */
+ if (m_reset_textures_timer < 0)
+ updateTextures(m_current_texture_modifier);
+
+ scene::ISceneNode *node = getSceneNode();
+ if (node && !m_prop.nametag.empty() && !m_is_local_player) {
+ // Add nametag
+ v3f pos;
+ pos.Y = m_prop.selectionbox.MaxEdge.Y + 0.3f;
+ m_nametag = m_client->getCamera()->addNametag(node,
+ m_prop.nametag, m_prop.nametag_color,
+ pos);
+ }
+
+ updateNodePos();
+ updateAnimation();
+ updateBonePosition();
+ updateAttachments();
+}
+
+void GenericCAO::updateLight(u8 light_at_pos)
+{
+ // Don't update light of attached one
+ if (getParent() != NULL) {
+ return;
+ }
+
+ updateLightNoCheck(light_at_pos);
+
+ // Update light of all children
+ for (u16 i : m_children) {
+ ClientActiveObject *obj = m_env->getActiveObject(i);
+ if (obj) {
+ obj->updateLightNoCheck(light_at_pos);
+ }
+ }
+}
+
+void GenericCAO::updateLightNoCheck(u8 light_at_pos)
+{
+ if (m_glow < 0)
+ return;
+
+ u8 li = decode_light(light_at_pos + m_glow);
+ if (li != m_last_light) {
+ m_last_light = li;
+ video::SColor color(255,li,li,li);
+ if (m_meshnode) {
+ setMeshColor(m_meshnode->getMesh(), color);
+ } else if (m_animated_meshnode) {
+ setAnimatedMeshColor(m_animated_meshnode, color);
+ } else if (m_wield_meshnode) {
+ m_wield_meshnode->setColor(color);
+ } else if (m_spritenode) {
+ m_spritenode->setColor(color);
+ }
+ }
+}
+
+v3s16 GenericCAO::getLightPosition()
+{
+ if (m_is_player)
+ return floatToInt(m_position + v3f(0, 0.5 * BS, 0), BS);
+
+ return floatToInt(m_position, BS);
+}
+
+void GenericCAO::updateNodePos()
+{
+ if (getParent() != NULL)
+ return;
+
+ scene::ISceneNode *node = getSceneNode();
+
+ if (node) {
+ v3s16 camera_offset = m_env->getCameraOffset();
+ node->setPosition(pos_translator.val_current - intToFloat(camera_offset, BS));
+ if (node != m_spritenode) { // rotate if not a sprite
+ v3f rot = m_is_local_player ? -m_rotation : -rot_translator.val_current;
+ node->setRotation(rot);
+ }
+ }
+}
+
+void GenericCAO::step(float dtime, ClientEnvironment *env)
+{
+ // Handel model of local player instantly to prevent lags
+ if (m_is_local_player) {
+ LocalPlayer *player = m_env->getLocalPlayer();
+ if (m_is_visible) {
+ int old_anim = player->last_animation;
+ float old_anim_speed = player->last_animation_speed;
+ m_position = player->getPosition();
+ m_rotation.Y = wrapDegrees_0_360(player->getYaw());
+ m_velocity = v3f(0,0,0);
+ m_acceleration = v3f(0,0,0);
+ pos_translator.val_current = m_position;
+ rot_translator.val_current = m_rotation;
+ const PlayerControl &controls = player->getPlayerControl();
+
+ bool walking = false;
+ if (controls.up || controls.down || controls.left || controls.right ||
+ controls.forw_move_joystick_axis != 0.f ||
+ controls.sidew_move_joystick_axis != 0.f)
+ walking = true;
+
+ f32 new_speed = player->local_animation_speed;
+ v2s32 new_anim = v2s32(0,0);
+ bool allow_update = false;
+
+ // increase speed if using fast or flying fast
+ if((g_settings->getBool("fast_move") &&
+ m_client->checkLocalPrivilege("fast")) &&
+ (controls.aux1 ||
+ (!player->touching_ground &&
+ g_settings->getBool("free_move") &&
+ m_client->checkLocalPrivilege("fly"))))
+ new_speed *= 1.5;
+ // slowdown speed if sneeking
+ if (controls.sneak && walking)
+ new_speed /= 2;
+
+ if (walking && (controls.LMB || controls.RMB)) {
+ new_anim = player->local_animations[3];
+ player->last_animation = WD_ANIM;
+ } else if(walking) {
+ new_anim = player->local_animations[1];
+ player->last_animation = WALK_ANIM;
+ } else if(controls.LMB || controls.RMB) {
+ new_anim = player->local_animations[2];
+ player->last_animation = DIG_ANIM;
+ }
+
+ // Apply animations if input detected and not attached
+ // or set idle animation
+ if ((new_anim.X + new_anim.Y) > 0 && !player->isAttached) {
+ allow_update = true;
+ m_animation_range = new_anim;
+ m_animation_speed = new_speed;
+ player->last_animation_speed = m_animation_speed;
+ } else {
+ player->last_animation = NO_ANIM;
+
+ if (old_anim != NO_ANIM) {
+ m_animation_range = player->local_animations[0];
+ updateAnimation();
+ }
+ }
+
+ // Update local player animations
+ if ((player->last_animation != old_anim ||
+ m_animation_speed != old_anim_speed) &&
+ player->last_animation != NO_ANIM && allow_update)
+ updateAnimation();
+
+ }
+ }
+
+ if (m_visuals_expired && m_smgr) {
+ m_visuals_expired = false;
+
+ // Attachments, part 1: All attached objects must be unparented first,
+ // or Irrlicht causes a segmentation fault
+ for (auto ci = m_children.begin(); ci != m_children.end();) {
+ if (m_env->attachement_parent_ids[*ci] != getId()) {
+ ci = m_children.erase(ci);
+ continue;
+ }
+ ClientActiveObject *obj = m_env->getActiveObject(*ci);
+ if (obj) {
+ scene::ISceneNode *child_node = obj->getSceneNode();
+ if (child_node)
+ child_node->setParent(m_smgr->getRootSceneNode());
+ }
+ ++ci;
+ }
+
+ removeFromScene(false);
+ addToScene(m_client->tsrc());
+
+ // Attachments, part 2: Now that the parent has been refreshed, put its attachments back
+ for (u16 cao_id : m_children) {
+ // Get the object of the child
+ ClientActiveObject *obj = m_env->getActiveObject(cao_id);
+ if (obj)
+ obj->setAttachments();
+ }
+ }
+
+ // Make sure m_is_visible is always applied
+ scene::ISceneNode *node = getSceneNode();
+ if (node)
+ node->setVisible(m_is_visible);
+
+ if(getParent() != NULL) // Attachments should be glued to their parent by Irrlicht
+ {
+ // Set these for later
+ m_position = getPosition();
+ m_velocity = v3f(0,0,0);
+ m_acceleration = v3f(0,0,0);
+ pos_translator.val_current = m_position;
+
+ if(m_is_local_player) // Update local player attachment position
+ {
+ LocalPlayer *player = m_env->getLocalPlayer();
+ player->overridePosition = getParent()->getPosition();
+ m_env->getLocalPlayer()->parent = getParent();
+ }
+ } else {
+ rot_translator.translate(dtime);
+ v3f lastpos = pos_translator.val_current;
+
+ if(m_prop.physical)
+ {
+ aabb3f box = m_prop.collisionbox;
+ box.MinEdge *= BS;
+ box.MaxEdge *= BS;
+ collisionMoveResult moveresult;
+ f32 pos_max_d = BS*0.125; // Distance per iteration
+ v3f p_pos = m_position;
+ v3f p_velocity = m_velocity;
+ moveresult = collisionMoveSimple(env,env->getGameDef(),
+ pos_max_d, box, m_prop.stepheight, dtime,
+ &p_pos, &p_velocity, m_acceleration,
+ this, m_prop.collideWithObjects);
+ // Apply results
+ m_position = p_pos;
+ m_velocity = p_velocity;
+
+ bool is_end_position = moveresult.collides;
+ pos_translator.update(m_position, is_end_position, dtime);
+ pos_translator.translate(dtime);
+ updateNodePos();
+ } else {
+ m_position += dtime * m_velocity + 0.5 * dtime * dtime * m_acceleration;
+ m_velocity += dtime * m_acceleration;
+ pos_translator.update(m_position, pos_translator.aim_is_end,
+ pos_translator.anim_time);
+ pos_translator.translate(dtime);
+ updateNodePos();
+ }
+
+ float moved = lastpos.getDistanceFrom(pos_translator.val_current);
+ m_step_distance_counter += moved;
+ if (m_step_distance_counter > 1.5f * BS) {
+ m_step_distance_counter = 0.0f;
+ if (!m_is_local_player && m_prop.makes_footstep_sound) {
+ const NodeDefManager *ndef = m_client->ndef();
+ v3s16 p = floatToInt(getPosition() +
+ v3f(0.0f, (m_prop.collisionbox.MinEdge.Y - 0.5f) * BS, 0.0f), BS);
+ MapNode n = m_env->getMap().getNodeNoEx(p);
+ SimpleSoundSpec spec = ndef->get(n).sound_footstep;
+ // Reduce footstep gain, as non-local-player footsteps are
+ // somehow louder.
+ spec.gain *= 0.6f;
+ m_client->sound()->playSoundAt(spec, false, getPosition());
+ }
+ }
+ }
+
+ m_anim_timer += dtime;
+ if(m_anim_timer >= m_anim_framelength)
+ {
+ m_anim_timer -= m_anim_framelength;
+ m_anim_frame++;
+ if(m_anim_frame >= m_anim_num_frames)
+ m_anim_frame = 0;
+ }
+
+ updateTexturePos();
+
+ if(m_reset_textures_timer >= 0)
+ {
+ m_reset_textures_timer -= dtime;
+ if(m_reset_textures_timer <= 0) {
+ m_reset_textures_timer = -1;
+ updateTextures(m_previous_texture_modifier);
+ }
+ }
+ if (!getParent() && std::fabs(m_prop.automatic_rotate) > 0.001) {
+ m_rotation.Y += dtime * m_prop.automatic_rotate * 180 / M_PI;
+ rot_translator.val_current = m_rotation;
+ updateNodePos();
+ }
+
+ if (!getParent() && m_prop.automatic_face_movement_dir &&
+ (fabs(m_velocity.Z) > 0.001 || fabs(m_velocity.X) > 0.001)) {
+
+ float target_yaw = atan2(m_velocity.Z, m_velocity.X) * 180 / M_PI
+ + m_prop.automatic_face_movement_dir_offset;
+ float max_rotation_delta =
+ dtime * m_prop.automatic_face_movement_max_rotation_per_sec;
+
+ wrappedApproachShortest(m_rotation.Y, target_yaw, max_rotation_delta, 360.f);
+ rot_translator.val_current = m_rotation;
+
+ updateNodePos();
+ }
+}
+
+void GenericCAO::updateTexturePos()
+{
+ if(m_spritenode)
+ {
+ scene::ICameraSceneNode* camera =
+ m_spritenode->getSceneManager()->getActiveCamera();
+ if(!camera)
+ return;
+ v3f cam_to_entity = m_spritenode->getAbsolutePosition()
+ - camera->getAbsolutePosition();
+ cam_to_entity.normalize();
+
+ int row = m_tx_basepos.Y;
+ int col = m_tx_basepos.X;
+
+ if (m_tx_select_horiz_by_yawpitch) {
+ if (cam_to_entity.Y > 0.75)
+ col += 5;
+ else if (cam_to_entity.Y < -0.75)
+ col += 4;
+ else {
+ float mob_dir =
+ atan2(cam_to_entity.Z, cam_to_entity.X) / M_PI * 180.;
+ float dir = mob_dir - m_rotation.Y;
+ dir = wrapDegrees_180(dir);
+ if (std::fabs(wrapDegrees_180(dir - 0)) <= 45.1f)
+ col += 2;
+ else if(std::fabs(wrapDegrees_180(dir - 90)) <= 45.1f)
+ col += 3;
+ else if(std::fabs(wrapDegrees_180(dir - 180)) <= 45.1f)
+ col += 0;
+ else if(std::fabs(wrapDegrees_180(dir + 90)) <= 45.1f)
+ col += 1;
+ else
+ col += 4;
+ }
+ }
+
+ // Animation goes downwards
+ row += m_anim_frame;
+
+ float txs = m_tx_size.X;
+ float tys = m_tx_size.Y;
+ setBillboardTextureMatrix(m_spritenode, txs, tys, col, row);
+ }
+}
+
+void GenericCAO::updateTextures(std::string mod)
+{
+ ITextureSource *tsrc = m_client->tsrc();
+
+ bool use_trilinear_filter = g_settings->getBool("trilinear_filter");
+ bool use_bilinear_filter = g_settings->getBool("bilinear_filter");
+ bool use_anisotropic_filter = g_settings->getBool("anisotropic_filter");
+
+ m_previous_texture_modifier = m_current_texture_modifier;
+ m_current_texture_modifier = mod;
+ m_glow = m_prop.glow;
+
+ video::E_MATERIAL_TYPE material_type = (m_prop.use_texture_alpha) ?
+ video::EMT_TRANSPARENT_ALPHA_CHANNEL : video::EMT_TRANSPARENT_ALPHA_CHANNEL_REF;
+
+ if (m_spritenode) {
+ if (m_prop.visual == "sprite") {
+ std::string texturestring = "unknown_node.png";
+ if (!m_prop.textures.empty())
+ texturestring = m_prop.textures[0];
+ texturestring += mod;
+ m_spritenode->getMaterial(0).MaterialType = material_type;
+ m_spritenode->setMaterialTexture(0,
+ tsrc->getTextureForMesh(texturestring));
+
+ // This allows setting per-material colors. However, until a real lighting
+ // system is added, the code below will have no effect. Once MineTest
+ // has directional lighting, it should work automatically.
+ if (!m_prop.colors.empty()) {
+ m_spritenode->getMaterial(0).AmbientColor = m_prop.colors[0];
+ m_spritenode->getMaterial(0).DiffuseColor = m_prop.colors[0];
+ m_spritenode->getMaterial(0).SpecularColor = m_prop.colors[0];
+ }
+
+ m_spritenode->getMaterial(0).setFlag(video::EMF_TRILINEAR_FILTER, use_trilinear_filter);
+ m_spritenode->getMaterial(0).setFlag(video::EMF_BILINEAR_FILTER, use_bilinear_filter);
+ m_spritenode->getMaterial(0).setFlag(video::EMF_ANISOTROPIC_FILTER, use_anisotropic_filter);
+ }
+ }
+
+ if (m_animated_meshnode) {
+ if (m_prop.visual == "mesh") {
+ for (u32 i = 0; i < m_prop.textures.size() &&
+ i < m_animated_meshnode->getMaterialCount(); ++i) {
+ std::string texturestring = m_prop.textures[i];
+ if (texturestring.empty())
+ continue; // Empty texture string means don't modify that material
+ texturestring += mod;
+ video::ITexture* texture = tsrc->getTextureForMesh(texturestring);
+ if (!texture) {
+ errorstream<<"GenericCAO::updateTextures(): Could not load texture "<<texturestring<<std::endl;
+ continue;
+ }
+
+ // Set material flags and texture
+ video::SMaterial& material = m_animated_meshnode->getMaterial(i);
+ material.MaterialType = material_type;
+ material.TextureLayer[0].Texture = texture;
+ material.setFlag(video::EMF_LIGHTING, true);
+ material.setFlag(video::EMF_BILINEAR_FILTER, false);
+ material.setFlag(video::EMF_BACK_FACE_CULLING, m_prop.backface_culling);
+
+ // don't filter low-res textures, makes them look blurry
+ // player models have a res of 64
+ const core::dimension2d<u32> &size = texture->getOriginalSize();
+ const u32 res = std::min(size.Height, size.Width);
+ use_trilinear_filter &= res > 64;
+ use_bilinear_filter &= res > 64;
+
+ m_animated_meshnode->getMaterial(i)
+ .setFlag(video::EMF_TRILINEAR_FILTER, use_trilinear_filter);
+ m_animated_meshnode->getMaterial(i)
+ .setFlag(video::EMF_BILINEAR_FILTER, use_bilinear_filter);
+ m_animated_meshnode->getMaterial(i)
+ .setFlag(video::EMF_ANISOTROPIC_FILTER, use_anisotropic_filter);
+ }
+ for (u32 i = 0; i < m_prop.colors.size() &&
+ i < m_animated_meshnode->getMaterialCount(); ++i)
+ {
+ // This allows setting per-material colors. However, until a real lighting
+ // system is added, the code below will have no effect. Once MineTest
+ // has directional lighting, it should work automatically.
+ m_animated_meshnode->getMaterial(i).AmbientColor = m_prop.colors[i];
+ m_animated_meshnode->getMaterial(i).DiffuseColor = m_prop.colors[i];
+ m_animated_meshnode->getMaterial(i).SpecularColor = m_prop.colors[i];
+ }
+ }
+ }
+ if(m_meshnode)
+ {
+ if(m_prop.visual == "cube")
+ {
+ for (u32 i = 0; i < 6; ++i)
+ {
+ std::string texturestring = "unknown_node.png";
+ if(m_prop.textures.size() > i)
+ texturestring = m_prop.textures[i];
+ texturestring += mod;
+
+
+ // Set material flags and texture
+ video::SMaterial& material = m_meshnode->getMaterial(i);
+ material.MaterialType = material_type;
+ material.setFlag(video::EMF_LIGHTING, false);
+ material.setFlag(video::EMF_BILINEAR_FILTER, false);
+ material.setTexture(0,
+ tsrc->getTextureForMesh(texturestring));
+ material.getTextureMatrix(0).makeIdentity();
+
+ // This allows setting per-material colors. However, until a real lighting
+ // system is added, the code below will have no effect. Once MineTest
+ // has directional lighting, it should work automatically.
+ if(m_prop.colors.size() > i)
+ {
+ m_meshnode->getMaterial(i).AmbientColor = m_prop.colors[i];
+ m_meshnode->getMaterial(i).DiffuseColor = m_prop.colors[i];
+ m_meshnode->getMaterial(i).SpecularColor = m_prop.colors[i];
+ }
+
+ m_meshnode->getMaterial(i).setFlag(video::EMF_TRILINEAR_FILTER, use_trilinear_filter);
+ m_meshnode->getMaterial(i).setFlag(video::EMF_BILINEAR_FILTER, use_bilinear_filter);
+ m_meshnode->getMaterial(i).setFlag(video::EMF_ANISOTROPIC_FILTER, use_anisotropic_filter);
+ }
+ } else if (m_prop.visual == "upright_sprite") {
+ scene::IMesh *mesh = m_meshnode->getMesh();
+ {
+ std::string tname = "unknown_object.png";
+ if (!m_prop.textures.empty())
+ tname = m_prop.textures[0];
+ tname += mod;
+ scene::IMeshBuffer *buf = mesh->getMeshBuffer(0);
+ buf->getMaterial().setTexture(0,
+ tsrc->getTextureForMesh(tname));
+
+ // This allows setting per-material colors. However, until a real lighting
+ // system is added, the code below will have no effect. Once MineTest
+ // has directional lighting, it should work automatically.
+ if(!m_prop.colors.empty()) {
+ buf->getMaterial().AmbientColor = m_prop.colors[0];
+ buf->getMaterial().DiffuseColor = m_prop.colors[0];
+ buf->getMaterial().SpecularColor = m_prop.colors[0];
+ }
+
+ buf->getMaterial().setFlag(video::EMF_TRILINEAR_FILTER, use_trilinear_filter);
+ buf->getMaterial().setFlag(video::EMF_BILINEAR_FILTER, use_bilinear_filter);
+ buf->getMaterial().setFlag(video::EMF_ANISOTROPIC_FILTER, use_anisotropic_filter);
+ }
+ {
+ std::string tname = "unknown_object.png";
+ if (m_prop.textures.size() >= 2)
+ tname = m_prop.textures[1];
+ else if (!m_prop.textures.empty())
+ tname = m_prop.textures[0];
+ tname += mod;
+ scene::IMeshBuffer *buf = mesh->getMeshBuffer(1);
+ buf->getMaterial().setTexture(0,
+ tsrc->getTextureForMesh(tname));
+
+ // This allows setting per-material colors. However, until a real lighting
+ // system is added, the code below will have no effect. Once MineTest
+ // has directional lighting, it should work automatically.
+ if (m_prop.colors.size() >= 2) {
+ buf->getMaterial().AmbientColor = m_prop.colors[1];
+ buf->getMaterial().DiffuseColor = m_prop.colors[1];
+ buf->getMaterial().SpecularColor = m_prop.colors[1];
+ setMeshColor(mesh, m_prop.colors[1]);
+ } else if (!m_prop.colors.empty()) {
+ buf->getMaterial().AmbientColor = m_prop.colors[0];
+ buf->getMaterial().DiffuseColor = m_prop.colors[0];
+ buf->getMaterial().SpecularColor = m_prop.colors[0];
+ setMeshColor(mesh, m_prop.colors[0]);
+ }
+
+ buf->getMaterial().setFlag(video::EMF_TRILINEAR_FILTER, use_trilinear_filter);
+ buf->getMaterial().setFlag(video::EMF_BILINEAR_FILTER, use_bilinear_filter);
+ buf->getMaterial().setFlag(video::EMF_ANISOTROPIC_FILTER, use_anisotropic_filter);
+ }
+ }
+ }
+}
+
+void GenericCAO::updateAnimation()
+{
+ if (!m_animated_meshnode)
+ return;
+
+ if (m_animated_meshnode->getStartFrame() != m_animation_range.X ||
+ m_animated_meshnode->getEndFrame() != m_animation_range.Y)
+ m_animated_meshnode->setFrameLoop(m_animation_range.X, m_animation_range.Y);
+ if (m_animated_meshnode->getAnimationSpeed() != m_animation_speed)
+ m_animated_meshnode->setAnimationSpeed(m_animation_speed);
+ m_animated_meshnode->setTransitionTime(m_animation_blend);
+// Requires Irrlicht 1.8 or greater
+#if (IRRLICHT_VERSION_MAJOR == 1 && IRRLICHT_VERSION_MINOR >= 8) || IRRLICHT_VERSION_MAJOR > 1
+ if (m_animated_meshnode->getLoopMode() != m_animation_loop)
+ m_animated_meshnode->setLoopMode(m_animation_loop);
+#endif
+}
+
+void GenericCAO::updateAnimationSpeed()
+{
+ if (!m_animated_meshnode)
+ return;
+
+ m_animated_meshnode->setAnimationSpeed(m_animation_speed);
+}
+
+void GenericCAO::updateBonePosition()
+{
+ if (m_bone_position.empty() || !m_animated_meshnode)
+ return;
+
+ m_animated_meshnode->setJointMode(irr::scene::EJUOR_CONTROL); // To write positions to the mesh on render
+ for(std::unordered_map<std::string, core::vector2d<v3f>>::const_iterator
+ ii = m_bone_position.begin(); ii != m_bone_position.end(); ++ii) {
+ std::string bone_name = (*ii).first;
+ v3f bone_pos = (*ii).second.X;
+ v3f bone_rot = (*ii).second.Y;
+ irr::scene::IBoneSceneNode* bone = m_animated_meshnode->getJointNode(bone_name.c_str());
+ if(bone)
+ {
+ bone->setPosition(bone_pos);
+ bone->setRotation(bone_rot);
+ }
+ }
+}
+
+void GenericCAO::updateAttachments()
+{
+
+ if (!getParent()) { // Detach or don't attach
+ scene::ISceneNode *node = getSceneNode();
+ if (node) {
+ v3f old_position = node->getAbsolutePosition();
+ v3f old_rotation = node->getRotation();
+ node->setParent(m_smgr->getRootSceneNode());
+ node->setPosition(old_position);
+ node->setRotation(old_rotation);
+ node->updateAbsolutePosition();
+ }
+ if (m_is_local_player) {
+ LocalPlayer *player = m_env->getLocalPlayer();
+ player->isAttached = false;
+ }
+ }
+ else // Attach
+ {
+ scene::ISceneNode *my_node = getSceneNode();
+
+ scene::ISceneNode *parent_node = getParent()->getSceneNode();
+ scene::IAnimatedMeshSceneNode *parent_animated_mesh_node =
+ getParent()->getAnimatedMeshSceneNode();
+ if (parent_animated_mesh_node && !m_attachment_bone.empty()) {
+ parent_node = parent_animated_mesh_node->getJointNode(m_attachment_bone.c_str());
+ }
+
+ if (my_node && parent_node) {
+ my_node->setParent(parent_node);
+ my_node->setPosition(m_attachment_position);
+ my_node->setRotation(m_attachment_rotation);
+ my_node->updateAbsolutePosition();
+ }
+ if (m_is_local_player) {
+ LocalPlayer *player = m_env->getLocalPlayer();
+ player->isAttached = true;
+ }
+ }
+}
+
+void GenericCAO::processMessage(const std::string &data)
+{
+ //infostream<<"GenericCAO: Got message"<<std::endl;
+ std::istringstream is(data, std::ios::binary);
+ // command
+ u8 cmd = readU8(is);
+ if (cmd == GENERIC_CMD_SET_PROPERTIES) {
+ m_prop = gob_read_set_properties(is);
+
+ m_selection_box = m_prop.selectionbox;
+ m_selection_box.MinEdge *= BS;
+ m_selection_box.MaxEdge *= BS;
+
+ m_tx_size.X = 1.0 / m_prop.spritediv.X;
+ m_tx_size.Y = 1.0 / m_prop.spritediv.Y;
+
+ if(!m_initial_tx_basepos_set){
+ m_initial_tx_basepos_set = true;
+ m_tx_basepos = m_prop.initial_sprite_basepos;
+ }
+ if (m_is_local_player) {
+ LocalPlayer *player = m_env->getLocalPlayer();
+ player->makes_footstep_sound = m_prop.makes_footstep_sound;
+ aabb3f collision_box = m_prop.collisionbox;
+ collision_box.MinEdge *= BS;
+ collision_box.MaxEdge *= BS;
+ player->setCollisionbox(collision_box);
+ player->setEyeHeight(m_prop.eye_height);
+ player->setZoomFOV(m_prop.zoom_fov);
+ }
+
+ if ((m_is_player && !m_is_local_player) && m_prop.nametag.empty())
+ m_prop.nametag = m_name;
+
+ expireVisuals();
+ } else if (cmd == GENERIC_CMD_UPDATE_POSITION) {
+ // Not sent by the server if this object is an attachment.
+ // We might however get here if the server notices the object being detached before the client.
+ m_position = readV3F1000(is);
+ m_velocity = readV3F1000(is);
+ m_acceleration = readV3F1000(is);
+
+ if (std::fabs(m_prop.automatic_rotate) < 0.001f)
+ m_rotation = readV3F1000(is);
+ else
+ readV3F1000(is);
+
+ m_rotation = wrapDegrees_0_360_v3f(m_rotation);
+ bool do_interpolate = readU8(is);
+ bool is_end_position = readU8(is);
+ float update_interval = readF1000(is);
+
+ // Place us a bit higher if we're physical, to not sink into
+ // the ground due to sucky collision detection...
+ if(m_prop.physical)
+ m_position += v3f(0,0.002,0);
+
+ if(getParent() != NULL) // Just in case
+ return;
+
+ if(do_interpolate)
+ {
+ if(!m_prop.physical)
+ pos_translator.update(m_position, is_end_position, update_interval);
+ } else {
+ pos_translator.init(m_position);
+ }
+ rot_translator.update(m_rotation, false, update_interval);
+ updateNodePos();
+ } else if (cmd == GENERIC_CMD_SET_TEXTURE_MOD) {
+ std::string mod = deSerializeString(is);
+
+ // immediatly reset a engine issued texture modifier if a mod sends a different one
+ if (m_reset_textures_timer > 0) {
+ m_reset_textures_timer = -1;
+ updateTextures(m_previous_texture_modifier);
+ }
+ updateTextures(mod);
+ } else if (cmd == GENERIC_CMD_SET_SPRITE) {
+ v2s16 p = readV2S16(is);
+ int num_frames = readU16(is);
+ float framelength = readF1000(is);
+ bool select_horiz_by_yawpitch = readU8(is);
+
+ m_tx_basepos = p;
+ m_anim_num_frames = num_frames;
+ m_anim_framelength = framelength;
+ m_tx_select_horiz_by_yawpitch = select_horiz_by_yawpitch;
+
+ updateTexturePos();
+ } else if (cmd == GENERIC_CMD_SET_PHYSICS_OVERRIDE) {
+ float override_speed = readF1000(is);
+ float override_jump = readF1000(is);
+ float override_gravity = readF1000(is);
+ // these are sent inverted so we get true when the server sends nothing
+ bool sneak = !readU8(is);
+ bool sneak_glitch = !readU8(is);
+ bool new_move = !readU8(is);
+
+
+ if(m_is_local_player)
+ {
+ LocalPlayer *player = m_env->getLocalPlayer();
+ player->physics_override_speed = override_speed;
+ player->physics_override_jump = override_jump;
+ player->physics_override_gravity = override_gravity;
+ player->physics_override_sneak = sneak;
+ player->physics_override_sneak_glitch = sneak_glitch;
+ player->physics_override_new_move = new_move;
+ }
+ } else if (cmd == GENERIC_CMD_SET_ANIMATION) {
+ // TODO: change frames send as v2s32 value
+ v2f range = readV2F1000(is);
+ if (!m_is_local_player) {
+ m_animation_range = v2s32((s32)range.X, (s32)range.Y);
+ m_animation_speed = readF1000(is);
+ m_animation_blend = readF1000(is);
+ // these are sent inverted so we get true when the server sends nothing
+ m_animation_loop = !readU8(is);
+ updateAnimation();
+ } else {
+ LocalPlayer *player = m_env->getLocalPlayer();
+ if(player->last_animation == NO_ANIM)
+ {
+ m_animation_range = v2s32((s32)range.X, (s32)range.Y);
+ m_animation_speed = readF1000(is);
+ m_animation_blend = readF1000(is);
+ // these are sent inverted so we get true when the server sends nothing
+ m_animation_loop = !readU8(is);
+ }
+ // update animation only if local animations present
+ // and received animation is unknown (except idle animation)
+ bool is_known = false;
+ for (int i = 1;i<4;i++)
+ {
+ if(m_animation_range.Y == player->local_animations[i].Y)
+ is_known = true;
+ }
+ if(!is_known ||
+ (player->local_animations[1].Y + player->local_animations[2].Y < 1))
+ {
+ updateAnimation();
+ }
+ }
+ } else if (cmd == GENERIC_CMD_SET_ANIMATION_SPEED) {
+ m_animation_speed = readF1000(is);
+ updateAnimationSpeed();
+ } else if (cmd == GENERIC_CMD_SET_BONE_POSITION) {
+ std::string bone = deSerializeString(is);
+ v3f position = readV3F1000(is);
+ v3f rotation = readV3F1000(is);
+ m_bone_position[bone] = core::vector2d<v3f>(position, rotation);
+
+ updateBonePosition();
+ } else if (cmd == GENERIC_CMD_ATTACH_TO) {
+ u16 parent_id = readS16(is);
+ u16 &old_parent_id = m_env->attachement_parent_ids[getId()];
+ if (parent_id != old_parent_id) {
+ if (GenericCAO *old_parent = m_env->getGenericCAO(old_parent_id)) {
+ old_parent->m_children.erase(std::remove(
+ m_children.begin(), m_children.end(),
+ getId()), m_children.end());
+ }
+ if (GenericCAO *new_parent = m_env->getGenericCAO(parent_id))
+ new_parent->m_children.push_back(getId());
+
+ old_parent_id = parent_id;
+ }
+
+ m_attachment_bone = deSerializeString(is);
+ m_attachment_position = readV3F1000(is);
+ m_attachment_rotation = readV3F1000(is);
+
+ // localplayer itself can't be attached to localplayer
+ if (!m_is_local_player) {
+ m_attached_to_local = getParent() != NULL && getParent()->isLocalPlayer();
+ // Objects attached to the local player should be hidden by default
+ m_is_visible = !m_attached_to_local;
+ }
+
+ updateAttachments();
+ } else if (cmd == GENERIC_CMD_PUNCHED) {
+ /*s16 damage =*/ readS16(is);
+ s16 result_hp = readS16(is);
+
+ // Use this instead of the send damage to not interfere with prediction
+ s16 damage = m_hp - result_hp;
+
+ m_hp = result_hp;
+
+ if (damage > 0)
+ {
+ if (m_hp <= 0)
+ {
+ // TODO: Execute defined fast response
+ // As there is no definition, make a smoke puff
+ ClientSimpleObject *simple = createSmokePuff(
+ m_smgr, m_env, m_position,
+ m_prop.visual_size * BS);
+ m_env->addSimpleObject(simple);
+ } else if (m_reset_textures_timer < 0) {
+ // TODO: Execute defined fast response
+ // Flashing shall suffice as there is no definition
+ m_reset_textures_timer = 0.05;
+ if(damage >= 2)
+ m_reset_textures_timer += 0.05 * damage;
+ updateTextures(m_current_texture_modifier + "^[brighten");
+ }
+ }
+ } else if (cmd == GENERIC_CMD_UPDATE_ARMOR_GROUPS) {
+ m_armor_groups.clear();
+ int armor_groups_size = readU16(is);
+ for(int i=0; i<armor_groups_size; i++)
+ {
+ std::string name = deSerializeString(is);
+ int rating = readS16(is);
+ m_armor_groups[name] = rating;
+ }
+ } else if (cmd == GENERIC_CMD_UPDATE_NAMETAG_ATTRIBUTES) {
+ // Deprecated, for backwards compatibility only.
+ readU8(is); // version
+ m_prop.nametag_color = readARGB8(is);
+ if (m_nametag != NULL) {
+ m_nametag->nametag_color = m_prop.nametag_color;
+ v3f pos;
+ pos.Y = m_prop.collisionbox.MaxEdge.Y + 0.3f;
+ m_nametag->nametag_pos = pos;
+ }
+ } else if (cmd == GENERIC_CMD_SPAWN_INFANT) {
+ u16 child_id = readU16(is);
+ u8 type = readU8(is);
+
+ if (GenericCAO *childobj = m_env->getGenericCAO(child_id)) {
+ childobj->processInitData(deSerializeLongString(is));
+ } else {
+ m_env->addActiveObject(child_id, type, deSerializeLongString(is));
+ }
+ } else {
+ warningstream << FUNCTION_NAME
+ << ": unknown command or outdated client \""
+ << +cmd << "\"" << std::endl;
+ }
+}
+
+/* \pre punchitem != NULL
+ */
+bool GenericCAO::directReportPunch(v3f dir, const ItemStack *punchitem,
+ float time_from_last_punch)
+{
+ assert(punchitem); // pre-condition
+ const ToolCapabilities *toolcap =
+ &punchitem->getToolCapabilities(m_client->idef());
+ PunchDamageResult result = getPunchDamage(
+ m_armor_groups,
+ toolcap,
+ punchitem,
+ time_from_last_punch);
+
+ if(result.did_punch && result.damage != 0)
+ {
+ if(result.damage < m_hp)
+ {
+ m_hp -= result.damage;
+ } else {
+ m_hp = 0;
+ // TODO: Execute defined fast response
+ // As there is no definition, make a smoke puff
+ ClientSimpleObject *simple = createSmokePuff(
+ m_smgr, m_env, m_position,
+ m_prop.visual_size * BS);
+ m_env->addSimpleObject(simple);
+ }
+ // TODO: Execute defined fast response
+ // Flashing shall suffice as there is no definition
+ if (m_reset_textures_timer < 0) {
+ m_reset_textures_timer = 0.05;
+ if (result.damage >= 2)
+ m_reset_textures_timer += 0.05 * result.damage;
+ updateTextures(m_current_texture_modifier + "^[brighten");
+ }
+ }
+
+ return false;
+}
+
+std::string GenericCAO::debugInfoText()
+{
+ std::ostringstream os(std::ios::binary);
+ os<<"GenericCAO hp="<<m_hp<<"\n";
+ os<<"armor={";
+ for(ItemGroupList::const_iterator i = m_armor_groups.begin();
+ i != m_armor_groups.end(); ++i)
+ {
+ os<<i->first<<"="<<i->second<<", ";
+ }
+ os<<"}";
+ return os.str();
+}
+
+// Prototype
+GenericCAO proto_GenericCAO(NULL, NULL);
diff --git a/src/client/content_cao.h b/src/client/content_cao.h
new file mode 100644
index 000000000..98932137e
--- /dev/null
+++ b/src/client/content_cao.h
@@ -0,0 +1,236 @@
+/*
+Minetest
+Copyright (C) 2010-2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+
+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 <map>
+#include "irrlichttypes_extrabloated.h"
+#include "clientobject.h"
+#include "object_properties.h"
+#include "itemgroup.h"
+#include "constants.h"
+
+class Camera;
+class Client;
+struct Nametag;
+
+/*
+ SmoothTranslator
+*/
+
+template<typename T>
+struct SmoothTranslator
+{
+ T val_old;
+ T val_current;
+ T val_target;
+ f32 anim_time = 0;
+ f32 anim_time_counter = 0;
+ bool aim_is_end = true;
+
+ SmoothTranslator() = default;
+
+ void init(T current);
+
+ void update(T new_target, bool is_end_position = false,
+ float update_interval = -1);
+
+ void translate(f32 dtime);
+};
+
+struct SmoothTranslatorWrapped : SmoothTranslator<f32>
+{
+ void translate(f32 dtime);
+};
+
+struct SmoothTranslatorWrappedv3f : SmoothTranslator<v3f>
+{
+ void translate(f32 dtime);
+};
+
+class GenericCAO : public ClientActiveObject
+{
+private:
+ // Only set at initialization
+ std::string m_name = "";
+ bool m_is_player = false;
+ bool m_is_local_player = false;
+ // Property-ish things
+ ObjectProperties m_prop;
+ //
+ scene::ISceneManager *m_smgr = nullptr;
+ Client *m_client = nullptr;
+ aabb3f m_selection_box = aabb3f(-BS/3.,-BS/3.,-BS/3., BS/3.,BS/3.,BS/3.);
+ scene::IMeshSceneNode *m_meshnode = nullptr;
+ scene::IAnimatedMeshSceneNode *m_animated_meshnode = nullptr;
+ WieldMeshSceneNode *m_wield_meshnode = nullptr;
+ scene::IBillboardSceneNode *m_spritenode = nullptr;
+ Nametag *m_nametag = nullptr;
+ v3f m_position = v3f(0.0f, 10.0f * BS, 0);
+ v3f m_velocity;
+ v3f m_acceleration;
+ v3f m_rotation;
+ s16 m_hp = 1;
+ SmoothTranslator<v3f> pos_translator;
+ SmoothTranslatorWrappedv3f rot_translator;
+ // Spritesheet/animation stuff
+ v2f m_tx_size = v2f(1,1);
+ v2s16 m_tx_basepos;
+ bool m_initial_tx_basepos_set = false;
+ bool m_tx_select_horiz_by_yawpitch = false;
+ v2s32 m_animation_range;
+ float m_animation_speed = 15.0f;
+ float m_animation_blend = 0.0f;
+ bool m_animation_loop = true;
+ // stores position and rotation for each bone name
+ std::unordered_map<std::string, core::vector2d<v3f>> m_bone_position;
+ std::string m_attachment_bone = "";
+ v3f m_attachment_position;
+ v3f m_attachment_rotation;
+ bool m_attached_to_local = false;
+ int m_anim_frame = 0;
+ int m_anim_num_frames = 1;
+ float m_anim_framelength = 0.2f;
+ float m_anim_timer = 0.0f;
+ ItemGroupList m_armor_groups;
+ float m_reset_textures_timer = -1.0f;
+ // stores texture modifier before punch update
+ std::string m_previous_texture_modifier = "";
+ // last applied texture modifier
+ std::string m_current_texture_modifier = "";
+ bool m_visuals_expired = false;
+ float m_step_distance_counter = 0.0f;
+ u8 m_last_light = 255;
+ bool m_is_visible = false;
+ s8 m_glow = 0;
+
+ std::vector<u16> m_children;
+
+public:
+ GenericCAO(Client *client, ClientEnvironment *env);
+
+ ~GenericCAO();
+
+ static ClientActiveObject* create(Client *client, ClientEnvironment *env)
+ {
+ return new GenericCAO(client, env);
+ }
+
+ inline ActiveObjectType getType() const
+ {
+ return ACTIVEOBJECT_TYPE_GENERIC;
+ }
+ inline const ItemGroupList &getGroups() const
+ {
+ return m_armor_groups;
+ }
+ void initialize(const std::string &data);
+
+ void processInitData(const std::string &data);
+
+ bool getCollisionBox(aabb3f *toset) const;
+
+ bool collideWithObjects() const;
+
+ virtual bool getSelectionBox(aabb3f *toset) const;
+
+ v3f getPosition();
+
+ inline const v3f &getRotation()
+ {
+ return m_rotation;
+ }
+
+ const bool isImmortal();
+
+ scene::ISceneNode *getSceneNode();
+
+ scene::IAnimatedMeshSceneNode *getAnimatedMeshSceneNode();
+
+ inline f32 getStepHeight() const
+ {
+ return m_prop.stepheight;
+ }
+
+ inline bool isLocalPlayer() const
+ {
+ return m_is_local_player;
+ }
+
+ inline bool isVisible() const
+ {
+ return m_is_visible;
+ }
+
+ inline void setVisible(bool toset)
+ {
+ m_is_visible = toset;
+ }
+
+ void setChildrenVisible(bool toset);
+
+ ClientActiveObject *getParent() const;
+
+ void setAttachments();
+
+ void removeFromScene(bool permanent);
+
+ void addToScene(ITextureSource *tsrc);
+
+ inline void expireVisuals()
+ {
+ m_visuals_expired = true;
+ }
+
+ void updateLight(u8 light_at_pos);
+
+ void updateLightNoCheck(u8 light_at_pos);
+
+ v3s16 getLightPosition();
+
+ void updateNodePos();
+
+ void step(float dtime, ClientEnvironment *env);
+
+ void updateTexturePos();
+
+ // std::string copy is mandatory as mod can be a class member and there is a swap
+ // on those class members... do NOT pass by reference
+ void updateTextures(std::string mod);
+
+ void updateAnimation();
+
+ void updateAnimationSpeed();
+
+ void updateBonePosition();
+
+ void updateAttachments();
+
+ void processMessage(const std::string &data);
+
+ bool directReportPunch(v3f dir, const ItemStack *punchitem=NULL,
+ float time_from_last_punch=1000000);
+
+ std::string debugInfoText();
+
+ std::string infoText()
+ {
+ return m_prop.infotext;
+ }
+};
diff --git a/src/client/content_cso.cpp b/src/client/content_cso.cpp
new file mode 100644
index 000000000..04c503f44
--- /dev/null
+++ b/src/client/content_cso.cpp
@@ -0,0 +1,77 @@
+/*
+Minetest
+Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+
+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 "content_cso.h"
+#include <IBillboardSceneNode.h>
+#include "client/tile.h"
+#include "clientenvironment.h"
+#include "client.h"
+#include "map.h"
+
+class SmokePuffCSO: public ClientSimpleObject
+{
+ float m_age = 0.0f;
+ scene::IBillboardSceneNode *m_spritenode = nullptr;
+public:
+ SmokePuffCSO(scene::ISceneManager *smgr,
+ ClientEnvironment *env, const v3f &pos, const v2f &size)
+ {
+ infostream<<"SmokePuffCSO: constructing"<<std::endl;
+ m_spritenode = smgr->addBillboardSceneNode(
+ NULL, v2f(1,1), pos, -1);
+ m_spritenode->setMaterialTexture(0,
+ env->getGameDef()->tsrc()->getTextureForMesh("smoke_puff.png"));
+ m_spritenode->setMaterialFlag(video::EMF_LIGHTING, false);
+ m_spritenode->setMaterialFlag(video::EMF_BILINEAR_FILTER, false);
+ //m_spritenode->setMaterialType(video::EMT_TRANSPARENT_ALPHA_CHANNEL_REF);
+ m_spritenode->setMaterialType(video::EMT_TRANSPARENT_ALPHA_CHANNEL);
+ m_spritenode->setMaterialFlag(video::EMF_FOG_ENABLE, true);
+ m_spritenode->setColor(video::SColor(255,0,0,0));
+ m_spritenode->setVisible(true);
+ m_spritenode->setSize(size);
+ /* Update brightness */
+ u8 light;
+ bool pos_ok;
+ MapNode n = env->getMap().getNodeNoEx(floatToInt(pos, BS), &pos_ok);
+ light = pos_ok ? decode_light(n.getLightBlend(env->getDayNightRatio(),
+ env->getGameDef()->ndef()))
+ : 64;
+ video::SColor color(255,light,light,light);
+ m_spritenode->setColor(color);
+ }
+ virtual ~SmokePuffCSO()
+ {
+ infostream<<"SmokePuffCSO: destructing"<<std::endl;
+ m_spritenode->remove();
+ }
+ void step(float dtime)
+ {
+ m_age += dtime;
+ if(m_age > 1.0){
+ m_to_be_removed = true;
+ }
+ }
+};
+
+ClientSimpleObject* createSmokePuff(scene::ISceneManager *smgr,
+ ClientEnvironment *env, v3f pos, v2f size)
+{
+ return new SmokePuffCSO(smgr, env, pos, size);
+}
+
diff --git a/src/client/content_cso.h b/src/client/content_cso.h
new file mode 100644
index 000000000..cc9213175
--- /dev/null
+++ b/src/client/content_cso.h
@@ -0,0 +1,26 @@
+/*
+Minetest
+Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+
+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 "clientsimpleobject.h"
+
+ClientSimpleObject* createSmokePuff(scene::ISceneManager *smgr,
+ ClientEnvironment *env, v3f pos, v2f size);
diff --git a/src/client/content_mapblock.cpp b/src/client/content_mapblock.cpp
new file mode 100644
index 000000000..4a0df6171
--- /dev/null
+++ b/src/client/content_mapblock.cpp
@@ -0,0 +1,1430 @@
+/*
+Minetest
+Copyright (C) 2010-2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+
+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 "content_mapblock.h"
+#include "util/numeric.h"
+#include "util/directiontables.h"
+#include "mapblock_mesh.h"
+#include "settings.h"
+#include "nodedef.h"
+#include "client/tile.h"
+#include "mesh.h"
+#include <IMeshManipulator.h>
+#include "client/meshgen/collector.h"
+#include "client/renderingengine.h"
+#include "client.h"
+#include "noise.h"
+
+// Distance of light extrapolation (for oversized nodes)
+// After this distance, it gives up and considers light level constant
+#define SMOOTH_LIGHTING_OVERSIZE 1.0
+
+// Node edge count (for glasslike-framed)
+#define FRAMED_EDGE_COUNT 12
+
+// Node neighbor count, including edge-connected, but not vertex-connected
+// (for glasslike-framed)
+// Corresponding offsets are listed in g_27dirs
+#define FRAMED_NEIGHBOR_COUNT 18
+
+static const v3s16 light_dirs[8] = {
+ v3s16(-1, -1, -1),
+ v3s16(-1, -1, 1),
+ v3s16(-1, 1, -1),
+ v3s16(-1, 1, 1),
+ v3s16( 1, -1, -1),
+ v3s16( 1, -1, 1),
+ v3s16( 1, 1, -1),
+ v3s16( 1, 1, 1),
+};
+
+// Standard index set to make a quad on 4 vertices
+static constexpr u16 quad_indices[] = {0, 1, 2, 2, 3, 0};
+
+const std::string MapblockMeshGenerator::raillike_groupname = "connect_to_raillike";
+
+MapblockMeshGenerator::MapblockMeshGenerator(MeshMakeData *input, MeshCollector *output)
+{
+ data = input;
+ collector = output;
+
+ nodedef = data->m_client->ndef();
+ meshmanip = RenderingEngine::get_scene_manager()->getMeshManipulator();
+
+ enable_mesh_cache = g_settings->getBool("enable_mesh_cache") &&
+ !data->m_smooth_lighting; // Mesh cache is not supported with smooth lighting
+
+ blockpos_nodes = data->m_blockpos * MAP_BLOCKSIZE;
+}
+
+void MapblockMeshGenerator::useTile(int index, u8 set_flags, u8 reset_flags, bool special)
+{
+ if (special)
+ getSpecialTile(index, &tile, p == data->m_crack_pos_relative);
+ else
+ getTile(index, &tile);
+ if (!data->m_smooth_lighting)
+ color = encode_light(light, f->light_source);
+
+ for (auto &layer : tile.layers) {
+ layer.material_flags |= set_flags;
+ layer.material_flags &= ~reset_flags;
+ }
+}
+
+// Returns a tile, ready for use, non-rotated.
+void MapblockMeshGenerator::getTile(int index, TileSpec *tile)
+{
+ getNodeTileN(n, p, index, data, *tile);
+}
+
+// Returns a tile, ready for use, rotated according to the node facedir.
+void MapblockMeshGenerator::getTile(v3s16 direction, TileSpec *tile)
+{
+ getNodeTile(n, p, direction, data, *tile);
+}
+
+// Returns a special tile, ready for use, non-rotated.
+void MapblockMeshGenerator::getSpecialTile(int index, TileSpec *tile, bool apply_crack)
+{
+ *tile = f->special_tiles[index];
+ TileLayer *top_layer = nullptr;
+
+ for (auto &layernum : tile->layers) {
+ TileLayer *layer = &layernum;
+ if (layer->texture_id == 0)
+ continue;
+ top_layer = layer;
+ if (!layer->has_color)
+ n.getColor(*f, &layer->color);
+ }
+
+ if (apply_crack)
+ top_layer->material_flags |= MATERIAL_FLAG_CRACK;
+}
+
+void MapblockMeshGenerator::drawQuad(v3f *coords, const v3s16 &normal,
+ float vertical_tiling)
+{
+ const v2f tcoords[4] = {v2f(0.0, 0.0), v2f(1.0, 0.0),
+ v2f(1.0, vertical_tiling), v2f(0.0, vertical_tiling)};
+ video::S3DVertex vertices[4];
+ bool shade_face = !f->light_source && (normal != v3s16(0, 0, 0));
+ v3f normal2(normal.X, normal.Y, normal.Z);
+ for (int j = 0; j < 4; j++) {
+ vertices[j].Pos = coords[j] + origin;
+ vertices[j].Normal = normal2;
+ if (data->m_smooth_lighting)
+ vertices[j].Color = blendLightColor(coords[j]);
+ else
+ vertices[j].Color = color;
+ if (shade_face)
+ applyFacesShading(vertices[j].Color, normal2);
+ vertices[j].TCoords = tcoords[j];
+ }
+ collector->append(tile, vertices, 4, quad_indices, 6);
+}
+
+// Create a cuboid.
+// tiles - the tiles (materials) to use (for all 6 faces)
+// tilecount - number of entries in tiles, 1<=tilecount<=6
+// lights - vertex light levels. The order is the same as in light_dirs.
+// NULL may be passed if smooth lighting is disabled.
+// txc - texture coordinates - this is a list of texture coordinates
+// for the opposite corners of each face - therefore, there
+// should be (2+2)*6=24 values in the list. The order of
+// the faces in the list is up-down-right-left-back-front
+// (compatible with ContentFeatures).
+void MapblockMeshGenerator::drawCuboid(const aabb3f &box,
+ TileSpec *tiles, int tilecount, const LightInfo *lights, const f32 *txc)
+{
+ assert(tilecount >= 1 && tilecount <= 6); // pre-condition
+
+ v3f min = box.MinEdge;
+ v3f max = box.MaxEdge;
+
+ video::SColor colors[6];
+ if (!data->m_smooth_lighting) {
+ for (int face = 0; face != 6; ++face) {
+ colors[face] = encode_light(light, f->light_source);
+ }
+ if (!f->light_source) {
+ applyFacesShading(colors[0], v3f(0, 1, 0));
+ applyFacesShading(colors[1], v3f(0, -1, 0));
+ applyFacesShading(colors[2], v3f(1, 0, 0));
+ applyFacesShading(colors[3], v3f(-1, 0, 0));
+ applyFacesShading(colors[4], v3f(0, 0, 1));
+ applyFacesShading(colors[5], v3f(0, 0, -1));
+ }
+ }
+
+ video::S3DVertex vertices[24] = {
+ // top
+ video::S3DVertex(min.X, max.Y, max.Z, 0, 1, 0, colors[0], txc[0], txc[1]),
+ video::S3DVertex(max.X, max.Y, max.Z, 0, 1, 0, colors[0], txc[2], txc[1]),
+ video::S3DVertex(max.X, max.Y, min.Z, 0, 1, 0, colors[0], txc[2], txc[3]),
+ video::S3DVertex(min.X, max.Y, min.Z, 0, 1, 0, colors[0], txc[0], txc[3]),
+ // bottom
+ video::S3DVertex(min.X, min.Y, min.Z, 0, -1, 0, colors[1], txc[4], txc[5]),
+ video::S3DVertex(max.X, min.Y, min.Z, 0, -1, 0, colors[1], txc[6], txc[5]),
+ video::S3DVertex(max.X, min.Y, max.Z, 0, -1, 0, colors[1], txc[6], txc[7]),
+ video::S3DVertex(min.X, min.Y, max.Z, 0, -1, 0, colors[1], txc[4], txc[7]),
+ // right
+ video::S3DVertex(max.X, max.Y, min.Z, 1, 0, 0, colors[2], txc[ 8], txc[9]),
+ video::S3DVertex(max.X, max.Y, max.Z, 1, 0, 0, colors[2], txc[10], txc[9]),
+ video::S3DVertex(max.X, min.Y, max.Z, 1, 0, 0, colors[2], txc[10], txc[11]),
+ video::S3DVertex(max.X, min.Y, min.Z, 1, 0, 0, colors[2], txc[ 8], txc[11]),
+ // left
+ video::S3DVertex(min.X, max.Y, max.Z, -1, 0, 0, colors[3], txc[12], txc[13]),
+ video::S3DVertex(min.X, max.Y, min.Z, -1, 0, 0, colors[3], txc[14], txc[13]),
+ video::S3DVertex(min.X, min.Y, min.Z, -1, 0, 0, colors[3], txc[14], txc[15]),
+ video::S3DVertex(min.X, min.Y, max.Z, -1, 0, 0, colors[3], txc[12], txc[15]),
+ // back
+ video::S3DVertex(max.X, max.Y, max.Z, 0, 0, 1, colors[4], txc[16], txc[17]),
+ video::S3DVertex(min.X, max.Y, max.Z, 0, 0, 1, colors[4], txc[18], txc[17]),
+ video::S3DVertex(min.X, min.Y, max.Z, 0, 0, 1, colors[4], txc[18], txc[19]),
+ video::S3DVertex(max.X, min.Y, max.Z, 0, 0, 1, colors[4], txc[16], txc[19]),
+ // front
+ video::S3DVertex(min.X, max.Y, min.Z, 0, 0, -1, colors[5], txc[20], txc[21]),
+ video::S3DVertex(max.X, max.Y, min.Z, 0, 0, -1, colors[5], txc[22], txc[21]),
+ video::S3DVertex(max.X, min.Y, min.Z, 0, 0, -1, colors[5], txc[22], txc[23]),
+ video::S3DVertex(min.X, min.Y, min.Z, 0, 0, -1, colors[5], txc[20], txc[23]),
+ };
+
+ static const u8 light_indices[24] = {
+ 3, 7, 6, 2,
+ 0, 4, 5, 1,
+ 6, 7, 5, 4,
+ 3, 2, 0, 1,
+ 7, 3, 1, 5,
+ 2, 6, 4, 0
+ };
+
+ for (int face = 0; face < 6; face++) {
+ int tileindex = MYMIN(face, tilecount - 1);
+ const TileSpec &tile = tiles[tileindex];
+ for (int j = 0; j < 4; j++) {
+ video::S3DVertex &vertex = vertices[face * 4 + j];
+ v2f &tcoords = vertex.TCoords;
+ switch (tile.rotation) {
+ case 0:
+ break;
+ case 1: // R90
+ tcoords.rotateBy(90, irr::core::vector2df(0, 0));
+ break;
+ case 2: // R180
+ tcoords.rotateBy(180, irr::core::vector2df(0, 0));
+ break;
+ case 3: // R270
+ tcoords.rotateBy(270, irr::core::vector2df(0, 0));
+ break;
+ case 4: // FXR90
+ tcoords.X = 1.0 - tcoords.X;
+ tcoords.rotateBy(90, irr::core::vector2df(0, 0));
+ break;
+ case 5: // FXR270
+ tcoords.X = 1.0 - tcoords.X;
+ tcoords.rotateBy(270, irr::core::vector2df(0, 0));
+ break;
+ case 6: // FYR90
+ tcoords.Y = 1.0 - tcoords.Y;
+ tcoords.rotateBy(90, irr::core::vector2df(0, 0));
+ break;
+ case 7: // FYR270
+ tcoords.Y = 1.0 - tcoords.Y;
+ tcoords.rotateBy(270, irr::core::vector2df(0, 0));
+ break;
+ case 8: // FX
+ tcoords.X = 1.0 - tcoords.X;
+ break;
+ case 9: // FY
+ tcoords.Y = 1.0 - tcoords.Y;
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ if (data->m_smooth_lighting) {
+ for (int j = 0; j < 24; ++j) {
+ video::S3DVertex &vertex = vertices[j];
+ vertex.Color = encode_light(
+ lights[light_indices[j]].getPair(MYMAX(0.0f, vertex.Normal.Y)),
+ f->light_source);
+ if (!f->light_source)
+ applyFacesShading(vertex.Color, vertex.Normal);
+ }
+ }
+
+ // Add to mesh collector
+ for (int k = 0; k < 6; ++k) {
+ int tileindex = MYMIN(k, tilecount - 1);
+ collector->append(tiles[tileindex], vertices + 4 * k, 4, quad_indices, 6);
+ }
+}
+
+// Gets the base lighting values for a node
+void MapblockMeshGenerator::getSmoothLightFrame()
+{
+ for (int k = 0; k < 8; ++k)
+ frame.sunlight[k] = false;
+ for (int k = 0; k < 8; ++k) {
+ LightPair light(getSmoothLightTransparent(blockpos_nodes + p, light_dirs[k], data));
+ frame.lightsDay[k] = light.lightDay;
+ frame.lightsNight[k] = light.lightNight;
+ // If there is direct sunlight and no ambient occlusion at some corner,
+ // mark the vertical edge (top and bottom corners) containing it.
+ if (light.lightDay == 255) {
+ frame.sunlight[k] = true;
+ frame.sunlight[k ^ 2] = true;
+ }
+ }
+}
+
+// Calculates vertex light level
+// vertex_pos - vertex position in the node (coordinates are clamped to [0.0, 1.0] or so)
+LightInfo MapblockMeshGenerator::blendLight(const v3f &vertex_pos)
+{
+ // Light levels at (logical) node corners are known. Here,
+ // trilinear interpolation is used to calculate light level
+ // at a given point in the node.
+ f32 x = core::clamp(vertex_pos.X / BS + 0.5, 0.0 - SMOOTH_LIGHTING_OVERSIZE, 1.0 + SMOOTH_LIGHTING_OVERSIZE);
+ f32 y = core::clamp(vertex_pos.Y / BS + 0.5, 0.0 - SMOOTH_LIGHTING_OVERSIZE, 1.0 + SMOOTH_LIGHTING_OVERSIZE);
+ f32 z = core::clamp(vertex_pos.Z / BS + 0.5, 0.0 - SMOOTH_LIGHTING_OVERSIZE, 1.0 + SMOOTH_LIGHTING_OVERSIZE);
+ f32 lightDay = 0.0; // daylight
+ f32 lightNight = 0.0;
+ f32 lightBoosted = 0.0; // daylight + direct sunlight, if any
+ for (int k = 0; k < 8; ++k) {
+ f32 dx = (k & 4) ? x : 1 - x;
+ f32 dy = (k & 2) ? y : 1 - y;
+ f32 dz = (k & 1) ? z : 1 - z;
+ // Use direct sunlight (255), if any; use daylight otherwise.
+ f32 light_boosted = frame.sunlight[k] ? 255 : frame.lightsDay[k];
+ lightDay += dx * dy * dz * frame.lightsDay[k];
+ lightNight += dx * dy * dz * frame.lightsNight[k];
+ lightBoosted += dx * dy * dz * light_boosted;
+ }
+ return LightInfo{lightDay, lightNight, lightBoosted};
+}
+
+// Calculates vertex color to be used in mapblock mesh
+// vertex_pos - vertex position in the node (coordinates are clamped to [0.0, 1.0] or so)
+// tile_color - node's tile color
+video::SColor MapblockMeshGenerator::blendLightColor(const v3f &vertex_pos)
+{
+ LightInfo light = blendLight(vertex_pos);
+ return encode_light(light.getPair(), f->light_source);
+}
+
+video::SColor MapblockMeshGenerator::blendLightColor(const v3f &vertex_pos,
+ const v3f &vertex_normal)
+{
+ LightInfo light = blendLight(vertex_pos);
+ video::SColor color = encode_light(light.getPair(MYMAX(0.0f, vertex_normal.Y)), f->light_source);
+ if (!f->light_source)
+ applyFacesShading(color, vertex_normal);
+ return color;
+}
+
+void MapblockMeshGenerator::generateCuboidTextureCoords(const aabb3f &box, f32 *coords)
+{
+ f32 tx1 = (box.MinEdge.X / BS) + 0.5;
+ f32 ty1 = (box.MinEdge.Y / BS) + 0.5;
+ f32 tz1 = (box.MinEdge.Z / BS) + 0.5;
+ f32 tx2 = (box.MaxEdge.X / BS) + 0.5;
+ f32 ty2 = (box.MaxEdge.Y / BS) + 0.5;
+ f32 tz2 = (box.MaxEdge.Z / BS) + 0.5;
+ f32 txc[24] = {
+ tx1, 1 - tz2, tx2, 1 - tz1, // up
+ tx1, tz1, tx2, tz2, // down
+ tz1, 1 - ty2, tz2, 1 - ty1, // right
+ 1 - tz2, 1 - ty2, 1 - tz1, 1 - ty1, // left
+ 1 - tx2, 1 - ty2, 1 - tx1, 1 - ty1, // back
+ tx1, 1 - ty2, tx2, 1 - ty1, // front
+ };
+ for (int i = 0; i != 24; ++i)
+ coords[i] = txc[i];
+}
+
+void MapblockMeshGenerator::drawAutoLightedCuboid(aabb3f box, const f32 *txc,
+ TileSpec *tiles, int tile_count)
+{
+ f32 texture_coord_buf[24];
+ f32 dx1 = box.MinEdge.X;
+ f32 dy1 = box.MinEdge.Y;
+ f32 dz1 = box.MinEdge.Z;
+ f32 dx2 = box.MaxEdge.X;
+ f32 dy2 = box.MaxEdge.Y;
+ f32 dz2 = box.MaxEdge.Z;
+ box.MinEdge += origin;
+ box.MaxEdge += origin;
+ if (!txc) {
+ generateCuboidTextureCoords(box, texture_coord_buf);
+ txc = texture_coord_buf;
+ }
+ if (!tiles) {
+ tiles = &tile;
+ tile_count = 1;
+ }
+ if (data->m_smooth_lighting) {
+ LightInfo lights[8];
+ for (int j = 0; j < 8; ++j) {
+ v3f d;
+ d.X = (j & 4) ? dx2 : dx1;
+ d.Y = (j & 2) ? dy2 : dy1;
+ d.Z = (j & 1) ? dz2 : dz1;
+ lights[j] = blendLight(d);
+ }
+ drawCuboid(box, tiles, tile_count, lights, txc);
+ } else {
+ drawCuboid(box, tiles, tile_count, nullptr, txc);
+ }
+}
+
+void MapblockMeshGenerator::prepareLiquidNodeDrawing()
+{
+ getSpecialTile(0, &tile_liquid_top);
+ getSpecialTile(1, &tile_liquid);
+
+ MapNode ntop = data->m_vmanip.getNodeNoEx(blockpos_nodes + v3s16(p.X, p.Y + 1, p.Z));
+ MapNode nbottom = data->m_vmanip.getNodeNoEx(blockpos_nodes + v3s16(p.X, p.Y - 1, p.Z));
+ c_flowing = nodedef->getId(f->liquid_alternative_flowing);
+ c_source = nodedef->getId(f->liquid_alternative_source);
+ top_is_same_liquid = (ntop.getContent() == c_flowing) || (ntop.getContent() == c_source);
+ draw_liquid_bottom = (nbottom.getContent() != c_flowing) && (nbottom.getContent() != c_source);
+ if (draw_liquid_bottom) {
+ const ContentFeatures &f2 = nodedef->get(nbottom.getContent());
+ if (f2.solidness > 1)
+ draw_liquid_bottom = false;
+ }
+
+ if (data->m_smooth_lighting)
+ return; // don't need to pre-compute anything in this case
+
+ if (f->light_source != 0) {
+ // If this liquid emits light and doesn't contain light, draw
+ // it at what it emits, for an increased effect
+ u8 e = decode_light(f->light_source);
+ light = LightPair(std::max(e, light.lightDay), std::max(e, light.lightNight));
+ } else if (nodedef->get(ntop).param_type == CPT_LIGHT) {
+ // Otherwise, use the light of the node on top if possible
+ light = LightPair(getInteriorLight(ntop, 0, nodedef));
+ }
+
+ color_liquid_top = encode_light(light, f->light_source);
+ color = encode_light(light, f->light_source);
+}
+
+void MapblockMeshGenerator::getLiquidNeighborhood()
+{
+ u8 range = rangelim(nodedef->get(c_flowing).liquid_range, 1, 8);
+
+ for (int w = -1; w <= 1; w++)
+ for (int u = -1; u <= 1; u++) {
+ NeighborData &neighbor = liquid_neighbors[w + 1][u + 1];
+ v3s16 p2 = p + v3s16(u, 0, w);
+ MapNode n2 = data->m_vmanip.getNodeNoEx(blockpos_nodes + p2);
+ neighbor.content = n2.getContent();
+ neighbor.level = -0.5 * BS;
+ neighbor.is_same_liquid = false;
+ neighbor.top_is_same_liquid = false;
+
+ if (neighbor.content == CONTENT_IGNORE)
+ continue;
+
+ if (neighbor.content == c_source) {
+ neighbor.is_same_liquid = true;
+ neighbor.level = 0.5 * BS;
+ } else if (neighbor.content == c_flowing) {
+ neighbor.is_same_liquid = true;
+ u8 liquid_level = (n2.param2 & LIQUID_LEVEL_MASK);
+ if (liquid_level <= LIQUID_LEVEL_MAX + 1 - range)
+ liquid_level = 0;
+ else
+ liquid_level -= (LIQUID_LEVEL_MAX + 1 - range);
+ neighbor.level = (-0.5 + (liquid_level + 0.5) / range) * BS;
+ }
+
+ // Check node above neighbor.
+ // NOTE: This doesn't get executed if neighbor
+ // doesn't exist
+ p2.Y++;
+ n2 = data->m_vmanip.getNodeNoEx(blockpos_nodes + p2);
+ if (n2.getContent() == c_source || n2.getContent() == c_flowing)
+ neighbor.top_is_same_liquid = true;
+ }
+}
+
+void MapblockMeshGenerator::calculateCornerLevels()
+{
+ for (int k = 0; k < 2; k++)
+ for (int i = 0; i < 2; i++)
+ corner_levels[k][i] = getCornerLevel(i, k);
+}
+
+f32 MapblockMeshGenerator::getCornerLevel(int i, int k)
+{
+ float sum = 0;
+ int count = 0;
+ int air_count = 0;
+ for (int dk = 0; dk < 2; dk++)
+ for (int di = 0; di < 2; di++) {
+ NeighborData &neighbor_data = liquid_neighbors[k + dk][i + di];
+ content_t content = neighbor_data.content;
+
+ // If top is liquid, draw starting from top of node
+ if (neighbor_data.top_is_same_liquid)
+ return 0.5 * BS;
+
+ // Source always has the full height
+ if (content == c_source)
+ return 0.5 * BS;
+
+ // Flowing liquid has level information
+ if (content == c_flowing) {
+ sum += neighbor_data.level;
+ count++;
+ } else if (content == CONTENT_AIR) {
+ air_count++;
+ if (air_count >= 2)
+ return -0.5 * BS + 0.2;
+ }
+ }
+ if (count > 0)
+ return sum / count;
+ return 0;
+}
+
+void MapblockMeshGenerator::drawLiquidSides()
+{
+ struct LiquidFaceDesc {
+ v3s16 dir; // XZ
+ v3s16 p[2]; // XZ only; 1 means +, 0 means -
+ };
+ struct UV {
+ int u, v;
+ };
+ static const LiquidFaceDesc base_faces[4] = {
+ {v3s16( 1, 0, 0), {v3s16(1, 0, 1), v3s16(1, 0, 0)}},
+ {v3s16(-1, 0, 0), {v3s16(0, 0, 0), v3s16(0, 0, 1)}},
+ {v3s16( 0, 0, 1), {v3s16(0, 0, 1), v3s16(1, 0, 1)}},
+ {v3s16( 0, 0, -1), {v3s16(1, 0, 0), v3s16(0, 0, 0)}},
+ };
+ static const UV base_vertices[4] = {
+ {0, 1},
+ {1, 1},
+ {1, 0},
+ {0, 0}
+ };
+
+ for (const auto &face : base_faces) {
+ const NeighborData &neighbor = liquid_neighbors[face.dir.Z + 1][face.dir.X + 1];
+
+ // No face between nodes of the same liquid, unless there is node
+ // at the top to which it should be connected. Again, unless the face
+ // there would be inside the liquid
+ if (neighbor.is_same_liquid) {
+ if (!top_is_same_liquid)
+ continue;
+ if (neighbor.top_is_same_liquid)
+ continue;
+ }
+
+ const ContentFeatures &neighbor_features = nodedef->get(neighbor.content);
+ // Don't draw face if neighbor is blocking the view
+ if (neighbor_features.solidness == 2)
+ continue;
+
+ video::S3DVertex vertices[4];
+ for (int j = 0; j < 4; j++) {
+ const UV &vertex = base_vertices[j];
+ const v3s16 &base = face.p[vertex.u];
+ v3f pos;
+ pos.X = (base.X - 0.5) * BS;
+ pos.Z = (base.Z - 0.5) * BS;
+ if (vertex.v)
+ pos.Y = neighbor.is_same_liquid ? corner_levels[base.Z][base.X] : -0.5 * BS;
+ else
+ pos.Y = !top_is_same_liquid ? corner_levels[base.Z][base.X] : 0.5 * BS;
+ if (data->m_smooth_lighting)
+ color = blendLightColor(pos);
+ pos += origin;
+ vertices[j] = video::S3DVertex(pos.X, pos.Y, pos.Z, 0, 0, 0, color, vertex.u, vertex.v);
+ };
+ collector->append(tile_liquid, vertices, 4, quad_indices, 6);
+ }
+}
+
+void MapblockMeshGenerator::drawLiquidTop()
+{
+ // To get backface culling right, the vertices need to go
+ // clockwise around the front of the face. And we happened to
+ // calculate corner levels in exact reverse order.
+ static const int corner_resolve[4][2] = {{0, 1}, {1, 1}, {1, 0}, {0, 0}};
+
+ video::S3DVertex vertices[4] = {
+ video::S3DVertex(-BS / 2, 0, BS / 2, 0, 0, 0, color_liquid_top, 0, 1),
+ video::S3DVertex( BS / 2, 0, BS / 2, 0, 0, 0, color_liquid_top, 1, 1),
+ video::S3DVertex( BS / 2, 0, -BS / 2, 0, 0, 0, color_liquid_top, 1, 0),
+ video::S3DVertex(-BS / 2, 0, -BS / 2, 0, 0, 0, color_liquid_top, 0, 0),
+ };
+
+ for (int i = 0; i < 4; i++) {
+ int u = corner_resolve[i][0];
+ int w = corner_resolve[i][1];
+ vertices[i].Pos.Y += corner_levels[w][u];
+ if (data->m_smooth_lighting)
+ vertices[i].Color = blendLightColor(vertices[i].Pos);
+ vertices[i].Pos += origin;
+ }
+
+ // Default downwards-flowing texture animation goes from
+ // -Z towards +Z, thus the direction is +Z.
+ // Rotate texture to make animation go in flow direction
+ // Positive if liquid moves towards +Z
+ f32 dz = (corner_levels[0][0] + corner_levels[0][1]) -
+ (corner_levels[1][0] + corner_levels[1][1]);
+ // Positive if liquid moves towards +X
+ f32 dx = (corner_levels[0][0] + corner_levels[1][0]) -
+ (corner_levels[0][1] + corner_levels[1][1]);
+ f32 tcoord_angle = atan2(dz, dx) * core::RADTODEG;
+ v2f tcoord_center(0.5, 0.5);
+ v2f tcoord_translate(blockpos_nodes.Z + p.Z, blockpos_nodes.X + p.X);
+ tcoord_translate.rotateBy(tcoord_angle);
+ tcoord_translate.X -= floor(tcoord_translate.X);
+ tcoord_translate.Y -= floor(tcoord_translate.Y);
+
+ for (video::S3DVertex &vertex : vertices) {
+ vertex.TCoords.rotateBy(tcoord_angle, tcoord_center);
+ vertex.TCoords += tcoord_translate;
+ }
+
+ std::swap(vertices[0].TCoords, vertices[2].TCoords);
+
+ collector->append(tile_liquid_top, vertices, 4, quad_indices, 6);
+}
+
+void MapblockMeshGenerator::drawLiquidBottom()
+{
+ video::S3DVertex vertices[4] = {
+ video::S3DVertex(-BS / 2, -BS / 2, -BS / 2, 0, 0, 0, color_liquid_top, 0, 0),
+ video::S3DVertex( BS / 2, -BS / 2, -BS / 2, 0, 0, 0, color_liquid_top, 1, 0),
+ video::S3DVertex( BS / 2, -BS / 2, BS / 2, 0, 0, 0, color_liquid_top, 1, 1),
+ video::S3DVertex(-BS / 2, -BS / 2, BS / 2, 0, 0, 0, color_liquid_top, 0, 1),
+ };
+
+ for (int i = 0; i < 4; i++) {
+ if (data->m_smooth_lighting)
+ vertices[i].Color = blendLightColor(vertices[i].Pos);
+ vertices[i].Pos += origin;
+ }
+
+ collector->append(tile_liquid_top, vertices, 4, quad_indices, 6);
+}
+
+void MapblockMeshGenerator::drawLiquidNode()
+{
+ prepareLiquidNodeDrawing();
+ getLiquidNeighborhood();
+ calculateCornerLevels();
+ drawLiquidSides();
+ if (!top_is_same_liquid)
+ drawLiquidTop();
+ if (draw_liquid_bottom)
+ drawLiquidBottom();
+}
+
+void MapblockMeshGenerator::drawGlasslikeNode()
+{
+ useTile(0, 0, 0);
+
+ for (int face = 0; face < 6; face++) {
+ // Check this neighbor
+ v3s16 dir = g_6dirs[face];
+ v3s16 neighbor_pos = blockpos_nodes + p + dir;
+ MapNode neighbor = data->m_vmanip.getNodeNoExNoEmerge(neighbor_pos);
+ // Don't make face if neighbor is of same type
+ if (neighbor.getContent() == n.getContent())
+ continue;
+ // Face at Z-
+ v3f vertices[4] = {
+ v3f(-BS / 2, BS / 2, -BS / 2),
+ v3f( BS / 2, BS / 2, -BS / 2),
+ v3f( BS / 2, -BS / 2, -BS / 2),
+ v3f(-BS / 2, -BS / 2, -BS / 2),
+ };
+
+ for (v3f &vertex : vertices) {
+ switch (face) {
+ case D6D_ZP:
+ vertex.rotateXZBy(180); break;
+ case D6D_YP:
+ vertex.rotateYZBy( 90); break;
+ case D6D_XP:
+ vertex.rotateXZBy( 90); break;
+ case D6D_ZN:
+ vertex.rotateXZBy( 0); break;
+ case D6D_YN:
+ vertex.rotateYZBy(-90); break;
+ case D6D_XN:
+ vertex.rotateXZBy(-90); break;
+ }
+ }
+ drawQuad(vertices, dir);
+ }
+}
+
+void MapblockMeshGenerator::drawGlasslikeFramedNode()
+{
+ TileSpec tiles[6];
+ for (int face = 0; face < 6; face++)
+ getTile(g_6dirs[face], &tiles[face]);
+
+ if (!data->m_smooth_lighting)
+ color = encode_light(light, f->light_source);
+
+ TileSpec glass_tiles[6];
+ for (auto &glass_tile : glass_tiles)
+ glass_tile = tiles[4];
+
+ u8 param2 = n.getParam2();
+ bool H_merge = !(param2 & 128);
+ bool V_merge = !(param2 & 64);
+ param2 &= 63;
+
+ static const float a = BS / 2.0f;
+ static const float g = a - 0.03f;
+ static const float b = 0.876f * (BS / 2.0f);
+
+ static const aabb3f frame_edges[FRAMED_EDGE_COUNT] = {
+ aabb3f( b, b, -a, a, a, a), // y+
+ aabb3f(-a, b, -a, -b, a, a), // y+
+ aabb3f( b, -a, -a, a, -b, a), // y-
+ aabb3f(-a, -a, -a, -b, -b, a), // y-
+ aabb3f( b, -a, b, a, a, a), // x+
+ aabb3f( b, -a, -a, a, a, -b), // x+
+ aabb3f(-a, -a, b, -b, a, a), // x-
+ aabb3f(-a, -a, -a, -b, a, -b), // x-
+ aabb3f(-a, b, b, a, a, a), // z+
+ aabb3f(-a, -a, b, a, -b, a), // z+
+ aabb3f(-a, -a, -a, a, -b, -b), // z-
+ aabb3f(-a, b, -a, a, a, -b), // z-
+ };
+
+ // tables of neighbour (connect if same type and merge allowed),
+ // checked with g_26dirs
+
+ // 1 = connect, 0 = face visible
+ bool nb[FRAMED_NEIGHBOR_COUNT] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};
+
+ // 1 = check
+ static const bool check_nb_vertical [FRAMED_NEIGHBOR_COUNT] =
+ {0,1,0,0,1,0, 0,0,0,0, 0,0,0,0, 0,0,0,0};
+ static const bool check_nb_horizontal [FRAMED_NEIGHBOR_COUNT] =
+ {1,0,1,1,0,1, 0,0,0,0, 1,1,1,1, 0,0,0,0};
+ static const bool check_nb_all [FRAMED_NEIGHBOR_COUNT] =
+ {1,1,1,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1};
+ const bool *check_nb = check_nb_all;
+
+ // neighbours checks for frames visibility
+ if (H_merge || V_merge) {
+ if (!H_merge)
+ check_nb = check_nb_vertical; // vertical-only merge
+ if (!V_merge)
+ check_nb = check_nb_horizontal; // horizontal-only merge
+ content_t current = n.getContent();
+ for (int i = 0; i < FRAMED_NEIGHBOR_COUNT; i++) {
+ if (!check_nb[i])
+ continue;
+ v3s16 n2p = blockpos_nodes + p + g_26dirs[i];
+ MapNode n2 = data->m_vmanip.getNodeNoEx(n2p);
+ content_t n2c = n2.getContent();
+ if (n2c == current)
+ nb[i] = 1;
+ }
+ }
+
+ // edge visibility
+
+ static const u8 nb_triplet[FRAMED_EDGE_COUNT][3] = {
+ {1, 2, 7}, {1, 5, 6}, {4, 2, 15}, {4, 5, 14},
+ {2, 0, 11}, {2, 3, 13}, {5, 0, 10}, {5, 3, 12},
+ {0, 1, 8}, {0, 4, 16}, {3, 4, 17}, {3, 1, 9},
+ };
+
+ tile = tiles[1];
+ for (int edge = 0; edge < FRAMED_EDGE_COUNT; edge++) {
+ bool edge_invisible;
+ if (nb[nb_triplet[edge][2]])
+ edge_invisible = nb[nb_triplet[edge][0]] & nb[nb_triplet[edge][1]];
+ else
+ edge_invisible = nb[nb_triplet[edge][0]] ^ nb[nb_triplet[edge][1]];
+ if (edge_invisible)
+ continue;
+ drawAutoLightedCuboid(frame_edges[edge]);
+ }
+
+ for (int face = 0; face < 6; face++) {
+ if (nb[face])
+ continue;
+
+ tile = glass_tiles[face];
+ // Face at Z-
+ v3f vertices[4] = {
+ v3f(-a, a, -g),
+ v3f( a, a, -g),
+ v3f( a, -a, -g),
+ v3f(-a, -a, -g),
+ };
+
+ for (v3f &vertex : vertices) {
+ switch (face) {
+ case D6D_ZP:
+ vertex.rotateXZBy(180); break;
+ case D6D_YP:
+ vertex.rotateYZBy( 90); break;
+ case D6D_XP:
+ vertex.rotateXZBy( 90); break;
+ case D6D_ZN:
+ vertex.rotateXZBy( 0); break;
+ case D6D_YN:
+ vertex.rotateYZBy(-90); break;
+ case D6D_XN:
+ vertex.rotateXZBy(-90); break;
+ }
+ }
+ v3s16 dir = g_6dirs[face];
+ drawQuad(vertices, dir);
+ }
+
+ // Optionally render internal liquid level defined by param2
+ // Liquid is textured with 1 tile defined in nodedef 'special_tiles'
+ if (param2 > 0 && f->param_type_2 == CPT2_GLASSLIKE_LIQUID_LEVEL &&
+ f->special_tiles[0].layers[0].texture) {
+ // Internal liquid level has param2 range 0 .. 63,
+ // convert it to -0.5 .. 0.5
+ float vlev = (param2 / 63.0f) * 2.0f - 1.0f;
+ getSpecialTile(0, &tile);
+ drawAutoLightedCuboid(aabb3f(-(nb[5] ? g : b),
+ -(nb[4] ? g : b),
+ -(nb[3] ? g : b),
+ (nb[2] ? g : b),
+ (nb[1] ? g : b) * vlev,
+ (nb[0] ? g : b)));
+ }
+}
+
+void MapblockMeshGenerator::drawAllfacesNode()
+{
+ static const aabb3f box(-BS / 2, -BS / 2, -BS / 2, BS / 2, BS / 2, BS / 2);
+ useTile(0, 0, 0);
+ drawAutoLightedCuboid(box);
+}
+
+void MapblockMeshGenerator::drawTorchlikeNode()
+{
+ u8 wall = n.getWallMounted(nodedef);
+ u8 tileindex = 0;
+ switch (wall) {
+ case DWM_YP: tileindex = 1; break; // ceiling
+ case DWM_YN: tileindex = 0; break; // floor
+ default: tileindex = 2; // side (or invalid—should we care?)
+ }
+ useTile(tileindex, MATERIAL_FLAG_CRACK_OVERLAY, MATERIAL_FLAG_BACKFACE_CULLING);
+
+ float size = BS / 2 * f->visual_scale;
+ v3f vertices[4] = {
+ v3f(-size, size, 0),
+ v3f( size, size, 0),
+ v3f( size, -size, 0),
+ v3f(-size, -size, 0),
+ };
+
+ for (v3f &vertex : vertices) {
+ switch (wall) {
+ case DWM_YP:
+ vertex.rotateXZBy(-45); break;
+ case DWM_YN:
+ vertex.rotateXZBy( 45); break;
+ case DWM_XP:
+ vertex.rotateXZBy( 0); break;
+ case DWM_XN:
+ vertex.rotateXZBy(180); break;
+ case DWM_ZP:
+ vertex.rotateXZBy( 90); break;
+ case DWM_ZN:
+ vertex.rotateXZBy(-90); break;
+ }
+ }
+ drawQuad(vertices);
+}
+
+void MapblockMeshGenerator::drawSignlikeNode()
+{
+ u8 wall = n.getWallMounted(nodedef);
+ useTile(0, MATERIAL_FLAG_CRACK_OVERLAY, MATERIAL_FLAG_BACKFACE_CULLING);
+ static const float offset = BS / 16;
+ float size = BS / 2 * f->visual_scale;
+ // Wall at X+ of node
+ v3f vertices[4] = {
+ v3f(BS / 2 - offset, size, size),
+ v3f(BS / 2 - offset, size, -size),
+ v3f(BS / 2 - offset, -size, -size),
+ v3f(BS / 2 - offset, -size, size),
+ };
+
+ for (v3f &vertex : vertices) {
+ switch (wall) {
+ case DWM_YP:
+ vertex.rotateXYBy( 90); break;
+ case DWM_YN:
+ vertex.rotateXYBy(-90); break;
+ case DWM_XP:
+ vertex.rotateXZBy( 0); break;
+ case DWM_XN:
+ vertex.rotateXZBy(180); break;
+ case DWM_ZP:
+ vertex.rotateXZBy( 90); break;
+ case DWM_ZN:
+ vertex.rotateXZBy(-90); break;
+ }
+ }
+ drawQuad(vertices);
+}
+
+void MapblockMeshGenerator::drawPlantlikeQuad(float rotation, float quad_offset,
+ bool offset_top_only)
+{
+ v3f vertices[4] = {
+ v3f(-scale, -BS / 2 + 2.0 * scale * plant_height, 0),
+ v3f( scale, -BS / 2 + 2.0 * scale * plant_height, 0),
+ v3f( scale, -BS / 2, 0),
+ v3f(-scale, -BS / 2, 0),
+ };
+ if (random_offset_Y) {
+ PseudoRandom yrng(face_num++ | p.X << 16 | p.Z << 8 | p.Y << 24);
+ offset.Y = -BS * ((yrng.next() % 16 / 16.0) * 0.125);
+ }
+ int offset_count = offset_top_only ? 2 : 4;
+ for (int i = 0; i < offset_count; i++)
+ vertices[i].Z += quad_offset;
+
+ for (v3f &vertex : vertices) {
+ vertex.rotateXZBy(rotation + rotate_degree);
+ vertex += offset;
+ }
+ drawQuad(vertices, v3s16(0, 0, 0), plant_height);
+}
+
+void MapblockMeshGenerator::drawPlantlike()
+{
+ draw_style = PLANT_STYLE_CROSS;
+ scale = BS / 2 * f->visual_scale;
+ offset = v3f(0, 0, 0);
+ rotate_degree = 0;
+ random_offset_Y = false;
+ face_num = 0;
+ plant_height = 1.0;
+
+ switch (f->param_type_2) {
+ case CPT2_MESHOPTIONS:
+ draw_style = PlantlikeStyle(n.param2 & MO_MASK_STYLE);
+ if (n.param2 & MO_BIT_SCALE_SQRT2)
+ scale *= 1.41421;
+ if (n.param2 & MO_BIT_RANDOM_OFFSET) {
+ PseudoRandom rng(p.X << 8 | p.Z | p.Y << 16);
+ offset.X = BS * ((rng.next() % 16 / 16.0) * 0.29 - 0.145);
+ offset.Z = BS * ((rng.next() % 16 / 16.0) * 0.29 - 0.145);
+ }
+ if (n.param2 & MO_BIT_RANDOM_OFFSET_Y)
+ random_offset_Y = true;
+ break;
+
+ case CPT2_DEGROTATE:
+ rotate_degree = n.param2 * 2;
+ break;
+
+ case CPT2_LEVELED:
+ plant_height = n.param2 / 16.0;
+ break;
+
+ default:
+ break;
+ }
+
+ switch (draw_style) {
+ case PLANT_STYLE_CROSS:
+ drawPlantlikeQuad(46);
+ drawPlantlikeQuad(-44);
+ break;
+
+ case PLANT_STYLE_CROSS2:
+ drawPlantlikeQuad(91);
+ drawPlantlikeQuad(1);
+ break;
+
+ case PLANT_STYLE_STAR:
+ drawPlantlikeQuad(121);
+ drawPlantlikeQuad(241);
+ drawPlantlikeQuad(1);
+ break;
+
+ case PLANT_STYLE_HASH:
+ drawPlantlikeQuad( 1, BS / 4);
+ drawPlantlikeQuad( 91, BS / 4);
+ drawPlantlikeQuad(181, BS / 4);
+ drawPlantlikeQuad(271, BS / 4);
+ break;
+
+ case PLANT_STYLE_HASH2:
+ drawPlantlikeQuad( 1, -BS / 2, true);
+ drawPlantlikeQuad( 91, -BS / 2, true);
+ drawPlantlikeQuad(181, -BS / 2, true);
+ drawPlantlikeQuad(271, -BS / 2, true);
+ break;
+ }
+}
+
+void MapblockMeshGenerator::drawPlantlikeNode()
+{
+ useTile();
+ drawPlantlike();
+}
+
+void MapblockMeshGenerator::drawPlantlikeRootedNode()
+{
+ useTile(0, MATERIAL_FLAG_CRACK_OVERLAY, 0, true);
+ origin += v3f(0.0, BS, 0.0);
+ p.Y++;
+ if (data->m_smooth_lighting) {
+ getSmoothLightFrame();
+ } else {
+ MapNode ntop = data->m_vmanip.getNodeNoEx(blockpos_nodes + p);
+ light = LightPair(getInteriorLight(ntop, 1, nodedef));
+ }
+ drawPlantlike();
+ p.Y--;
+}
+
+void MapblockMeshGenerator::drawFirelikeQuad(float rotation, float opening_angle,
+ float offset_h, float offset_v)
+{
+ v3f vertices[4] = {
+ v3f(-scale, -BS / 2 + scale * 2, 0),
+ v3f( scale, -BS / 2 + scale * 2, 0),
+ v3f( scale, -BS / 2, 0),
+ v3f(-scale, -BS / 2, 0),
+ };
+
+ for (v3f &vertex : vertices) {
+ vertex.rotateYZBy(opening_angle);
+ vertex.Z += offset_h;
+ vertex.rotateXZBy(rotation);
+ vertex.Y += offset_v;
+ }
+ drawQuad(vertices);
+}
+
+void MapblockMeshGenerator::drawFirelikeNode()
+{
+ useTile();
+ scale = BS / 2 * f->visual_scale;
+
+ // Check for adjacent nodes
+ bool neighbors = false;
+ bool neighbor[6] = {0, 0, 0, 0, 0, 0};
+ content_t current = n.getContent();
+ for (int i = 0; i < 6; i++) {
+ v3s16 n2p = blockpos_nodes + p + g_6dirs[i];
+ MapNode n2 = data->m_vmanip.getNodeNoEx(n2p);
+ content_t n2c = n2.getContent();
+ if (n2c != CONTENT_IGNORE && n2c != CONTENT_AIR && n2c != current) {
+ neighbor[i] = true;
+ neighbors = true;
+ }
+ }
+ bool drawBasicFire = neighbor[D6D_YN] || !neighbors;
+ bool drawBottomFire = neighbor[D6D_YP];
+
+ if (drawBasicFire || neighbor[D6D_ZP])
+ drawFirelikeQuad(0, -10, 0.4 * BS);
+ else if (drawBottomFire)
+ drawFirelikeQuad(0, 70, 0.47 * BS, 0.484 * BS);
+
+ if (drawBasicFire || neighbor[D6D_XN])
+ drawFirelikeQuad(90, -10, 0.4 * BS);
+ else if (drawBottomFire)
+ drawFirelikeQuad(90, 70, 0.47 * BS, 0.484 * BS);
+
+ if (drawBasicFire || neighbor[D6D_ZN])
+ drawFirelikeQuad(180, -10, 0.4 * BS);
+ else if (drawBottomFire)
+ drawFirelikeQuad(180, 70, 0.47 * BS, 0.484 * BS);
+
+ if (drawBasicFire || neighbor[D6D_XP])
+ drawFirelikeQuad(270, -10, 0.4 * BS);
+ else if (drawBottomFire)
+ drawFirelikeQuad(270, 70, 0.47 * BS, 0.484 * BS);
+
+ if (drawBasicFire) {
+ drawFirelikeQuad(45, 0, 0.0);
+ drawFirelikeQuad(-45, 0, 0.0);
+ }
+}
+
+void MapblockMeshGenerator::drawFencelikeNode()
+{
+ useTile(0, 0, 0);
+ TileSpec tile_nocrack = tile;
+
+ for (auto &layer : tile_nocrack.layers)
+ layer.material_flags &= ~MATERIAL_FLAG_CRACK;
+
+ // Put wood the right way around in the posts
+ TileSpec tile_rot = tile;
+ tile_rot.rotation = 1;
+
+ static const f32 post_rad = BS / 8;
+ static const f32 bar_rad = BS / 16;
+ static const f32 bar_len = BS / 2 - post_rad;
+
+ // The post - always present
+ static const aabb3f post(-post_rad, -BS / 2, -post_rad,
+ post_rad, BS / 2, post_rad);
+ static const f32 postuv[24] = {
+ 0.375, 0.375, 0.625, 0.625,
+ 0.375, 0.375, 0.625, 0.625,
+ 0.000, 0.000, 0.250, 1.000,
+ 0.250, 0.000, 0.500, 1.000,
+ 0.500, 0.000, 0.750, 1.000,
+ 0.750, 0.000, 1.000, 1.000,
+ };
+ tile = tile_rot;
+ drawAutoLightedCuboid(post, postuv);
+
+ tile = tile_nocrack;
+
+ // Now a section of fence, +X, if there's a post there
+ v3s16 p2 = p;
+ p2.X++;
+ MapNode n2 = data->m_vmanip.getNodeNoEx(blockpos_nodes + p2);
+ const ContentFeatures *f2 = &nodedef->get(n2);
+ if (f2->drawtype == NDT_FENCELIKE) {
+ static const aabb3f bar_x1(BS / 2 - bar_len, BS / 4 - bar_rad, -bar_rad,
+ BS / 2 + bar_len, BS / 4 + bar_rad, bar_rad);
+ static const aabb3f bar_x2(BS / 2 - bar_len, -BS / 4 - bar_rad, -bar_rad,
+ BS / 2 + bar_len, -BS / 4 + bar_rad, bar_rad);
+ static const f32 xrailuv[24] = {
+ 0.000, 0.125, 1.000, 0.250,
+ 0.000, 0.250, 1.000, 0.375,
+ 0.375, 0.375, 0.500, 0.500,
+ 0.625, 0.625, 0.750, 0.750,
+ 0.000, 0.500, 1.000, 0.625,
+ 0.000, 0.875, 1.000, 1.000,
+ };
+ drawAutoLightedCuboid(bar_x1, xrailuv);
+ drawAutoLightedCuboid(bar_x2, xrailuv);
+ }
+
+ // Now a section of fence, +Z, if there's a post there
+ p2 = p;
+ p2.Z++;
+ n2 = data->m_vmanip.getNodeNoEx(blockpos_nodes + p2);
+ f2 = &nodedef->get(n2);
+ if (f2->drawtype == NDT_FENCELIKE) {
+ static const aabb3f bar_z1(-bar_rad, BS / 4 - bar_rad, BS / 2 - bar_len,
+ bar_rad, BS / 4 + bar_rad, BS / 2 + bar_len);
+ static const aabb3f bar_z2(-bar_rad, -BS / 4 - bar_rad, BS / 2 - bar_len,
+ bar_rad, -BS / 4 + bar_rad, BS / 2 + bar_len);
+ static const f32 zrailuv[24] = {
+ 0.1875, 0.0625, 0.3125, 0.3125, // cannot rotate; stretch
+ 0.2500, 0.0625, 0.3750, 0.3125, // for wood texture instead
+ 0.0000, 0.5625, 1.0000, 0.6875,
+ 0.0000, 0.3750, 1.0000, 0.5000,
+ 0.3750, 0.3750, 0.5000, 0.5000,
+ 0.6250, 0.6250, 0.7500, 0.7500,
+ };
+ drawAutoLightedCuboid(bar_z1, zrailuv);
+ drawAutoLightedCuboid(bar_z2, zrailuv);
+ }
+}
+
+bool MapblockMeshGenerator::isSameRail(v3s16 dir)
+{
+ MapNode node2 = data->m_vmanip.getNodeNoEx(blockpos_nodes + p + dir);
+ if (node2.getContent() == n.getContent())
+ return true;
+ const ContentFeatures &def2 = nodedef->get(node2);
+ return ((def2.drawtype == NDT_RAILLIKE) &&
+ (def2.getGroup(raillike_groupname) == raillike_group));
+}
+
+void MapblockMeshGenerator::drawRaillikeNode()
+{
+ static const v3s16 direction[4] = {
+ v3s16( 0, 0, 1),
+ v3s16( 0, 0, -1),
+ v3s16(-1, 0, 0),
+ v3s16( 1, 0, 0),
+ };
+ static const int slope_angle[4] = {0, 180, 90, -90};
+
+ enum RailTile {
+ straight,
+ curved,
+ junction,
+ cross,
+ };
+ struct RailDesc {
+ int tile_index;
+ int angle;
+ };
+ static const RailDesc rail_kinds[16] = {
+ // +x -x -z +z
+ //-------------
+ {straight, 0}, // . . . .
+ {straight, 0}, // . . . +Z
+ {straight, 0}, // . . -Z .
+ {straight, 0}, // . . -Z +Z
+ {straight, 90}, // . -X . .
+ { curved, 180}, // . -X . +Z
+ { curved, 270}, // . -X -Z .
+ {junction, 180}, // . -X -Z +Z
+ {straight, 90}, // +X . . .
+ { curved, 90}, // +X . . +Z
+ { curved, 0}, // +X . -Z .
+ {junction, 0}, // +X . -Z +Z
+ {straight, 90}, // +X -X . .
+ {junction, 90}, // +X -X . +Z
+ {junction, 270}, // +X -X -Z .
+ { cross, 0}, // +X -X -Z +Z
+ };
+
+ raillike_group = nodedef->get(n).getGroup(raillike_groupname);
+
+ int code = 0;
+ int angle;
+ int tile_index;
+ bool sloped = false;
+ for (int dir = 0; dir < 4; dir++) {
+ bool rail_above = isSameRail(direction[dir] + v3s16(0, 1, 0));
+ if (rail_above) {
+ sloped = true;
+ angle = slope_angle[dir];
+ }
+ if (rail_above ||
+ isSameRail(direction[dir]) ||
+ isSameRail(direction[dir] + v3s16(0, -1, 0)))
+ code |= 1 << dir;
+ }
+
+ if (sloped) {
+ tile_index = straight;
+ } else {
+ tile_index = rail_kinds[code].tile_index;
+ angle = rail_kinds[code].angle;
+ }
+
+ useTile(tile_index, MATERIAL_FLAG_CRACK_OVERLAY, MATERIAL_FLAG_BACKFACE_CULLING);
+
+ static const float offset = BS / 64;
+ static const float size = BS / 2;
+ float y2 = sloped ? size : -size;
+ v3f vertices[4] = {
+ v3f(-size, y2 + offset, size),
+ v3f( size, y2 + offset, size),
+ v3f( size, -size + offset, -size),
+ v3f(-size, -size + offset, -size),
+ };
+ if (angle)
+ for (v3f &vertex : vertices)
+ vertex.rotateXZBy(angle);
+ drawQuad(vertices);
+}
+
+void MapblockMeshGenerator::drawNodeboxNode()
+{
+ static const v3s16 tile_dirs[6] = {
+ v3s16(0, 1, 0),
+ v3s16(0, -1, 0),
+ v3s16(1, 0, 0),
+ v3s16(-1, 0, 0),
+ v3s16(0, 0, 1),
+ v3s16(0, 0, -1)
+ };
+
+ // we have this order for some reason...
+ static const v3s16 connection_dirs[6] = {
+ v3s16( 0, 1, 0), // top
+ v3s16( 0, -1, 0), // bottom
+ v3s16( 0, 0, -1), // front
+ v3s16(-1, 0, 0), // left
+ v3s16( 0, 0, 1), // back
+ v3s16( 1, 0, 0), // right
+ };
+
+ TileSpec tiles[6];
+ for (int face = 0; face < 6; face++) {
+ // Handles facedir rotation for textures
+ getTile(tile_dirs[face], &tiles[face]);
+ }
+
+ // locate possible neighboring nodes to connect to
+ int neighbors_set = 0;
+ if (f->node_box.type == NODEBOX_CONNECTED) {
+ for (int dir = 0; dir != 6; dir++) {
+ int flag = 1 << dir;
+ v3s16 p2 = blockpos_nodes + p + connection_dirs[dir];
+ MapNode n2 = data->m_vmanip.getNodeNoEx(p2);
+ if (nodedef->nodeboxConnects(n, n2, flag))
+ neighbors_set |= flag;
+ }
+ }
+
+ std::vector<aabb3f> boxes;
+ n.getNodeBoxes(nodedef, &boxes, neighbors_set);
+ for (const auto &box : boxes)
+ drawAutoLightedCuboid(box, nullptr, tiles, 6);
+}
+
+void MapblockMeshGenerator::drawMeshNode()
+{
+ u8 facedir = 0;
+ scene::IMesh* mesh;
+ bool private_mesh; // as a grab/drop pair is not thread-safe
+
+ if (f->param_type_2 == CPT2_FACEDIR ||
+ f->param_type_2 == CPT2_COLORED_FACEDIR) {
+ facedir = n.getFaceDir(nodedef);
+ } else if (f->param_type_2 == CPT2_WALLMOUNTED ||
+ f->param_type_2 == CPT2_COLORED_WALLMOUNTED) {
+ // Convert wallmounted to 6dfacedir.
+ // When cache enabled, it is already converted.
+ facedir = n.getWallMounted(nodedef);
+ if (!enable_mesh_cache)
+ facedir = wallmounted_to_facedir[facedir];
+ }
+
+ if (!data->m_smooth_lighting && f->mesh_ptr[facedir]) {
+ // use cached meshes
+ private_mesh = false;
+ mesh = f->mesh_ptr[facedir];
+ } else if (f->mesh_ptr[0]) {
+ // no cache, clone and rotate mesh
+ private_mesh = true;
+ mesh = cloneMesh(f->mesh_ptr[0]);
+ rotateMeshBy6dFacedir(mesh, facedir);
+ recalculateBoundingBox(mesh);
+ meshmanip->recalculateNormals(mesh, true, false);
+ } else
+ return;
+
+ int mesh_buffer_count = mesh->getMeshBufferCount();
+ for (int j = 0; j < mesh_buffer_count; j++) {
+ useTile(j);
+ scene::IMeshBuffer *buf = mesh->getMeshBuffer(j);
+ video::S3DVertex *vertices = (video::S3DVertex *)buf->getVertices();
+ int vertex_count = buf->getVertexCount();
+
+ if (data->m_smooth_lighting) {
+ // Mesh is always private here. So the lighting is applied to each
+ // vertex right here.
+ for (int k = 0; k < vertex_count; k++) {
+ video::S3DVertex &vertex = vertices[k];
+ vertex.Color = blendLightColor(vertex.Pos, vertex.Normal);
+ vertex.Pos += origin;
+ }
+ collector->append(tile, vertices, vertex_count,
+ buf->getIndices(), buf->getIndexCount());
+ } else {
+ // Don't modify the mesh, it may not be private here.
+ // Instead, let the collector process colors, etc.
+ collector->append(tile, vertices, vertex_count,
+ buf->getIndices(), buf->getIndexCount(), origin,
+ color, f->light_source);
+ }
+ }
+ if (private_mesh)
+ mesh->drop();
+}
+
+// also called when the drawtype is known but should have been pre-converted
+void MapblockMeshGenerator::errorUnknownDrawtype()
+{
+ infostream << "Got drawtype " << f->drawtype << std::endl;
+ FATAL_ERROR("Unknown drawtype");
+}
+
+void MapblockMeshGenerator::drawNode()
+{
+ // skip some drawtypes early
+ switch (f->drawtype) {
+ case NDT_NORMAL: // Drawn by MapBlockMesh
+ case NDT_AIRLIKE: // Not drawn at all
+ case NDT_LIQUID: // Drawn by MapBlockMesh
+ return;
+ default:
+ break;
+ }
+ origin = intToFloat(p, BS);
+ if (data->m_smooth_lighting)
+ getSmoothLightFrame();
+ else
+ light = LightPair(getInteriorLight(n, 1, nodedef));
+ switch (f->drawtype) {
+ case NDT_FLOWINGLIQUID: drawLiquidNode(); break;
+ case NDT_GLASSLIKE: drawGlasslikeNode(); break;
+ case NDT_GLASSLIKE_FRAMED: drawGlasslikeFramedNode(); break;
+ case NDT_ALLFACES: drawAllfacesNode(); break;
+ case NDT_TORCHLIKE: drawTorchlikeNode(); break;
+ case NDT_SIGNLIKE: drawSignlikeNode(); break;
+ case NDT_PLANTLIKE: drawPlantlikeNode(); break;
+ case NDT_PLANTLIKE_ROOTED: drawPlantlikeRootedNode(); break;
+ case NDT_FIRELIKE: drawFirelikeNode(); break;
+ case NDT_FENCELIKE: drawFencelikeNode(); break;
+ case NDT_RAILLIKE: drawRaillikeNode(); break;
+ case NDT_NODEBOX: drawNodeboxNode(); break;
+ case NDT_MESH: drawMeshNode(); break;
+ default: errorUnknownDrawtype(); break;
+ }
+}
+
+/*
+ TODO: Fix alpha blending for special nodes
+ Currently only the last element rendered is blended correct
+*/
+void MapblockMeshGenerator::generate()
+{
+ for (p.Z = 0; p.Z < MAP_BLOCKSIZE; p.Z++)
+ for (p.Y = 0; p.Y < MAP_BLOCKSIZE; p.Y++)
+ for (p.X = 0; p.X < MAP_BLOCKSIZE; p.X++) {
+ n = data->m_vmanip.getNodeNoEx(blockpos_nodes + p);
+ f = &nodedef->get(n);
+ drawNode();
+ }
+}
+
+void MapblockMeshGenerator::renderSingle(content_t node)
+{
+ p = {0, 0, 0};
+ n = MapNode(node, 0xff, 0x00);
+ f = &nodedef->get(n);
+ drawNode();
+}
diff --git a/src/client/content_mapblock.h b/src/client/content_mapblock.h
new file mode 100644
index 000000000..97947cdbe
--- /dev/null
+++ b/src/client/content_mapblock.h
@@ -0,0 +1,178 @@
+/*
+Minetest
+Copyright (C) 2010-2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+
+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 "nodedef.h"
+#include <IMeshManipulator.h>
+
+struct MeshMakeData;
+struct MeshCollector;
+
+struct LightPair {
+ u8 lightDay;
+ u8 lightNight;
+
+ LightPair() = default;
+ explicit LightPair(u16 value) : lightDay(value & 0xff), lightNight(value >> 8) {}
+ LightPair(u8 valueA, u8 valueB) : lightDay(valueA), lightNight(valueB) {}
+ LightPair(float valueA, float valueB) :
+ lightDay(core::clamp(core::round32(valueA), 0, 255)),
+ lightNight(core::clamp(core::round32(valueB), 0, 255)) {}
+ operator u16() const { return lightDay | lightNight << 8; }
+};
+
+struct LightInfo {
+ float light_day;
+ float light_night;
+ float light_boosted;
+
+ LightPair getPair(float sunlight_boost = 0.0) const
+ {
+ return LightPair(
+ (1 - sunlight_boost) * light_day
+ + sunlight_boost * light_boosted,
+ light_night);
+ }
+};
+
+struct LightFrame {
+ f32 lightsDay[8];
+ f32 lightsNight[8];
+ bool sunlight[8];
+};
+
+class MapblockMeshGenerator
+{
+public:
+ MeshMakeData *data;
+ MeshCollector *collector;
+
+ const NodeDefManager *nodedef;
+ scene::IMeshManipulator *meshmanip;
+
+// options
+ bool enable_mesh_cache;
+
+// current node
+ v3s16 blockpos_nodes;
+ v3s16 p;
+ v3f origin;
+ MapNode n;
+ const ContentFeatures *f;
+ LightPair light;
+ LightFrame frame;
+ video::SColor color;
+ TileSpec tile;
+ float scale;
+
+// lighting
+ void getSmoothLightFrame();
+ LightInfo blendLight(const v3f &vertex_pos);
+ video::SColor blendLightColor(const v3f &vertex_pos);
+ video::SColor blendLightColor(const v3f &vertex_pos, const v3f &vertex_normal);
+
+ void useTile(int index = 0, u8 set_flags = MATERIAL_FLAG_CRACK_OVERLAY,
+ u8 reset_flags = 0, bool special = false);
+ void getTile(int index, TileSpec *tile);
+ void getTile(v3s16 direction, TileSpec *tile);
+ void getSpecialTile(int index, TileSpec *tile, bool apply_crack = false);
+
+// face drawing
+ void drawQuad(v3f *vertices, const v3s16 &normal = v3s16(0, 0, 0),
+ float vertical_tiling = 1.0);
+
+// cuboid drawing!
+ void drawCuboid(const aabb3f &box, TileSpec *tiles, int tilecount,
+ const LightInfo *lights , const f32 *txc);
+ void generateCuboidTextureCoords(aabb3f const &box, f32 *coords);
+ void drawAutoLightedCuboid(aabb3f box, const f32 *txc = NULL,
+ TileSpec *tiles = NULL, int tile_count = 0);
+
+// liquid-specific
+ bool top_is_same_liquid;
+ bool draw_liquid_bottom;
+ TileSpec tile_liquid;
+ TileSpec tile_liquid_top;
+ content_t c_flowing;
+ content_t c_source;
+ video::SColor color_liquid_top;
+ struct NeighborData {
+ f32 level;
+ content_t content;
+ bool is_same_liquid;
+ bool top_is_same_liquid;
+ };
+ NeighborData liquid_neighbors[3][3];
+ f32 corner_levels[2][2];
+
+ void prepareLiquidNodeDrawing();
+ void getLiquidNeighborhood();
+ void calculateCornerLevels();
+ f32 getCornerLevel(int i, int k);
+ void drawLiquidSides();
+ void drawLiquidTop();
+ void drawLiquidBottom();
+
+// raillike-specific
+ // name of the group that enables connecting to raillike nodes of different kind
+ static const std::string raillike_groupname;
+ int raillike_group;
+ bool isSameRail(v3s16 dir);
+
+// plantlike-specific
+ PlantlikeStyle draw_style;
+ v3f offset;
+ int rotate_degree;
+ bool random_offset_Y;
+ int face_num;
+ float plant_height;
+
+ void drawPlantlikeQuad(float rotation, float quad_offset = 0,
+ bool offset_top_only = false);
+ void drawPlantlike();
+
+// firelike-specific
+ void drawFirelikeQuad(float rotation, float opening_angle,
+ float offset_h, float offset_v = 0.0);
+
+// drawtypes
+ void drawLiquidNode();
+ void drawGlasslikeNode();
+ void drawGlasslikeFramedNode();
+ void drawAllfacesNode();
+ void drawTorchlikeNode();
+ void drawSignlikeNode();
+ void drawPlantlikeNode();
+ void drawPlantlikeRootedNode();
+ void drawFirelikeNode();
+ void drawFencelikeNode();
+ void drawRaillikeNode();
+ void drawNodeboxNode();
+ void drawMeshNode();
+
+// common
+ void errorUnknownDrawtype();
+ void drawNode();
+
+public:
+ MapblockMeshGenerator(MeshMakeData *input, MeshCollector *output);
+ void generate();
+ void renderSingle(content_t node);
+};
diff --git a/src/client/filecache.cpp b/src/client/filecache.cpp
new file mode 100644
index 000000000..3d1b302a8
--- /dev/null
+++ b/src/client/filecache.cpp
@@ -0,0 +1,89 @@
+/*
+Minetest
+Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+Copyright (C) 2013 Jonathan Neuschäfer <j.neuschaefer@gmx.net>
+
+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 "filecache.h"
+
+#include "network/networkprotocol.h"
+#include "log.h"
+#include "filesys.h"
+#include <string>
+#include <iostream>
+#include <fstream>
+#include <cstdlib>
+
+bool FileCache::loadByPath(const std::string &path, std::ostream &os)
+{
+ std::ifstream fis(path.c_str(), std::ios_base::binary);
+
+ if(!fis.good()){
+ verbosestream<<"FileCache: File not found in cache: "
+ <<path<<std::endl;
+ return false;
+ }
+
+ bool bad = false;
+ for(;;){
+ char buf[1024];
+ fis.read(buf, 1024);
+ std::streamsize len = fis.gcount();
+ os.write(buf, len);
+ if(fis.eof())
+ break;
+ if(!fis.good()){
+ bad = true;
+ break;
+ }
+ }
+ if(bad){
+ errorstream<<"FileCache: Failed to read file from cache: \""
+ <<path<<"\""<<std::endl;
+ }
+
+ return !bad;
+}
+
+bool FileCache::updateByPath(const std::string &path, const std::string &data)
+{
+ std::ofstream file(path.c_str(), std::ios_base::binary |
+ std::ios_base::trunc);
+
+ if(!file.good())
+ {
+ errorstream<<"FileCache: Can't write to file at "
+ <<path<<std::endl;
+ return false;
+ }
+
+ file.write(data.c_str(), data.length());
+ file.close();
+
+ return !file.fail();
+}
+
+bool FileCache::update(const std::string &name, const std::string &data)
+{
+ std::string path = m_dir + DIR_DELIM + name;
+ return updateByPath(path, data);
+}
+bool FileCache::load(const std::string &name, std::ostream &os)
+{
+ std::string path = m_dir + DIR_DELIM + name;
+ return loadByPath(path, os);
+}
diff --git a/src/client/filecache.h b/src/client/filecache.h
new file mode 100644
index 000000000..96e4c8ba1
--- /dev/null
+++ b/src/client/filecache.h
@@ -0,0 +1,42 @@
+/*
+Minetest
+Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+Copyright (C) 2013 Jonathan Neuschäfer <j.neuschaefer@gmx.net>
+
+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 <iostream>
+#include <string>
+
+class FileCache
+{
+public:
+ /*
+ 'dir' is the file cache directory to use.
+ */
+ FileCache(const std::string &dir) : m_dir(dir) {}
+
+ bool update(const std::string &name, const std::string &data);
+ bool load(const std::string &name, std::ostream &os);
+
+private:
+ std::string m_dir;
+
+ bool loadByPath(const std::string &path, std::ostream &os);
+ bool updateByPath(const std::string &path, const std::string &data);
+};
diff --git a/src/client/fontengine.cpp b/src/client/fontengine.cpp
new file mode 100644
index 000000000..dc98fb1e4
--- /dev/null
+++ b/src/client/fontengine.cpp
@@ -0,0 +1,505 @@
+/*
+Minetest
+Copyright (C) 2010-2014 sapier <sapier at gmx dot net>
+
+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 "fontengine.h"
+#include <cmath>
+#include "client/renderingengine.h"
+#include "config.h"
+#include "porting.h"
+#include "filesys.h"
+
+#if USE_FREETYPE
+#include "gettext.h"
+#include "irrlicht_changes/CGUITTFont.h"
+#endif
+
+/** maximum size distance for getting a "similar" font size */
+#define MAX_FONT_SIZE_OFFSET 10
+
+/** reference to access font engine, has to be initialized by main */
+FontEngine* g_fontengine = NULL;
+
+/** callback to be used on change of font size setting */
+static void font_setting_changed(const std::string &name, void *userdata)
+{
+ g_fontengine->readSettings();
+}
+
+/******************************************************************************/
+FontEngine::FontEngine(Settings* main_settings, gui::IGUIEnvironment* env) :
+ m_settings(main_settings),
+ m_env(env)
+{
+
+ for (u32 &i : m_default_size) {
+ i = (FontMode) FONT_SIZE_UNSPECIFIED;
+ }
+
+ assert(m_settings != NULL); // pre-condition
+ assert(m_env != NULL); // pre-condition
+ assert(m_env->getSkin() != NULL); // pre-condition
+
+ m_currentMode = FM_Simple;
+
+#if USE_FREETYPE
+ if (g_settings->getBool("freetype")) {
+ m_default_size[FM_Standard] = m_settings->getU16("font_size");
+ m_default_size[FM_Fallback] = m_settings->getU16("fallback_font_size");
+ m_default_size[FM_Mono] = m_settings->getU16("mono_font_size");
+
+ if (is_yes(gettext("needs_fallback_font"))) {
+ m_currentMode = FM_Fallback;
+ }
+ else {
+ m_currentMode = FM_Standard;
+ }
+ }
+
+ // having freetype but not using it is quite a strange case so we need to do
+ // special handling for it
+ if (m_currentMode == FM_Simple) {
+ std::stringstream fontsize;
+ fontsize << DEFAULT_FONT_SIZE;
+ m_settings->setDefault("font_size", fontsize.str());
+ m_settings->setDefault("mono_font_size", fontsize.str());
+ }
+#endif
+
+ m_default_size[FM_Simple] = m_settings->getU16("font_size");
+ m_default_size[FM_SimpleMono] = m_settings->getU16("mono_font_size");
+
+ updateSkin();
+
+ if (m_currentMode == FM_Standard) {
+ m_settings->registerChangedCallback("font_size", font_setting_changed, NULL);
+ m_settings->registerChangedCallback("font_path", font_setting_changed, NULL);
+ m_settings->registerChangedCallback("font_shadow", font_setting_changed, NULL);
+ m_settings->registerChangedCallback("font_shadow_alpha", font_setting_changed, NULL);
+ }
+ else if (m_currentMode == FM_Fallback) {
+ m_settings->registerChangedCallback("fallback_font_size", font_setting_changed, NULL);
+ m_settings->registerChangedCallback("fallback_font_path", font_setting_changed, NULL);
+ m_settings->registerChangedCallback("fallback_font_shadow", font_setting_changed, NULL);
+ m_settings->registerChangedCallback("fallback_font_shadow_alpha", font_setting_changed, NULL);
+ }
+
+ m_settings->registerChangedCallback("mono_font_path", font_setting_changed, NULL);
+ m_settings->registerChangedCallback("mono_font_size", font_setting_changed, NULL);
+ m_settings->registerChangedCallback("screen_dpi", font_setting_changed, NULL);
+ m_settings->registerChangedCallback("gui_scaling", font_setting_changed, NULL);
+}
+
+/******************************************************************************/
+FontEngine::~FontEngine()
+{
+ cleanCache();
+}
+
+/******************************************************************************/
+void FontEngine::cleanCache()
+{
+ for (auto &font_cache_it : m_font_cache) {
+
+ for (auto &font_it : font_cache_it) {
+ font_it.second->drop();
+ font_it.second = NULL;
+ }
+ font_cache_it.clear();
+ }
+}
+
+/******************************************************************************/
+irr::gui::IGUIFont* FontEngine::getFont(unsigned int font_size, FontMode mode)
+{
+ if (mode == FM_Unspecified) {
+ mode = m_currentMode;
+ }
+ else if ((mode == FM_Mono) && (m_currentMode == FM_Simple)) {
+ mode = FM_SimpleMono;
+ }
+
+ if (font_size == FONT_SIZE_UNSPECIFIED) {
+ font_size = m_default_size[mode];
+ }
+
+ if ((font_size == m_lastSize) && (mode == m_lastMode)) {
+ return m_lastFont;
+ }
+
+ if (m_font_cache[mode].find(font_size) == m_font_cache[mode].end()) {
+ initFont(font_size, mode);
+ }
+
+ if (m_font_cache[mode].find(font_size) == m_font_cache[mode].end()) {
+ return NULL;
+ }
+
+ m_lastSize = font_size;
+ m_lastMode = mode;
+ m_lastFont = m_font_cache[mode][font_size];
+
+ return m_font_cache[mode][font_size];
+}
+
+/******************************************************************************/
+unsigned int FontEngine::getTextHeight(unsigned int font_size, FontMode mode)
+{
+ irr::gui::IGUIFont* font = getFont(font_size, mode);
+
+ // use current skin font as fallback
+ if (font == NULL) {
+ font = m_env->getSkin()->getFont();
+ }
+ FATAL_ERROR_IF(font == NULL, "Could not get skin font");
+
+ return font->getDimension(L"Some unimportant example String").Height;
+}
+
+/******************************************************************************/
+unsigned int FontEngine::getTextWidth(const std::wstring& text,
+ unsigned int font_size, FontMode mode)
+{
+ irr::gui::IGUIFont* font = getFont(font_size, mode);
+
+ // use current skin font as fallback
+ if (font == NULL) {
+ font = m_env->getSkin()->getFont();
+ }
+ FATAL_ERROR_IF(font == NULL, "Could not get font");
+
+ return font->getDimension(text.c_str()).Width;
+}
+
+
+/** get line height for a specific font (including empty room between lines) */
+unsigned int FontEngine::getLineHeight(unsigned int font_size, FontMode mode)
+{
+ irr::gui::IGUIFont* font = getFont(font_size, mode);
+
+ // use current skin font as fallback
+ if (font == NULL) {
+ font = m_env->getSkin()->getFont();
+ }
+ FATAL_ERROR_IF(font == NULL, "Could not get font");
+
+ return font->getDimension(L"Some unimportant example String").Height
+ + font->getKerningHeight();
+}
+
+/******************************************************************************/
+unsigned int FontEngine::getDefaultFontSize()
+{
+ return m_default_size[m_currentMode];
+}
+
+/******************************************************************************/
+void FontEngine::readSettings()
+{
+#if USE_FREETYPE
+ if (g_settings->getBool("freetype")) {
+ m_default_size[FM_Standard] = m_settings->getU16("font_size");
+ m_default_size[FM_Fallback] = m_settings->getU16("fallback_font_size");
+ m_default_size[FM_Mono] = m_settings->getU16("mono_font_size");
+
+ if (is_yes(gettext("needs_fallback_font"))) {
+ m_currentMode = FM_Fallback;
+ }
+ else {
+ m_currentMode = FM_Standard;
+ }
+ }
+#endif
+ m_default_size[FM_Simple] = m_settings->getU16("font_size");
+ m_default_size[FM_SimpleMono] = m_settings->getU16("mono_font_size");
+
+ cleanCache();
+ updateFontCache();
+ updateSkin();
+}
+
+/******************************************************************************/
+void FontEngine::updateSkin()
+{
+ gui::IGUIFont *font = getFont();
+
+ if (font)
+ m_env->getSkin()->setFont(font);
+ else
+ errorstream << "FontEngine: Default font file: " <<
+ "\n\t\"" << m_settings->get("font_path") << "\"" <<
+ "\n\trequired for current screen configuration was not found" <<
+ " or was invalid file format." <<
+ "\n\tUsing irrlicht default font." << std::endl;
+
+ // If we did fail to create a font our own make irrlicht find a default one
+ font = m_env->getSkin()->getFont();
+ FATAL_ERROR_IF(font == NULL, "Could not create/get font");
+
+ u32 text_height = font->getDimension(L"Hello, world!").Height;
+ infostream << "text_height=" << text_height << std::endl;
+}
+
+/******************************************************************************/
+void FontEngine::updateFontCache()
+{
+ /* the only font to be initialized is default one,
+ * all others are re-initialized on demand */
+ initFont(m_default_size[m_currentMode], m_currentMode);
+
+ /* reset font quick access */
+ m_lastMode = FM_Unspecified;
+ m_lastSize = 0;
+ m_lastFont = NULL;
+}
+
+/******************************************************************************/
+void FontEngine::initFont(unsigned int basesize, FontMode mode)
+{
+
+ std::string font_config_prefix;
+
+ if (mode == FM_Unspecified) {
+ mode = m_currentMode;
+ }
+
+ switch (mode) {
+
+ case FM_Standard:
+ font_config_prefix = "";
+ break;
+
+ case FM_Fallback:
+ font_config_prefix = "fallback_";
+ break;
+
+ case FM_Mono:
+ font_config_prefix = "mono_";
+ if (m_currentMode == FM_Simple)
+ mode = FM_SimpleMono;
+ break;
+
+ case FM_Simple: /* Fallthrough */
+ case FM_SimpleMono: /* Fallthrough */
+ default:
+ font_config_prefix = "";
+
+ }
+
+ if (m_font_cache[mode].find(basesize) != m_font_cache[mode].end())
+ return;
+
+ if ((mode == FM_Simple) || (mode == FM_SimpleMono)) {
+ initSimpleFont(basesize, mode);
+ return;
+ }
+#if USE_FREETYPE
+ else {
+ if (!is_yes(m_settings->get("freetype"))) {
+ return;
+ }
+ u32 size = std::floor(RenderingEngine::getDisplayDensity() *
+ m_settings->getFloat("gui_scaling") * basesize);
+ u32 font_shadow = 0;
+ u32 font_shadow_alpha = 0;
+
+ try {
+ font_shadow =
+ g_settings->getU16(font_config_prefix + "font_shadow");
+ } catch (SettingNotFoundException&) {}
+ try {
+ font_shadow_alpha =
+ g_settings->getU16(font_config_prefix + "font_shadow_alpha");
+ } catch (SettingNotFoundException&) {}
+
+ std::string font_path = g_settings->get(font_config_prefix + "font_path");
+
+ irr::gui::IGUIFont* font = gui::CGUITTFont::createTTFont(m_env,
+ font_path.c_str(), size, true, true, font_shadow,
+ font_shadow_alpha);
+
+ if (font) {
+ m_font_cache[mode][basesize] = font;
+ return;
+ }
+
+ if (font_config_prefix == "mono_") {
+ const std::string &mono_font_path = m_settings->getDefault("mono_font_path");
+
+ if (font_path != mono_font_path) {
+ // try original mono font
+ errorstream << "FontEngine: failed to load custom mono "
+ "font: " << font_path << ", trying to fall back to "
+ "original mono font" << std::endl;
+
+ font = gui::CGUITTFont::createTTFont(m_env,
+ mono_font_path.c_str(), size, true, true,
+ font_shadow, font_shadow_alpha);
+
+ if (font) {
+ m_font_cache[mode][basesize] = font;
+ return;
+ }
+ }
+ } else {
+ // try fallback font
+ errorstream << "FontEngine: failed to load: " << font_path <<
+ ", trying to fall back to fallback font" << std::endl;
+
+ font_path = g_settings->get(font_config_prefix + "fallback_font_path");
+
+ font = gui::CGUITTFont::createTTFont(m_env,
+ font_path.c_str(), size, true, true, font_shadow,
+ font_shadow_alpha);
+
+ if (font) {
+ m_font_cache[mode][basesize] = font;
+ return;
+ }
+
+ const std::string &fallback_font_path = m_settings->getDefault("fallback_font_path");
+
+ if (font_path != fallback_font_path) {
+ // try original fallback font
+ errorstream << "FontEngine: failed to load custom fallback "
+ "font: " << font_path << ", trying to fall back to "
+ "original fallback font" << std::endl;
+
+ font = gui::CGUITTFont::createTTFont(m_env,
+ fallback_font_path.c_str(), size, true, true,
+ font_shadow, font_shadow_alpha);
+
+ if (font) {
+ m_font_cache[mode][basesize] = font;
+ return;
+ }
+ }
+ }
+
+ // give up
+ errorstream << "FontEngine: failed to load freetype font: "
+ << font_path << std::endl;
+ errorstream << "minetest can not continue without a valid font. "
+ "Please correct the 'font_path' setting or install the font "
+ "file in the proper location" << std::endl;
+ abort();
+ }
+#endif
+}
+
+/** initialize a font without freetype */
+void FontEngine::initSimpleFont(unsigned int basesize, FontMode mode)
+{
+ assert(mode == FM_Simple || mode == FM_SimpleMono); // pre-condition
+
+ std::string font_path;
+ if (mode == FM_Simple) {
+ font_path = m_settings->get("font_path");
+ } else {
+ font_path = m_settings->get("mono_font_path");
+ }
+ std::string basename = font_path;
+ std::string ending = font_path.substr(font_path.length() -4);
+
+ if (ending == ".ttf") {
+ errorstream << "FontEngine: Not trying to open \"" << font_path
+ << "\" which seems to be a truetype font." << std::endl;
+ return;
+ }
+
+ if ((ending == ".xml") || (ending == ".png")) {
+ basename = font_path.substr(0,font_path.length()-4);
+ }
+
+ if (basesize == FONT_SIZE_UNSPECIFIED)
+ basesize = DEFAULT_FONT_SIZE;
+
+ u32 size = std::floor(
+ RenderingEngine::getDisplayDensity() *
+ m_settings->getFloat("gui_scaling") *
+ basesize);
+
+ irr::gui::IGUIFont* font = NULL;
+
+ for(unsigned int offset = 0; offset < MAX_FONT_SIZE_OFFSET; offset++) {
+
+ // try opening positive offset
+ std::stringstream fontsize_plus_png;
+ fontsize_plus_png << basename << "_" << (size + offset) << ".png";
+
+ if (fs::PathExists(fontsize_plus_png.str())) {
+ font = m_env->getFont(fontsize_plus_png.str().c_str());
+
+ if (font) {
+ verbosestream << "FontEngine: found font: " << fontsize_plus_png.str() << std::endl;
+ break;
+ }
+ }
+
+ std::stringstream fontsize_plus_xml;
+ fontsize_plus_xml << basename << "_" << (size + offset) << ".xml";
+
+ if (fs::PathExists(fontsize_plus_xml.str())) {
+ font = m_env->getFont(fontsize_plus_xml.str().c_str());
+
+ if (font) {
+ verbosestream << "FontEngine: found font: " << fontsize_plus_xml.str() << std::endl;
+ break;
+ }
+ }
+
+ // try negative offset
+ std::stringstream fontsize_minus_png;
+ fontsize_minus_png << basename << "_" << (size - offset) << ".png";
+
+ if (fs::PathExists(fontsize_minus_png.str())) {
+ font = m_env->getFont(fontsize_minus_png.str().c_str());
+
+ if (font) {
+ verbosestream << "FontEngine: found font: " << fontsize_minus_png.str() << std::endl;
+ break;
+ }
+ }
+
+ std::stringstream fontsize_minus_xml;
+ fontsize_minus_xml << basename << "_" << (size - offset) << ".xml";
+
+ if (fs::PathExists(fontsize_minus_xml.str())) {
+ font = m_env->getFont(fontsize_minus_xml.str().c_str());
+
+ if (font) {
+ verbosestream << "FontEngine: found font: " << fontsize_minus_xml.str() << std::endl;
+ break;
+ }
+ }
+ }
+
+ // try name direct
+ if (font == NULL) {
+ if (fs::PathExists(font_path)) {
+ font = m_env->getFont(font_path.c_str());
+ if (font)
+ verbosestream << "FontEngine: found font: " << font_path << std::endl;
+ }
+ }
+
+ if (font) {
+ font->grab();
+ m_font_cache[mode][basesize] = font;
+ }
+}
diff --git a/src/client/fontengine.h b/src/client/fontengine.h
new file mode 100644
index 000000000..a75618f86
--- /dev/null
+++ b/src/client/fontengine.h
@@ -0,0 +1,128 @@
+/*
+Minetest
+Copyright (C) 2010-2014 sapier <sapier at gmx dot net>
+
+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 <map>
+#include <vector>
+#include "util/basic_macros.h"
+#include <IGUIFont.h>
+#include <IGUISkin.h>
+#include <IGUIEnvironment.h>
+#include "settings.h"
+
+#define FONT_SIZE_UNSPECIFIED 0xFFFFFFFF
+
+enum FontMode {
+ FM_Standard = 0,
+ FM_Mono,
+ FM_Fallback,
+ FM_Simple,
+ FM_SimpleMono,
+ FM_MaxMode,
+ FM_Unspecified
+};
+
+class FontEngine
+{
+public:
+
+ FontEngine(Settings* main_settings, gui::IGUIEnvironment* env);
+
+ ~FontEngine();
+
+ /** get Font */
+ irr::gui::IGUIFont* getFont(unsigned int font_size=FONT_SIZE_UNSPECIFIED,
+ FontMode mode=FM_Unspecified);
+
+ /** get text height for a specific font */
+ unsigned int getTextHeight(unsigned int font_size=FONT_SIZE_UNSPECIFIED,
+ FontMode mode=FM_Unspecified);
+
+ /** get text width if a text for a specific font */
+ unsigned int getTextWidth(const std::string& text,
+ unsigned int font_size=FONT_SIZE_UNSPECIFIED,
+ FontMode mode=FM_Unspecified)
+ {
+ return getTextWidth(utf8_to_wide(text));
+ }
+
+ /** get text width if a text for a specific font */
+ unsigned int getTextWidth(const std::wstring& text,
+ unsigned int font_size=FONT_SIZE_UNSPECIFIED,
+ FontMode mode=FM_Unspecified);
+
+ /** get line height for a specific font (including empty room between lines) */
+ unsigned int getLineHeight(unsigned int font_size=FONT_SIZE_UNSPECIFIED,
+ FontMode mode=FM_Unspecified);
+
+ /** get default font size */
+ unsigned int getDefaultFontSize();
+
+ /** initialize font engine */
+ void initialize(Settings* main_settings, gui::IGUIEnvironment* env);
+
+ /** update internal parameters from settings */
+ void readSettings();
+
+private:
+ /** update content of font cache in case of a setting change made it invalid */
+ void updateFontCache();
+
+ /** initialize a new font */
+ void initFont(unsigned int basesize, FontMode mode=FM_Unspecified);
+
+ /** initialize a font without freetype */
+ void initSimpleFont(unsigned int basesize, FontMode mode);
+
+ /** update current minetest skin with font changes */
+ void updateSkin();
+
+ /** clean cache */
+ void cleanCache();
+
+ /** pointer to settings for registering callbacks or reading config */
+ Settings* m_settings = nullptr;
+
+ /** pointer to irrlicht gui environment */
+ gui::IGUIEnvironment* m_env = nullptr;
+
+ /** internal storage for caching fonts of different size */
+ std::map<unsigned int, irr::gui::IGUIFont*> m_font_cache[FM_MaxMode];
+
+ /** default font size to use */
+ unsigned int m_default_size[FM_MaxMode];
+
+ /** current font engine mode */
+ FontMode m_currentMode = FM_Standard;
+
+ /** font mode of last request */
+ FontMode m_lastMode;
+
+ /** size of last request */
+ unsigned int m_lastSize = 0;
+
+ /** last font returned */
+ irr::gui::IGUIFont* m_lastFont = nullptr;
+
+ DISABLE_CLASS_COPY(FontEngine);
+};
+
+/** interface to access main font engine*/
+extern FontEngine* g_fontengine;
diff --git a/src/client/game.cpp b/src/client/game.cpp
new file mode 100644
index 000000000..6cf6723e9
--- /dev/null
+++ b/src/client/game.cpp
@@ -0,0 +1,4218 @@
+/*
+Minetest
+Copyright (C) 2010-2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+
+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 "game.h"
+
+#include <iomanip>
+#include <cmath>
+#include "client/renderingengine.h"
+#include "camera.h"
+#include "client.h"
+#include "client/clientevent.h"
+#include "client/gameui.h"
+#include "client/inputhandler.h"
+#include "client/tile.h" // For TextureSource
+#include "client/keys.h"
+#include "client/joystick_controller.h"
+#include "clientmap.h"
+#include "clouds.h"
+#include "config.h"
+#include "content_cao.h"
+#include "client/event_manager.h"
+#include "fontengine.h"
+#include "itemdef.h"
+#include "log.h"
+#include "filesys.h"
+#include "gettext.h"
+#include "gui/guiChatConsole.h"
+#include "gui/guiConfirmRegistration.h"
+#include "gui/guiFormSpecMenu.h"
+#include "gui/guiKeyChangeMenu.h"
+#include "gui/guiPasswordChange.h"
+#include "gui/guiVolumeChange.h"
+#include "gui/mainmenumanager.h"
+#include "gui/profilergraph.h"
+#include "mapblock.h"
+#include "minimap.h"
+#include "nodedef.h" // Needed for determining pointing to nodes
+#include "nodemetadata.h"
+#include "particles.h"
+#include "porting.h"
+#include "profiler.h"
+#include "quicktune_shortcutter.h"
+#include "raycast.h"
+#include "server.h"
+#include "settings.h"
+#include "shader.h"
+#include "sky.h"
+#include "translation.h"
+#include "util/basic_macros.h"
+#include "util/directiontables.h"
+#include "util/pointedthing.h"
+#include "irrlicht_changes/static_text.h"
+#include "version.h"
+#include "script/scripting_client.h"
+
+#if USE_SOUND
+ #include "client/sound_openal.h"
+#else
+ #include "client/sound.h"
+#endif
+/*
+ Text input system
+*/
+
+struct TextDestNodeMetadata : public TextDest
+{
+ TextDestNodeMetadata(v3s16 p, Client *client)
+ {
+ m_p = p;
+ m_client = client;
+ }
+ // This is deprecated I guess? -celeron55
+ void gotText(const std::wstring &text)
+ {
+ std::string ntext = wide_to_utf8(text);
+ infostream << "Submitting 'text' field of node at (" << m_p.X << ","
+ << m_p.Y << "," << m_p.Z << "): " << ntext << std::endl;
+ StringMap fields;
+ fields["text"] = ntext;
+ m_client->sendNodemetaFields(m_p, "", fields);
+ }
+ void gotText(const StringMap &fields)
+ {
+ m_client->sendNodemetaFields(m_p, "", fields);
+ }
+
+ v3s16 m_p;
+ Client *m_client;
+};
+
+struct TextDestPlayerInventory : public TextDest
+{
+ TextDestPlayerInventory(Client *client)
+ {
+ m_client = client;
+ m_formname = "";
+ }
+ TextDestPlayerInventory(Client *client, const std::string &formname)
+ {
+ m_client = client;
+ m_formname = formname;
+ }
+ void gotText(const StringMap &fields)
+ {
+ m_client->sendInventoryFields(m_formname, fields);
+ }
+
+ Client *m_client;
+};
+
+struct LocalFormspecHandler : public TextDest
+{
+ LocalFormspecHandler(const std::string &formname)
+ {
+ m_formname = formname;
+ }
+
+ LocalFormspecHandler(const std::string &formname, Client *client):
+ m_client(client)
+ {
+ m_formname = formname;
+ }
+
+ void gotText(const StringMap &fields)
+ {
+ if (m_formname == "MT_PAUSE_MENU") {
+ if (fields.find("btn_sound") != fields.end()) {
+ g_gamecallback->changeVolume();
+ return;
+ }
+
+ if (fields.find("btn_key_config") != fields.end()) {
+ g_gamecallback->keyConfig();
+ return;
+ }
+
+ if (fields.find("btn_exit_menu") != fields.end()) {
+ g_gamecallback->disconnect();
+ return;
+ }
+
+ if (fields.find("btn_exit_os") != fields.end()) {
+ g_gamecallback->exitToOS();
+#ifndef __ANDROID__
+ RenderingEngine::get_raw_device()->closeDevice();
+#endif
+ return;
+ }
+
+ if (fields.find("btn_change_password") != fields.end()) {
+ g_gamecallback->changePassword();
+ return;
+ }
+
+ if (fields.find("quit") != fields.end()) {
+ return;
+ }
+
+ if (fields.find("btn_continue") != fields.end()) {
+ return;
+ }
+ }
+
+ if (m_formname == "MT_DEATH_SCREEN") {
+ assert(m_client != 0);
+ m_client->sendRespawn();
+ return;
+ }
+
+ if (m_client && m_client->moddingEnabled())
+ m_client->getScript()->on_formspec_input(m_formname, fields);
+ }
+
+ Client *m_client = nullptr;
+};
+
+/* Form update callback */
+
+class NodeMetadataFormSource: public IFormSource
+{
+public:
+ NodeMetadataFormSource(ClientMap *map, v3s16 p):
+ m_map(map),
+ m_p(p)
+ {
+ }
+ const std::string &getForm() const
+ {
+ static const std::string empty_string = "";
+ NodeMetadata *meta = m_map->getNodeMetadata(m_p);
+
+ if (!meta)
+ return empty_string;
+
+ return meta->getString("formspec");
+ }
+
+ virtual std::string resolveText(const std::string &str)
+ {
+ NodeMetadata *meta = m_map->getNodeMetadata(m_p);
+
+ if (!meta)
+ return str;
+
+ return meta->resolveString(str);
+ }
+
+ ClientMap *m_map;
+ v3s16 m_p;
+};
+
+class PlayerInventoryFormSource: public IFormSource
+{
+public:
+ PlayerInventoryFormSource(Client *client):
+ m_client(client)
+ {
+ }
+
+ const std::string &getForm() const
+ {
+ LocalPlayer *player = m_client->getEnv().getLocalPlayer();
+ return player->inventory_formspec;
+ }
+
+ Client *m_client;
+};
+
+class NodeDugEvent: public MtEvent
+{
+public:
+ v3s16 p;
+ MapNode n;
+
+ NodeDugEvent(v3s16 p, MapNode n):
+ p(p),
+ n(n)
+ {}
+ MtEvent::Type getType() const
+ {
+ return MtEvent::NODE_DUG;
+ }
+};
+
+class SoundMaker
+{
+ ISoundManager *m_sound;
+ const NodeDefManager *m_ndef;
+public:
+ bool makes_footstep_sound;
+ float m_player_step_timer;
+
+ SimpleSoundSpec m_player_step_sound;
+ SimpleSoundSpec m_player_leftpunch_sound;
+ SimpleSoundSpec m_player_rightpunch_sound;
+
+ SoundMaker(ISoundManager *sound, const NodeDefManager *ndef):
+ m_sound(sound),
+ m_ndef(ndef),
+ makes_footstep_sound(true),
+ m_player_step_timer(0)
+ {
+ }
+
+ void playPlayerStep()
+ {
+ if (m_player_step_timer <= 0 && m_player_step_sound.exists()) {
+ m_player_step_timer = 0.03;
+ if (makes_footstep_sound)
+ m_sound->playSound(m_player_step_sound, false);
+ }
+ }
+
+ static void viewBobbingStep(MtEvent *e, void *data)
+ {
+ SoundMaker *sm = (SoundMaker *)data;
+ sm->playPlayerStep();
+ }
+
+ static void playerRegainGround(MtEvent *e, void *data)
+ {
+ SoundMaker *sm = (SoundMaker *)data;
+ sm->playPlayerStep();
+ }
+
+ static void playerJump(MtEvent *e, void *data)
+ {
+ //SoundMaker *sm = (SoundMaker*)data;
+ }
+
+ static void cameraPunchLeft(MtEvent *e, void *data)
+ {
+ SoundMaker *sm = (SoundMaker *)data;
+ sm->m_sound->playSound(sm->m_player_leftpunch_sound, false);
+ }
+
+ static void cameraPunchRight(MtEvent *e, void *data)
+ {
+ SoundMaker *sm = (SoundMaker *)data;
+ sm->m_sound->playSound(sm->m_player_rightpunch_sound, false);
+ }
+
+ static void nodeDug(MtEvent *e, void *data)
+ {
+ SoundMaker *sm = (SoundMaker *)data;
+ NodeDugEvent *nde = (NodeDugEvent *)e;
+ sm->m_sound->playSound(sm->m_ndef->get(nde->n).sound_dug, false);
+ }
+
+ static void playerDamage(MtEvent *e, void *data)
+ {
+ SoundMaker *sm = (SoundMaker *)data;
+ sm->m_sound->playSound(SimpleSoundSpec("player_damage", 0.5), false);
+ }
+
+ static void playerFallingDamage(MtEvent *e, void *data)
+ {
+ SoundMaker *sm = (SoundMaker *)data;
+ sm->m_sound->playSound(SimpleSoundSpec("player_falling_damage", 0.5), false);
+ }
+
+ void registerReceiver(MtEventManager *mgr)
+ {
+ mgr->reg(MtEvent::VIEW_BOBBING_STEP, SoundMaker::viewBobbingStep, this);
+ mgr->reg(MtEvent::PLAYER_REGAIN_GROUND, SoundMaker::playerRegainGround, this);
+ mgr->reg(MtEvent::PLAYER_JUMP, SoundMaker::playerJump, this);
+ mgr->reg(MtEvent::CAMERA_PUNCH_LEFT, SoundMaker::cameraPunchLeft, this);
+ mgr->reg(MtEvent::CAMERA_PUNCH_RIGHT, SoundMaker::cameraPunchRight, this);
+ mgr->reg(MtEvent::NODE_DUG, SoundMaker::nodeDug, this);
+ mgr->reg(MtEvent::PLAYER_DAMAGE, SoundMaker::playerDamage, this);
+ mgr->reg(MtEvent::PLAYER_FALLING_DAMAGE, SoundMaker::playerFallingDamage, this);
+ }
+
+ void step(float dtime)
+ {
+ m_player_step_timer -= dtime;
+ }
+};
+
+// Locally stored sounds don't need to be preloaded because of this
+class GameOnDemandSoundFetcher: public OnDemandSoundFetcher
+{
+ std::set<std::string> m_fetched;
+private:
+ void paths_insert(std::set<std::string> &dst_paths,
+ const std::string &base,
+ const std::string &name)
+ {
+ dst_paths.insert(base + DIR_DELIM + "sounds" + DIR_DELIM + name + ".ogg");
+ dst_paths.insert(base + DIR_DELIM + "sounds" + DIR_DELIM + name + ".0.ogg");
+ dst_paths.insert(base + DIR_DELIM + "sounds" + DIR_DELIM + name + ".1.ogg");
+ dst_paths.insert(base + DIR_DELIM + "sounds" + DIR_DELIM + name + ".2.ogg");
+ dst_paths.insert(base + DIR_DELIM + "sounds" + DIR_DELIM + name + ".3.ogg");
+ dst_paths.insert(base + DIR_DELIM + "sounds" + DIR_DELIM + name + ".4.ogg");
+ dst_paths.insert(base + DIR_DELIM + "sounds" + DIR_DELIM + name + ".5.ogg");
+ dst_paths.insert(base + DIR_DELIM + "sounds" + DIR_DELIM + name + ".6.ogg");
+ dst_paths.insert(base + DIR_DELIM + "sounds" + DIR_DELIM + name + ".7.ogg");
+ dst_paths.insert(base + DIR_DELIM + "sounds" + DIR_DELIM + name + ".8.ogg");
+ dst_paths.insert(base + DIR_DELIM + "sounds" + DIR_DELIM + name + ".9.ogg");
+ }
+public:
+ void fetchSounds(const std::string &name,
+ std::set<std::string> &dst_paths,
+ std::set<std::string> &dst_datas)
+ {
+ if (m_fetched.count(name))
+ return;
+
+ m_fetched.insert(name);
+
+ paths_insert(dst_paths, porting::path_share, name);
+ paths_insert(dst_paths, porting::path_user, name);
+ }
+};
+
+
+// before 1.8 there isn't a "integer interface", only float
+#if (IRRLICHT_VERSION_MAJOR == 1 && IRRLICHT_VERSION_MINOR < 8)
+typedef f32 SamplerLayer_t;
+#else
+typedef s32 SamplerLayer_t;
+#endif
+
+
+class GameGlobalShaderConstantSetter : public IShaderConstantSetter
+{
+ Sky *m_sky;
+ bool *m_force_fog_off;
+ f32 *m_fog_range;
+ bool m_fog_enabled;
+ CachedPixelShaderSetting<float, 4> m_sky_bg_color;
+ CachedPixelShaderSetting<float> m_fog_distance;
+ CachedVertexShaderSetting<float> m_animation_timer_vertex;
+ CachedPixelShaderSetting<float> m_animation_timer_pixel;
+ CachedPixelShaderSetting<float, 3> m_day_light;
+ CachedPixelShaderSetting<float, 3> m_eye_position_pixel;
+ CachedVertexShaderSetting<float, 3> m_eye_position_vertex;
+ CachedPixelShaderSetting<float, 3> m_minimap_yaw;
+ CachedPixelShaderSetting<SamplerLayer_t> m_base_texture;
+ CachedPixelShaderSetting<SamplerLayer_t> m_normal_texture;
+ CachedPixelShaderSetting<SamplerLayer_t> m_texture_flags;
+ Client *m_client;
+
+public:
+ void onSettingsChange(const std::string &name)
+ {
+ if (name == "enable_fog")
+ m_fog_enabled = g_settings->getBool("enable_fog");
+ }
+
+ static void settingsCallback(const std::string &name, void *userdata)
+ {
+ reinterpret_cast<GameGlobalShaderConstantSetter*>(userdata)->onSettingsChange(name);
+ }
+
+ void setSky(Sky *sky) { m_sky = sky; }
+
+ GameGlobalShaderConstantSetter(Sky *sky, bool *force_fog_off,
+ f32 *fog_range, Client *client) :
+ m_sky(sky),
+ m_force_fog_off(force_fog_off),
+ m_fog_range(fog_range),
+ m_sky_bg_color("skyBgColor"),
+ m_fog_distance("fogDistance"),
+ m_animation_timer_vertex("animationTimer"),
+ m_animation_timer_pixel("animationTimer"),
+ m_day_light("dayLight"),
+ m_eye_position_pixel("eyePosition"),
+ m_eye_position_vertex("eyePosition"),
+ m_minimap_yaw("yawVec"),
+ m_base_texture("baseTexture"),
+ m_normal_texture("normalTexture"),
+ m_texture_flags("textureFlags"),
+ m_client(client)
+ {
+ g_settings->registerChangedCallback("enable_fog", settingsCallback, this);
+ m_fog_enabled = g_settings->getBool("enable_fog");
+ }
+
+ ~GameGlobalShaderConstantSetter()
+ {
+ g_settings->deregisterChangedCallback("enable_fog", settingsCallback, this);
+ }
+
+ virtual void onSetConstants(video::IMaterialRendererServices *services,
+ bool is_highlevel)
+ {
+ if (!is_highlevel)
+ return;
+
+ // Background color
+ video::SColor bgcolor = m_sky->getBgColor();
+ video::SColorf bgcolorf(bgcolor);
+ float bgcolorfa[4] = {
+ bgcolorf.r,
+ bgcolorf.g,
+ bgcolorf.b,
+ bgcolorf.a,
+ };
+ m_sky_bg_color.set(bgcolorfa, services);
+
+ // Fog distance
+ float fog_distance = 10000 * BS;
+
+ if (m_fog_enabled && !*m_force_fog_off)
+ fog_distance = *m_fog_range;
+
+ m_fog_distance.set(&fog_distance, services);
+
+ u32 daynight_ratio = (float)m_client->getEnv().getDayNightRatio();
+ video::SColorf sunlight;
+ get_sunlight_color(&sunlight, daynight_ratio);
+ float dnc[3] = {
+ sunlight.r,
+ sunlight.g,
+ sunlight.b };
+ m_day_light.set(dnc, services);
+
+ u32 animation_timer = porting::getTimeMs() % 100000;
+ float animation_timer_f = (float)animation_timer / 100000.f;
+ m_animation_timer_vertex.set(&animation_timer_f, services);
+ m_animation_timer_pixel.set(&animation_timer_f, services);
+
+ float eye_position_array[3];
+ v3f epos = m_client->getEnv().getLocalPlayer()->getEyePosition();
+#if (IRRLICHT_VERSION_MAJOR == 1 && IRRLICHT_VERSION_MINOR < 8)
+ eye_position_array[0] = epos.X;
+ eye_position_array[1] = epos.Y;
+ eye_position_array[2] = epos.Z;
+#else
+ epos.getAs3Values(eye_position_array);
+#endif
+ m_eye_position_pixel.set(eye_position_array, services);
+ m_eye_position_vertex.set(eye_position_array, services);
+
+ if (m_client->getMinimap()) {
+ float minimap_yaw_array[3];
+ v3f minimap_yaw = m_client->getMinimap()->getYawVec();
+#if (IRRLICHT_VERSION_MAJOR == 1 && IRRLICHT_VERSION_MINOR < 8)
+ minimap_yaw_array[0] = minimap_yaw.X;
+ minimap_yaw_array[1] = minimap_yaw.Y;
+ minimap_yaw_array[2] = minimap_yaw.Z;
+#else
+ minimap_yaw.getAs3Values(minimap_yaw_array);
+#endif
+ m_minimap_yaw.set(minimap_yaw_array, services);
+ }
+
+ SamplerLayer_t base_tex = 0,
+ normal_tex = 1,
+ flags_tex = 2;
+ m_base_texture.set(&base_tex, services);
+ m_normal_texture.set(&normal_tex, services);
+ m_texture_flags.set(&flags_tex, services);
+ }
+};
+
+
+class GameGlobalShaderConstantSetterFactory : public IShaderConstantSetterFactory
+{
+ Sky *m_sky;
+ bool *m_force_fog_off;
+ f32 *m_fog_range;
+ Client *m_client;
+ std::vector<GameGlobalShaderConstantSetter *> created_nosky;
+public:
+ GameGlobalShaderConstantSetterFactory(bool *force_fog_off,
+ f32 *fog_range, Client *client) :
+ m_sky(NULL),
+ m_force_fog_off(force_fog_off),
+ m_fog_range(fog_range),
+ m_client(client)
+ {}
+
+ void setSky(Sky *sky) {
+ m_sky = sky;
+ for (GameGlobalShaderConstantSetter *ggscs : created_nosky) {
+ ggscs->setSky(m_sky);
+ }
+ created_nosky.clear();
+ }
+
+ virtual IShaderConstantSetter* create()
+ {
+ GameGlobalShaderConstantSetter *scs = new GameGlobalShaderConstantSetter(
+ m_sky, m_force_fog_off, m_fog_range, m_client);
+ if (!m_sky)
+ created_nosky.push_back(scs);
+ return scs;
+ }
+};
+
+#ifdef __ANDROID__
+#define SIZE_TAG "size[11,5.5]"
+#else
+#define SIZE_TAG "size[11,5.5,true]" // Fixed size on desktop
+#endif
+
+/****************************************************************************
+
+ ****************************************************************************/
+
+const float object_hit_delay = 0.2;
+
+struct FpsControl {
+ u32 last_time, busy_time, sleep_time;
+};
+
+
+/* The reason the following structs are not anonymous structs within the
+ * class is that they are not used by the majority of member functions and
+ * many functions that do require objects of thse types do not modify them
+ * (so they can be passed as a const qualified parameter)
+ */
+
+struct GameRunData {
+ u16 dig_index;
+ u16 new_playeritem;
+ PointedThing pointed_old;
+ bool digging;
+ bool ldown_for_dig;
+ bool dig_instantly;
+ bool digging_blocked;
+ bool left_punch;
+ bool update_wielded_item_trigger;
+ bool reset_jump_timer;
+ float nodig_delay_timer;
+ float dig_time;
+ float dig_time_complete;
+ float repeat_rightclick_timer;
+ float object_hit_delay_timer;
+ float time_from_last_punch;
+ ClientActiveObject *selected_object;
+
+ float jump_timer;
+ float damage_flash;
+ float update_draw_list_timer;
+
+ f32 fog_range;
+
+ v3f update_draw_list_last_cam_dir;
+
+ float time_of_day_smooth;
+};
+
+class Game;
+
+struct ClientEventHandler
+{
+ void (Game::*handler)(ClientEvent *, CameraOrientation *);
+};
+
+/****************************************************************************
+ THE GAME
+ ****************************************************************************/
+
+/* This is not intended to be a public class. If a public class becomes
+ * desirable then it may be better to create another 'wrapper' class that
+ * hides most of the stuff in this class (nothing in this class is required
+ * by any other file) but exposes the public methods/data only.
+ */
+class Game {
+public:
+ Game();
+ ~Game();
+
+ bool startup(bool *kill,
+ bool random_input,
+ InputHandler *input,
+ const std::string &map_dir,
+ const std::string &playername,
+ const std::string &password,
+ // If address is "", local server is used and address is updated
+ std::string *address,
+ u16 port,
+ std::string &error_message,
+ bool *reconnect,
+ ChatBackend *chat_backend,
+ const SubgameSpec &gamespec, // Used for local game
+ bool simple_singleplayer_mode);
+
+ void run();
+ void shutdown();
+
+protected:
+
+ void extendedResourceCleanup();
+
+ // Basic initialisation
+ bool init(const std::string &map_dir, std::string *address,
+ u16 port,
+ const SubgameSpec &gamespec);
+ bool initSound();
+ bool createSingleplayerServer(const std::string &map_dir,
+ const SubgameSpec &gamespec, u16 port, std::string *address);
+
+ // Client creation
+ bool createClient(const std::string &playername,
+ const std::string &password, std::string *address, u16 port);
+ bool initGui();
+
+ // Client connection
+ bool connectToServer(const std::string &playername,
+ const std::string &password, std::string *address, u16 port,
+ bool *connect_ok, bool *aborted);
+ bool getServerContent(bool *aborted);
+
+ // Main loop
+
+ void updateInteractTimers(f32 dtime);
+ bool checkConnection();
+ bool handleCallbacks();
+ void processQueues();
+ void updateProfilers(const RunStats &stats, const FpsControl &draw_times, f32 dtime);
+ void addProfilerGraphs(const RunStats &stats, const FpsControl &draw_times, f32 dtime);
+ void updateStats(RunStats *stats, const FpsControl &draw_times, f32 dtime);
+
+ // Input related
+ void processUserInput(f32 dtime);
+ void processKeyInput();
+ void processItemSelection(u16 *new_playeritem);
+
+ void dropSelectedItem(bool single_item = false);
+ void openInventory();
+ void openConsole(float scale, const wchar_t *line=NULL);
+ void toggleFreeMove();
+ void toggleFreeMoveAlt();
+ void toggleFast();
+ void toggleNoClip();
+ void toggleCinematic();
+ void toggleAutoforward();
+
+ void toggleMinimap(bool shift_pressed);
+ void toggleFog();
+ void toggleDebug();
+ void toggleUpdateCamera();
+
+ void increaseViewRange();
+ void decreaseViewRange();
+ void toggleFullViewRange();
+ void checkZoomEnabled();
+
+ void updateCameraDirection(CameraOrientation *cam, float dtime);
+ void updateCameraOrientation(CameraOrientation *cam, float dtime);
+ void updatePlayerControl(const CameraOrientation &cam);
+ void step(f32 *dtime);
+ void processClientEvents(CameraOrientation *cam);
+ void updateCamera(u32 busy_time, f32 dtime);
+ void updateSound(f32 dtime);
+ void processPlayerInteraction(f32 dtime, bool show_hud, bool show_debug);
+ /*!
+ * Returns the object or node the player is pointing at.
+ * Also updates the selected thing in the Hud.
+ *
+ * @param[in] shootline the shootline, starting from
+ * the camera position. This also gives the maximal distance
+ * of the search.
+ * @param[in] liquids_pointable if false, liquids are ignored
+ * @param[in] look_for_object if false, objects are ignored
+ * @param[in] camera_offset offset of the camera
+ * @param[out] selected_object the selected object or
+ * NULL if not found
+ */
+ PointedThing updatePointedThing(
+ const core::line3d<f32> &shootline, bool liquids_pointable,
+ bool look_for_object, const v3s16 &camera_offset);
+ void handlePointingAtNothing(const ItemStack &playerItem);
+ void handlePointingAtNode(const PointedThing &pointed,
+ const ItemDefinition &playeritem_def, const ItemStack &playeritem,
+ const ToolCapabilities &playeritem_toolcap, f32 dtime);
+ void handlePointingAtObject(const PointedThing &pointed, const ItemStack &playeritem,
+ const v3f &player_position, bool show_debug);
+ void handleDigging(const PointedThing &pointed, const v3s16 &nodepos,
+ const ToolCapabilities &playeritem_toolcap, f32 dtime);
+ void updateFrame(ProfilerGraph *graph, RunStats *stats, f32 dtime,
+ const CameraOrientation &cam);
+ void updateProfilerGraphs(ProfilerGraph *graph);
+
+ // Misc
+ void limitFps(FpsControl *fps_timings, f32 *dtime);
+
+ void showOverlayMessage(const char *msg, float dtime, int percent,
+ bool draw_clouds = true);
+
+ static void settingChangedCallback(const std::string &setting_name, void *data);
+ void readSettings();
+
+ inline bool isKeyDown(GameKeyType k)
+ {
+ return input->isKeyDown(k);
+ }
+ inline bool wasKeyDown(GameKeyType k)
+ {
+ return input->wasKeyDown(k);
+ }
+
+#ifdef __ANDROID__
+ void handleAndroidChatInput();
+#endif
+
+private:
+ struct Flags {
+ bool force_fog_off = false;
+ bool disable_camera_update = false;
+ };
+
+ void showDeathFormspec();
+ void showPauseMenu();
+
+ // ClientEvent handlers
+ void handleClientEvent_None(ClientEvent *event, CameraOrientation *cam);
+ void handleClientEvent_PlayerDamage(ClientEvent *event, CameraOrientation *cam);
+ void handleClientEvent_PlayerForceMove(ClientEvent *event, CameraOrientation *cam);
+ void handleClientEvent_Deathscreen(ClientEvent *event, CameraOrientation *cam);
+ void handleClientEvent_ShowFormSpec(ClientEvent *event, CameraOrientation *cam);
+ void handleClientEvent_ShowLocalFormSpec(ClientEvent *event, CameraOrientation *cam);
+ void handleClientEvent_HandleParticleEvent(ClientEvent *event,
+ CameraOrientation *cam);
+ void handleClientEvent_HudAdd(ClientEvent *event, CameraOrientation *cam);
+ void handleClientEvent_HudRemove(ClientEvent *event, CameraOrientation *cam);
+ void handleClientEvent_HudChange(ClientEvent *event, CameraOrientation *cam);
+ void handleClientEvent_SetSky(ClientEvent *event, CameraOrientation *cam);
+ void handleClientEvent_OverrideDayNigthRatio(ClientEvent *event,
+ CameraOrientation *cam);
+ void handleClientEvent_CloudParams(ClientEvent *event, CameraOrientation *cam);
+
+ void updateChat(f32 dtime, const v2u32 &screensize);
+
+ bool nodePlacementPrediction(const ItemDefinition &playeritem_def,
+ const ItemStack &playeritem, const v3s16 &nodepos, const v3s16 &neighbourpos);
+ static const ClientEventHandler clientEventHandler[CLIENTEVENT_MAX];
+
+ InputHandler *input = nullptr;
+
+ Client *client = nullptr;
+ Server *server = nullptr;
+
+ IWritableTextureSource *texture_src = nullptr;
+ IWritableShaderSource *shader_src = nullptr;
+
+ // When created, these will be filled with data received from the server
+ IWritableItemDefManager *itemdef_manager = nullptr;
+ NodeDefManager *nodedef_manager = nullptr;
+
+ GameOnDemandSoundFetcher soundfetcher; // useful when testing
+ ISoundManager *sound = nullptr;
+ bool sound_is_dummy = false;
+ SoundMaker *soundmaker = nullptr;
+
+ ChatBackend *chat_backend = nullptr;
+
+ GUIFormSpecMenu *current_formspec = nullptr;
+ //default: "". If other than "", empty show_formspec packets will only close the formspec when the formname matches
+ std::string cur_formname;
+
+ EventManager *eventmgr = nullptr;
+ QuicktuneShortcutter *quicktune = nullptr;
+ bool registration_confirmation_shown = false;
+
+ std::unique_ptr<GameUI> m_game_ui;
+ GUIChatConsole *gui_chat_console = nullptr; // Free using ->Drop()
+ MapDrawControl *draw_control = nullptr;
+ Camera *camera = nullptr;
+ Clouds *clouds = nullptr; // Free using ->Drop()
+ Sky *sky = nullptr; // Free using ->Drop()
+ Inventory *local_inventory = nullptr;
+ Hud *hud = nullptr;
+ Minimap *mapper = nullptr;
+
+ GameRunData runData;
+ Flags m_flags;
+
+ /* 'cache'
+ This class does take ownership/responsibily for cleaning up etc of any of
+ these items (e.g. device)
+ */
+ IrrlichtDevice *device;
+ video::IVideoDriver *driver;
+ scene::ISceneManager *smgr;
+ bool *kill;
+ std::string *error_message;
+ bool *reconnect_requested;
+ scene::ISceneNode *skybox;
+
+ bool random_input;
+ bool simple_singleplayer_mode;
+ /* End 'cache' */
+
+ /* Pre-calculated values
+ */
+ int crack_animation_length;
+
+ IntervalLimiter profiler_interval;
+
+ /*
+ * TODO: Local caching of settings is not optimal and should at some stage
+ * be updated to use a global settings object for getting thse values
+ * (as opposed to the this local caching). This can be addressed in
+ * a later release.
+ */
+ bool m_cache_doubletap_jump;
+ bool m_cache_enable_clouds;
+ bool m_cache_enable_joysticks;
+ bool m_cache_enable_particles;
+ bool m_cache_enable_fog;
+ bool m_cache_enable_noclip;
+ bool m_cache_enable_free_move;
+ f32 m_cache_mouse_sensitivity;
+ f32 m_cache_joystick_frustum_sensitivity;
+ f32 m_repeat_right_click_time;
+ f32 m_cache_cam_smoothing;
+ f32 m_cache_fog_start;
+
+ bool m_invert_mouse = false;
+ bool m_first_loop_after_window_activation = false;
+ bool m_camera_offset_changed = false;
+
+ bool m_does_lost_focus_pause_game = false;
+
+#ifdef __ANDROID__
+ bool m_cache_hold_aux1;
+ bool m_android_chat_open;
+#endif
+};
+
+Game::Game() :
+ m_game_ui(new GameUI())
+{
+ g_settings->registerChangedCallback("doubletap_jump",
+ &settingChangedCallback, this);
+ g_settings->registerChangedCallback("enable_clouds",
+ &settingChangedCallback, this);
+ g_settings->registerChangedCallback("doubletap_joysticks",
+ &settingChangedCallback, this);
+ g_settings->registerChangedCallback("enable_particles",
+ &settingChangedCallback, this);
+ g_settings->registerChangedCallback("enable_fog",
+ &settingChangedCallback, this);
+ g_settings->registerChangedCallback("mouse_sensitivity",
+ &settingChangedCallback, this);
+ g_settings->registerChangedCallback("joystick_frustum_sensitivity",
+ &settingChangedCallback, this);
+ g_settings->registerChangedCallback("repeat_rightclick_time",
+ &settingChangedCallback, this);
+ g_settings->registerChangedCallback("noclip",
+ &settingChangedCallback, this);
+ g_settings->registerChangedCallback("free_move",
+ &settingChangedCallback, this);
+ g_settings->registerChangedCallback("cinematic",
+ &settingChangedCallback, this);
+ g_settings->registerChangedCallback("cinematic_camera_smoothing",
+ &settingChangedCallback, this);
+ g_settings->registerChangedCallback("camera_smoothing",
+ &settingChangedCallback, this);
+
+ readSettings();
+
+#ifdef __ANDROID__
+ m_cache_hold_aux1 = false; // This is initialised properly later
+#endif
+
+}
+
+
+
+/****************************************************************************
+ MinetestApp Public
+ ****************************************************************************/
+
+Game::~Game()
+{
+ delete client;
+ delete soundmaker;
+ if (!sound_is_dummy)
+ delete sound;
+
+ delete server; // deleted first to stop all server threads
+
+ delete hud;
+ delete local_inventory;
+ delete camera;
+ delete quicktune;
+ delete eventmgr;
+ delete texture_src;
+ delete shader_src;
+ delete nodedef_manager;
+ delete itemdef_manager;
+ delete draw_control;
+
+ extendedResourceCleanup();
+
+ g_settings->deregisterChangedCallback("doubletap_jump",
+ &settingChangedCallback, this);
+ g_settings->deregisterChangedCallback("enable_clouds",
+ &settingChangedCallback, this);
+ g_settings->deregisterChangedCallback("enable_particles",
+ &settingChangedCallback, this);
+ g_settings->deregisterChangedCallback("enable_fog",
+ &settingChangedCallback, this);
+ g_settings->deregisterChangedCallback("mouse_sensitivity",
+ &settingChangedCallback, this);
+ g_settings->deregisterChangedCallback("repeat_rightclick_time",
+ &settingChangedCallback, this);
+ g_settings->deregisterChangedCallback("noclip",
+ &settingChangedCallback, this);
+ g_settings->deregisterChangedCallback("free_move",
+ &settingChangedCallback, this);
+ g_settings->deregisterChangedCallback("cinematic",
+ &settingChangedCallback, this);
+ g_settings->deregisterChangedCallback("cinematic_camera_smoothing",
+ &settingChangedCallback, this);
+ g_settings->deregisterChangedCallback("camera_smoothing",
+ &settingChangedCallback, this);
+}
+
+bool Game::startup(bool *kill,
+ bool random_input,
+ InputHandler *input,
+ const std::string &map_dir,
+ const std::string &playername,
+ const std::string &password,
+ std::string *address, // can change if simple_singleplayer_mode
+ u16 port,
+ std::string &error_message,
+ bool *reconnect,
+ ChatBackend *chat_backend,
+ const SubgameSpec &gamespec,
+ bool simple_singleplayer_mode)
+{
+ // "cache"
+ this->device = RenderingEngine::get_raw_device();
+ this->kill = kill;
+ this->error_message = &error_message;
+ this->reconnect_requested = reconnect;
+ this->random_input = random_input;
+ this->input = input;
+ this->chat_backend = chat_backend;
+ this->simple_singleplayer_mode = simple_singleplayer_mode;
+
+ input->keycache.populate();
+
+ driver = device->getVideoDriver();
+ smgr = RenderingEngine::get_scene_manager();
+
+ RenderingEngine::get_scene_manager()->getParameters()->
+ setAttribute(scene::OBJ_LOADER_IGNORE_MATERIAL_FILES, true);
+
+ // Reinit runData
+ runData = GameRunData();
+ runData.time_from_last_punch = 10.0;
+ runData.update_wielded_item_trigger = true;
+
+ m_game_ui->initFlags();
+
+ m_invert_mouse = g_settings->getBool("invert_mouse");
+ m_first_loop_after_window_activation = true;
+
+ g_translations->clear();
+
+ if (!init(map_dir, address, port, gamespec))
+ return false;
+
+ if (!createClient(playername, password, address, port))
+ return false;
+
+ RenderingEngine::initialize(client, hud);
+
+ return true;
+}
+
+
+void Game::run()
+{
+ ProfilerGraph graph;
+ RunStats stats = { 0 };
+ CameraOrientation cam_view_target = { 0 };
+ CameraOrientation cam_view = { 0 };
+ FpsControl draw_times = { 0 };
+ f32 dtime; // in seconds
+
+ /* Clear the profiler */
+ Profiler::GraphValues dummyvalues;
+ g_profiler->graphGet(dummyvalues);
+
+ draw_times.last_time = RenderingEngine::get_timer_time();
+
+ set_light_table(g_settings->getFloat("display_gamma"));
+
+#ifdef __ANDROID__
+ m_cache_hold_aux1 = g_settings->getBool("fast_move")
+ && client->checkPrivilege("fast");
+#endif
+
+ irr::core::dimension2d<u32> previous_screen_size(g_settings->getU16("screen_w"),
+ g_settings->getU16("screen_h"));
+
+ while (RenderingEngine::run()
+ && !(*kill || g_gamecallback->shutdown_requested
+ || (server && server->isShutdownRequested()))) {
+
+ const irr::core::dimension2d<u32> &current_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<u32>(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;
+ }
+
+ /* Must be called immediately after a device->run() call because it
+ * uses device->getTimer()->getTime()
+ */
+ limitFps(&draw_times, &dtime);
+
+ updateStats(&stats, draw_times, dtime);
+ updateInteractTimers(dtime);
+
+ if (!checkConnection())
+ break;
+ if (!handleCallbacks())
+ break;
+
+ processQueues();
+
+ m_game_ui->clearInfoText();
+ hud->resizeHotbar();
+
+ updateProfilers(stats, draw_times, dtime);
+ processUserInput(dtime);
+ // Update camera before player movement to avoid camera lag of one frame
+ updateCameraDirection(&cam_view_target, dtime);
+ cam_view.camera_yaw += (cam_view_target.camera_yaw -
+ cam_view.camera_yaw) * m_cache_cam_smoothing;
+ cam_view.camera_pitch += (cam_view_target.camera_pitch -
+ cam_view.camera_pitch) * m_cache_cam_smoothing;
+ updatePlayerControl(cam_view);
+ step(&dtime);
+ processClientEvents(&cam_view_target);
+ updateCamera(draw_times.busy_time, dtime);
+ updateSound(dtime);
+ processPlayerInteraction(dtime, m_game_ui->m_flags.show_hud,
+ m_game_ui->m_flags.show_debug);
+ updateFrame(&graph, &stats, dtime, cam_view);
+ updateProfilerGraphs(&graph);
+
+ // Update if minimap has been disabled by the server
+ m_game_ui->m_flags.show_minimap &= client->shouldShowMinimap();
+
+ if (m_does_lost_focus_pause_game && !device->isWindowFocused() && !isMenuActive()) {
+ showPauseMenu();
+ }
+ }
+}
+
+
+void Game::shutdown()
+{
+ RenderingEngine::finalize();
+#if IRRLICHT_VERSION_MAJOR == 1 && IRRLICHT_VERSION_MINOR <= 8
+ if (g_settings->get("3d_mode") == "pageflip") {
+ driver->setRenderTarget(irr::video::ERT_STEREO_BOTH_BUFFERS);
+ }
+#endif
+ if (current_formspec)
+ current_formspec->quitMenu();
+
+ showOverlayMessage("Shutting down...", 0, 0, false);
+
+ if (clouds)
+ clouds->drop();
+
+ if (gui_chat_console)
+ gui_chat_console->drop();
+
+ if (sky)
+ sky->drop();
+
+ /* cleanup menus */
+ while (g_menumgr.menuCount() > 0) {
+ g_menumgr.m_stack.front()->setVisible(false);
+ g_menumgr.deletingMenu(g_menumgr.m_stack.front());
+ }
+
+ if (current_formspec) {
+ current_formspec->drop();
+ current_formspec = NULL;
+ }
+
+ chat_backend->addMessage(L"", L"# Disconnected.");
+ chat_backend->addMessage(L"", L"");
+
+ if (client) {
+ client->Stop();
+ while (!client->isShutdown()) {
+ assert(texture_src != NULL);
+ assert(shader_src != NULL);
+ texture_src->processQueue();
+ shader_src->processQueue();
+ sleep_ms(100);
+ }
+ }
+}
+
+
+/****************************************************************************/
+/****************************************************************************
+ Startup
+ ****************************************************************************/
+/****************************************************************************/
+
+bool Game::init(
+ const std::string &map_dir,
+ std::string *address,
+ u16 port,
+ const SubgameSpec &gamespec)
+{
+ texture_src = createTextureSource();
+
+ showOverlayMessage("Loading...", 0, 0);
+
+ shader_src = createShaderSource();
+
+ itemdef_manager = createItemDefManager();
+ nodedef_manager = createNodeDefManager();
+
+ eventmgr = new EventManager();
+ quicktune = new QuicktuneShortcutter();
+
+ if (!(texture_src && shader_src && itemdef_manager && nodedef_manager
+ && eventmgr && quicktune))
+ return false;
+
+ if (!initSound())
+ return false;
+
+ // Create a server if not connecting to an existing one
+ if (address->empty()) {
+ if (!createSingleplayerServer(map_dir, gamespec, port, address))
+ return false;
+ }
+
+ return true;
+}
+
+bool Game::initSound()
+{
+#if USE_SOUND
+ if (g_settings->getBool("enable_sound")) {
+ infostream << "Attempting to use OpenAL audio" << std::endl;
+ sound = createOpenALSoundManager(g_sound_manager_singleton.get(), &soundfetcher);
+ if (!sound)
+ infostream << "Failed to initialize OpenAL audio" << std::endl;
+ } else
+ infostream << "Sound disabled." << std::endl;
+#endif
+
+ if (!sound) {
+ infostream << "Using dummy audio." << std::endl;
+ sound = &dummySoundManager;
+ sound_is_dummy = true;
+ }
+
+ soundmaker = new SoundMaker(sound, nodedef_manager);
+ if (!soundmaker)
+ return false;
+
+ soundmaker->registerReceiver(eventmgr);
+
+ return true;
+}
+
+bool Game::createSingleplayerServer(const std::string &map_dir,
+ const SubgameSpec &gamespec, u16 port, std::string *address)
+{
+ showOverlayMessage("Creating server...", 0, 5);
+
+ std::string bind_str = g_settings->get("bind_address");
+ Address bind_addr(0, 0, 0, 0, port);
+
+ if (g_settings->getBool("ipv6_server")) {
+ bind_addr.setAddress((IPv6AddressBytes *) NULL);
+ }
+
+ try {
+ bind_addr.Resolve(bind_str.c_str());
+ } catch (ResolveError &e) {
+ infostream << "Resolving bind address \"" << bind_str
+ << "\" failed: " << e.what()
+ << " -- Listening on all addresses." << std::endl;
+ }
+
+ if (bind_addr.isIPv6() && !g_settings->getBool("enable_ipv6")) {
+ *error_message = "Unable to listen on " +
+ bind_addr.serializeString() +
+ " because IPv6 is disabled";
+ errorstream << *error_message << std::endl;
+ return false;
+ }
+
+ server = new Server(map_dir, gamespec, simple_singleplayer_mode, bind_addr, false);
+ server->init();
+ server->start();
+
+ return true;
+}
+
+bool Game::createClient(const std::string &playername,
+ const std::string &password, std::string *address, u16 port)
+{
+ showOverlayMessage("Creating client...", 0, 10);
+
+ draw_control = new MapDrawControl;
+ if (!draw_control)
+ return false;
+
+ bool could_connect, connect_aborted;
+#ifdef HAVE_TOUCHSCREENGUI
+ if (g_touchscreengui) {
+ g_touchscreengui->init(texture_src);
+ g_touchscreengui->hide();
+ }
+#endif
+ if (!connectToServer(playername, password, address, port,
+ &could_connect, &connect_aborted))
+ return false;
+
+ if (!could_connect) {
+ if (error_message->empty() && !connect_aborted) {
+ // Should not happen if error messages are set properly
+ *error_message = "Connection failed for unknown reason";
+ errorstream << *error_message << std::endl;
+ }
+ return false;
+ }
+
+ if (!getServerContent(&connect_aborted)) {
+ if (error_message->empty() && !connect_aborted) {
+ // Should not happen if error messages are set properly
+ *error_message = "Connection failed for unknown reason";
+ errorstream << *error_message << std::endl;
+ }
+ return false;
+ }
+
+ GameGlobalShaderConstantSetterFactory *scsf = new GameGlobalShaderConstantSetterFactory(
+ &m_flags.force_fog_off, &runData.fog_range, client);
+ shader_src->addShaderConstantSetterFactory(scsf);
+
+ // Update cached textures, meshes and materials
+ client->afterContentReceived();
+
+ /* Camera
+ */
+ camera = new Camera(*draw_control, client);
+ if (!camera || !camera->successfullyCreated(*error_message))
+ return false;
+ client->setCamera(camera);
+
+ /* Clouds
+ */
+ if (m_cache_enable_clouds) {
+ clouds = new Clouds(smgr, -1, time(0));
+ if (!clouds) {
+ *error_message = "Memory allocation error (clouds)";
+ errorstream << *error_message << std::endl;
+ return false;
+ }
+ }
+
+ /* Skybox
+ */
+ sky = new Sky(-1, texture_src);
+ scsf->setSky(sky);
+ skybox = NULL; // This is used/set later on in the main run loop
+
+ local_inventory = new Inventory(itemdef_manager);
+
+ if (!(sky && local_inventory)) {
+ *error_message = "Memory allocation error (sky or local inventory)";
+ errorstream << *error_message << std::endl;
+ return false;
+ }
+
+ /* Pre-calculated values
+ */
+ video::ITexture *t = texture_src->getTexture("crack_anylength.png");
+ if (t) {
+ v2u32 size = t->getOriginalSize();
+ crack_animation_length = size.Y / size.X;
+ } else {
+ crack_animation_length = 5;
+ }
+
+ if (!initGui())
+ return false;
+
+ /* Set window caption
+ */
+ std::wstring str = utf8_to_wide(PROJECT_NAME_C);
+ str += L" ";
+ str += utf8_to_wide(g_version_hash);
+ str += L" [";
+ str += driver->getName();
+ str += L"]";
+ device->setWindowCaption(str.c_str());
+
+ LocalPlayer *player = client->getEnv().getLocalPlayer();
+ player->hurt_tilt_timer = 0;
+ player->hurt_tilt_strength = 0;
+
+ hud = new Hud(guienv, client, player, local_inventory);
+
+ if (!hud) {
+ *error_message = "Memory error: could not create HUD";
+ errorstream << *error_message << std::endl;
+ return false;
+ }
+
+ mapper = client->getMinimap();
+ if (mapper)
+ mapper->setMinimapMode(MINIMAP_MODE_OFF);
+
+ return true;
+}
+
+bool Game::initGui()
+{
+ m_game_ui->init();
+
+ // Remove stale "recent" chat messages from previous connections
+ chat_backend->clearRecentChat();
+
+ // Make sure the size of the recent messages buffer is right
+ chat_backend->applySettings();
+
+ // Chat backend and console
+ gui_chat_console = new GUIChatConsole(guienv, guienv->getRootGUIElement(),
+ -1, chat_backend, client, &g_menumgr);
+ if (!gui_chat_console) {
+ *error_message = "Could not allocate memory for chat console";
+ errorstream << *error_message << std::endl;
+ return false;
+ }
+
+#ifdef HAVE_TOUCHSCREENGUI
+
+ if (g_touchscreengui)
+ g_touchscreengui->show();
+
+#endif
+
+ return true;
+}
+
+bool Game::connectToServer(const std::string &playername,
+ const std::string &password, std::string *address, u16 port,
+ bool *connect_ok, bool *connection_aborted)
+{
+ *connect_ok = false; // Let's not be overly optimistic
+ *connection_aborted = false;
+ bool local_server_mode = false;
+
+ showOverlayMessage("Resolving address...", 0, 15);
+
+ Address connect_address(0, 0, 0, 0, port);
+
+ try {
+ connect_address.Resolve(address->c_str());
+
+ if (connect_address.isZero()) { // i.e. INADDR_ANY, IN6ADDR_ANY
+ //connect_address.Resolve("localhost");
+ if (connect_address.isIPv6()) {
+ IPv6AddressBytes addr_bytes;
+ addr_bytes.bytes[15] = 1;
+ connect_address.setAddress(&addr_bytes);
+ } else {
+ connect_address.setAddress(127, 0, 0, 1);
+ }
+ local_server_mode = true;
+ }
+ } catch (ResolveError &e) {
+ *error_message = std::string("Couldn't resolve address: ") + e.what();
+ errorstream << *error_message << std::endl;
+ return false;
+ }
+
+ if (connect_address.isIPv6() && !g_settings->getBool("enable_ipv6")) {
+ *error_message = "Unable to connect to " +
+ connect_address.serializeString() +
+ " because IPv6 is disabled";
+ errorstream << *error_message << std::endl;
+ return false;
+ }
+
+ client = new Client(playername.c_str(), password, *address,
+ *draw_control, texture_src, shader_src,
+ itemdef_manager, nodedef_manager, sound, eventmgr,
+ connect_address.isIPv6(), m_game_ui.get());
+
+ if (!client)
+ return false;
+
+ client->m_simple_singleplayer_mode = simple_singleplayer_mode;
+
+ infostream << "Connecting to server at ";
+ connect_address.print(&infostream);
+ infostream << std::endl;
+
+ client->connect(connect_address,
+ simple_singleplayer_mode || local_server_mode);
+
+ /*
+ Wait for server to accept connection
+ */
+
+ try {
+ input->clear();
+
+ FpsControl fps_control = { 0 };
+ f32 dtime;
+ f32 wait_time = 0; // in seconds
+
+ fps_control.last_time = RenderingEngine::get_timer_time();
+
+ while (RenderingEngine::run()) {
+
+ limitFps(&fps_control, &dtime);
+
+ // Update client and server
+ client->step(dtime);
+
+ if (server != NULL)
+ server->step(dtime);
+
+ // End condition
+ if (client->getState() == LC_Init) {
+ *connect_ok = true;
+ break;
+ }
+
+ // Break conditions
+ if (*connection_aborted)
+ break;
+
+ if (client->accessDenied()) {
+ *error_message = "Access denied. Reason: "
+ + client->accessDeniedReason();
+ *reconnect_requested = client->reconnectRequested();
+ errorstream << *error_message << std::endl;
+ break;
+ }
+
+ if (input->cancelPressed()) {
+ *connection_aborted = true;
+ infostream << "Connect aborted [Escape]" << std::endl;
+ break;
+ }
+
+ if (client->m_is_registration_confirmation_state) {
+ if (registration_confirmation_shown) {
+ // Keep drawing the GUI
+ RenderingEngine::draw_menu_scene(guienv, dtime, true);
+ } else {
+ registration_confirmation_shown = true;
+ (new GUIConfirmRegistration(guienv, guienv->getRootGUIElement(), -1,
+ &g_menumgr, client, playername, password, *address, connection_aborted))->drop();
+ }
+ } else {
+ wait_time += dtime;
+ // Only time out if we aren't waiting for the server we started
+ if (!address->empty() && wait_time > 10) {
+ *error_message = "Connection timed out.";
+ errorstream << *error_message << std::endl;
+ break;
+ }
+
+ // Update status
+ showOverlayMessage("Connecting to server...", dtime, 20);
+ }
+ }
+ } catch (con::PeerNotFoundException &e) {
+ // TODO: Should something be done here? At least an info/error
+ // message?
+ return false;
+ }
+
+ return true;
+}
+
+bool Game::getServerContent(bool *aborted)
+{
+ input->clear();
+
+ FpsControl fps_control = { 0 };
+ f32 dtime; // in seconds
+
+ fps_control.last_time = RenderingEngine::get_timer_time();
+
+ while (RenderingEngine::run()) {
+
+ limitFps(&fps_control, &dtime);
+
+ // Update client and server
+ client->step(dtime);
+
+ if (server != NULL)
+ server->step(dtime);
+
+ // End condition
+ if (client->mediaReceived() && client->itemdefReceived() &&
+ client->nodedefReceived()) {
+ break;
+ }
+
+ // Error conditions
+ if (!checkConnection())
+ return false;
+
+ if (client->getState() < LC_Init) {
+ *error_message = "Client disconnected";
+ errorstream << *error_message << std::endl;
+ return false;
+ }
+
+ if (input->cancelPressed()) {
+ *aborted = true;
+ infostream << "Connect aborted [Escape]" << std::endl;
+ return false;
+ }
+
+ // Display status
+ int progress = 25;
+
+ if (!client->itemdefReceived()) {
+ const wchar_t *text = wgettext("Item definitions...");
+ progress = 25;
+ RenderingEngine::draw_load_screen(text, guienv, texture_src,
+ dtime, progress);
+ delete[] text;
+ } else if (!client->nodedefReceived()) {
+ const wchar_t *text = wgettext("Node definitions...");
+ progress = 30;
+ RenderingEngine::draw_load_screen(text, guienv, texture_src,
+ dtime, progress);
+ delete[] text;
+ } else {
+ std::stringstream message;
+ std::fixed(message);
+ message.precision(0);
+ message << gettext("Media...") << " " << (client->mediaReceiveProgress()*100) << "%";
+ message.precision(2);
+
+ if ((USE_CURL == 0) ||
+ (!g_settings->getBool("enable_remote_media_server"))) {
+ float cur = client->getCurRate();
+ std::string cur_unit = gettext("KiB/s");
+
+ if (cur > 900) {
+ cur /= 1024.0;
+ cur_unit = gettext("MiB/s");
+ }
+
+ message << " (" << cur << ' ' << cur_unit << ")";
+ }
+
+ progress = 30 + client->mediaReceiveProgress() * 35 + 0.5;
+ RenderingEngine::draw_load_screen(utf8_to_wide(message.str()), guienv,
+ texture_src, dtime, progress);
+ }
+ }
+
+ return true;
+}
+
+
+/****************************************************************************/
+/****************************************************************************
+ Run
+ ****************************************************************************/
+/****************************************************************************/
+
+inline void Game::updateInteractTimers(f32 dtime)
+{
+ if (runData.nodig_delay_timer >= 0)
+ runData.nodig_delay_timer -= dtime;
+
+ if (runData.object_hit_delay_timer >= 0)
+ runData.object_hit_delay_timer -= dtime;
+
+ runData.time_from_last_punch += dtime;
+}
+
+
+/* returns false if game should exit, otherwise true
+ */
+inline bool Game::checkConnection()
+{
+ if (client->accessDenied()) {
+ *error_message = "Access denied. Reason: "
+ + client->accessDeniedReason();
+ *reconnect_requested = client->reconnectRequested();
+ errorstream << *error_message << std::endl;
+ return false;
+ }
+
+ return true;
+}
+
+
+/* returns false if game should exit, otherwise true
+ */
+inline bool Game::handleCallbacks()
+{
+ if (g_gamecallback->disconnect_requested) {
+ g_gamecallback->disconnect_requested = false;
+ return false;
+ }
+
+ if (g_gamecallback->changepassword_requested) {
+ (new GUIPasswordChange(guienv, guiroot, -1,
+ &g_menumgr, client))->drop();
+ g_gamecallback->changepassword_requested = false;
+ }
+
+ if (g_gamecallback->changevolume_requested) {
+ (new GUIVolumeChange(guienv, guiroot, -1,
+ &g_menumgr))->drop();
+ g_gamecallback->changevolume_requested = false;
+ }
+
+ if (g_gamecallback->keyconfig_requested) {
+ (new GUIKeyChangeMenu(guienv, guiroot, -1,
+ &g_menumgr))->drop();
+ g_gamecallback->keyconfig_requested = false;
+ }
+
+ if (g_gamecallback->keyconfig_changed) {
+ input->keycache.populate(); // update the cache with new settings
+ g_gamecallback->keyconfig_changed = false;
+ }
+
+ return true;
+}
+
+
+void Game::processQueues()
+{
+ texture_src->processQueue();
+ itemdef_manager->processQueue(client);
+ shader_src->processQueue();
+}
+
+
+void Game::updateProfilers(const RunStats &stats, const FpsControl &draw_times, f32 dtime)
+{
+ float profiler_print_interval =
+ g_settings->getFloat("profiler_print_interval");
+ bool print_to_log = true;
+
+ if (profiler_print_interval == 0) {
+ print_to_log = false;
+ profiler_print_interval = 5;
+ }
+
+ if (profiler_interval.step(dtime, profiler_print_interval)) {
+ if (print_to_log) {
+ infostream << "Profiler:" << std::endl;
+ g_profiler->print(infostream);
+ }
+
+ m_game_ui->updateProfiler();
+ g_profiler->clear();
+ }
+
+ addProfilerGraphs(stats, draw_times, dtime);
+}
+
+
+void Game::addProfilerGraphs(const RunStats &stats,
+ const FpsControl &draw_times, f32 dtime)
+{
+ g_profiler->graphAdd("mainloop_other",
+ draw_times.busy_time / 1000.0f - stats.drawtime / 1000.0f);
+
+ if (draw_times.sleep_time != 0)
+ g_profiler->graphAdd("mainloop_sleep", draw_times.sleep_time / 1000.0f);
+ g_profiler->graphAdd("mainloop_dtime", dtime);
+
+ g_profiler->add("Elapsed time", dtime);
+ g_profiler->avg("FPS", 1. / dtime);
+}
+
+
+void Game::updateStats(RunStats *stats, const FpsControl &draw_times,
+ f32 dtime)
+{
+
+ f32 jitter;
+ Jitter *jp;
+
+ /* Time average and jitter calculation
+ */
+ jp = &stats->dtime_jitter;
+ jp->avg = jp->avg * 0.96 + dtime * 0.04;
+
+ jitter = dtime - jp->avg;
+
+ if (jitter > jp->max)
+ jp->max = jitter;
+
+ jp->counter += dtime;
+
+ if (jp->counter > 0.0) {
+ jp->counter -= 3.0;
+ jp->max_sample = jp->max;
+ jp->max_fraction = jp->max_sample / (jp->avg + 0.001);
+ jp->max = 0.0;
+ }
+
+ /* Busytime average and jitter calculation
+ */
+ jp = &stats->busy_time_jitter;
+ jp->avg = jp->avg + draw_times.busy_time * 0.02;
+
+ jitter = draw_times.busy_time - jp->avg;
+
+ if (jitter > jp->max)
+ jp->max = jitter;
+ if (jitter < jp->min)
+ jp->min = jitter;
+
+ jp->counter += dtime;
+
+ if (jp->counter > 0.0) {
+ jp->counter -= 3.0;
+ jp->max_sample = jp->max;
+ jp->min_sample = jp->min;
+ jp->max = 0.0;
+ jp->min = 0.0;
+ }
+}
+
+
+
+/****************************************************************************
+ Input handling
+ ****************************************************************************/
+
+void Game::processUserInput(f32 dtime)
+{
+ // Reset input if window not active or some menu is active
+ if (!device->isWindowActive() || isMenuActive() || guienv->hasFocus(gui_chat_console)) {
+ input->clear();
+#ifdef HAVE_TOUCHSCREENGUI
+ g_touchscreengui->hide();
+#endif
+ }
+#ifdef HAVE_TOUCHSCREENGUI
+ else if (g_touchscreengui) {
+ /* on touchscreengui step may generate own input events which ain't
+ * what we want in case we just did clear them */
+ g_touchscreengui->step(dtime);
+ }
+#endif
+
+ if (!guienv->hasFocus(gui_chat_console) && gui_chat_console->isOpen()) {
+ gui_chat_console->closeConsoleAtOnce();
+ }
+
+ // Input handler step() (used by the random input generator)
+ input->step(dtime);
+
+#ifdef __ANDROID__
+ if (current_formspec != NULL)
+ current_formspec->getAndroidUIInput();
+ else
+ handleAndroidChatInput();
+#endif
+
+ // Increase timer for double tap of "keymap_jump"
+ if (m_cache_doubletap_jump && runData.jump_timer <= 0.2f)
+ runData.jump_timer += dtime;
+
+ processKeyInput();
+ processItemSelection(&runData.new_playeritem);
+}
+
+
+void Game::processKeyInput()
+{
+ if (wasKeyDown(KeyType::DROP)) {
+ dropSelectedItem(isKeyDown(KeyType::SNEAK));
+ } else if (wasKeyDown(KeyType::AUTOFORWARD)) {
+ toggleAutoforward();
+ } else if (wasKeyDown(KeyType::BACKWARD)) {
+ if (g_settings->getBool("continuous_forward"))
+ toggleAutoforward();
+ } else if (wasKeyDown(KeyType::INVENTORY)) {
+ openInventory();
+ } else if (input->cancelPressed()) {
+ if (!gui_chat_console->isOpenInhibited()) {
+ showPauseMenu();
+ }
+ } else if (wasKeyDown(KeyType::CHAT)) {
+ openConsole(0.2, L"");
+ } else if (wasKeyDown(KeyType::CMD)) {
+ openConsole(0.2, L"/");
+ } else if (wasKeyDown(KeyType::CMD_LOCAL)) {
+ if (client->moddingEnabled())
+ openConsole(0.2, L".");
+ else
+ m_game_ui->showStatusText(wgettext("CSM is disabled"));
+ } else if (wasKeyDown(KeyType::CONSOLE)) {
+ openConsole(core::clamp(g_settings->getFloat("console_height"), 0.1f, 1.0f));
+ } else if (wasKeyDown(KeyType::FREEMOVE)) {
+ toggleFreeMove();
+ } else if (wasKeyDown(KeyType::JUMP)) {
+ toggleFreeMoveAlt();
+ } else if (wasKeyDown(KeyType::FASTMOVE)) {
+ toggleFast();
+ } else if (wasKeyDown(KeyType::NOCLIP)) {
+ toggleNoClip();
+ } else if (wasKeyDown(KeyType::MUTE)) {
+ bool new_mute_sound = !g_settings->getBool("mute_sound");
+ g_settings->setBool("mute_sound", new_mute_sound);
+ if (new_mute_sound)
+ m_game_ui->showTranslatedStatusText("Sound muted");
+ else
+ m_game_ui->showTranslatedStatusText("Sound unmuted");
+ } else if (wasKeyDown(KeyType::INC_VOLUME)) {
+ float new_volume = rangelim(g_settings->getFloat("sound_volume") + 0.1f, 0.0f, 1.0f);
+ wchar_t buf[100];
+ g_settings->setFloat("sound_volume", new_volume);
+ const wchar_t *str = wgettext("Volume changed to %d%%");
+ swprintf(buf, sizeof(buf) / sizeof(wchar_t), str, myround(new_volume * 100));
+ delete[] str;
+ m_game_ui->showStatusText(buf);
+ } else if (wasKeyDown(KeyType::DEC_VOLUME)) {
+ float new_volume = rangelim(g_settings->getFloat("sound_volume") - 0.1f, 0.0f, 1.0f);
+ wchar_t buf[100];
+ g_settings->setFloat("sound_volume", new_volume);
+ const wchar_t *str = wgettext("Volume changed to %d%%");
+ swprintf(buf, sizeof(buf) / sizeof(wchar_t), str, myround(new_volume * 100));
+ delete[] str;
+ m_game_ui->showStatusText(buf);
+ } else if (wasKeyDown(KeyType::CINEMATIC)) {
+ toggleCinematic();
+ } else if (wasKeyDown(KeyType::SCREENSHOT)) {
+ client->makeScreenshot();
+ } else if (wasKeyDown(KeyType::TOGGLE_HUD)) {
+ m_game_ui->toggleHud();
+ } else if (wasKeyDown(KeyType::MINIMAP)) {
+ toggleMinimap(isKeyDown(KeyType::SNEAK));
+ } else if (wasKeyDown(KeyType::TOGGLE_CHAT)) {
+ m_game_ui->toggleChat();
+ } else if (wasKeyDown(KeyType::TOGGLE_FOG)) {
+ toggleFog();
+ } else if (wasKeyDown(KeyType::TOGGLE_UPDATE_CAMERA)) {
+ toggleUpdateCamera();
+ } else if (wasKeyDown(KeyType::TOGGLE_DEBUG)) {
+ toggleDebug();
+ } else if (wasKeyDown(KeyType::TOGGLE_PROFILER)) {
+ m_game_ui->toggleProfiler();
+ } else if (wasKeyDown(KeyType::INCREASE_VIEWING_RANGE)) {
+ increaseViewRange();
+ } else if (wasKeyDown(KeyType::DECREASE_VIEWING_RANGE)) {
+ decreaseViewRange();
+ } else if (wasKeyDown(KeyType::RANGESELECT)) {
+ toggleFullViewRange();
+ } else if (wasKeyDown(KeyType::ZOOM)) {
+ checkZoomEnabled();
+ } else if (wasKeyDown(KeyType::QUICKTUNE_NEXT)) {
+ quicktune->next();
+ } else if (wasKeyDown(KeyType::QUICKTUNE_PREV)) {
+ quicktune->prev();
+ } else if (wasKeyDown(KeyType::QUICKTUNE_INC)) {
+ quicktune->inc();
+ } else if (wasKeyDown(KeyType::QUICKTUNE_DEC)) {
+ quicktune->dec();
+ }
+
+ if (!isKeyDown(KeyType::JUMP) && runData.reset_jump_timer) {
+ runData.reset_jump_timer = false;
+ runData.jump_timer = 0.0f;
+ }
+
+ if (quicktune->hasMessage()) {
+ m_game_ui->showStatusText(utf8_to_wide(quicktune->getMessage()));
+ }
+}
+
+void Game::processItemSelection(u16 *new_playeritem)
+{
+ LocalPlayer *player = client->getEnv().getLocalPlayer();
+
+ /* Item selection using mouse wheel
+ */
+ *new_playeritem = client->getPlayerItem();
+
+ s32 wheel = input->getMouseWheel();
+ u16 max_item = MYMIN(PLAYER_INVENTORY_SIZE - 1,
+ player->hud_hotbar_itemcount - 1);
+
+ s32 dir = wheel;
+
+ if (input->joystick.wasKeyDown(KeyType::SCROLL_DOWN) ||
+ wasKeyDown(KeyType::HOTBAR_NEXT)) {
+ dir = -1;
+ }
+
+ if (input->joystick.wasKeyDown(KeyType::SCROLL_UP) ||
+ wasKeyDown(KeyType::HOTBAR_PREV)) {
+ dir = 1;
+ }
+
+ if (dir < 0)
+ *new_playeritem = *new_playeritem < max_item ? *new_playeritem + 1 : 0;
+ else if (dir > 0)
+ *new_playeritem = *new_playeritem > 0 ? *new_playeritem - 1 : max_item;
+ // else dir == 0
+
+ /* Item selection using hotbar slot keys
+ */
+ for (u16 i = 0; i < 23; i++) {
+ if (wasKeyDown((GameKeyType) (KeyType::SLOT_1 + i))) {
+ if (i < PLAYER_INVENTORY_SIZE && i < player->hud_hotbar_itemcount) {
+ *new_playeritem = i;
+ infostream << "Selected item: " << new_playeritem << std::endl;
+ }
+ break;
+ }
+ }
+}
+
+
+void Game::dropSelectedItem(bool single_item)
+{
+ IDropAction *a = new IDropAction();
+ a->count = single_item ? 1 : 0;
+ a->from_inv.setCurrentPlayer();
+ a->from_list = "main";
+ a->from_i = client->getPlayerItem();
+ client->inventoryAction(a);
+}
+
+
+void Game::openInventory()
+{
+ /*
+ * Don't permit to open inventory is CAO or player doesn't exists.
+ * This prevent showing an empty inventory at player load
+ */
+
+ LocalPlayer *player = client->getEnv().getLocalPlayer();
+ if (!player || !player->getCAO())
+ return;
+
+ infostream << "the_game: " << "Launching inventory" << std::endl;
+
+ PlayerInventoryFormSource *fs_src = new PlayerInventoryFormSource(client);
+
+ InventoryLocation inventoryloc;
+ inventoryloc.setCurrentPlayer();
+
+ if (!client->moddingEnabled()
+ || !client->getScript()->on_inventory_open(fs_src->m_client->getInventory(inventoryloc))) {
+ TextDest *txt_dst = new TextDestPlayerInventory(client);
+ GUIFormSpecMenu::create(current_formspec, client, &input->joystick, fs_src,
+ txt_dst, client->getFormspecPrepend());
+ cur_formname = "";
+ current_formspec->setFormSpec(fs_src->getForm(), inventoryloc);
+ }
+}
+
+
+void Game::openConsole(float scale, const wchar_t *line)
+{
+ assert(scale > 0.0f && scale <= 1.0f);
+
+#ifdef __ANDROID__
+ porting::showInputDialog(gettext("ok"), "", "", 2);
+ m_android_chat_open = true;
+#else
+ if (gui_chat_console->isOpenInhibited())
+ return;
+ gui_chat_console->openConsole(scale);
+ if (line) {
+ gui_chat_console->setCloseOnEnter(true);
+ gui_chat_console->replaceAndAddToHistory(line);
+ }
+#endif
+}
+
+#ifdef __ANDROID__
+void Game::handleAndroidChatInput()
+{
+ if (m_android_chat_open && porting::getInputDialogState() == 0) {
+ std::string text = porting::getInputDialogValue();
+ client->typeChatMessage(utf8_to_wide(text));
+ }
+}
+#endif
+
+
+void Game::toggleFreeMove()
+{
+ bool free_move = !g_settings->getBool("free_move");
+ g_settings->set("free_move", bool_to_cstr(free_move));
+
+ if (free_move) {
+ if (client->checkPrivilege("fly")) {
+ m_game_ui->showTranslatedStatusText("Fly mode enabled");
+ } else {
+ m_game_ui->showTranslatedStatusText("Fly mode enabled (note: no 'fly' privilege)");
+ }
+ } else {
+ m_game_ui->showTranslatedStatusText("Fly mode disabled");
+ }
+}
+
+void Game::toggleFreeMoveAlt()
+{
+ if (m_cache_doubletap_jump && runData.jump_timer < 0.2f)
+ toggleFreeMove();
+
+ runData.reset_jump_timer = true;
+}
+
+
+void Game::toggleFast()
+{
+ bool fast_move = !g_settings->getBool("fast_move");
+ bool has_fast_privs = client->checkPrivilege("fast");
+ g_settings->set("fast_move", bool_to_cstr(fast_move));
+
+ if (fast_move) {
+ if (has_fast_privs) {
+ m_game_ui->showTranslatedStatusText("Fast mode enabled");
+ } else {
+ m_game_ui->showTranslatedStatusText("Fast mode enabled (note: no 'fast' privilege)");
+ }
+ } else {
+ m_game_ui->showTranslatedStatusText("Fast mode disabled");
+ }
+
+#ifdef __ANDROID__
+ m_cache_hold_aux1 = fast_move && has_fast_privs;
+#endif
+}
+
+
+void Game::toggleNoClip()
+{
+ bool noclip = !g_settings->getBool("noclip");
+ g_settings->set("noclip", bool_to_cstr(noclip));
+
+ if (noclip) {
+ if (client->checkPrivilege("noclip")) {
+ m_game_ui->showTranslatedStatusText("Noclip mode enabled");
+ } else {
+ m_game_ui->showTranslatedStatusText("Noclip mode enabled (note: no 'noclip' privilege)");
+ }
+ } else {
+ m_game_ui->showTranslatedStatusText("Noclip mode disabled");
+ }
+}
+
+void Game::toggleCinematic()
+{
+ bool cinematic = !g_settings->getBool("cinematic");
+ g_settings->set("cinematic", bool_to_cstr(cinematic));
+
+ if (cinematic)
+ m_game_ui->showTranslatedStatusText("Cinematic mode enabled");
+ else
+ m_game_ui->showTranslatedStatusText("Cinematic mode disabled");
+}
+
+// Autoforward by toggling continuous forward.
+void Game::toggleAutoforward()
+{
+ bool autorun_enabled = !g_settings->getBool("continuous_forward");
+ g_settings->set("continuous_forward", bool_to_cstr(autorun_enabled));
+
+ if (autorun_enabled)
+ m_game_ui->showTranslatedStatusText("Automatic forwards enabled");
+ else
+ m_game_ui->showTranslatedStatusText("Automatic forwards disabled");
+}
+
+void Game::toggleMinimap(bool shift_pressed)
+{
+ if (!mapper || !m_game_ui->m_flags.show_hud || !g_settings->getBool("enable_minimap"))
+ return;
+
+ if (shift_pressed) {
+ mapper->toggleMinimapShape();
+ return;
+ }
+
+ u32 hud_flags = client->getEnv().getLocalPlayer()->hud_flags;
+
+ MinimapMode mode = MINIMAP_MODE_OFF;
+ if (hud_flags & HUD_FLAG_MINIMAP_VISIBLE) {
+ mode = mapper->getMinimapMode();
+ mode = (MinimapMode)((int)mode + 1);
+ // If radar is disabled and in, or switching to, radar mode
+ if (!(hud_flags & HUD_FLAG_MINIMAP_RADAR_VISIBLE) && mode > 3)
+ mode = MINIMAP_MODE_OFF;
+ }
+
+ m_game_ui->m_flags.show_minimap = true;
+ switch (mode) {
+ case MINIMAP_MODE_SURFACEx1:
+ m_game_ui->showTranslatedStatusText("Minimap in surface mode, Zoom x1");
+ break;
+ case MINIMAP_MODE_SURFACEx2:
+ m_game_ui->showTranslatedStatusText("Minimap in surface mode, Zoom x2");
+ break;
+ case MINIMAP_MODE_SURFACEx4:
+ m_game_ui->showTranslatedStatusText("Minimap in surface mode, Zoom x4");
+ break;
+ case MINIMAP_MODE_RADARx1:
+ m_game_ui->showTranslatedStatusText("Minimap in radar mode, Zoom x1");
+ break;
+ case MINIMAP_MODE_RADARx2:
+ m_game_ui->showTranslatedStatusText("Minimap in radar mode, Zoom x2");
+ break;
+ case MINIMAP_MODE_RADARx4:
+ m_game_ui->showTranslatedStatusText("Minimap in radar mode, Zoom x4");
+ break;
+ default:
+ mode = MINIMAP_MODE_OFF;
+ m_game_ui->m_flags.show_minimap = false;
+ if (hud_flags & HUD_FLAG_MINIMAP_VISIBLE)
+ m_game_ui->showTranslatedStatusText("Minimap hidden");
+ else
+ m_game_ui->showTranslatedStatusText("Minimap currently disabled by game or mod");
+ }
+
+ mapper->setMinimapMode(mode);
+}
+
+void Game::toggleFog()
+{
+ bool fog_enabled = g_settings->getBool("enable_fog");
+ g_settings->setBool("enable_fog", !fog_enabled);
+ if (fog_enabled)
+ m_game_ui->showTranslatedStatusText("Fog disabled");
+ else
+ m_game_ui->showTranslatedStatusText("Fog enabled");
+}
+
+
+void Game::toggleDebug()
+{
+ // Initial / 4x toggle: Chat only
+ // 1x toggle: Debug text with chat
+ // 2x toggle: Debug text with profiler graph
+ // 3x toggle: Debug text and wireframe
+ if (!m_game_ui->m_flags.show_debug) {
+ m_game_ui->m_flags.show_debug = true;
+ m_game_ui->m_flags.show_profiler_graph = false;
+ draw_control->show_wireframe = false;
+ m_game_ui->showTranslatedStatusText("Debug info shown");
+ } else if (!m_game_ui->m_flags.show_profiler_graph && !draw_control->show_wireframe) {
+ m_game_ui->m_flags.show_profiler_graph = true;
+ m_game_ui->showTranslatedStatusText("Profiler graph shown");
+ } else if (!draw_control->show_wireframe && client->checkPrivilege("debug")) {
+ m_game_ui->m_flags.show_profiler_graph = false;
+ draw_control->show_wireframe = true;
+ m_game_ui->showTranslatedStatusText("Wireframe shown");
+ } else {
+ m_game_ui->m_flags.show_debug = false;
+ m_game_ui->m_flags.show_profiler_graph = false;
+ draw_control->show_wireframe = false;
+ if (client->checkPrivilege("debug")) {
+ m_game_ui->showTranslatedStatusText("Debug info, profiler graph, and wireframe hidden");
+ } else {
+ m_game_ui->showTranslatedStatusText("Debug info and profiler graph hidden");
+ }
+ }
+}
+
+
+void Game::toggleUpdateCamera()
+{
+ m_flags.disable_camera_update = !m_flags.disable_camera_update;
+ if (m_flags.disable_camera_update)
+ m_game_ui->showTranslatedStatusText("Camera update disabled");
+ else
+ m_game_ui->showTranslatedStatusText("Camera update enabled");
+}
+
+
+void Game::increaseViewRange()
+{
+ s16 range = g_settings->getS16("viewing_range");
+ s16 range_new = range + 10;
+
+ wchar_t buf[255];
+ const wchar_t *str;
+ if (range_new > 4000) {
+ range_new = 4000;
+ str = wgettext("Viewing range is at maximum: %d");
+ swprintf(buf, sizeof(buf) / sizeof(wchar_t), str, range_new);
+ delete[] str;
+ m_game_ui->showStatusText(buf);
+
+ } else {
+ str = wgettext("Viewing range changed to %d");
+ swprintf(buf, sizeof(buf) / sizeof(wchar_t), str, range_new);
+ delete[] str;
+ m_game_ui->showStatusText(buf);
+ }
+ g_settings->set("viewing_range", itos(range_new));
+}
+
+
+void Game::decreaseViewRange()
+{
+ s16 range = g_settings->getS16("viewing_range");
+ s16 range_new = range - 10;
+
+ wchar_t buf[255];
+ const wchar_t *str;
+ if (range_new < 20) {
+ range_new = 20;
+ str = wgettext("Viewing range is at minimum: %d");
+ swprintf(buf, sizeof(buf) / sizeof(wchar_t), str, range_new);
+ delete[] str;
+ m_game_ui->showStatusText(buf);
+ } else {
+ str = wgettext("Viewing range changed to %d");
+ swprintf(buf, sizeof(buf) / sizeof(wchar_t), str, range_new);
+ delete[] str;
+ m_game_ui->showStatusText(buf);
+ }
+ g_settings->set("viewing_range", itos(range_new));
+}
+
+
+void Game::toggleFullViewRange()
+{
+ draw_control->range_all = !draw_control->range_all;
+ if (draw_control->range_all)
+ m_game_ui->showTranslatedStatusText("Enabled unlimited viewing range");
+ else
+ m_game_ui->showTranslatedStatusText("Disabled unlimited viewing range");
+}
+
+
+void Game::checkZoomEnabled()
+{
+ LocalPlayer *player = client->getEnv().getLocalPlayer();
+ if (player->getZoomFOV() < 0.001f)
+ m_game_ui->showTranslatedStatusText("Zoom currently disabled by game or mod");
+}
+
+
+void Game::updateCameraDirection(CameraOrientation *cam, float dtime)
+{
+ if ((device->isWindowActive() && device->isWindowFocused()
+ && !isMenuActive()) || random_input) {
+
+#ifndef __ANDROID__
+ if (!random_input) {
+ // Mac OSX gets upset if this is set every frame
+ if (device->getCursorControl()->isVisible())
+ device->getCursorControl()->setVisible(false);
+ }
+#endif
+
+ if (m_first_loop_after_window_activation) {
+ m_first_loop_after_window_activation = false;
+
+ input->setMousePos(driver->getScreenSize().Width / 2,
+ driver->getScreenSize().Height / 2);
+ } else {
+ updateCameraOrientation(cam, dtime);
+ }
+
+ } else {
+
+#ifndef ANDROID
+ // Mac OSX gets upset if this is set every frame
+ if (!device->getCursorControl()->isVisible())
+ device->getCursorControl()->setVisible(true);
+#endif
+
+ m_first_loop_after_window_activation = true;
+
+ }
+}
+
+void Game::updateCameraOrientation(CameraOrientation *cam, float dtime)
+{
+#ifdef HAVE_TOUCHSCREENGUI
+ if (g_touchscreengui) {
+ cam->camera_yaw += g_touchscreengui->getYawChange();
+ cam->camera_pitch = g_touchscreengui->getPitch();
+ } else {
+#endif
+ v2s32 center(driver->getScreenSize().Width / 2, driver->getScreenSize().Height / 2);
+ v2s32 dist = input->getMousePos() - center;
+
+ if (m_invert_mouse || camera->getCameraMode() == CAMERA_MODE_THIRD_FRONT) {
+ dist.Y = -dist.Y;
+ }
+
+ cam->camera_yaw -= dist.X * m_cache_mouse_sensitivity;
+ cam->camera_pitch += dist.Y * m_cache_mouse_sensitivity;
+
+ if (dist.X != 0 || dist.Y != 0)
+ input->setMousePos(center.X, center.Y);
+#ifdef HAVE_TOUCHSCREENGUI
+ }
+#endif
+
+ if (m_cache_enable_joysticks) {
+ f32 c = m_cache_joystick_frustum_sensitivity * (1.f / 32767.f) * dtime;
+ cam->camera_yaw -= input->joystick.getAxisWithoutDead(JA_FRUSTUM_HORIZONTAL) * c;
+ cam->camera_pitch += input->joystick.getAxisWithoutDead(JA_FRUSTUM_VERTICAL) * c;
+ }
+
+ cam->camera_pitch = rangelim(cam->camera_pitch, -89.5, 89.5);
+}
+
+
+void Game::updatePlayerControl(const CameraOrientation &cam)
+{
+ //TimeTaker tt("update player control", NULL, PRECISION_NANO);
+
+ // DO NOT use the isKeyDown method for the forward, backward, left, right
+ // buttons, as the code that uses the controls needs to be able to
+ // distinguish between the two in order to know when to use joysticks.
+
+ PlayerControl control(
+ input->isKeyDown(KeyType::FORWARD),
+ input->isKeyDown(KeyType::BACKWARD),
+ input->isKeyDown(KeyType::LEFT),
+ input->isKeyDown(KeyType::RIGHT),
+ isKeyDown(KeyType::JUMP),
+ isKeyDown(KeyType::SPECIAL1),
+ isKeyDown(KeyType::SNEAK),
+ isKeyDown(KeyType::ZOOM),
+ input->getLeftState(),
+ input->getRightState(),
+ cam.camera_pitch,
+ cam.camera_yaw,
+ input->joystick.getAxisWithoutDead(JA_SIDEWARD_MOVE),
+ input->joystick.getAxisWithoutDead(JA_FORWARD_MOVE)
+ );
+
+ u32 keypress_bits =
+ ( (u32)(isKeyDown(KeyType::FORWARD) & 0x1) << 0) |
+ ( (u32)(isKeyDown(KeyType::BACKWARD) & 0x1) << 1) |
+ ( (u32)(isKeyDown(KeyType::LEFT) & 0x1) << 2) |
+ ( (u32)(isKeyDown(KeyType::RIGHT) & 0x1) << 3) |
+ ( (u32)(isKeyDown(KeyType::JUMP) & 0x1) << 4) |
+ ( (u32)(isKeyDown(KeyType::SPECIAL1) & 0x1) << 5) |
+ ( (u32)(isKeyDown(KeyType::SNEAK) & 0x1) << 6) |
+ ( (u32)(input->getLeftState() & 0x1) << 7) |
+ ( (u32)(input->getRightState() & 0x1) << 8
+ );
+
+#ifdef ANDROID
+ /* For Android, simulate holding down AUX1 (fast move) if the user has
+ * the fast_move setting toggled on. If there is an aux1 key defined for
+ * Android then its meaning is inverted (i.e. holding aux1 means walk and
+ * not fast)
+ */
+ if (m_cache_hold_aux1) {
+ control.aux1 = control.aux1 ^ true;
+ keypress_bits ^= ((u32)(1U << 5));
+ }
+#endif
+
+ LocalPlayer *player = client->getEnv().getLocalPlayer();
+
+ // autojump if set: simulate "jump" key
+ if (player->getAutojump()) {
+ control.jump = true;
+ keypress_bits |= 1U << 4;
+ }
+
+ client->setPlayerControl(control);
+ player->keyPressed = keypress_bits;
+
+ //tt.stop();
+}
+
+
+inline void Game::step(f32 *dtime)
+{
+ bool can_be_and_is_paused =
+ (simple_singleplayer_mode && g_menumgr.pausesGame());
+
+ if (can_be_and_is_paused) { // This is for a singleplayer server
+ *dtime = 0; // No time passes
+ } else {
+ if (server)
+ server->step(*dtime);
+
+ client->step(*dtime);
+ }
+}
+
+const ClientEventHandler Game::clientEventHandler[CLIENTEVENT_MAX] = {
+ {&Game::handleClientEvent_None},
+ {&Game::handleClientEvent_PlayerDamage},
+ {&Game::handleClientEvent_PlayerForceMove},
+ {&Game::handleClientEvent_Deathscreen},
+ {&Game::handleClientEvent_ShowFormSpec},
+ {&Game::handleClientEvent_ShowLocalFormSpec},
+ {&Game::handleClientEvent_HandleParticleEvent},
+ {&Game::handleClientEvent_HandleParticleEvent},
+ {&Game::handleClientEvent_HandleParticleEvent},
+ {&Game::handleClientEvent_HudAdd},
+ {&Game::handleClientEvent_HudRemove},
+ {&Game::handleClientEvent_HudChange},
+ {&Game::handleClientEvent_SetSky},
+ {&Game::handleClientEvent_OverrideDayNigthRatio},
+ {&Game::handleClientEvent_CloudParams},
+};
+
+void Game::handleClientEvent_None(ClientEvent *event, CameraOrientation *cam)
+{
+ FATAL_ERROR("ClientEvent type None received");
+}
+
+void Game::handleClientEvent_PlayerDamage(ClientEvent *event, CameraOrientation *cam)
+{
+ if (client->moddingEnabled()) {
+ client->getScript()->on_damage_taken(event->player_damage.amount);
+ }
+
+ // Damage flash and hurt tilt are not used at death
+ if (client->getHP() > 0) {
+ runData.damage_flash += 95.0f + 3.2f * event->player_damage.amount;
+ runData.damage_flash = MYMIN(runData.damage_flash, 127.0f);
+
+ LocalPlayer *player = client->getEnv().getLocalPlayer();
+
+ player->hurt_tilt_timer = 1.5f;
+ player->hurt_tilt_strength =
+ rangelim(event->player_damage.amount / 4.0f, 1.0f, 4.0f);
+ }
+
+ // Play damage sound
+ client->getEventManager()->put(new SimpleTriggerEvent(MtEvent::PLAYER_DAMAGE));
+}
+
+void Game::handleClientEvent_PlayerForceMove(ClientEvent *event, CameraOrientation *cam)
+{
+ cam->camera_yaw = event->player_force_move.yaw;
+ cam->camera_pitch = event->player_force_move.pitch;
+}
+
+void Game::handleClientEvent_Deathscreen(ClientEvent *event, CameraOrientation *cam)
+{
+ // If CSM enabled, deathscreen is handled by CSM code in
+ // builtin/client/init.lua
+ if (client->moddingEnabled())
+ client->getScript()->on_death();
+ else
+ showDeathFormspec();
+
+ /* Handle visualization */
+ LocalPlayer *player = client->getEnv().getLocalPlayer();
+ runData.damage_flash = 0;
+ player->hurt_tilt_timer = 0;
+ player->hurt_tilt_strength = 0;
+}
+
+void Game::handleClientEvent_ShowFormSpec(ClientEvent *event, CameraOrientation *cam)
+{
+ if (event->show_formspec.formspec->empty()) {
+ if (current_formspec && (event->show_formspec.formname->empty()
+ || *(event->show_formspec.formname) == cur_formname)) {
+ current_formspec->quitMenu();
+ }
+ } else {
+ FormspecFormSource *fs_src =
+ new FormspecFormSource(*(event->show_formspec.formspec));
+ TextDestPlayerInventory *txt_dst =
+ new TextDestPlayerInventory(client, *(event->show_formspec.formname));
+
+ GUIFormSpecMenu::create(current_formspec, client, &input->joystick,
+ fs_src, txt_dst, client->getFormspecPrepend());
+ cur_formname = *(event->show_formspec.formname);
+ }
+
+ delete event->show_formspec.formspec;
+ delete event->show_formspec.formname;
+}
+
+void Game::handleClientEvent_ShowLocalFormSpec(ClientEvent *event, CameraOrientation *cam)
+{
+ FormspecFormSource *fs_src = new FormspecFormSource(*event->show_formspec.formspec);
+ LocalFormspecHandler *txt_dst =
+ new LocalFormspecHandler(*event->show_formspec.formname, client);
+ GUIFormSpecMenu::create(current_formspec, client, &input->joystick,
+ fs_src, txt_dst, client->getFormspecPrepend());
+
+ delete event->show_formspec.formspec;
+ delete event->show_formspec.formname;
+}
+
+void Game::handleClientEvent_HandleParticleEvent(ClientEvent *event,
+ CameraOrientation *cam)
+{
+ LocalPlayer *player = client->getEnv().getLocalPlayer();
+ client->getParticleManager()->handleParticleEvent(event, client, player);
+}
+
+void Game::handleClientEvent_HudAdd(ClientEvent *event, CameraOrientation *cam)
+{
+ LocalPlayer *player = client->getEnv().getLocalPlayer();
+ auto &hud_server_to_client = client->getHUDTranslationMap();
+
+ u32 server_id = event->hudadd.server_id;
+ // ignore if we already have a HUD with that ID
+ auto i = hud_server_to_client.find(server_id);
+ if (i != hud_server_to_client.end()) {
+ delete event->hudadd.pos;
+ delete event->hudadd.name;
+ delete event->hudadd.scale;
+ delete event->hudadd.text;
+ delete event->hudadd.align;
+ delete event->hudadd.offset;
+ delete event->hudadd.world_pos;
+ delete event->hudadd.size;
+ return;
+ }
+
+ HudElement *e = new HudElement;
+ e->type = (HudElementType)event->hudadd.type;
+ e->pos = *event->hudadd.pos;
+ e->name = *event->hudadd.name;
+ e->scale = *event->hudadd.scale;
+ e->text = *event->hudadd.text;
+ e->number = event->hudadd.number;
+ e->item = event->hudadd.item;
+ e->dir = event->hudadd.dir;
+ e->align = *event->hudadd.align;
+ e->offset = *event->hudadd.offset;
+ e->world_pos = *event->hudadd.world_pos;
+ e->size = *event->hudadd.size;
+ hud_server_to_client[server_id] = player->addHud(e);
+
+ delete event->hudadd.pos;
+ delete event->hudadd.name;
+ delete event->hudadd.scale;
+ delete event->hudadd.text;
+ delete event->hudadd.align;
+ delete event->hudadd.offset;
+ delete event->hudadd.world_pos;
+ delete event->hudadd.size;
+}
+
+void Game::handleClientEvent_HudRemove(ClientEvent *event, CameraOrientation *cam)
+{
+ LocalPlayer *player = client->getEnv().getLocalPlayer();
+ HudElement *e = player->removeHud(event->hudrm.id);
+ delete e;
+}
+
+void Game::handleClientEvent_HudChange(ClientEvent *event, CameraOrientation *cam)
+{
+ LocalPlayer *player = client->getEnv().getLocalPlayer();
+
+ u32 id = event->hudchange.id;
+ HudElement *e = player->getHud(id);
+
+ if (e == NULL) {
+ delete event->hudchange.v3fdata;
+ delete event->hudchange.v2fdata;
+ delete event->hudchange.sdata;
+ delete event->hudchange.v2s32data;
+ return;
+ }
+
+ switch (event->hudchange.stat) {
+ case HUD_STAT_POS:
+ e->pos = *event->hudchange.v2fdata;
+ break;
+
+ case HUD_STAT_NAME:
+ e->name = *event->hudchange.sdata;
+ break;
+
+ case HUD_STAT_SCALE:
+ e->scale = *event->hudchange.v2fdata;
+ break;
+
+ case HUD_STAT_TEXT:
+ e->text = *event->hudchange.sdata;
+ break;
+
+ case HUD_STAT_NUMBER:
+ e->number = event->hudchange.data;
+ break;
+
+ case HUD_STAT_ITEM:
+ e->item = event->hudchange.data;
+ break;
+
+ case HUD_STAT_DIR:
+ e->dir = event->hudchange.data;
+ break;
+
+ case HUD_STAT_ALIGN:
+ e->align = *event->hudchange.v2fdata;
+ break;
+
+ case HUD_STAT_OFFSET:
+ e->offset = *event->hudchange.v2fdata;
+ break;
+
+ case HUD_STAT_WORLD_POS:
+ e->world_pos = *event->hudchange.v3fdata;
+ break;
+
+ case HUD_STAT_SIZE:
+ e->size = *event->hudchange.v2s32data;
+ break;
+ }
+
+ delete event->hudchange.v3fdata;
+ delete event->hudchange.v2fdata;
+ delete event->hudchange.sdata;
+ delete event->hudchange.v2s32data;
+}
+
+void Game::handleClientEvent_SetSky(ClientEvent *event, CameraOrientation *cam)
+{
+ sky->setVisible(false);
+ // Whether clouds are visible in front of a custom skybox
+ sky->setCloudsEnabled(event->set_sky.clouds);
+
+ if (skybox) {
+ skybox->remove();
+ skybox = NULL;
+ }
+
+ // Handle according to type
+ if (*event->set_sky.type == "regular") {
+ sky->setVisible(true);
+ sky->setCloudsEnabled(true);
+ } else if (*event->set_sky.type == "skybox" &&
+ event->set_sky.params->size() == 6) {
+ sky->setFallbackBgColor(*event->set_sky.bgcolor);
+ skybox = RenderingEngine::get_scene_manager()->addSkyBoxSceneNode(
+ texture_src->getTextureForMesh((*event->set_sky.params)[0]),
+ texture_src->getTextureForMesh((*event->set_sky.params)[1]),
+ texture_src->getTextureForMesh((*event->set_sky.params)[2]),
+ texture_src->getTextureForMesh((*event->set_sky.params)[3]),
+ texture_src->getTextureForMesh((*event->set_sky.params)[4]),
+ texture_src->getTextureForMesh((*event->set_sky.params)[5]));
+ }
+ // Handle everything else as plain color
+ else {
+ if (*event->set_sky.type != "plain")
+ infostream << "Unknown sky type: "
+ << (*event->set_sky.type) << std::endl;
+
+ sky->setFallbackBgColor(*event->set_sky.bgcolor);
+ }
+
+ delete event->set_sky.bgcolor;
+ delete event->set_sky.type;
+ delete event->set_sky.params;
+}
+
+void Game::handleClientEvent_OverrideDayNigthRatio(ClientEvent *event,
+ CameraOrientation *cam)
+{
+ client->getEnv().setDayNightRatioOverride(
+ event->override_day_night_ratio.do_override,
+ event->override_day_night_ratio.ratio_f * 1000.0f);
+}
+
+void Game::handleClientEvent_CloudParams(ClientEvent *event, CameraOrientation *cam)
+{
+ if (!clouds)
+ return;
+
+ clouds->setDensity(event->cloud_params.density);
+ clouds->setColorBright(video::SColor(event->cloud_params.color_bright));
+ clouds->setColorAmbient(video::SColor(event->cloud_params.color_ambient));
+ clouds->setHeight(event->cloud_params.height);
+ clouds->setThickness(event->cloud_params.thickness);
+ clouds->setSpeed(v2f(event->cloud_params.speed_x, event->cloud_params.speed_y));
+}
+
+void Game::processClientEvents(CameraOrientation *cam)
+{
+ while (client->hasClientEvents()) {
+ std::unique_ptr<ClientEvent> event(client->getClientEvent());
+ FATAL_ERROR_IF(event->type >= CLIENTEVENT_MAX, "Invalid clientevent type");
+ const ClientEventHandler& evHandler = clientEventHandler[event->type];
+ (this->*evHandler.handler)(event.get(), cam);
+ }
+}
+
+void Game::updateChat(f32 dtime, const v2u32 &screensize)
+{
+ // Add chat log output for errors to be shown in chat
+ static LogOutputBuffer chat_log_error_buf(g_logger, LL_ERROR);
+
+ // Get new messages from error log buffer
+ while (!chat_log_error_buf.empty()) {
+ std::wstring error_message = utf8_to_wide(chat_log_error_buf.get());
+ if (!g_settings->getBool("disable_escape_sequences")) {
+ error_message.insert(0, L"\x1b(c@red)");
+ error_message.append(L"\x1b(c@white)");
+ }
+ chat_backend->addMessage(L"", error_message);
+ }
+
+ // Get new messages from client
+ std::wstring message;
+ while (client->getChatMessage(message)) {
+ chat_backend->addUnparsedMessage(message);
+ }
+
+ // Remove old messages
+ chat_backend->step(dtime);
+
+ // Display all messages in a static text element
+ m_game_ui->setChatText(chat_backend->getRecentChat(),
+ chat_backend->getRecentBuffer().getLineCount());
+}
+
+void Game::updateCamera(u32 busy_time, f32 dtime)
+{
+ LocalPlayer *player = client->getEnv().getLocalPlayer();
+
+ /*
+ For interaction purposes, get info about the held item
+ - What item is it?
+ - Is it a usable item?
+ - Can it point to liquids?
+ */
+ ItemStack playeritem;
+ {
+ InventoryList *mlist = local_inventory->getList("main");
+
+ if (mlist && client->getPlayerItem() < mlist->getSize())
+ playeritem = mlist->getItem(client->getPlayerItem());
+ }
+
+ if (playeritem.getDefinition(itemdef_manager).name.empty()) { // override the hand
+ InventoryList *hlist = local_inventory->getList("hand");
+ if (hlist)
+ playeritem = hlist->getItem(0);
+ }
+
+
+ ToolCapabilities playeritem_toolcap =
+ playeritem.getToolCapabilities(itemdef_manager);
+
+ v3s16 old_camera_offset = camera->getOffset();
+
+ if (wasKeyDown(KeyType::CAMERA_MODE)) {
+ GenericCAO *playercao = player->getCAO();
+
+ // If playercao not loaded, don't change camera
+ if (!playercao)
+ return;
+
+ camera->toggleCameraMode();
+
+ playercao->setVisible(camera->getCameraMode() > CAMERA_MODE_FIRST);
+ playercao->setChildrenVisible(camera->getCameraMode() > CAMERA_MODE_FIRST);
+ }
+
+ float full_punch_interval = playeritem_toolcap.full_punch_interval;
+ float tool_reload_ratio = runData.time_from_last_punch / full_punch_interval;
+
+ tool_reload_ratio = MYMIN(tool_reload_ratio, 1.0);
+ camera->update(player, dtime, busy_time / 1000.0f, tool_reload_ratio);
+ camera->step(dtime);
+
+ v3f camera_position = camera->getPosition();
+ v3f camera_direction = camera->getDirection();
+ f32 camera_fov = camera->getFovMax();
+ v3s16 camera_offset = camera->getOffset();
+
+ m_camera_offset_changed = (camera_offset != old_camera_offset);
+
+ if (!m_flags.disable_camera_update) {
+ client->getEnv().getClientMap().updateCamera(camera_position,
+ camera_direction, camera_fov, camera_offset);
+
+ if (m_camera_offset_changed) {
+ client->updateCameraOffset(camera_offset);
+ client->getEnv().updateCameraOffset(camera_offset);
+
+ if (clouds)
+ clouds->updateCameraOffset(camera_offset);
+ }
+ }
+}
+
+
+void Game::updateSound(f32 dtime)
+{
+ // Update sound listener
+ v3s16 camera_offset = camera->getOffset();
+ sound->updateListener(camera->getCameraNode()->getPosition() + intToFloat(camera_offset, BS),
+ v3f(0, 0, 0), // velocity
+ camera->getDirection(),
+ camera->getCameraNode()->getUpVector());
+
+ bool mute_sound = g_settings->getBool("mute_sound");
+ if (mute_sound) {
+ sound->setListenerGain(0.0f);
+ } else {
+ // Check if volume is in the proper range, else fix it.
+ float old_volume = g_settings->getFloat("sound_volume");
+ float new_volume = rangelim(old_volume, 0.0f, 1.0f);
+ sound->setListenerGain(new_volume);
+
+ if (old_volume != new_volume) {
+ g_settings->setFloat("sound_volume", new_volume);
+ }
+ }
+
+ LocalPlayer *player = client->getEnv().getLocalPlayer();
+
+ // Tell the sound maker whether to make footstep sounds
+ soundmaker->makes_footstep_sound = player->makes_footstep_sound;
+
+ // Update sound maker
+ if (player->makes_footstep_sound)
+ soundmaker->step(dtime);
+
+ ClientMap &map = client->getEnv().getClientMap();
+ MapNode n = map.getNodeNoEx(player->getFootstepNodePos());
+ soundmaker->m_player_step_sound = nodedef_manager->get(n).sound_footstep;
+}
+
+
+void Game::processPlayerInteraction(f32 dtime, bool show_hud, bool show_debug)
+{
+ LocalPlayer *player = client->getEnv().getLocalPlayer();
+
+ ItemStack playeritem;
+ {
+ InventoryList *mlist = local_inventory->getList("main");
+
+ if (mlist && client->getPlayerItem() < mlist->getSize())
+ playeritem = mlist->getItem(client->getPlayerItem());
+ }
+
+ const ItemDefinition &playeritem_def =
+ playeritem.getDefinition(itemdef_manager);
+ InventoryList *hlist = local_inventory->getList("hand");
+ const ItemDefinition &hand_def =
+ hlist ? hlist->getItem(0).getDefinition(itemdef_manager) : itemdef_manager->get("");
+
+ v3f player_position = player->getPosition();
+ v3f camera_position = camera->getPosition();
+ v3f camera_direction = camera->getDirection();
+ v3s16 camera_offset = camera->getOffset();
+
+
+ /*
+ Calculate what block is the crosshair pointing to
+ */
+
+ f32 d = playeritem_def.range; // max. distance
+ f32 d_hand = hand_def.range;
+
+ if (d < 0 && d_hand >= 0)
+ d = d_hand;
+ else if (d < 0)
+ d = 4.0;
+
+ core::line3d<f32> shootline;
+
+ if (camera->getCameraMode() != CAMERA_MODE_THIRD_FRONT) {
+ shootline = core::line3d<f32>(camera_position,
+ camera_position + camera_direction * BS * d);
+ } else {
+ // prevent player pointing anything in front-view
+ shootline = core::line3d<f32>(camera_position,camera_position);
+ }
+
+#ifdef HAVE_TOUCHSCREENGUI
+
+ if ((g_settings->getBool("touchtarget")) && (g_touchscreengui)) {
+ shootline = g_touchscreengui->getShootline();
+ // Scale shootline to the acual distance the player can reach
+ shootline.end = shootline.start
+ + shootline.getVector().normalize() * BS * d;
+ shootline.start += intToFloat(camera_offset, BS);
+ shootline.end += intToFloat(camera_offset, BS);
+ }
+
+#endif
+
+ PointedThing pointed = updatePointedThing(shootline,
+ playeritem_def.liquids_pointable,
+ !runData.ldown_for_dig,
+ camera_offset);
+
+ if (pointed != runData.pointed_old) {
+ infostream << "Pointing at " << pointed.dump() << std::endl;
+ hud->updateSelectionMesh(camera_offset);
+ }
+
+ if (runData.digging_blocked && !input->getLeftState()) {
+ // allow digging again if button is not pressed
+ runData.digging_blocked = false;
+ }
+
+ /*
+ Stop digging when
+ - releasing left mouse button
+ - pointing away from node
+ */
+ if (runData.digging) {
+ if (input->getLeftReleased()) {
+ infostream << "Left button released"
+ << " (stopped digging)" << std::endl;
+ runData.digging = false;
+ } else if (pointed != runData.pointed_old) {
+ if (pointed.type == POINTEDTHING_NODE
+ && runData.pointed_old.type == POINTEDTHING_NODE
+ && pointed.node_undersurface
+ == runData.pointed_old.node_undersurface) {
+ // Still pointing to the same node, but a different face.
+ // Don't reset.
+ } else {
+ infostream << "Pointing away from node"
+ << " (stopped digging)" << std::endl;
+ runData.digging = false;
+ hud->updateSelectionMesh(camera_offset);
+ }
+ }
+
+ if (!runData.digging) {
+ client->interact(1, runData.pointed_old);
+ client->setCrack(-1, v3s16(0, 0, 0));
+ runData.dig_time = 0.0;
+ }
+ } else if (runData.dig_instantly && input->getLeftReleased()) {
+ // Remove e.g. torches faster when clicking instead of holding LMB
+ runData.nodig_delay_timer = 0;
+ runData.dig_instantly = false;
+ }
+
+ if (!runData.digging && runData.ldown_for_dig && !input->getLeftState()) {
+ runData.ldown_for_dig = false;
+ }
+
+ runData.left_punch = false;
+
+ soundmaker->m_player_leftpunch_sound.name = "";
+
+ // Prepare for repeating, unless we're not supposed to
+ if (input->getRightState() && !g_settings->getBool("safe_dig_and_place"))
+ runData.repeat_rightclick_timer += dtime;
+ else
+ runData.repeat_rightclick_timer = 0;
+
+ if (playeritem_def.usable && input->getLeftState()) {
+ if (input->getLeftClicked() && (!client->moddingEnabled()
+ || !client->getScript()->on_item_use(playeritem, pointed)))
+ client->interact(4, pointed);
+ } else if (pointed.type == POINTEDTHING_NODE) {
+ ToolCapabilities playeritem_toolcap =
+ playeritem.getToolCapabilities(itemdef_manager);
+ if (playeritem.name.empty()) {
+ const ToolCapabilities *handToolcap = hlist
+ ? &hlist->getItem(0).getToolCapabilities(itemdef_manager)
+ : itemdef_manager->get("").tool_capabilities;
+
+ if (handToolcap != nullptr)
+ playeritem_toolcap = *handToolcap;
+ }
+ handlePointingAtNode(pointed, playeritem_def, playeritem,
+ playeritem_toolcap, dtime);
+ } else if (pointed.type == POINTEDTHING_OBJECT) {
+ handlePointingAtObject(pointed, playeritem, player_position, show_debug);
+ } else if (input->getLeftState()) {
+ // When button is held down in air, show continuous animation
+ runData.left_punch = true;
+ } else if (input->getRightClicked()) {
+ handlePointingAtNothing(playeritem);
+ }
+
+ runData.pointed_old = pointed;
+
+ if (runData.left_punch || input->getLeftClicked())
+ camera->setDigging(0); // left click animation
+
+ input->resetLeftClicked();
+ input->resetRightClicked();
+
+ input->resetLeftReleased();
+ input->resetRightReleased();
+}
+
+
+PointedThing Game::updatePointedThing(
+ const core::line3d<f32> &shootline,
+ bool liquids_pointable,
+ bool look_for_object,
+ const v3s16 &camera_offset)
+{
+ std::vector<aabb3f> *selectionboxes = hud->getSelectionBoxes();
+ selectionboxes->clear();
+ hud->setSelectedFaceNormal(v3f(0.0, 0.0, 0.0));
+ static thread_local const bool show_entity_selectionbox = g_settings->getBool(
+ "show_entity_selectionbox");
+
+ ClientEnvironment &env = client->getEnv();
+ ClientMap &map = env.getClientMap();
+ const NodeDefManager *nodedef = map.getNodeDefManager();
+
+ runData.selected_object = NULL;
+
+ RaycastState s(shootline, look_for_object, liquids_pointable);
+ PointedThing result;
+ env.continueRaycast(&s, &result);
+ if (result.type == POINTEDTHING_OBJECT) {
+ runData.selected_object = client->getEnv().getActiveObject(result.object_id);
+ aabb3f selection_box;
+ if (show_entity_selectionbox && runData.selected_object->doShowSelectionBox() &&
+ runData.selected_object->getSelectionBox(&selection_box)) {
+ v3f pos = runData.selected_object->getPosition();
+ selectionboxes->push_back(aabb3f(selection_box));
+ hud->setSelectionPos(pos, camera_offset);
+ }
+ } else if (result.type == POINTEDTHING_NODE) {
+ // Update selection boxes
+ MapNode n = map.getNodeNoEx(result.node_undersurface);
+ std::vector<aabb3f> boxes;
+ n.getSelectionBoxes(nodedef, &boxes,
+ n.getNeighbors(result.node_undersurface, &map));
+
+ f32 d = 0.002 * BS;
+ for (std::vector<aabb3f>::const_iterator i = boxes.begin();
+ i != boxes.end(); ++i) {
+ aabb3f box = *i;
+ box.MinEdge -= v3f(d, d, d);
+ box.MaxEdge += v3f(d, d, d);
+ selectionboxes->push_back(box);
+ }
+ hud->setSelectionPos(intToFloat(result.node_undersurface, BS),
+ camera_offset);
+ hud->setSelectedFaceNormal(v3f(
+ result.intersection_normal.X,
+ result.intersection_normal.Y,
+ result.intersection_normal.Z));
+ }
+
+ // Update selection mesh light level and vertex colors
+ if (!selectionboxes->empty()) {
+ v3f pf = hud->getSelectionPos();
+ v3s16 p = floatToInt(pf, BS);
+
+ // Get selection mesh light level
+ MapNode n = map.getNodeNoEx(p);
+ u16 node_light = getInteriorLight(n, -1, nodedef);
+ u16 light_level = node_light;
+
+ for (const v3s16 &dir : g_6dirs) {
+ n = map.getNodeNoEx(p + dir);
+ node_light = getInteriorLight(n, -1, nodedef);
+ if (node_light > light_level)
+ light_level = node_light;
+ }
+
+ u32 daynight_ratio = client->getEnv().getDayNightRatio();
+ video::SColor c;
+ final_color_blend(&c, light_level, daynight_ratio);
+
+ // Modify final color a bit with time
+ u32 timer = porting::getTimeMs() % 5000;
+ float timerf = (float) (irr::core::PI * ((timer / 2500.0) - 0.5));
+ float sin_r = 0.08f * std::sin(timerf);
+ float sin_g = 0.08f * std::sin(timerf + irr::core::PI * 0.5f);
+ float sin_b = 0.08f * std::sin(timerf + irr::core::PI);
+ c.setRed(core::clamp(core::round32(c.getRed() * (0.8 + sin_r)), 0, 255));
+ c.setGreen(core::clamp(core::round32(c.getGreen() * (0.8 + sin_g)), 0, 255));
+ c.setBlue(core::clamp(core::round32(c.getBlue() * (0.8 + sin_b)), 0, 255));
+
+ // Set mesh final color
+ hud->setSelectionMeshColor(c);
+ }
+ return result;
+}
+
+
+void Game::handlePointingAtNothing(const ItemStack &playerItem)
+{
+ infostream << "Right Clicked in Air" << std::endl;
+ PointedThing fauxPointed;
+ fauxPointed.type = POINTEDTHING_NOTHING;
+ client->interact(5, fauxPointed);
+}
+
+
+void Game::handlePointingAtNode(const PointedThing &pointed,
+ const ItemDefinition &playeritem_def, const ItemStack &playeritem,
+ const ToolCapabilities &playeritem_toolcap, f32 dtime)
+{
+ v3s16 nodepos = pointed.node_undersurface;
+ v3s16 neighbourpos = pointed.node_abovesurface;
+
+ /*
+ Check information text of node
+ */
+
+ ClientMap &map = client->getEnv().getClientMap();
+
+ if (runData.nodig_delay_timer <= 0.0 && input->getLeftState()
+ && !runData.digging_blocked
+ && client->checkPrivilege("interact")) {
+ handleDigging(pointed, nodepos, playeritem_toolcap, dtime);
+ }
+
+ // This should be done after digging handling
+ NodeMetadata *meta = map.getNodeMetadata(nodepos);
+
+ if (meta) {
+ m_game_ui->setInfoText(unescape_translate(utf8_to_wide(
+ meta->getString("infotext"))));
+ } else {
+ MapNode n = map.getNodeNoEx(nodepos);
+
+ if (nodedef_manager->get(n).tiledef[0].name == "unknown_node.png") {
+ m_game_ui->setInfoText(L"Unknown node: " +
+ utf8_to_wide(nodedef_manager->get(n).name));
+ }
+ }
+
+ if ((input->getRightClicked() ||
+ runData.repeat_rightclick_timer >= m_repeat_right_click_time) &&
+ client->checkPrivilege("interact")) {
+ runData.repeat_rightclick_timer = 0;
+ infostream << "Ground right-clicked" << std::endl;
+
+ if (meta && !meta->getString("formspec").empty() && !random_input
+ && !isKeyDown(KeyType::SNEAK)) {
+ // Report right click to server
+ if (nodedef_manager->get(map.getNodeNoEx(nodepos)).rightclickable) {
+ client->interact(3, pointed);
+ }
+
+ infostream << "Launching custom inventory view" << std::endl;
+
+ InventoryLocation inventoryloc;
+ inventoryloc.setNodeMeta(nodepos);
+
+ NodeMetadataFormSource *fs_src = new NodeMetadataFormSource(
+ &client->getEnv().getClientMap(), nodepos);
+ TextDest *txt_dst = new TextDestNodeMetadata(nodepos, client);
+
+ GUIFormSpecMenu::create(current_formspec, client, &input->joystick, fs_src,
+ txt_dst, client->getFormspecPrepend());
+ cur_formname.clear();
+
+ current_formspec->setFormSpec(meta->getString("formspec"), inventoryloc);
+ } else {
+ // Report right click to server
+
+ camera->setDigging(1); // right click animation (always shown for feedback)
+
+ // If the wielded item has node placement prediction,
+ // make that happen
+ bool placed = nodePlacementPrediction(playeritem_def, playeritem, nodepos,
+ neighbourpos);
+
+ if (placed) {
+ // Report to server
+ client->interact(3, pointed);
+ // Read the sound
+ soundmaker->m_player_rightpunch_sound =
+ playeritem_def.sound_place;
+
+ if (client->moddingEnabled())
+ client->getScript()->on_placenode(pointed, playeritem_def);
+ } else {
+ soundmaker->m_player_rightpunch_sound =
+ SimpleSoundSpec();
+
+ if (playeritem_def.node_placement_prediction.empty() ||
+ nodedef_manager->get(map.getNodeNoEx(nodepos)).rightclickable) {
+ client->interact(3, pointed); // Report to server
+ } else {
+ soundmaker->m_player_rightpunch_sound =
+ playeritem_def.sound_place_failed;
+ }
+ }
+ }
+ }
+}
+
+bool Game::nodePlacementPrediction(const ItemDefinition &playeritem_def,
+ const ItemStack &playeritem, const v3s16 &nodepos, const v3s16 &neighbourpos)
+{
+ std::string prediction = playeritem_def.node_placement_prediction;
+ const NodeDefManager *nodedef = client->ndef();
+ ClientMap &map = client->getEnv().getClientMap();
+ MapNode node;
+ bool is_valid_position;
+
+ node = map.getNodeNoEx(nodepos, &is_valid_position);
+ if (!is_valid_position)
+ return false;
+
+ if (!prediction.empty() && !nodedef->get(node).rightclickable) {
+ verbosestream << "Node placement prediction for "
+ << playeritem_def.name << " is "
+ << prediction << std::endl;
+ v3s16 p = neighbourpos;
+
+ // Place inside node itself if buildable_to
+ MapNode n_under = map.getNodeNoEx(nodepos, &is_valid_position);
+ if (is_valid_position)
+ {
+ if (nodedef->get(n_under).buildable_to)
+ p = nodepos;
+ else {
+ node = map.getNodeNoEx(p, &is_valid_position);
+ if (is_valid_position &&!nodedef->get(node).buildable_to)
+ return false;
+ }
+ }
+
+ // Find id of predicted node
+ content_t id;
+ bool found = nodedef->getId(prediction, id);
+
+ if (!found) {
+ errorstream << "Node placement prediction failed for "
+ << playeritem_def.name << " (places "
+ << prediction
+ << ") - Name not known" << std::endl;
+ return false;
+ }
+
+ const ContentFeatures &predicted_f = nodedef->get(id);
+
+ // Predict param2 for facedir and wallmounted nodes
+ u8 param2 = 0;
+
+ if (predicted_f.param_type_2 == CPT2_WALLMOUNTED ||
+ predicted_f.param_type_2 == CPT2_COLORED_WALLMOUNTED) {
+ v3s16 dir = nodepos - neighbourpos;
+
+ if (abs(dir.Y) > MYMAX(abs(dir.X), abs(dir.Z))) {
+ param2 = dir.Y < 0 ? 1 : 0;
+ } else if (abs(dir.X) > abs(dir.Z)) {
+ param2 = dir.X < 0 ? 3 : 2;
+ } else {
+ param2 = dir.Z < 0 ? 5 : 4;
+ }
+ }
+
+ if (predicted_f.param_type_2 == CPT2_FACEDIR ||
+ predicted_f.param_type_2 == CPT2_COLORED_FACEDIR) {
+ v3s16 dir = nodepos - floatToInt(client->getEnv().getLocalPlayer()->getPosition(), BS);
+
+ if (abs(dir.X) > abs(dir.Z)) {
+ param2 = dir.X < 0 ? 3 : 1;
+ } else {
+ param2 = dir.Z < 0 ? 2 : 0;
+ }
+ }
+
+ assert(param2 <= 5);
+
+ //Check attachment if node is in group attached_node
+ if (((ItemGroupList) predicted_f.groups)["attached_node"] != 0) {
+ static v3s16 wallmounted_dirs[8] = {
+ v3s16(0, 1, 0),
+ v3s16(0, -1, 0),
+ v3s16(1, 0, 0),
+ v3s16(-1, 0, 0),
+ v3s16(0, 0, 1),
+ v3s16(0, 0, -1),
+ };
+ v3s16 pp;
+
+ if (predicted_f.param_type_2 == CPT2_WALLMOUNTED ||
+ predicted_f.param_type_2 == CPT2_COLORED_WALLMOUNTED)
+ pp = p + wallmounted_dirs[param2];
+ else
+ pp = p + v3s16(0, -1, 0);
+
+ if (!nodedef->get(map.getNodeNoEx(pp)).walkable)
+ return false;
+ }
+
+ // Apply color
+ if ((predicted_f.param_type_2 == CPT2_COLOR
+ || predicted_f.param_type_2 == CPT2_COLORED_FACEDIR
+ || predicted_f.param_type_2 == CPT2_COLORED_WALLMOUNTED)) {
+ const std::string &indexstr = playeritem.metadata.getString(
+ "palette_index", 0);
+ if (!indexstr.empty()) {
+ s32 index = mystoi(indexstr);
+ if (predicted_f.param_type_2 == CPT2_COLOR) {
+ param2 = index;
+ } else if (predicted_f.param_type_2
+ == CPT2_COLORED_WALLMOUNTED) {
+ // param2 = pure palette index + other
+ param2 = (index & 0xf8) | (param2 & 0x07);
+ } else if (predicted_f.param_type_2
+ == CPT2_COLORED_FACEDIR) {
+ // param2 = pure palette index + other
+ param2 = (index & 0xe0) | (param2 & 0x1f);
+ }
+ }
+ }
+
+ // Add node to client map
+ MapNode n(id, 0, param2);
+
+ try {
+ LocalPlayer *player = client->getEnv().getLocalPlayer();
+
+ // Dont place node when player would be inside new node
+ // NOTE: This is to be eventually implemented by a mod as client-side Lua
+ if (!nodedef->get(n).walkable ||
+ g_settings->getBool("enable_build_where_you_stand") ||
+ (client->checkPrivilege("noclip") && g_settings->getBool("noclip")) ||
+ (nodedef->get(n).walkable &&
+ neighbourpos != player->getStandingNodePos() + v3s16(0, 1, 0) &&
+ neighbourpos != player->getStandingNodePos() + v3s16(0, 2, 0))) {
+
+ // This triggers the required mesh update too
+ client->addNode(p, n);
+ return true;
+ }
+ } catch (InvalidPositionException &e) {
+ errorstream << "Node placement prediction failed for "
+ << playeritem_def.name << " (places "
+ << prediction
+ << ") - Position not loaded" << std::endl;
+ }
+ }
+
+ return false;
+}
+
+void Game::handlePointingAtObject(const PointedThing &pointed, const ItemStack &playeritem,
+ const v3f &player_position, bool show_debug)
+{
+ std::wstring infotext = unescape_translate(
+ utf8_to_wide(runData.selected_object->infoText()));
+
+ if (show_debug) {
+ if (!infotext.empty()) {
+ infotext += L"\n";
+ }
+ infotext += utf8_to_wide(runData.selected_object->debugInfoText());
+ }
+
+ m_game_ui->setInfoText(infotext);
+
+ if (input->getLeftState()) {
+ bool do_punch = false;
+ bool do_punch_damage = false;
+
+ if (runData.object_hit_delay_timer <= 0.0) {
+ do_punch = true;
+ do_punch_damage = true;
+ runData.object_hit_delay_timer = object_hit_delay;
+ }
+
+ if (input->getLeftClicked())
+ do_punch = true;
+
+ if (do_punch) {
+ infostream << "Left-clicked object" << std::endl;
+ runData.left_punch = true;
+ }
+
+ if (do_punch_damage) {
+ // Report direct punch
+ v3f objpos = runData.selected_object->getPosition();
+ v3f dir = (objpos - player_position).normalize();
+ ItemStack item = playeritem;
+ if (playeritem.name.empty()) {
+ InventoryList *hlist = local_inventory->getList("hand");
+ if (hlist) {
+ item = hlist->getItem(0);
+ }
+ }
+
+ bool disable_send = runData.selected_object->directReportPunch(
+ dir, &item, runData.time_from_last_punch);
+ runData.time_from_last_punch = 0;
+
+ if (!disable_send)
+ client->interact(0, pointed);
+ }
+ } else if (input->getRightClicked()) {
+ infostream << "Right-clicked object" << std::endl;
+ client->interact(3, pointed); // place
+ }
+}
+
+
+void Game::handleDigging(const PointedThing &pointed, const v3s16 &nodepos,
+ const ToolCapabilities &playeritem_toolcap, f32 dtime)
+{
+ LocalPlayer *player = client->getEnv().getLocalPlayer();
+ ClientMap &map = client->getEnv().getClientMap();
+ MapNode n = client->getEnv().getClientMap().getNodeNoEx(nodepos);
+
+ // NOTE: Similar piece of code exists on the server side for
+ // cheat detection.
+ // Get digging parameters
+ DigParams params = getDigParams(nodedef_manager->get(n).groups,
+ &playeritem_toolcap);
+
+ // If can't dig, try hand
+ if (!params.diggable) {
+ InventoryList *hlist = local_inventory->getList("hand");
+ const ToolCapabilities *tp = hlist
+ ? &hlist->getItem(0).getToolCapabilities(itemdef_manager)
+ : itemdef_manager->get("").tool_capabilities;
+
+ if (tp)
+ params = getDigParams(nodedef_manager->get(n).groups, tp);
+ }
+
+ if (!params.diggable) {
+ // I guess nobody will wait for this long
+ runData.dig_time_complete = 10000000.0;
+ } else {
+ runData.dig_time_complete = params.time;
+
+ if (m_cache_enable_particles) {
+ const ContentFeatures &features = client->getNodeDefManager()->get(n);
+ client->getParticleManager()->addNodeParticle(client,
+ player, nodepos, n, features);
+ }
+ }
+
+ if (!runData.digging) {
+ infostream << "Started digging" << std::endl;
+ runData.dig_instantly = runData.dig_time_complete == 0;
+ if (client->moddingEnabled() && client->getScript()->on_punchnode(nodepos, n))
+ return;
+ client->interact(0, pointed);
+ runData.digging = true;
+ runData.ldown_for_dig = true;
+ }
+
+ if (!runData.dig_instantly) {
+ runData.dig_index = (float)crack_animation_length
+ * runData.dig_time
+ / runData.dig_time_complete;
+ } else {
+ // This is for e.g. torches
+ runData.dig_index = crack_animation_length;
+ }
+
+ SimpleSoundSpec sound_dig = nodedef_manager->get(n).sound_dig;
+
+ if (sound_dig.exists() && params.diggable) {
+ if (sound_dig.name == "__group") {
+ if (!params.main_group.empty()) {
+ soundmaker->m_player_leftpunch_sound.gain = 0.5;
+ soundmaker->m_player_leftpunch_sound.name =
+ std::string("default_dig_") +
+ params.main_group;
+ }
+ } else {
+ soundmaker->m_player_leftpunch_sound = sound_dig;
+ }
+ }
+
+ // Don't show cracks if not diggable
+ if (runData.dig_time_complete >= 100000.0) {
+ } else if (runData.dig_index < crack_animation_length) {
+ //TimeTaker timer("client.setTempMod");
+ //infostream<<"dig_index="<<dig_index<<std::endl;
+ client->setCrack(runData.dig_index, nodepos);
+ } else {
+ infostream << "Digging completed" << std::endl;
+ client->setCrack(-1, v3s16(0, 0, 0));
+
+ runData.dig_time = 0;
+ runData.digging = false;
+ // we successfully dug, now block it from repeating if we want to be safe
+ if (g_settings->getBool("safe_dig_and_place"))
+ runData.digging_blocked = true;
+
+ runData.nodig_delay_timer =
+ runData.dig_time_complete / (float)crack_animation_length;
+
+ // We don't want a corresponding delay to very time consuming nodes
+ // and nodes without digging time (e.g. torches) get a fixed delay.
+ if (runData.nodig_delay_timer > 0.3)
+ runData.nodig_delay_timer = 0.3;
+ else if (runData.dig_instantly)
+ runData.nodig_delay_timer = 0.15;
+
+ bool is_valid_position;
+ MapNode wasnode = map.getNodeNoEx(nodepos, &is_valid_position);
+ if (is_valid_position) {
+ if (client->moddingEnabled() &&
+ client->getScript()->on_dignode(nodepos, wasnode)) {
+ return;
+ }
+
+ const ContentFeatures &f = client->ndef()->get(wasnode);
+ if (f.node_dig_prediction == "air") {
+ client->removeNode(nodepos);
+ } else if (!f.node_dig_prediction.empty()) {
+ content_t id;
+ bool found = client->ndef()->getId(f.node_dig_prediction, id);
+ if (found)
+ client->addNode(nodepos, id, true);
+ }
+ // implicit else: no prediction
+ }
+
+ client->interact(2, pointed);
+
+ if (m_cache_enable_particles) {
+ const ContentFeatures &features =
+ client->getNodeDefManager()->get(wasnode);
+ client->getParticleManager()->addDiggingParticles(client,
+ player, nodepos, wasnode, features);
+ }
+
+
+ // Send event to trigger sound
+ client->getEventManager()->put(new NodeDugEvent(nodepos, wasnode));
+ }
+
+ if (runData.dig_time_complete < 100000.0) {
+ runData.dig_time += dtime;
+ } else {
+ runData.dig_time = 0;
+ client->setCrack(-1, nodepos);
+ }
+
+ camera->setDigging(0); // left click animation
+}
+
+
+void Game::updateFrame(ProfilerGraph *graph, RunStats *stats, f32 dtime,
+ const CameraOrientation &cam)
+{
+ LocalPlayer *player = client->getEnv().getLocalPlayer();
+
+ /*
+ Fog range
+ */
+
+ if (draw_control->range_all) {
+ runData.fog_range = 100000 * BS;
+ } else {
+ runData.fog_range = draw_control->wanted_range * BS;
+ }
+
+ /*
+ Calculate general brightness
+ */
+ u32 daynight_ratio = client->getEnv().getDayNightRatio();
+ float time_brightness = decode_light_f((float)daynight_ratio / 1000.0);
+ float direct_brightness;
+ bool sunlight_seen;
+
+ if (m_cache_enable_noclip && m_cache_enable_free_move) {
+ direct_brightness = time_brightness;
+ sunlight_seen = true;
+ } else {
+ ScopeProfiler sp(g_profiler, "Detecting background light", SPT_AVG);
+ float old_brightness = sky->getBrightness();
+ direct_brightness = client->getEnv().getClientMap()
+ .getBackgroundBrightness(MYMIN(runData.fog_range * 1.2, 60 * BS),
+ daynight_ratio, (int)(old_brightness * 255.5), &sunlight_seen)
+ / 255.0;
+ }
+
+ float time_of_day_smooth = runData.time_of_day_smooth;
+ float time_of_day = client->getEnv().getTimeOfDayF();
+
+ static const float maxsm = 0.05f;
+ static const float todsm = 0.05f;
+
+ if (std::fabs(time_of_day - time_of_day_smooth) > maxsm &&
+ std::fabs(time_of_day - time_of_day_smooth + 1.0) > maxsm &&
+ std::fabs(time_of_day - time_of_day_smooth - 1.0) > maxsm)
+ time_of_day_smooth = time_of_day;
+
+ if (time_of_day_smooth > 0.8 && time_of_day < 0.2)
+ time_of_day_smooth = time_of_day_smooth * (1.0 - todsm)
+ + (time_of_day + 1.0) * todsm;
+ else
+ time_of_day_smooth = time_of_day_smooth * (1.0 - todsm)
+ + time_of_day * todsm;
+
+ runData.time_of_day_smooth = time_of_day_smooth;
+
+ sky->update(time_of_day_smooth, time_brightness, direct_brightness,
+ sunlight_seen, camera->getCameraMode(), player->getYaw(),
+ player->getPitch());
+
+ /*
+ Update clouds
+ */
+ if (clouds) {
+ if (sky->getCloudsVisible()) {
+ clouds->setVisible(true);
+ clouds->step(dtime);
+ // camera->getPosition is not enough for 3rd person views
+ v3f camera_node_position = camera->getCameraNode()->getPosition();
+ v3s16 camera_offset = camera->getOffset();
+ camera_node_position.X = camera_node_position.X + camera_offset.X * BS;
+ camera_node_position.Y = camera_node_position.Y + camera_offset.Y * BS;
+ camera_node_position.Z = camera_node_position.Z + camera_offset.Z * BS;
+ clouds->update(camera_node_position,
+ sky->getCloudColor());
+ if (clouds->isCameraInsideCloud() && m_cache_enable_fog) {
+ // if inside clouds, and fog enabled, use that as sky
+ // color(s)
+ video::SColor clouds_dark = clouds->getColor()
+ .getInterpolated(video::SColor(255, 0, 0, 0), 0.9);
+ sky->overrideColors(clouds_dark, clouds->getColor());
+ sky->setBodiesVisible(false);
+ runData.fog_range = std::fmin(runData.fog_range * 0.5f, 32.0f * BS);
+ // do not draw clouds after all
+ clouds->setVisible(false);
+ }
+ } else {
+ clouds->setVisible(false);
+ }
+ }
+
+ /*
+ Update particles
+ */
+ client->getParticleManager()->step(dtime);
+
+ /*
+ Fog
+ */
+
+ if (m_cache_enable_fog) {
+ driver->setFog(
+ sky->getBgColor(),
+ video::EFT_FOG_LINEAR,
+ runData.fog_range * m_cache_fog_start,
+ runData.fog_range * 1.0,
+ 0.01,
+ false, // pixel fog
+ true // range fog
+ );
+ } else {
+ driver->setFog(
+ sky->getBgColor(),
+ video::EFT_FOG_LINEAR,
+ 100000 * BS,
+ 110000 * BS,
+ 0.01f,
+ false, // pixel fog
+ false // range fog
+ );
+ }
+
+ /*
+ Get chat messages from client
+ */
+
+ v2u32 screensize = driver->getScreenSize();
+
+ updateChat(dtime, screensize);
+
+ /*
+ Inventory
+ */
+
+ if (client->getPlayerItem() != runData.new_playeritem)
+ client->selectPlayerItem(runData.new_playeritem);
+
+ // Update local inventory if it has changed
+ if (client->getLocalInventoryUpdated()) {
+ //infostream<<"Updating local inventory"<<std::endl;
+ client->getLocalInventory(*local_inventory);
+ runData.update_wielded_item_trigger = true;
+ }
+
+ if (runData.update_wielded_item_trigger) {
+ // Update wielded tool
+ InventoryList *mlist = local_inventory->getList("main");
+
+ if (mlist && (client->getPlayerItem() < mlist->getSize())) {
+ ItemStack item = mlist->getItem(client->getPlayerItem());
+ if (item.getDefinition(itemdef_manager).name.empty()) { // override the hand
+ InventoryList *hlist = local_inventory->getList("hand");
+ if (hlist)
+ item = hlist->getItem(0);
+ }
+ camera->wield(item);
+ }
+
+ runData.update_wielded_item_trigger = false;
+ }
+
+ /*
+ Update block draw list every 200ms or when camera direction has
+ changed much
+ */
+ runData.update_draw_list_timer += dtime;
+
+ v3f camera_direction = camera->getDirection();
+ if (runData.update_draw_list_timer >= 0.2
+ || runData.update_draw_list_last_cam_dir.getDistanceFrom(camera_direction) > 0.2
+ || m_camera_offset_changed) {
+ runData.update_draw_list_timer = 0;
+ client->getEnv().getClientMap().updateDrawList();
+ runData.update_draw_list_last_cam_dir = camera_direction;
+ }
+
+ m_game_ui->update(*stats, client, draw_control, cam, runData.pointed_old, dtime);
+
+ /*
+ make sure menu is on top
+ 1. Delete formspec menu reference if menu was removed
+ 2. Else, make sure formspec menu is on top
+ */
+ if (current_formspec) {
+ if (current_formspec->getReferenceCount() == 1) {
+ current_formspec->drop();
+ current_formspec = NULL;
+ } else if (isMenuActive()) {
+ guiroot->bringToFront(current_formspec);
+ }
+ }
+
+ /*
+ Drawing begins
+ */
+ const video::SColor &skycolor = sky->getSkyColor();
+
+ TimeTaker tt_draw("mainloop: draw");
+ driver->beginScene(true, true, skycolor);
+
+ bool draw_wield_tool = (m_game_ui->m_flags.show_hud &&
+ (player->hud_flags & HUD_FLAG_WIELDITEM_VISIBLE) &&
+ (camera->getCameraMode() == CAMERA_MODE_FIRST));
+ bool draw_crosshair = (
+ (player->hud_flags & HUD_FLAG_CROSSHAIR_VISIBLE) &&
+ (camera->getCameraMode() != CAMERA_MODE_THIRD_FRONT));
+#ifdef HAVE_TOUCHSCREENGUI
+ try {
+ draw_crosshair = !g_settings->getBool("touchtarget");
+ } catch (SettingNotFoundException) {
+ }
+#endif
+ RenderingEngine::draw_scene(skycolor, m_game_ui->m_flags.show_hud,
+ m_game_ui->m_flags.show_minimap, draw_wield_tool, draw_crosshair);
+
+ /*
+ Profiler graph
+ */
+ if (m_game_ui->m_flags.show_profiler_graph)
+ graph->draw(10, screensize.Y - 10, driver, g_fontengine->getFont());
+
+ /*
+ Damage flash
+ */
+ if (runData.damage_flash > 0.0f) {
+ video::SColor color(runData.damage_flash, 180, 0, 0);
+ driver->draw2DRectangle(color,
+ core::rect<s32>(0, 0, screensize.X, screensize.Y),
+ NULL);
+
+ runData.damage_flash -= 384.0f * dtime;
+ }
+
+ /*
+ Damage camera tilt
+ */
+ if (player->hurt_tilt_timer > 0.0f) {
+ player->hurt_tilt_timer -= dtime * 6.0f;
+
+ if (player->hurt_tilt_timer < 0.0f)
+ player->hurt_tilt_strength = 0.0f;
+ }
+
+ /*
+ Update minimap pos and rotation
+ */
+ if (mapper && m_game_ui->m_flags.show_minimap && m_game_ui->m_flags.show_hud) {
+ mapper->setPos(floatToInt(player->getPosition(), BS));
+ mapper->setAngle(player->getYaw());
+ }
+
+ /*
+ End scene
+ */
+ driver->endScene();
+
+ stats->drawtime = tt_draw.stop(true);
+ g_profiler->graphAdd("mainloop_draw", stats->drawtime / 1000.0f);
+}
+
+/* Log times and stuff for visualization */
+inline void Game::updateProfilerGraphs(ProfilerGraph *graph)
+{
+ Profiler::GraphValues values;
+ g_profiler->graphGet(values);
+ graph->put(values);
+}
+
+
+
+/****************************************************************************
+ Misc
+ ****************************************************************************/
+
+/* On some computers framerate doesn't seem to be automatically limited
+ */
+inline void Game::limitFps(FpsControl *fps_timings, f32 *dtime)
+{
+ // not using getRealTime is necessary for wine
+ device->getTimer()->tick(); // Maker sure device time is up-to-date
+ u32 time = device->getTimer()->getTime();
+ u32 last_time = fps_timings->last_time;
+
+ if (time > last_time) // Make sure time hasn't overflowed
+ fps_timings->busy_time = time - last_time;
+ else
+ fps_timings->busy_time = 0;
+
+ u32 frametime_min = 1000 / (g_menumgr.pausesGame()
+ ? g_settings->getFloat("pause_fps_max")
+ : g_settings->getFloat("fps_max"));
+
+ if (fps_timings->busy_time < frametime_min) {
+ fps_timings->sleep_time = frametime_min - fps_timings->busy_time;
+ device->sleep(fps_timings->sleep_time);
+ } else {
+ fps_timings->sleep_time = 0;
+ }
+
+ /* Get the new value of the device timer. Note that device->sleep() may
+ * not sleep for the entire requested time as sleep may be interrupted and
+ * therefore it is arguably more accurate to get the new time from the
+ * device rather than calculating it by adding sleep_time to time.
+ */
+
+ device->getTimer()->tick(); // Update device timer
+ time = device->getTimer()->getTime();
+
+ if (time > last_time) // Make sure last_time hasn't overflowed
+ *dtime = (time - last_time) / 1000.0;
+ else
+ *dtime = 0;
+
+ fps_timings->last_time = time;
+}
+
+void Game::showOverlayMessage(const char *msg, float dtime, int percent, bool draw_clouds)
+{
+ const wchar_t *wmsg = wgettext(msg);
+ RenderingEngine::draw_load_screen(wmsg, guienv, texture_src, dtime, percent,
+ draw_clouds);
+ delete[] wmsg;
+}
+
+void Game::settingChangedCallback(const std::string &setting_name, void *data)
+{
+ ((Game *)data)->readSettings();
+}
+
+void Game::readSettings()
+{
+ m_cache_doubletap_jump = g_settings->getBool("doubletap_jump");
+ m_cache_enable_clouds = g_settings->getBool("enable_clouds");
+ m_cache_enable_joysticks = g_settings->getBool("enable_joysticks");
+ m_cache_enable_particles = g_settings->getBool("enable_particles");
+ m_cache_enable_fog = g_settings->getBool("enable_fog");
+ m_cache_mouse_sensitivity = g_settings->getFloat("mouse_sensitivity");
+ m_cache_joystick_frustum_sensitivity = g_settings->getFloat("joystick_frustum_sensitivity");
+ m_repeat_right_click_time = g_settings->getFloat("repeat_rightclick_time");
+
+ m_cache_enable_noclip = g_settings->getBool("noclip");
+ m_cache_enable_free_move = g_settings->getBool("free_move");
+
+ m_cache_fog_start = g_settings->getFloat("fog_start");
+
+ m_cache_cam_smoothing = 0;
+ if (g_settings->getBool("cinematic"))
+ m_cache_cam_smoothing = 1 - g_settings->getFloat("cinematic_camera_smoothing");
+ else
+ m_cache_cam_smoothing = 1 - g_settings->getFloat("camera_smoothing");
+
+ m_cache_fog_start = rangelim(m_cache_fog_start, 0.0f, 0.99f);
+ m_cache_cam_smoothing = rangelim(m_cache_cam_smoothing, 0.01f, 1.0f);
+ m_cache_mouse_sensitivity = rangelim(m_cache_mouse_sensitivity, 0.001, 100.0);
+
+ m_does_lost_focus_pause_game = g_settings->getBool("pause_on_lost_focus");
+}
+
+/****************************************************************************/
+/****************************************************************************
+ Shutdown / cleanup
+ ****************************************************************************/
+/****************************************************************************/
+
+void Game::extendedResourceCleanup()
+{
+ // Extended resource accounting
+ infostream << "Irrlicht resources after cleanup:" << std::endl;
+ infostream << "\tRemaining meshes : "
+ << RenderingEngine::get_mesh_cache()->getMeshCount() << std::endl;
+ infostream << "\tRemaining textures : "
+ << driver->getTextureCount() << std::endl;
+
+ for (unsigned int i = 0; i < driver->getTextureCount(); i++) {
+ irr::video::ITexture *texture = driver->getTextureByIndex(i);
+ infostream << "\t\t" << i << ":" << texture->getName().getPath().c_str()
+ << std::endl;
+ }
+
+ clearTextureNameCache();
+ infostream << "\tRemaining materials: "
+ << driver-> getMaterialRendererCount()
+ << " (note: irrlicht doesn't support removing renderers)" << std::endl;
+}
+
+void Game::showDeathFormspec()
+{
+ static std::string formspec =
+ std::string(FORMSPEC_VERSION_STRING) +
+ SIZE_TAG
+ "bgcolor[#320000b4;true]"
+ "label[4.85,1.35;" + gettext("You died") + "]"
+ "button_exit[4,3;3,0.5;btn_respawn;" + gettext("Respawn") + "]"
+ ;
+
+ /* Create menu */
+ /* Note: FormspecFormSource and LocalFormspecHandler *
+ * are deleted by guiFormSpecMenu */
+ FormspecFormSource *fs_src = new FormspecFormSource(formspec);
+ LocalFormspecHandler *txt_dst = new LocalFormspecHandler("MT_DEATH_SCREEN", client);
+
+ GUIFormSpecMenu::create(current_formspec, client, &input->joystick, fs_src,
+ txt_dst, client->getFormspecPrepend());
+ current_formspec->setFocus("btn_respawn");
+}
+
+#define GET_KEY_NAME(KEY) gettext(getKeySetting(#KEY).name())
+void Game::showPauseMenu()
+{
+#ifdef __ANDROID__
+ static const std::string control_text = strgettext("Default Controls:\n"
+ "No menu visible:\n"
+ "- single tap: button activate\n"
+ "- double tap: place/use\n"
+ "- slide finger: look around\n"
+ "Menu/Inventory visible:\n"
+ "- double tap (outside):\n"
+ " -->close\n"
+ "- touch stack, touch slot:\n"
+ " --> move stack\n"
+ "- touch&drag, tap 2nd finger\n"
+ " --> place single item to slot\n"
+ );
+#else
+ static const std::string control_text_template = strgettext("Controls:\n"
+ "- %s: move forwards\n"
+ "- %s: move backwards\n"
+ "- %s: move left\n"
+ "- %s: move right\n"
+ "- %s: jump/climb\n"
+ "- %s: sneak/go down\n"
+ "- %s: drop item\n"
+ "- %s: inventory\n"
+ "- Mouse: turn/look\n"
+ "- Mouse left: dig/punch\n"
+ "- Mouse right: place/use\n"
+ "- Mouse wheel: select item\n"
+ "- %s: chat\n"
+ );
+
+ char control_text_buf[600];
+
+ porting::mt_snprintf(control_text_buf, sizeof(control_text_buf), control_text_template.c_str(),
+ GET_KEY_NAME(keymap_forward),
+ GET_KEY_NAME(keymap_backward),
+ GET_KEY_NAME(keymap_left),
+ GET_KEY_NAME(keymap_right),
+ GET_KEY_NAME(keymap_jump),
+ GET_KEY_NAME(keymap_sneak),
+ GET_KEY_NAME(keymap_drop),
+ GET_KEY_NAME(keymap_inventory),
+ GET_KEY_NAME(keymap_chat)
+ );
+
+ std::string control_text = std::string(control_text_buf);
+ str_formspec_escape(control_text);
+#endif
+
+ float ypos = simple_singleplayer_mode ? 0.7f : 0.1f;
+ std::ostringstream os;
+
+ os << FORMSPEC_VERSION_STRING << SIZE_TAG
+ << "button_exit[4," << (ypos++) << ";3,0.5;btn_continue;"
+ << strgettext("Continue") << "]";
+
+ if (!simple_singleplayer_mode) {
+ os << "button_exit[4," << (ypos++) << ";3,0.5;btn_change_password;"
+ << strgettext("Change Password") << "]";
+ } else {
+ os << "field[4.95,0;5,1.5;;" << strgettext("Game paused") << ";]";
+ }
+
+#ifndef __ANDROID__
+ os << "button_exit[4," << (ypos++) << ";3,0.5;btn_sound;"
+ << strgettext("Sound Volume") << "]";
+ os << "button_exit[4," << (ypos++) << ";3,0.5;btn_key_config;"
+ << strgettext("Change Keys") << "]";
+#endif
+ os << "button_exit[4," << (ypos++) << ";3,0.5;btn_exit_menu;"
+ << strgettext("Exit to Menu") << "]";
+ os << "button_exit[4," << (ypos++) << ";3,0.5;btn_exit_os;"
+ << strgettext("Exit to OS") << "]"
+ << "textarea[7.5,0.25;3.9,6.25;;" << control_text << ";]"
+ << "textarea[0.4,0.25;3.9,6.25;;" << PROJECT_NAME_C " " VERSION_STRING "\n"
+ << "\n"
+ << strgettext("Game info:") << "\n";
+ const std::string &address = client->getAddressName();
+ static const std::string mode = strgettext("- Mode: ");
+ if (!simple_singleplayer_mode) {
+ Address serverAddress = client->getServerAddress();
+ if (!address.empty()) {
+ os << mode << strgettext("Remote server") << "\n"
+ << strgettext("- Address: ") << address;
+ } else {
+ os << mode << strgettext("Hosting server");
+ }
+ os << "\n" << strgettext("- Port: ") << serverAddress.getPort() << "\n";
+ } else {
+ os << mode << strgettext("Singleplayer") << "\n";
+ }
+ if (simple_singleplayer_mode || address.empty()) {
+ static const std::string on = strgettext("On");
+ static const std::string off = strgettext("Off");
+ const std::string &damage = g_settings->getBool("enable_damage") ? on : off;
+ const std::string &creative = g_settings->getBool("creative_mode") ? on : off;
+ const std::string &announced = g_settings->getBool("server_announce") ? on : off;
+ os << strgettext("- Damage: ") << damage << "\n"
+ << strgettext("- Creative Mode: ") << creative << "\n";
+ if (!simple_singleplayer_mode) {
+ const std::string &pvp = g_settings->getBool("enable_pvp") ? on : off;
+ os << strgettext("- PvP: ") << pvp << "\n"
+ << strgettext("- Public: ") << announced << "\n";
+ std::string server_name = g_settings->get("server_name");
+ str_formspec_escape(server_name);
+ if (announced == on && !server_name.empty())
+ os << strgettext("- Server Name: ") << server_name;
+
+ }
+ }
+ os << ";]";
+
+ /* Create menu */
+ /* Note: FormspecFormSource and LocalFormspecHandler *
+ * are deleted by guiFormSpecMenu */
+ FormspecFormSource *fs_src = new FormspecFormSource(os.str());
+ LocalFormspecHandler *txt_dst = new LocalFormspecHandler("MT_PAUSE_MENU");
+
+ GUIFormSpecMenu::create(current_formspec, client, &input->joystick,
+ fs_src, txt_dst, client->getFormspecPrepend());
+ current_formspec->setFocus("btn_continue");
+ current_formspec->doPause = true;
+}
+
+/****************************************************************************/
+/****************************************************************************
+ extern function for launching the game
+ ****************************************************************************/
+/****************************************************************************/
+
+void the_game(bool *kill,
+ bool random_input,
+ InputHandler *input,
+ const std::string &map_dir,
+ const std::string &playername,
+ const std::string &password,
+ const std::string &address, // If empty local server is created
+ u16 port,
+
+ std::string &error_message,
+ ChatBackend &chat_backend,
+ bool *reconnect_requested,
+ const SubgameSpec &gamespec, // Used for local game
+ bool simple_singleplayer_mode)
+{
+ Game game;
+
+ /* Make a copy of the server address because if a local singleplayer server
+ * is created then this is updated and we don't want to change the value
+ * passed to us by the calling function
+ */
+ std::string server_address = address;
+
+ try {
+
+ if (game.startup(kill, random_input, input, map_dir,
+ playername, password, &server_address, port, error_message,
+ reconnect_requested, &chat_backend, gamespec,
+ simple_singleplayer_mode)) {
+ game.run();
+ game.shutdown();
+ }
+
+ } catch (SerializationError &e) {
+ error_message = std::string("A serialization error occurred:\n")
+ + e.what() + "\n\nThe server is probably "
+ " running a different version of " PROJECT_NAME_C ".";
+ errorstream << error_message << std::endl;
+ } catch (ServerError &e) {
+ error_message = e.what();
+ errorstream << "ServerError: " << error_message << std::endl;
+ } catch (ModError &e) {
+ error_message = e.what() + strgettext("\nCheck debug.txt for details.");
+ errorstream << "ModError: " << error_message << std::endl;
+ }
+}
diff --git a/src/client/game.h b/src/client/game.h
new file mode 100644
index 000000000..69e6eed0b
--- /dev/null
+++ b/src/client/game.h
@@ -0,0 +1,56 @@
+/*
+Minetest
+Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+
+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.h"
+#include <string>
+
+class InputHandler;
+class ChatBackend; /* to avoid having to include chat.h */
+struct SubgameSpec;
+
+struct Jitter {
+ f32 max, min, avg, counter, max_sample, min_sample, max_fraction;
+};
+
+struct RunStats {
+ u32 drawtime;
+
+ Jitter dtime_jitter, busy_time_jitter;
+};
+
+struct CameraOrientation {
+ f32 camera_yaw; // "right/left"
+ f32 camera_pitch; // "up/down"
+};
+
+void the_game(bool *kill,
+ bool random_input,
+ InputHandler *input,
+ const std::string &map_dir,
+ const std::string &playername,
+ const std::string &password,
+ const std::string &address, // If "", local server is used
+ u16 port,
+ std::string &error_message,
+ ChatBackend &chat_backend,
+ bool *reconnect_requested,
+ const SubgameSpec &gamespec, // Used for local game
+ bool simple_singleplayer_mode);
diff --git a/src/client/guiscalingfilter.cpp b/src/client/guiscalingfilter.cpp
new file mode 100644
index 000000000..3b4377da5
--- /dev/null
+++ b/src/client/guiscalingfilter.cpp
@@ -0,0 +1,169 @@
+/*
+Copyright (C) 2015 Aaron Suen <warr1024@gmail.com>
+
+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 "guiscalingfilter.h"
+#include "imagefilters.h"
+#include "porting.h"
+#include "settings.h"
+#include "util/numeric.h"
+#include <cstdio>
+#include "client/renderingengine.h"
+
+/* Maintain a static cache to store the images that correspond to textures
+ * in a format that's manipulable by code. Some platforms exhibit issues
+ * converting textures back into images repeatedly, and some don't even
+ * allow it at all.
+ */
+std::map<io::path, video::IImage *> g_imgCache;
+
+/* Maintain a static cache of all pre-scaled textures. These need to be
+ * cleared as well when the cached images.
+ */
+std::map<io::path, video::ITexture *> g_txrCache;
+
+/* Manually insert an image into the cache, useful to avoid texture-to-image
+ * conversion whenever we can intercept it.
+ */
+void guiScalingCache(io::path key, video::IVideoDriver *driver, video::IImage *value)
+{
+ if (!g_settings->getBool("gui_scaling_filter"))
+ return;
+ video::IImage *copied = driver->createImage(value->getColorFormat(),
+ value->getDimension());
+ value->copyTo(copied);
+ g_imgCache[key] = copied;
+}
+
+// Manually clear the cache, e.g. when switching to different worlds.
+void guiScalingCacheClear()
+{
+ for (auto &it : g_imgCache) {
+ if (it.second)
+ it.second->drop();
+ }
+ g_imgCache.clear();
+ for (auto &it : g_txrCache) {
+ if (it.second)
+ RenderingEngine::get_video_driver()->removeTexture(it.second);
+ }
+ g_txrCache.clear();
+}
+
+/* Get a cached, high-quality pre-scaled texture for display purposes. If the
+ * texture is not already cached, attempt to create it. Returns a pre-scaled texture,
+ * or the original texture if unable to pre-scale it.
+ */
+video::ITexture *guiScalingResizeCached(video::IVideoDriver *driver,
+ video::ITexture *src, const core::rect<s32> &srcrect,
+ const core::rect<s32> &destrect)
+{
+ if (src == NULL)
+ return src;
+ if (!g_settings->getBool("gui_scaling_filter"))
+ return src;
+
+ // Calculate scaled texture name.
+ char rectstr[200];
+ porting::mt_snprintf(rectstr, sizeof(rectstr), "%d:%d:%d:%d:%d:%d",
+ srcrect.UpperLeftCorner.X,
+ srcrect.UpperLeftCorner.Y,
+ srcrect.getWidth(),
+ srcrect.getHeight(),
+ destrect.getWidth(),
+ destrect.getHeight());
+ io::path origname = src->getName().getPath();
+ io::path scalename = origname + "@guiScalingFilter:" + rectstr;
+
+ // Search for existing scaled texture.
+ video::ITexture *scaled = g_txrCache[scalename];
+ if (scaled)
+ return scaled;
+
+ // Try to find the texture converted to an image in the cache.
+ // If the image was not found, try to extract it from the texture.
+ video::IImage* srcimg = g_imgCache[origname];
+ if (srcimg == NULL) {
+ if (!g_settings->getBool("gui_scaling_filter_txr2img"))
+ return src;
+ srcimg = driver->createImageFromData(src->getColorFormat(),
+ src->getSize(), src->lock(), false);
+ src->unlock();
+ g_imgCache[origname] = srcimg;
+ }
+
+ // Create a new destination image and scale the source into it.
+ imageCleanTransparent(srcimg, 0);
+ video::IImage *destimg = driver->createImage(src->getColorFormat(),
+ core::dimension2d<u32>((u32)destrect.getWidth(),
+ (u32)destrect.getHeight()));
+ imageScaleNNAA(srcimg, srcrect, destimg);
+
+#ifdef __ANDROID__
+ // Android is very picky about textures being powers of 2, so expand
+ // the image dimensions to the next power of 2, if necessary, for
+ // that platform.
+ video::IImage *po2img = driver->createImage(src->getColorFormat(),
+ core::dimension2d<u32>(npot2((u32)destrect.getWidth()),
+ npot2((u32)destrect.getHeight())));
+ po2img->fill(video::SColor(0, 0, 0, 0));
+ destimg->copyTo(po2img);
+ destimg->drop();
+ destimg = po2img;
+#endif
+
+ // Convert the scaled image back into a texture.
+ scaled = driver->addTexture(scalename, destimg, NULL);
+ destimg->drop();
+ g_txrCache[scalename] = scaled;
+
+ return scaled;
+}
+
+/* Convenience wrapper for guiScalingResizeCached that accepts parameters that
+ * are available at GUI imagebutton creation time.
+ */
+video::ITexture *guiScalingImageButton(video::IVideoDriver *driver,
+ video::ITexture *src, s32 width, s32 height)
+{
+ if (src == NULL)
+ return src;
+ return guiScalingResizeCached(driver, src,
+ core::rect<s32>(0, 0, src->getSize().Width, src->getSize().Height),
+ core::rect<s32>(0, 0, width, height));
+}
+
+/* Replacement for driver->draw2DImage() that uses the high-quality pre-scaled
+ * texture, if configured.
+ */
+void draw2DImageFilterScaled(video::IVideoDriver *driver, video::ITexture *txr,
+ const core::rect<s32> &destrect, const core::rect<s32> &srcrect,
+ const core::rect<s32> *cliprect, const video::SColor *const colors,
+ bool usealpha)
+{
+ // Attempt to pre-scale image in software in high quality.
+ video::ITexture *scaled = guiScalingResizeCached(driver, txr, srcrect, destrect);
+ if (scaled == NULL)
+ return;
+
+ // Correct source rect based on scaled image.
+ const core::rect<s32> mysrcrect = (scaled != txr)
+ ? core::rect<s32>(0, 0, destrect.getWidth(), destrect.getHeight())
+ : srcrect;
+
+ driver->draw2DImage(scaled, destrect, mysrcrect, cliprect, colors, usealpha);
+}
diff --git a/src/client/guiscalingfilter.h b/src/client/guiscalingfilter.h
new file mode 100644
index 000000000..4661bf8da
--- /dev/null
+++ b/src/client/guiscalingfilter.h
@@ -0,0 +1,50 @@
+/*
+Copyright (C) 2015 Aaron Suen <warr1024@gmail.com>
+
+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"
+
+/* Manually insert an image into the cache, useful to avoid texture-to-image
+ * conversion whenever we can intercept it.
+ */
+void guiScalingCache(io::path key, video::IVideoDriver *driver, video::IImage *value);
+
+// Manually clear the cache, e.g. when switching to different worlds.
+void guiScalingCacheClear();
+
+/* Get a cached, high-quality pre-scaled texture for display purposes. If the
+ * texture is not already cached, attempt to create it. Returns a pre-scaled texture,
+ * or the original texture if unable to pre-scale it.
+ */
+video::ITexture *guiScalingResizeCached(video::IVideoDriver *driver, video::ITexture *src,
+ const core::rect<s32> &srcrect, const core::rect<s32> &destrect);
+
+/* Convenience wrapper for guiScalingResizeCached that accepts parameters that
+ * are available at GUI imagebutton creation time.
+ */
+video::ITexture *guiScalingImageButton(video::IVideoDriver *driver, video::ITexture *src,
+ s32 width, s32 height);
+
+/* Replacement for driver->draw2DImage() that uses the high-quality pre-scaled
+ * texture, if configured.
+ */
+void draw2DImageFilterScaled(video::IVideoDriver *driver, video::ITexture *txr,
+ const core::rect<s32> &destrect, const core::rect<s32> &srcrect,
+ const core::rect<s32> *cliprect = 0, const video::SColor *const colors = 0,
+ bool usealpha = false);
diff --git a/src/client/imagefilters.cpp b/src/client/imagefilters.cpp
new file mode 100644
index 000000000..dd029628f
--- /dev/null
+++ b/src/client/imagefilters.cpp
@@ -0,0 +1,172 @@
+/*
+Copyright (C) 2015 Aaron Suen <warr1024@gmail.com>
+
+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 "imagefilters.h"
+#include "util/numeric.h"
+#include <cmath>
+
+/* Fill in RGB values for transparent pixels, to correct for odd colors
+ * appearing at borders when blending. This is because many PNG optimizers
+ * like to discard RGB values of transparent pixels, but when blending then
+ * with non-transparent neighbors, their RGB values will shpw up nonetheless.
+ *
+ * This function modifies the original image in-place.
+ *
+ * Parameter "threshold" is the alpha level below which pixels are considered
+ * transparent. Should be 127 for 3d where alpha is threshold, but 0 for
+ * 2d where alpha is blended.
+ */
+void imageCleanTransparent(video::IImage *src, u32 threshold)
+{
+ core::dimension2d<u32> dim = src->getDimension();
+
+ // Walk each pixel looking for fully transparent ones.
+ // Note: loop y around x for better cache locality.
+ for (u32 ctry = 0; ctry < dim.Height; ctry++)
+ for (u32 ctrx = 0; ctrx < dim.Width; ctrx++) {
+
+ // Ignore opaque pixels.
+ irr::video::SColor c = src->getPixel(ctrx, ctry);
+ if (c.getAlpha() > threshold)
+ continue;
+
+ // Sample size and total weighted r, g, b values.
+ u32 ss = 0, sr = 0, sg = 0, sb = 0;
+
+ // Walk each neighbor pixel (clipped to image bounds).
+ for (u32 sy = (ctry < 1) ? 0 : (ctry - 1);
+ sy <= (ctry + 1) && sy < dim.Height; sy++)
+ for (u32 sx = (ctrx < 1) ? 0 : (ctrx - 1);
+ sx <= (ctrx + 1) && sx < dim.Width; sx++) {
+
+ // Ignore transparent pixels.
+ irr::video::SColor d = src->getPixel(sx, sy);
+ if (d.getAlpha() <= threshold)
+ continue;
+
+ // Add RGB values weighted by alpha.
+ u32 a = d.getAlpha();
+ ss += a;
+ sr += a * d.getRed();
+ sg += a * d.getGreen();
+ sb += a * d.getBlue();
+ }
+
+ // If we found any neighbor RGB data, set pixel to average
+ // weighted by alpha.
+ if (ss > 0) {
+ c.setRed(sr / ss);
+ c.setGreen(sg / ss);
+ c.setBlue(sb / ss);
+ src->setPixel(ctrx, ctry, c);
+ }
+ }
+}
+
+/* Scale a region of an image into another image, using nearest-neighbor with
+ * anti-aliasing; treat pixels as crisp rectangles, but blend them at boundaries
+ * to prevent non-integer scaling ratio artifacts. Note that this may cause
+ * some blending at the edges where pixels don't line up perfectly, but this
+ * filter is designed to produce the most accurate results for both upscaling
+ * and downscaling.
+ */
+void imageScaleNNAA(video::IImage *src, const core::rect<s32> &srcrect, video::IImage *dest)
+{
+ double sx, sy, minsx, maxsx, minsy, maxsy, area, ra, ga, ba, aa, pw, ph, pa;
+ u32 dy, dx;
+ video::SColor pxl;
+
+ // Cache rectsngle boundaries.
+ double sox = srcrect.UpperLeftCorner.X * 1.0;
+ double soy = srcrect.UpperLeftCorner.Y * 1.0;
+ double sw = srcrect.getWidth() * 1.0;
+ double sh = srcrect.getHeight() * 1.0;
+
+ // Walk each destination image pixel.
+ // Note: loop y around x for better cache locality.
+ core::dimension2d<u32> dim = dest->getDimension();
+ for (dy = 0; dy < dim.Height; dy++)
+ for (dx = 0; dx < dim.Width; dx++) {
+
+ // Calculate floating-point source rectangle bounds.
+ // Do some basic clipping, and for mirrored/flipped rects,
+ // make sure min/max are in the right order.
+ minsx = sox + (dx * sw / dim.Width);
+ minsx = rangelim(minsx, 0, sw);
+ maxsx = minsx + sw / dim.Width;
+ maxsx = rangelim(maxsx, 0, sw);
+ if (minsx > maxsx)
+ SWAP(double, minsx, maxsx);
+ minsy = soy + (dy * sh / dim.Height);
+ minsy = rangelim(minsy, 0, sh);
+ maxsy = minsy + sh / dim.Height;
+ maxsy = rangelim(maxsy, 0, sh);
+ if (minsy > maxsy)
+ SWAP(double, minsy, maxsy);
+
+ // Total area, and integral of r, g, b values over that area,
+ // initialized to zero, to be summed up in next loops.
+ area = 0;
+ ra = 0;
+ ga = 0;
+ ba = 0;
+ aa = 0;
+
+ // Loop over the integral pixel positions described by those bounds.
+ for (sy = floor(minsy); sy < maxsy; sy++)
+ for (sx = floor(minsx); sx < maxsx; sx++) {
+
+ // Calculate width, height, then area of dest pixel
+ // that's covered by this source pixel.
+ pw = 1;
+ if (minsx > sx)
+ pw += sx - minsx;
+ if (maxsx < (sx + 1))
+ pw += maxsx - sx - 1;
+ ph = 1;
+ if (minsy > sy)
+ ph += sy - minsy;
+ if (maxsy < (sy + 1))
+ ph += maxsy - sy - 1;
+ pa = pw * ph;
+
+ // Get source pixel and add it to totals, weighted
+ // by covered area and alpha.
+ pxl = src->getPixel((u32)sx, (u32)sy);
+ area += pa;
+ ra += pa * pxl.getRed();
+ ga += pa * pxl.getGreen();
+ ba += pa * pxl.getBlue();
+ aa += pa * pxl.getAlpha();
+ }
+
+ // Set the destination image pixel to the average color.
+ if (area > 0) {
+ pxl.setRed(ra / area + 0.5);
+ pxl.setGreen(ga / area + 0.5);
+ pxl.setBlue(ba / area + 0.5);
+ pxl.setAlpha(aa / area + 0.5);
+ } else {
+ pxl.setRed(0);
+ pxl.setGreen(0);
+ pxl.setBlue(0);
+ pxl.setAlpha(0);
+ }
+ dest->setPixel(dx, dy, pxl);
+ }
+}
diff --git a/src/client/imagefilters.h b/src/client/imagefilters.h
new file mode 100644
index 000000000..5676faf85
--- /dev/null
+++ b/src/client/imagefilters.h
@@ -0,0 +1,43 @@
+/*
+Copyright (C) 2015 Aaron Suen <warr1024@gmail.com>
+
+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"
+
+/* Fill in RGB values for transparent pixels, to correct for odd colors
+ * appearing at borders when blending. This is because many PNG optimizers
+ * like to discard RGB values of transparent pixels, but when blending then
+ * with non-transparent neighbors, their RGB values will shpw up nonetheless.
+ *
+ * This function modifies the original image in-place.
+ *
+ * Parameter "threshold" is the alpha level below which pixels are considered
+ * transparent. Should be 127 for 3d where alpha is threshold, but 0 for
+ * 2d where alpha is blended.
+ */
+void imageCleanTransparent(video::IImage *src, u32 threshold);
+
+/* Scale a region of an image into another image, using nearest-neighbor with
+ * anti-aliasing; treat pixels as crisp rectangles, but blend them at boundaries
+ * to prevent non-integer scaling ratio artifacts. Note that this may cause
+ * some blending at the edges where pixels don't line up perfectly, but this
+ * filter is designed to produce the most accurate results for both upscaling
+ * and downscaling.
+ */
+void imageScaleNNAA(video::IImage *src, const core::rect<s32> &srcrect, video::IImage *dest);
diff --git a/src/client/keycode.cpp b/src/client/keycode.cpp
new file mode 100644
index 000000000..646d181e0
--- /dev/null
+++ b/src/client/keycode.cpp
@@ -0,0 +1,384 @@
+/*
+Minetest
+Copyright (C) 2010-2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+
+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 "keycode.h"
+#include "exceptions.h"
+#include "settings.h"
+#include "log.h"
+#include "debug.h"
+#include "util/hex.h"
+#include "util/string.h"
+#include "util/basic_macros.h"
+
+class UnknownKeycode : public BaseException
+{
+public:
+ UnknownKeycode(const char *s) :
+ BaseException(s) {};
+};
+
+struct table_key {
+ const char *Name;
+ irr::EKEY_CODE Key;
+ wchar_t Char; // L'\0' means no character assigned
+ const char *LangName; // NULL means it doesn't have a human description
+};
+
+#define DEFINEKEY1(x, lang) /* Irrlicht key without character */ \
+ { #x, irr::x, L'\0', lang },
+#define DEFINEKEY2(x, ch, lang) /* Irrlicht key with character */ \
+ { #x, irr::x, ch, lang },
+#define DEFINEKEY3(ch) /* single Irrlicht key (e.g. KEY_KEY_X) */ \
+ { "KEY_KEY_" TOSTRING(ch), irr::KEY_KEY_ ## ch, (wchar_t) *TOSTRING(ch), TOSTRING(ch) },
+#define DEFINEKEY4(ch) /* single Irrlicht function key (e.g. KEY_F3) */ \
+ { "KEY_F" TOSTRING(ch), irr::KEY_F ## ch, L'\0', "F" TOSTRING(ch) },
+#define DEFINEKEY5(ch) /* key without Irrlicht keycode */ \
+ { ch, irr::KEY_KEY_CODES_COUNT, (wchar_t) *ch, ch },
+
+#define N_(text) text
+
+static const struct table_key table[] = {
+ // Keys that can be reliably mapped between Char and Key
+ DEFINEKEY3(0)
+ DEFINEKEY3(1)
+ DEFINEKEY3(2)
+ DEFINEKEY3(3)
+ DEFINEKEY3(4)
+ DEFINEKEY3(5)
+ DEFINEKEY3(6)
+ DEFINEKEY3(7)
+ DEFINEKEY3(8)
+ DEFINEKEY3(9)
+ DEFINEKEY3(A)
+ DEFINEKEY3(B)
+ DEFINEKEY3(C)
+ DEFINEKEY3(D)
+ DEFINEKEY3(E)
+ DEFINEKEY3(F)
+ DEFINEKEY3(G)
+ DEFINEKEY3(H)
+ DEFINEKEY3(I)
+ DEFINEKEY3(J)
+ DEFINEKEY3(K)
+ DEFINEKEY3(L)
+ DEFINEKEY3(M)
+ DEFINEKEY3(N)
+ DEFINEKEY3(O)
+ DEFINEKEY3(P)
+ DEFINEKEY3(Q)
+ DEFINEKEY3(R)
+ DEFINEKEY3(S)
+ DEFINEKEY3(T)
+ DEFINEKEY3(U)
+ DEFINEKEY3(V)
+ DEFINEKEY3(W)
+ DEFINEKEY3(X)
+ DEFINEKEY3(Y)
+ DEFINEKEY3(Z)
+ DEFINEKEY2(KEY_PLUS, L'+', "+")
+ DEFINEKEY2(KEY_COMMA, L',', ",")
+ DEFINEKEY2(KEY_MINUS, L'-', "-")
+ DEFINEKEY2(KEY_PERIOD, L'.', ".")
+
+ // Keys without a Char
+ DEFINEKEY1(KEY_LBUTTON, N_("Left Button"))
+ DEFINEKEY1(KEY_RBUTTON, N_("Right Button"))
+ DEFINEKEY1(KEY_CANCEL, N_("Cancel"))
+ DEFINEKEY1(KEY_MBUTTON, N_("Middle Button"))
+ DEFINEKEY1(KEY_XBUTTON1, N_("X Button 1"))
+ DEFINEKEY1(KEY_XBUTTON2, N_("X Button 2"))
+ DEFINEKEY1(KEY_BACK, N_("Backspace"))
+ DEFINEKEY1(KEY_TAB, N_("Tab"))
+ DEFINEKEY1(KEY_CLEAR, N_("Clear"))
+ DEFINEKEY1(KEY_RETURN, N_("Return"))
+ DEFINEKEY1(KEY_SHIFT, N_("Shift"))
+ DEFINEKEY1(KEY_CONTROL, N_("Control"))
+ DEFINEKEY1(KEY_MENU, N_("Menu"))
+ DEFINEKEY1(KEY_PAUSE, N_("Pause"))
+ DEFINEKEY1(KEY_CAPITAL, N_("Caps Lock"))
+ DEFINEKEY1(KEY_SPACE, N_("Space"))
+ DEFINEKEY1(KEY_PRIOR, N_("Page up"))
+ DEFINEKEY1(KEY_NEXT, N_("Page down"))
+ DEFINEKEY1(KEY_END, N_("End"))
+ DEFINEKEY1(KEY_HOME, N_("Home"))
+ DEFINEKEY1(KEY_LEFT, N_("Left"))
+ DEFINEKEY1(KEY_UP, N_("Up"))
+ DEFINEKEY1(KEY_RIGHT, N_("Right"))
+ DEFINEKEY1(KEY_DOWN, N_("Down"))
+ DEFINEKEY1(KEY_SELECT, N_("Select"))
+ DEFINEKEY1(KEY_PRINT, N_("Print"))
+ DEFINEKEY1(KEY_EXECUT, N_("Execute"))
+ DEFINEKEY1(KEY_SNAPSHOT, N_("Snapshot"))
+ DEFINEKEY1(KEY_INSERT, N_("Insert"))
+ DEFINEKEY1(KEY_DELETE, N_("Delete"))
+ DEFINEKEY1(KEY_HELP, N_("Help"))
+ DEFINEKEY1(KEY_LWIN, N_("Left Windows"))
+ DEFINEKEY1(KEY_RWIN, N_("Right Windows"))
+ DEFINEKEY1(KEY_NUMPAD0, N_("Numpad 0")) // These are not assigned to a char
+ DEFINEKEY1(KEY_NUMPAD1, N_("Numpad 1")) // to prevent interference with KEY_KEY_[0-9].
+ DEFINEKEY1(KEY_NUMPAD2, N_("Numpad 2"))
+ DEFINEKEY1(KEY_NUMPAD3, N_("Numpad 3"))
+ DEFINEKEY1(KEY_NUMPAD4, N_("Numpad 4"))
+ DEFINEKEY1(KEY_NUMPAD5, N_("Numpad 5"))
+ DEFINEKEY1(KEY_NUMPAD6, N_("Numpad 6"))
+ DEFINEKEY1(KEY_NUMPAD7, N_("Numpad 7"))
+ DEFINEKEY1(KEY_NUMPAD8, N_("Numpad 8"))
+ DEFINEKEY1(KEY_NUMPAD9, N_("Numpad 9"))
+ DEFINEKEY1(KEY_MULTIPLY, N_("Numpad *"))
+ DEFINEKEY1(KEY_ADD, N_("Numpad +"))
+ DEFINEKEY1(KEY_SEPARATOR, N_("Numpad ."))
+ DEFINEKEY1(KEY_SUBTRACT, N_("Numpad -"))
+ DEFINEKEY1(KEY_DECIMAL, NULL)
+ DEFINEKEY1(KEY_DIVIDE, N_("Numpad /"))
+ DEFINEKEY4(1)
+ DEFINEKEY4(2)
+ DEFINEKEY4(3)
+ DEFINEKEY4(4)
+ DEFINEKEY4(5)
+ DEFINEKEY4(6)
+ DEFINEKEY4(7)
+ DEFINEKEY4(8)
+ DEFINEKEY4(9)
+ DEFINEKEY4(10)
+ DEFINEKEY4(11)
+ DEFINEKEY4(12)
+ DEFINEKEY4(13)
+ DEFINEKEY4(14)
+ DEFINEKEY4(15)
+ DEFINEKEY4(16)
+ DEFINEKEY4(17)
+ DEFINEKEY4(18)
+ DEFINEKEY4(19)
+ DEFINEKEY4(20)
+ DEFINEKEY4(21)
+ DEFINEKEY4(22)
+ DEFINEKEY4(23)
+ DEFINEKEY4(24)
+ DEFINEKEY1(KEY_NUMLOCK, N_("Num Lock"))
+ DEFINEKEY1(KEY_SCROLL, N_("Scroll Lock"))
+ DEFINEKEY1(KEY_LSHIFT, N_("Left Shift"))
+ DEFINEKEY1(KEY_RSHIFT, N_("Right Shift"))
+ DEFINEKEY1(KEY_LCONTROL, N_("Left Control"))
+ DEFINEKEY1(KEY_RCONTROL, N_("Right Control"))
+ DEFINEKEY1(KEY_LMENU, N_("Left Menu"))
+ DEFINEKEY1(KEY_RMENU, N_("Right Menu"))
+
+ // Rare/weird keys
+ DEFINEKEY1(KEY_KANA, "Kana")
+ DEFINEKEY1(KEY_HANGUEL, "Hangul")
+ DEFINEKEY1(KEY_HANGUL, "Hangul")
+ DEFINEKEY1(KEY_JUNJA, "Junja")
+ DEFINEKEY1(KEY_FINAL, "Final")
+ DEFINEKEY1(KEY_KANJI, "Kanji")
+ DEFINEKEY1(KEY_HANJA, "Hanja")
+ DEFINEKEY1(KEY_ESCAPE, N_("IME Escape"))
+ DEFINEKEY1(KEY_CONVERT, N_("IME Convert"))
+ DEFINEKEY1(KEY_NONCONVERT, N_("IME Nonconvert"))
+ DEFINEKEY1(KEY_ACCEPT, N_("IME Accept"))
+ DEFINEKEY1(KEY_MODECHANGE, N_("IME Mode Change"))
+ DEFINEKEY1(KEY_APPS, N_("Apps"))
+ DEFINEKEY1(KEY_SLEEP, N_("Sleep"))
+#if !(IRRLICHT_VERSION_MAJOR <= 1 && IRRLICHT_VERSION_MINOR <= 7 && IRRLICHT_VERSION_REVISION < 3)
+ DEFINEKEY1(KEY_OEM_1, "OEM 1") // KEY_OEM_[0-9] and KEY_OEM_102 are assigned to multiple
+ DEFINEKEY1(KEY_OEM_2, "OEM 2") // different chars (on different platforms too) and thus w/o char
+ DEFINEKEY1(KEY_OEM_3, "OEM 3")
+ DEFINEKEY1(KEY_OEM_4, "OEM 4")
+ DEFINEKEY1(KEY_OEM_5, "OEM 5")
+ DEFINEKEY1(KEY_OEM_6, "OEM 6")
+ DEFINEKEY1(KEY_OEM_7, "OEM 7")
+ DEFINEKEY1(KEY_OEM_8, "OEM 8")
+ DEFINEKEY1(KEY_OEM_AX, "OEM AX")
+ DEFINEKEY1(KEY_OEM_102, "OEM 102")
+#endif
+ DEFINEKEY1(KEY_ATTN, "Attn")
+ DEFINEKEY1(KEY_CRSEL, "CrSel")
+ DEFINEKEY1(KEY_EXSEL, "ExSel")
+ DEFINEKEY1(KEY_EREOF, N_("Erase EOF"))
+ DEFINEKEY1(KEY_PLAY, N_("Play"))
+ DEFINEKEY1(KEY_ZOOM, N_("Zoom"))
+ DEFINEKEY1(KEY_PA1, "PA1")
+ DEFINEKEY1(KEY_OEM_CLEAR, N_("OEM Clear"))
+
+ // Keys without Irrlicht keycode
+ DEFINEKEY5("!")
+ DEFINEKEY5("\"")
+ DEFINEKEY5("#")
+ DEFINEKEY5("$")
+ DEFINEKEY5("%")
+ DEFINEKEY5("&")
+ DEFINEKEY5("'")
+ DEFINEKEY5("(")
+ DEFINEKEY5(")")
+ DEFINEKEY5("*")
+ DEFINEKEY5("/")
+ DEFINEKEY5(":")
+ DEFINEKEY5(";")
+ DEFINEKEY5("<")
+ DEFINEKEY5("=")
+ DEFINEKEY5(">")
+ DEFINEKEY5("?")
+ DEFINEKEY5("@")
+ DEFINEKEY5("[")
+ DEFINEKEY5("\\")
+ DEFINEKEY5("]")
+ DEFINEKEY5("^")
+ DEFINEKEY5("_")
+};
+
+#undef N_
+
+
+struct table_key lookup_keyname(const char *name)
+{
+ for (const auto &table_key : table) {
+ if (strcmp(table_key.Name, name) == 0)
+ return table_key;
+ }
+
+ throw UnknownKeycode(name);
+}
+
+struct table_key lookup_keykey(irr::EKEY_CODE key)
+{
+ for (const auto &table_key : table) {
+ if (table_key.Key == key)
+ return table_key;
+ }
+
+ std::ostringstream os;
+ os << "<Keycode " << (int) key << ">";
+ throw UnknownKeycode(os.str().c_str());
+}
+
+struct table_key lookup_keychar(wchar_t Char)
+{
+ for (const auto &table_key : table) {
+ if (table_key.Char == Char)
+ return table_key;
+ }
+
+ std::ostringstream os;
+ os << "<Char " << hex_encode((char*) &Char, sizeof(wchar_t)) << ">";
+ throw UnknownKeycode(os.str().c_str());
+}
+
+KeyPress::KeyPress(const char *name)
+{
+ if (strlen(name) == 0) {
+ Key = irr::KEY_KEY_CODES_COUNT;
+ Char = L'\0';
+ m_name = "";
+ return;
+ }
+
+ if (strlen(name) <= 4) {
+ // Lookup by resulting character
+ int chars_read = mbtowc(&Char, name, 1);
+ FATAL_ERROR_IF(chars_read != 1, "Unexpected multibyte character");
+ try {
+ struct table_key k = lookup_keychar(Char);
+ m_name = k.Name;
+ Key = k.Key;
+ return;
+ } catch (UnknownKeycode &e) {};
+ } else {
+ // Lookup by name
+ m_name = name;
+ try {
+ struct table_key k = lookup_keyname(name);
+ Key = k.Key;
+ Char = k.Char;
+ return;
+ } catch (UnknownKeycode &e) {};
+ }
+
+ // It's not a known key, complain and try to do something
+ Key = irr::KEY_KEY_CODES_COUNT;
+ int chars_read = mbtowc(&Char, name, 1);
+ FATAL_ERROR_IF(chars_read != 1, "Unexpected multibyte character");
+ m_name = "";
+ warningstream << "KeyPress: Unknown key '" << name << "', falling back to first char.";
+}
+
+KeyPress::KeyPress(const irr::SEvent::SKeyInput &in, bool prefer_character)
+{
+ if (prefer_character)
+ Key = irr::KEY_KEY_CODES_COUNT;
+ else
+ Key = in.Key;
+ Char = in.Char;
+
+ try {
+ if (valid_kcode(Key))
+ m_name = lookup_keykey(Key).Name;
+ else
+ m_name = lookup_keychar(Char).Name;
+ } catch (UnknownKeycode &e) {
+ m_name = "";
+ };
+}
+
+const char *KeyPress::sym() const
+{
+ return m_name.c_str();
+}
+
+const char *KeyPress::name() const
+{
+ if (m_name.empty())
+ return "";
+ const char *ret;
+ if (valid_kcode(Key))
+ ret = lookup_keykey(Key).LangName;
+ else
+ ret = lookup_keychar(Char).LangName;
+ return ret ? ret : "<Unnamed key>";
+}
+
+const KeyPress EscapeKey("KEY_ESCAPE");
+const KeyPress CancelKey("KEY_CANCEL");
+
+/*
+ Key config
+*/
+
+// A simple cache for quicker lookup
+std::unordered_map<std::string, KeyPress> g_key_setting_cache;
+
+KeyPress getKeySetting(const char *settingname)
+{
+ std::unordered_map<std::string, KeyPress>::iterator n;
+ n = g_key_setting_cache.find(settingname);
+ if (n != g_key_setting_cache.end())
+ return n->second;
+
+ KeyPress k(g_settings->get(settingname).c_str());
+ g_key_setting_cache[settingname] = k;
+ return k;
+}
+
+void clearKeyCache()
+{
+ g_key_setting_cache.clear();
+}
+
+irr::EKEY_CODE keyname_to_keycode(const char *name)
+{
+ return lookup_keyname(name).Key;
+}
diff --git a/src/client/keycode.h b/src/client/keycode.h
new file mode 100644
index 000000000..7036705d1
--- /dev/null
+++ b/src/client/keycode.h
@@ -0,0 +1,67 @@
+/*
+Minetest
+Copyright (C) 2010-2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+
+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.h"
+#include "Keycodes.h"
+#include <IEventReceiver.h>
+#include <string>
+
+/* A key press, consisting of either an Irrlicht keycode
+ or an actual char */
+
+class KeyPress
+{
+public:
+ KeyPress() = default;
+
+ KeyPress(const char *name);
+
+ KeyPress(const irr::SEvent::SKeyInput &in, bool prefer_character = false);
+
+ bool operator==(const KeyPress &o) const
+ {
+ return (Char > 0 && Char == o.Char) || (valid_kcode(Key) && Key == o.Key);
+ }
+
+ const char *sym() const;
+ const char *name() const;
+
+protected:
+ static bool valid_kcode(irr::EKEY_CODE k)
+ {
+ return k > 0 && k < irr::KEY_KEY_CODES_COUNT;
+ }
+
+ irr::EKEY_CODE Key = irr::KEY_KEY_CODES_COUNT;
+ wchar_t Char = L'\0';
+ std::string m_name = "";
+};
+
+extern const KeyPress EscapeKey;
+extern const KeyPress CancelKey;
+
+// Key configuration getter
+KeyPress getKeySetting(const char *settingname);
+
+// Clear fast lookup cache
+void clearKeyCache();
+
+irr::EKEY_CODE keyname_to_keycode(const char *name);
diff --git a/src/client/localplayer.cpp b/src/client/localplayer.cpp
new file mode 100644
index 000000000..1c65d3b4d
--- /dev/null
+++ b/src/client/localplayer.cpp
@@ -0,0 +1,1158 @@
+/*
+Minetest
+Copyright (C) 2010-2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+
+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 "localplayer.h"
+#include <cmath>
+#include "event.h"
+#include "collision.h"
+#include "nodedef.h"
+#include "settings.h"
+#include "environment.h"
+#include "map.h"
+#include "client.h"
+#include "content_cao.h"
+
+/*
+ LocalPlayer
+*/
+
+LocalPlayer::LocalPlayer(Client *client, const char *name):
+ Player(name, client->idef()),
+ m_client(client)
+{
+}
+
+static aabb3f getNodeBoundingBox(const std::vector<aabb3f> &nodeboxes)
+{
+ if (nodeboxes.empty())
+ return aabb3f(0, 0, 0, 0, 0, 0);
+
+ aabb3f b_max;
+
+ std::vector<aabb3f>::const_iterator it = nodeboxes.begin();
+ b_max = aabb3f(it->MinEdge, it->MaxEdge);
+
+ ++it;
+ for (; it != nodeboxes.end(); ++it)
+ b_max.addInternalBox(*it);
+
+ return b_max;
+}
+
+bool LocalPlayer::updateSneakNode(Map *map, const v3f &position,
+ const v3f &sneak_max)
+{
+ static const v3s16 dir9_center[9] = {
+ v3s16( 0, 0, 0),
+ v3s16( 1, 0, 0),
+ v3s16(-1, 0, 0),
+ v3s16( 0, 0, 1),
+ v3s16( 0, 0, -1),
+ v3s16( 1, 0, 1),
+ v3s16(-1, 0, 1),
+ v3s16( 1, 0, -1),
+ v3s16(-1, 0, -1)
+ };
+
+ const NodeDefManager *nodemgr = m_client->ndef();
+ MapNode node;
+ bool is_valid_position;
+ bool new_sneak_node_exists = m_sneak_node_exists;
+
+ // We want the top of the sneak node to be below the players feet
+ f32 position_y_mod = 0.05 * BS;
+ if (m_sneak_node_exists)
+ position_y_mod = m_sneak_node_bb_top.MaxEdge.Y - position_y_mod;
+
+ // Get position of current standing node
+ const v3s16 current_node = floatToInt(position - v3f(0, position_y_mod, 0), BS);
+
+ if (current_node != m_sneak_node) {
+ new_sneak_node_exists = false;
+ } else {
+ node = map->getNodeNoEx(current_node, &is_valid_position);
+ if (!is_valid_position || !nodemgr->get(node).walkable)
+ new_sneak_node_exists = false;
+ }
+
+ // Keep old sneak node
+ if (new_sneak_node_exists)
+ return true;
+
+ // Get new sneak node
+ m_sneak_ladder_detected = false;
+ f32 min_distance_f = 100000.0 * BS;
+
+ for (const auto &d : dir9_center) {
+ const v3s16 p = current_node + d;
+ const v3f pf = intToFloat(p, BS);
+ const v2f diff(position.X - pf.X, position.Z - pf.Z);
+ f32 distance_f = diff.getLength();
+
+ if (distance_f > min_distance_f ||
+ fabs(diff.X) > (.5 + .1) * BS + sneak_max.X ||
+ fabs(diff.Y) > (.5 + .1) * BS + sneak_max.Z)
+ continue;
+
+
+ // The node to be sneaked on has to be walkable
+ node = map->getNodeNoEx(p, &is_valid_position);
+ if (!is_valid_position || !nodemgr->get(node).walkable)
+ continue;
+ // And the node(s) above have to be nonwalkable
+ bool ok = true;
+ if (!physics_override_sneak_glitch) {
+ u16 height = ceilf(
+ (m_collisionbox.MaxEdge.Y - m_collisionbox.MinEdge.Y) / BS
+ );
+ for (u16 y = 1; y <= height; y++) {
+ node = map->getNodeNoEx(p + v3s16(0, y, 0), &is_valid_position);
+ if (!is_valid_position || nodemgr->get(node).walkable) {
+ ok = false;
+ break;
+ }
+ }
+ } else {
+ // legacy behaviour: check just one node
+ node = map->getNodeNoEx(p + v3s16(0, 1, 0), &is_valid_position);
+ ok = is_valid_position && !nodemgr->get(node).walkable;
+ }
+ if (!ok)
+ continue;
+
+ min_distance_f = distance_f;
+ m_sneak_node = p;
+ new_sneak_node_exists = true;
+ }
+
+ if (!new_sneak_node_exists)
+ return false;
+
+ // Update saved top bounding box of sneak node
+ node = map->getNodeNoEx(m_sneak_node);
+ std::vector<aabb3f> nodeboxes;
+ node.getCollisionBoxes(nodemgr, &nodeboxes);
+ m_sneak_node_bb_top = getNodeBoundingBox(nodeboxes);
+
+ if (physics_override_sneak_glitch) {
+ // Detect sneak ladder:
+ // Node two meters above sneak node must be solid
+ node = map->getNodeNoEx(m_sneak_node + v3s16(0, 2, 0),
+ &is_valid_position);
+ if (is_valid_position && nodemgr->get(node).walkable) {
+ // Node three meters above: must be non-solid
+ node = map->getNodeNoEx(m_sneak_node + v3s16(0, 3, 0),
+ &is_valid_position);
+ m_sneak_ladder_detected = is_valid_position &&
+ !nodemgr->get(node).walkable;
+ }
+ }
+ return true;
+}
+
+void LocalPlayer::move(f32 dtime, Environment *env, f32 pos_max_d,
+ std::vector<CollisionInfo> *collision_info)
+{
+ if (!collision_info || collision_info->empty()) {
+ // Node below the feet, update each ClientEnvironment::step()
+ m_standing_node = floatToInt(m_position, BS) - v3s16(0, 1, 0);
+ }
+
+ // Temporary option for old move code
+ if (!physics_override_new_move) {
+ old_move(dtime, env, pos_max_d, collision_info);
+ return;
+ }
+
+ Map *map = &env->getMap();
+ const NodeDefManager *nodemgr = m_client->ndef();
+
+ v3f position = getPosition();
+
+ // Copy parent position if local player is attached
+ if (isAttached) {
+ setPosition(overridePosition);
+ return;
+ }
+
+ PlayerSettings &player_settings = getPlayerSettings();
+
+ // Skip collision detection if noclip mode is used
+ bool fly_allowed = m_client->checkLocalPrivilege("fly");
+ bool noclip = m_client->checkLocalPrivilege("noclip") && player_settings.noclip;
+ bool free_move = player_settings.free_move && fly_allowed;
+
+ if (noclip && free_move) {
+ position += m_speed * dtime;
+ setPosition(position);
+ return;
+ }
+
+ /*
+ Collision detection
+ */
+
+ bool is_valid_position;
+ MapNode node;
+ v3s16 pp;
+
+ /*
+ Check if player is in liquid (the oscillating value)
+ */
+
+ // If in liquid, the threshold of coming out is at higher y
+ if (in_liquid)
+ {
+ pp = floatToInt(position + v3f(0,BS*0.1,0), BS);
+ node = map->getNodeNoEx(pp, &is_valid_position);
+ if (is_valid_position) {
+ in_liquid = nodemgr->get(node.getContent()).isLiquid();
+ liquid_viscosity = nodemgr->get(node.getContent()).liquid_viscosity;
+ } else {
+ in_liquid = false;
+ }
+ }
+ // If not in liquid, the threshold of going in is at lower y
+ else
+ {
+ pp = floatToInt(position + v3f(0,BS*0.5,0), BS);
+ node = map->getNodeNoEx(pp, &is_valid_position);
+ if (is_valid_position) {
+ in_liquid = nodemgr->get(node.getContent()).isLiquid();
+ liquid_viscosity = nodemgr->get(node.getContent()).liquid_viscosity;
+ } else {
+ in_liquid = false;
+ }
+ }
+
+
+ /*
+ Check if player is in liquid (the stable value)
+ */
+ pp = floatToInt(position + v3f(0,0,0), BS);
+ node = map->getNodeNoEx(pp, &is_valid_position);
+ if (is_valid_position) {
+ in_liquid_stable = nodemgr->get(node.getContent()).isLiquid();
+ } else {
+ in_liquid_stable = false;
+ }
+
+ /*
+ Check if player is climbing
+ */
+
+
+ pp = floatToInt(position + v3f(0,0.5*BS,0), BS);
+ v3s16 pp2 = floatToInt(position + v3f(0,-0.2*BS,0), BS);
+ node = map->getNodeNoEx(pp, &is_valid_position);
+ bool is_valid_position2;
+ MapNode node2 = map->getNodeNoEx(pp2, &is_valid_position2);
+
+ if (!(is_valid_position && is_valid_position2)) {
+ is_climbing = false;
+ } else {
+ is_climbing = (nodemgr->get(node.getContent()).climbable
+ || nodemgr->get(node2.getContent()).climbable) && !free_move;
+ }
+
+ /*
+ Collision uncertainty radius
+ Make it a bit larger than the maximum distance of movement
+ */
+ //f32 d = pos_max_d * 1.1;
+ // A fairly large value in here makes moving smoother
+ f32 d = 0.15*BS;
+
+ // This should always apply, otherwise there are glitches
+ sanity_check(d > pos_max_d);
+
+ // Player object property step height is multiplied by BS in
+ // /src/script/common/c_content.cpp and /src/content_sao.cpp
+ float player_stepheight = (m_cao == nullptr) ? 0.0f :
+ (touching_ground ? m_cao->getStepHeight() : (0.2f * BS));
+
+ v3f accel_f = v3f(0,0,0);
+ const v3f initial_position = position;
+ const v3f initial_speed = m_speed;
+
+ collisionMoveResult result = collisionMoveSimple(env, m_client,
+ pos_max_d, m_collisionbox, player_stepheight, dtime,
+ &position, &m_speed, accel_f);
+
+ bool could_sneak = control.sneak && !free_move && !in_liquid &&
+ !is_climbing && physics_override_sneak;
+
+ // Add new collisions to the vector
+ if (collision_info && !free_move) {
+ v3f diff = intToFloat(m_standing_node, BS) - position;
+ f32 distance = diff.getLength();
+ // Force update each ClientEnvironment::step()
+ bool is_first = collision_info->empty();
+
+ for (const auto &colinfo : result.collisions) {
+ collision_info->push_back(colinfo);
+
+ if (colinfo.type != COLLISION_NODE ||
+ colinfo.new_speed.Y != 0 ||
+ (could_sneak && m_sneak_node_exists))
+ continue;
+
+ diff = intToFloat(colinfo.node_p, BS) - position;
+
+ // Find nearest colliding node
+ f32 len = diff.getLength();
+ if (is_first || len < distance) {
+ m_standing_node = colinfo.node_p;
+ distance = len;
+ }
+ }
+ }
+
+ /*
+ If the player's feet touch the topside of any node, this is
+ set to true.
+
+ Player is allowed to jump when this is true.
+ */
+ bool touching_ground_was = touching_ground;
+ touching_ground = result.touching_ground;
+ bool sneak_can_jump = false;
+
+ // Max. distance (X, Z) over border for sneaking determined by collision box
+ // * 0.49 to keep the center just barely on the node
+ v3f sneak_max = m_collisionbox.getExtent() * 0.49;
+
+ if (m_sneak_ladder_detected) {
+ // restore legacy behaviour (this makes the m_speed.Y hack necessary)
+ sneak_max = v3f(0.4 * BS, 0, 0.4 * BS);
+ }
+
+ /*
+ If sneaking, keep on top of last walked node and don't fall off
+ */
+ if (could_sneak && m_sneak_node_exists) {
+ const v3f sn_f = intToFloat(m_sneak_node, BS);
+ const v3f bmin = sn_f + m_sneak_node_bb_top.MinEdge;
+ const v3f bmax = sn_f + m_sneak_node_bb_top.MaxEdge;
+ const v3f old_pos = position;
+ const v3f old_speed = m_speed;
+ f32 y_diff = bmax.Y - position.Y;
+ m_standing_node = m_sneak_node;
+
+ // (BS * 0.6f) is the basic stepheight while standing on ground
+ if (y_diff < BS * 0.6f) {
+ // Only center player when they're on the node
+ position.X = rangelim(position.X,
+ bmin.X - sneak_max.X, bmax.X + sneak_max.X);
+ position.Z = rangelim(position.Z,
+ bmin.Z - sneak_max.Z, bmax.Z + sneak_max.Z);
+
+ if (position.X != old_pos.X)
+ m_speed.X = 0;
+ if (position.Z != old_pos.Z)
+ m_speed.Z = 0;
+ }
+
+ if (y_diff > 0 && m_speed.Y <= 0 &&
+ (physics_override_sneak_glitch || y_diff < BS * 0.6f)) {
+ // Move player to the maximal height when falling or when
+ // the ledge is climbed on the next step.
+
+ // Smoothen the movement (based on 'position.Y = bmax.Y')
+ position.Y += y_diff * dtime * 22.0f + BS * 0.01f;
+ position.Y = std::min(position.Y, bmax.Y);
+ m_speed.Y = 0;
+ }
+
+ // Allow jumping on node edges while sneaking
+ if (m_speed.Y == 0 || m_sneak_ladder_detected)
+ sneak_can_jump = true;
+
+ if (collision_info &&
+ m_speed.Y - old_speed.Y > BS) {
+ // Collide with sneak node, report fall damage
+ CollisionInfo sn_info;
+ sn_info.node_p = m_sneak_node;
+ sn_info.old_speed = old_speed;
+ sn_info.new_speed = m_speed;
+ collision_info->push_back(sn_info);
+ }
+ }
+
+ /*
+ Find the next sneak node if necessary
+ */
+ bool new_sneak_node_exists = false;
+
+ if (could_sneak)
+ new_sneak_node_exists = updateSneakNode(map, position, sneak_max);
+
+ /*
+ Set new position but keep sneak node set
+ */
+ setPosition(position);
+ m_sneak_node_exists = new_sneak_node_exists;
+
+ /*
+ Report collisions
+ */
+
+ if(!result.standing_on_object && !touching_ground_was && touching_ground) {
+ m_client->getEventManager()->put(new SimpleTriggerEvent(MtEvent::PLAYER_REGAIN_GROUND));
+
+ // Set camera impact value to be used for view bobbing
+ camera_impact = getSpeed().Y * -1;
+ }
+
+ {
+ camera_barely_in_ceiling = false;
+ v3s16 camera_np = floatToInt(getEyePosition(), BS);
+ MapNode n = map->getNodeNoEx(camera_np);
+ if(n.getContent() != CONTENT_IGNORE){
+ if(nodemgr->get(n).walkable && nodemgr->get(n).solidness == 2){
+ camera_barely_in_ceiling = true;
+ }
+ }
+ }
+
+ /*
+ Check properties of the node on which the player is standing
+ */
+ const ContentFeatures &f = nodemgr->get(map->getNodeNoEx(m_standing_node));
+ // Determine if jumping is possible
+ m_can_jump = (touching_ground && !in_liquid && !is_climbing)
+ || sneak_can_jump;
+ if (itemgroup_get(f.groups, "disable_jump"))
+ m_can_jump = false;
+
+ // Jump key pressed while jumping off from a bouncy block
+ if (m_can_jump && control.jump && itemgroup_get(f.groups, "bouncy") &&
+ m_speed.Y >= -0.5 * BS) {
+ float jumpspeed = movement_speed_jump * physics_override_jump;
+ if (m_speed.Y > 1) {
+ // Reduce boost when speed already is high
+ m_speed.Y += jumpspeed / (1 + (m_speed.Y / 16 ));
+ } else {
+ m_speed.Y += jumpspeed;
+ }
+ setSpeed(m_speed);
+ m_can_jump = false;
+ }
+
+ // Autojump
+ handleAutojump(dtime, env, result, initial_position, initial_speed, pos_max_d);
+}
+
+void LocalPlayer::move(f32 dtime, Environment *env, f32 pos_max_d)
+{
+ move(dtime, env, pos_max_d, NULL);
+}
+
+void LocalPlayer::applyControl(float dtime, Environment *env)
+{
+ // Clear stuff
+ swimming_vertical = false;
+
+ setPitch(control.pitch);
+ setYaw(control.yaw);
+
+ // Nullify speed and don't run positioning code if the player is attached
+ if(isAttached)
+ {
+ setSpeed(v3f(0,0,0));
+ return;
+ }
+
+ PlayerSettings &player_settings = getPlayerSettings();
+
+ v3f move_direction = v3f(0,0,1);
+ move_direction.rotateXZBy(getYaw());
+
+ v3f speedH = v3f(0,0,0); // Horizontal (X, Z)
+ v3f speedV = v3f(0,0,0); // Vertical (Y)
+
+ bool fly_allowed = m_client->checkLocalPrivilege("fly");
+ bool fast_allowed = m_client->checkLocalPrivilege("fast");
+
+ bool free_move = fly_allowed && player_settings.free_move;
+ bool fast_move = fast_allowed && player_settings.fast_move;
+ // When aux1_descends is enabled the fast key is used to go down, so fast isn't possible
+ bool fast_climb = fast_move && control.aux1 && !player_settings.aux1_descends;
+ bool continuous_forward = player_settings.continuous_forward;
+ bool always_fly_fast = player_settings.always_fly_fast;
+
+ // Whether superspeed mode is used or not
+ bool superspeed = false;
+
+ if (always_fly_fast && free_move && fast_move)
+ superspeed = true;
+
+ // Old descend control
+ if (player_settings.aux1_descends)
+ {
+ // If free movement and fast movement, always move fast
+ if(free_move && fast_move)
+ superspeed = true;
+
+ // Auxiliary button 1 (E)
+ if(control.aux1)
+ {
+ if(free_move)
+ {
+ // In free movement mode, aux1 descends
+ if(fast_move)
+ speedV.Y = -movement_speed_fast;
+ else
+ speedV.Y = -movement_speed_walk;
+ }
+ else if(in_liquid || in_liquid_stable)
+ {
+ speedV.Y = -movement_speed_walk;
+ swimming_vertical = true;
+ }
+ else if(is_climbing)
+ {
+ speedV.Y = -movement_speed_climb;
+ }
+ else
+ {
+ // If not free movement but fast is allowed, aux1 is
+ // "Turbo button"
+ if(fast_move)
+ superspeed = true;
+ }
+ }
+ }
+ // New minecraft-like descend control
+ else
+ {
+ // Auxiliary button 1 (E)
+ if(control.aux1)
+ {
+ if(!is_climbing)
+ {
+ // aux1 is "Turbo button"
+ if(fast_move)
+ superspeed = true;
+ }
+ }
+
+ if(control.sneak)
+ {
+ if(free_move)
+ {
+ // In free movement mode, sneak descends
+ if (fast_move && (control.aux1 || always_fly_fast))
+ speedV.Y = -movement_speed_fast;
+ else
+ speedV.Y = -movement_speed_walk;
+ }
+ else if(in_liquid || in_liquid_stable)
+ {
+ if(fast_climb)
+ speedV.Y = -movement_speed_fast;
+ else
+ speedV.Y = -movement_speed_walk;
+ swimming_vertical = true;
+ }
+ else if(is_climbing)
+ {
+ if(fast_climb)
+ speedV.Y = -movement_speed_fast;
+ else
+ speedV.Y = -movement_speed_climb;
+ }
+ }
+ }
+
+ if (continuous_forward)
+ speedH += move_direction;
+
+ if (control.up) {
+ if (continuous_forward) {
+ if (fast_move)
+ superspeed = true;
+ } else {
+ speedH += move_direction;
+ }
+ }
+ if (control.down) {
+ speedH -= move_direction;
+ }
+ if (!control.up && !control.down) {
+ speedH -= move_direction *
+ (control.forw_move_joystick_axis / 32767.f);
+ }
+ if (control.left) {
+ speedH += move_direction.crossProduct(v3f(0,1,0));
+ }
+ if (control.right) {
+ speedH += move_direction.crossProduct(v3f(0,-1,0));
+ }
+ if (!control.left && !control.right) {
+ speedH -= move_direction.crossProduct(v3f(0,1,0)) *
+ (control.sidew_move_joystick_axis / 32767.f);
+ }
+ if(control.jump)
+ {
+ if (free_move) {
+ if (player_settings.aux1_descends || always_fly_fast) {
+ if (fast_move)
+ speedV.Y = movement_speed_fast;
+ else
+ speedV.Y = movement_speed_walk;
+ } else {
+ if(fast_move && control.aux1)
+ speedV.Y = movement_speed_fast;
+ else
+ speedV.Y = movement_speed_walk;
+ }
+ }
+ else if(m_can_jump)
+ {
+ /*
+ NOTE: The d value in move() affects jump height by
+ raising the height at which the jump speed is kept
+ at its starting value
+ */
+ v3f speedJ = getSpeed();
+ if(speedJ.Y >= -0.5 * BS) {
+ speedJ.Y = movement_speed_jump * physics_override_jump;
+ setSpeed(speedJ);
+ m_client->getEventManager()->put(new SimpleTriggerEvent(MtEvent::PLAYER_JUMP));
+ }
+ }
+ else if(in_liquid)
+ {
+ if(fast_climb)
+ speedV.Y = movement_speed_fast;
+ else
+ speedV.Y = movement_speed_walk;
+ swimming_vertical = true;
+ }
+ else if(is_climbing)
+ {
+ if(fast_climb)
+ speedV.Y = movement_speed_fast;
+ else
+ speedV.Y = movement_speed_climb;
+ }
+ }
+
+ // The speed of the player (Y is ignored)
+ if(superspeed || (is_climbing && fast_climb) || ((in_liquid || in_liquid_stable) && fast_climb))
+ speedH = speedH.normalize() * movement_speed_fast;
+ else if(control.sneak && !free_move && !in_liquid && !in_liquid_stable)
+ speedH = speedH.normalize() * movement_speed_crouch;
+ else
+ speedH = speedH.normalize() * movement_speed_walk;
+
+ // Acceleration increase
+ f32 incH = 0; // Horizontal (X, Z)
+ f32 incV = 0; // Vertical (Y)
+ if((!touching_ground && !free_move && !is_climbing && !in_liquid) || (!free_move && m_can_jump && control.jump))
+ {
+ // Jumping and falling
+ if(superspeed || (fast_move && control.aux1))
+ incH = movement_acceleration_fast * BS * dtime;
+ else
+ incH = movement_acceleration_air * BS * dtime;
+ incV = 0; // No vertical acceleration in air
+ }
+ else if (superspeed || (is_climbing && fast_climb) || ((in_liquid || in_liquid_stable) && fast_climb))
+ incH = incV = movement_acceleration_fast * BS * dtime;
+ else
+ incH = incV = movement_acceleration_default * BS * dtime;
+
+ float slip_factor = 1.0f;
+ if (!free_move)
+ slip_factor = getSlipFactor(env, speedH);
+
+ // Accelerate to target speed with maximum increment
+ accelerateHorizontal(speedH * physics_override_speed,
+ incH * physics_override_speed * slip_factor);
+ accelerateVertical(speedV * physics_override_speed,
+ incV * physics_override_speed);
+}
+
+v3s16 LocalPlayer::getStandingNodePos()
+{
+ if(m_sneak_node_exists)
+ return m_sneak_node;
+ return m_standing_node;
+}
+
+v3s16 LocalPlayer::getFootstepNodePos()
+{
+ if (in_liquid_stable)
+ // Emit swimming sound if the player is in liquid
+ return floatToInt(getPosition(), BS);
+ if (touching_ground)
+ // BS * 0.05 below the player's feet ensures a 1/16th height
+ // nodebox is detected instead of the node below it.
+ return floatToInt(getPosition() - v3f(0, BS * 0.05f, 0), BS);
+ // A larger distance below is necessary for a footstep sound
+ // when landing after a jump or fall. BS * 0.5 ensures water
+ // sounds when swimming in 1 node deep water.
+ return floatToInt(getPosition() - v3f(0, BS * 0.5f, 0), BS);
+}
+
+v3s16 LocalPlayer::getLightPosition() const
+{
+ return floatToInt(m_position + v3f(0,BS+BS/2,0), BS);
+}
+
+v3f LocalPlayer::getEyeOffset() const
+{
+ float eye_height = camera_barely_in_ceiling ?
+ m_eye_height - 0.125f : m_eye_height;
+ return v3f(0, BS * eye_height, 0);
+}
+
+// Horizontal acceleration (X and Z), Y direction is ignored
+void LocalPlayer::accelerateHorizontal(const v3f &target_speed,
+ const f32 max_increase)
+{
+ if (max_increase == 0)
+ return;
+
+ v3f d_wanted = target_speed - m_speed;
+ d_wanted.Y = 0.0f;
+ f32 dl = d_wanted.getLength();
+ if (dl > max_increase)
+ dl = max_increase;
+
+ v3f d = d_wanted.normalize() * dl;
+
+ m_speed.X += d.X;
+ m_speed.Z += d.Z;
+}
+
+// Vertical acceleration (Y), X and Z directions are ignored
+void LocalPlayer::accelerateVertical(const v3f &target_speed, const f32 max_increase)
+{
+ if (max_increase == 0)
+ return;
+
+ f32 d_wanted = target_speed.Y - m_speed.Y;
+ if (d_wanted > max_increase)
+ d_wanted = max_increase;
+ else if (d_wanted < -max_increase)
+ d_wanted = -max_increase;
+
+ m_speed.Y += d_wanted;
+}
+
+// Temporary option for old move code
+void LocalPlayer::old_move(f32 dtime, Environment *env, f32 pos_max_d,
+ std::vector<CollisionInfo> *collision_info)
+{
+ Map *map = &env->getMap();
+ const NodeDefManager *nodemgr = m_client->ndef();
+
+ v3f position = getPosition();
+
+ // Copy parent position if local player is attached
+ if (isAttached) {
+ setPosition(overridePosition);
+ m_sneak_node_exists = false;
+ return;
+ }
+
+ PlayerSettings &player_settings = getPlayerSettings();
+
+ // Skip collision detection if noclip mode is used
+ bool fly_allowed = m_client->checkLocalPrivilege("fly");
+ bool noclip = m_client->checkLocalPrivilege("noclip") && player_settings.noclip;
+ bool free_move = noclip && fly_allowed && player_settings.free_move;
+ if (free_move) {
+ position += m_speed * dtime;
+ setPosition(position);
+ m_sneak_node_exists = false;
+ return;
+ }
+
+ /*
+ Collision detection
+ */
+ bool is_valid_position;
+ MapNode node;
+ v3s16 pp;
+
+ /*
+ Check if player is in liquid (the oscillating value)
+ */
+ if (in_liquid) {
+ // If in liquid, the threshold of coming out is at higher y
+ pp = floatToInt(position + v3f(0, BS * 0.1, 0), BS);
+ node = map->getNodeNoEx(pp, &is_valid_position);
+ if (is_valid_position) {
+ in_liquid = nodemgr->get(node.getContent()).isLiquid();
+ liquid_viscosity = nodemgr->get(node.getContent()).liquid_viscosity;
+ } else {
+ in_liquid = false;
+ }
+ } else {
+ // If not in liquid, the threshold of going in is at lower y
+ pp = floatToInt(position + v3f(0, BS * 0.5, 0), BS);
+ node = map->getNodeNoEx(pp, &is_valid_position);
+ if (is_valid_position) {
+ in_liquid = nodemgr->get(node.getContent()).isLiquid();
+ liquid_viscosity = nodemgr->get(node.getContent()).liquid_viscosity;
+ } else {
+ in_liquid = false;
+ }
+ }
+
+ /*
+ Check if player is in liquid (the stable value)
+ */
+ pp = floatToInt(position + v3f(0, 0, 0), BS);
+ node = map->getNodeNoEx(pp, &is_valid_position);
+ if (is_valid_position)
+ in_liquid_stable = nodemgr->get(node.getContent()).isLiquid();
+ else
+ in_liquid_stable = false;
+
+ /*
+ Check if player is climbing
+ */
+ pp = floatToInt(position + v3f(0, 0.5 * BS, 0), BS);
+ v3s16 pp2 = floatToInt(position + v3f(0, -0.2 * BS, 0), BS);
+ node = map->getNodeNoEx(pp, &is_valid_position);
+ bool is_valid_position2;
+ MapNode node2 = map->getNodeNoEx(pp2, &is_valid_position2);
+
+ if (!(is_valid_position && is_valid_position2))
+ is_climbing = false;
+ else
+ is_climbing = (nodemgr->get(node.getContent()).climbable ||
+ nodemgr->get(node2.getContent()).climbable) && !free_move;
+
+ /*
+ Collision uncertainty radius
+ Make it a bit larger than the maximum distance of movement
+ */
+ //f32 d = pos_max_d * 1.1;
+ // A fairly large value in here makes moving smoother
+ f32 d = 0.15 * BS;
+ // This should always apply, otherwise there are glitches
+ sanity_check(d > pos_max_d);
+ // Maximum distance over border for sneaking
+ f32 sneak_max = BS * 0.4;
+
+ /*
+ If sneaking, keep in range from the last walked node and don't
+ fall off from it
+ */
+ if (control.sneak && m_sneak_node_exists &&
+ !(fly_allowed && player_settings.free_move) && !in_liquid &&
+ physics_override_sneak) {
+ f32 maxd = 0.5 * BS + sneak_max;
+ v3f lwn_f = intToFloat(m_sneak_node, BS);
+ position.X = rangelim(position.X, lwn_f.X - maxd, lwn_f.X + maxd);
+ position.Z = rangelim(position.Z, lwn_f.Z - maxd, lwn_f.Z + maxd);
+
+ if (!is_climbing) {
+ // Move up if necessary
+ f32 new_y = (lwn_f.Y - 0.5 * BS) + m_sneak_node_bb_ymax;
+ if (position.Y < new_y)
+ position.Y = new_y;
+ /*
+ Collision seems broken, since player is sinking when
+ sneaking over the edges of current sneaking_node.
+ TODO (when fixed): Set Y-speed only to 0 when position.Y < new_y.
+ */
+ if (m_speed.Y < 0)
+ m_speed.Y = 0;
+ }
+ }
+
+ // this shouldn't be hardcoded but transmitted from server
+ float player_stepheight = touching_ground ? (BS * 0.6) : (BS * 0.2);
+
+ v3f accel_f = v3f(0, 0, 0);
+ const v3f initial_position = position;
+ const v3f initial_speed = m_speed;
+
+ collisionMoveResult result = collisionMoveSimple(env, m_client,
+ pos_max_d, m_collisionbox, player_stepheight, dtime,
+ &position, &m_speed, accel_f);
+
+ /*
+ If the player's feet touch the topside of any node, this is
+ set to true.
+
+ Player is allowed to jump when this is true.
+ */
+ bool touching_ground_was = touching_ground;
+ touching_ground = result.touching_ground;
+
+ //bool standing_on_unloaded = result.standing_on_unloaded;
+
+ /*
+ Check the nodes under the player to see from which node the
+ player is sneaking from, if any. If the node from under
+ the player has been removed, the player falls.
+ */
+ f32 position_y_mod = 0.05 * BS;
+ if (m_sneak_node_bb_ymax > 0)
+ position_y_mod = m_sneak_node_bb_ymax - position_y_mod;
+ v3s16 current_node = floatToInt(position - v3f(0, position_y_mod, 0), BS);
+ if (m_sneak_node_exists &&
+ nodemgr->get(map->getNodeNoEx(m_old_node_below)).name == "air" &&
+ m_old_node_below_type != "air") {
+ // Old node appears to have been removed; that is,
+ // it wasn't air before but now it is
+ m_need_to_get_new_sneak_node = false;
+ m_sneak_node_exists = false;
+ } else if (nodemgr->get(map->getNodeNoEx(current_node)).name != "air") {
+ // We are on something, so make sure to recalculate the sneak
+ // node.
+ m_need_to_get_new_sneak_node = true;
+ }
+
+ if (m_need_to_get_new_sneak_node && physics_override_sneak) {
+ m_sneak_node_bb_ymax = 0;
+ v3s16 pos_i_bottom = floatToInt(position - v3f(0, position_y_mod, 0), BS);
+ v2f player_p2df(position.X, position.Z);
+ f32 min_distance_f = 100000.0 * BS;
+ // If already seeking from some node, compare to it.
+ v3s16 new_sneak_node = m_sneak_node;
+ for (s16 x= -1; x <= 1; x++)
+ for (s16 z= -1; z <= 1; z++) {
+ v3s16 p = pos_i_bottom + v3s16(x, 0, z);
+ v3f pf = intToFloat(p, BS);
+ v2f node_p2df(pf.X, pf.Z);
+ f32 distance_f = player_p2df.getDistanceFrom(node_p2df);
+ f32 max_axis_distance_f = MYMAX(
+ std::fabs(player_p2df.X - node_p2df.X),
+ std::fabs(player_p2df.Y - node_p2df.Y));
+
+ if (distance_f > min_distance_f ||
+ max_axis_distance_f > 0.5 * BS + sneak_max + 0.1 * BS)
+ continue;
+
+ // The node to be sneaked on has to be walkable
+ node = map->getNodeNoEx(p, &is_valid_position);
+ if (!is_valid_position || !nodemgr->get(node).walkable)
+ continue;
+ // And the node above it has to be nonwalkable
+ node = map->getNodeNoEx(p + v3s16(0, 1, 0), &is_valid_position);
+ if (!is_valid_position || nodemgr->get(node).walkable)
+ continue;
+ // If not 'sneak_glitch' the node 2 nodes above it has to be nonwalkable
+ if (!physics_override_sneak_glitch) {
+ node =map->getNodeNoEx(p + v3s16(0, 2, 0), &is_valid_position);
+ if (!is_valid_position || nodemgr->get(node).walkable)
+ continue;
+ }
+
+ min_distance_f = distance_f;
+ new_sneak_node = p;
+ }
+
+ bool sneak_node_found = (min_distance_f < 100000.0 * BS * 0.9);
+
+ m_sneak_node = new_sneak_node;
+ m_sneak_node_exists = sneak_node_found;
+
+ if (sneak_node_found) {
+ f32 cb_max = 0;
+ MapNode n = map->getNodeNoEx(m_sneak_node);
+ std::vector<aabb3f> nodeboxes;
+ n.getCollisionBoxes(nodemgr, &nodeboxes);
+ for (const auto &box : nodeboxes) {
+ if (box.MaxEdge.Y > cb_max)
+ cb_max = box.MaxEdge.Y;
+ }
+ m_sneak_node_bb_ymax = cb_max;
+ }
+
+ /*
+ If sneaking, the player's collision box can be in air, so
+ this has to be set explicitly
+ */
+ if (sneak_node_found && control.sneak)
+ touching_ground = true;
+ }
+
+ /*
+ Set new position but keep sneak node set
+ */
+ bool sneak_node_exists = m_sneak_node_exists;
+ setPosition(position);
+ m_sneak_node_exists = sneak_node_exists;
+
+ /*
+ Report collisions
+ */
+ // Dont report if flying
+ if (collision_info && !(player_settings.free_move && fly_allowed)) {
+ for (const auto &info : result.collisions) {
+ collision_info->push_back(info);
+ }
+ }
+
+ if (!result.standing_on_object && !touching_ground_was && touching_ground) {
+ m_client->getEventManager()->put(new SimpleTriggerEvent(MtEvent::PLAYER_REGAIN_GROUND));
+ // Set camera impact value to be used for view bobbing
+ camera_impact = getSpeed().Y * -1;
+ }
+
+ {
+ camera_barely_in_ceiling = false;
+ v3s16 camera_np = floatToInt(getEyePosition(), BS);
+ MapNode n = map->getNodeNoEx(camera_np);
+ if (n.getContent() != CONTENT_IGNORE) {
+ if (nodemgr->get(n).walkable && nodemgr->get(n).solidness == 2)
+ camera_barely_in_ceiling = true;
+ }
+ }
+
+ /*
+ Update the node last under the player
+ */
+ m_old_node_below = floatToInt(position - v3f(0, BS / 2, 0), BS);
+ m_old_node_below_type = nodemgr->get(map->getNodeNoEx(m_old_node_below)).name;
+
+ /*
+ Check properties of the node on which the player is standing
+ */
+ const ContentFeatures &f = nodemgr->get(map->getNodeNoEx(getStandingNodePos()));
+ // Determine if jumping is possible
+ m_can_jump = touching_ground && !in_liquid;
+ if (itemgroup_get(f.groups, "disable_jump"))
+ m_can_jump = false;
+ // Jump key pressed while jumping off from a bouncy block
+ if (m_can_jump && control.jump && itemgroup_get(f.groups, "bouncy") &&
+ m_speed.Y >= -0.5 * BS) {
+ float jumpspeed = movement_speed_jump * physics_override_jump;
+ if (m_speed.Y > 1) {
+ // Reduce boost when speed already is high
+ m_speed.Y += jumpspeed / (1 + (m_speed.Y / 16 ));
+ } else {
+ m_speed.Y += jumpspeed;
+ }
+ setSpeed(m_speed);
+ m_can_jump = false;
+ }
+
+ // Autojump
+ handleAutojump(dtime, env, result, initial_position, initial_speed, pos_max_d);
+}
+
+float LocalPlayer::getSlipFactor(Environment *env, const v3f &speedH)
+{
+ // Slip on slippery nodes
+ const NodeDefManager *nodemgr = env->getGameDef()->ndef();
+ Map *map = &env->getMap();
+ const ContentFeatures &f = nodemgr->get(map->getNodeNoEx(
+ getStandingNodePos()));
+ int slippery = 0;
+ if (f.walkable)
+ slippery = itemgroup_get(f.groups, "slippery");
+
+ if (slippery >= 1) {
+ if (speedH == v3f(0.0f)) {
+ slippery = slippery * 2;
+ }
+ return core::clamp(1.0f / (slippery + 1), 0.001f, 1.0f);
+ }
+ return 1.0f;
+}
+
+void LocalPlayer::handleAutojump(f32 dtime, Environment *env,
+ const collisionMoveResult &result, const v3f &initial_position,
+ const v3f &initial_speed, f32 pos_max_d)
+{
+ PlayerSettings &player_settings = getPlayerSettings();
+ if (!player_settings.autojump)
+ return;
+
+ if (m_autojump) {
+ // release autojump after a given time
+ m_autojump_time -= dtime;
+ if (m_autojump_time <= 0.0f)
+ m_autojump = false;
+ return;
+ }
+
+ bool control_forward = control.up ||
+ (!control.up && !control.down &&
+ control.forw_move_joystick_axis < -0.05);
+ bool could_autojump =
+ m_can_jump && !control.jump && !control.sneak && control_forward;
+ if (!could_autojump)
+ return;
+
+ bool horizontal_collision = false;
+ for (const auto &colinfo : result.collisions) {
+ if (colinfo.type == COLLISION_NODE && colinfo.plane != 1) {
+ horizontal_collision = true;
+ break; // one is enough
+ }
+ }
+
+ // must be running against something to trigger autojumping
+ if (!horizontal_collision)
+ return;
+
+ // check for nodes above
+ v3f headpos_min = m_position + m_collisionbox.MinEdge * 0.99f;
+ v3f headpos_max = m_position + m_collisionbox.MaxEdge * 0.99f;
+ headpos_min.Y = headpos_max.Y; // top face of collision box
+ v3s16 ceilpos_min = floatToInt(headpos_min, BS) + v3s16(0, 1, 0);
+ v3s16 ceilpos_max = floatToInt(headpos_max, BS) + v3s16(0, 1, 0);
+ const NodeDefManager *ndef = env->getGameDef()->ndef();
+ bool is_position_valid;
+ for (s16 z = ceilpos_min.Z; z <= ceilpos_max.Z; z++) {
+ for (s16 x = ceilpos_min.X; x <= ceilpos_max.X; x++) {
+ MapNode n = env->getMap().getNodeNoEx(v3s16(x, ceilpos_max.Y, z), &is_position_valid);
+
+ if (!is_position_valid)
+ break; // won't collide with the void outside
+ if (n.getContent() == CONTENT_IGNORE)
+ return; // players collide with ignore blocks -> same as walkable
+ const ContentFeatures &f = ndef->get(n);
+ if (f.walkable)
+ return; // would bump head, don't jump
+ }
+ }
+
+ float jump_height = 1.1f; // TODO: better than a magic number
+ v3f jump_pos = initial_position + v3f(0.0f, jump_height * BS, 0.0f);
+ v3f jump_speed = initial_speed;
+
+ // try at peak of jump, zero step height
+ collisionMoveResult jump_result = collisionMoveSimple(env, m_client, pos_max_d,
+ m_collisionbox, 0.0f, dtime, &jump_pos, &jump_speed,
+ v3f(0, 0, 0));
+
+ // see if we can get a little bit farther horizontally if we had
+ // jumped
+ v3f run_delta = m_position - initial_position;
+ run_delta.Y = 0.0f;
+ v3f jump_delta = jump_pos - initial_position;
+ jump_delta.Y = 0.0f;
+ if (jump_delta.getLengthSQ() > run_delta.getLengthSQ() * 1.01f) {
+ m_autojump = true;
+ m_autojump_time = 0.1f;
+ }
+}
diff --git a/src/client/localplayer.h b/src/client/localplayer.h
new file mode 100644
index 000000000..7148bc4de
--- /dev/null
+++ b/src/client/localplayer.h
@@ -0,0 +1,198 @@
+/*
+Minetest
+Copyright (C) 2010-2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+
+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 "player.h"
+#include "environment.h"
+#include "constants.h"
+#include "settings.h"
+#include <list>
+
+class Client;
+class Environment;
+class GenericCAO;
+class ClientActiveObject;
+class ClientEnvironment;
+class IGameDef;
+struct collisionMoveResult;
+
+enum LocalPlayerAnimations
+{
+ NO_ANIM,
+ WALK_ANIM,
+ DIG_ANIM,
+ WD_ANIM
+}; // no local animation, walking, digging, both
+
+class LocalPlayer : public Player
+{
+public:
+ LocalPlayer(Client *client, const char *name);
+ virtual ~LocalPlayer() = default;
+
+ ClientActiveObject *parent = nullptr;
+
+ // Initialize hp to 0, so that no hearts will be shown if server
+ // doesn't support health points
+ u16 hp = 0;
+ bool isAttached = false;
+ bool touching_ground = false;
+ // This oscillates so that the player jumps a bit above the surface
+ bool in_liquid = false;
+ // This is more stable and defines the maximum speed of the player
+ bool in_liquid_stable = false;
+ // Gets the viscosity of water to calculate friction
+ u8 liquid_viscosity = 0;
+ bool is_climbing = false;
+ bool swimming_vertical = false;
+
+ float physics_override_speed = 1.0f;
+ float physics_override_jump = 1.0f;
+ float physics_override_gravity = 1.0f;
+ bool physics_override_sneak = true;
+ bool physics_override_sneak_glitch = false;
+ // Temporary option for old move code
+ bool physics_override_new_move = true;
+
+ v3f overridePosition;
+
+ void move(f32 dtime, Environment *env, f32 pos_max_d);
+ void move(f32 dtime, Environment *env, f32 pos_max_d,
+ std::vector<CollisionInfo> *collision_info);
+ // Temporary option for old move code
+ void old_move(f32 dtime, Environment *env, f32 pos_max_d,
+ std::vector<CollisionInfo> *collision_info);
+
+ void applyControl(float dtime, Environment *env);
+
+ v3s16 getStandingNodePos();
+ v3s16 getFootstepNodePos();
+
+ // Used to check if anything changed and prevent sending packets if not
+ v3f last_position;
+ v3f last_speed;
+ float last_pitch = 0.0f;
+ float last_yaw = 0.0f;
+ unsigned int last_keyPressed = 0;
+ u8 last_camera_fov = 0;
+ u8 last_wanted_range = 0;
+
+ float camera_impact = 0.0f;
+
+ bool makes_footstep_sound = true;
+
+ int last_animation = NO_ANIM;
+ float last_animation_speed;
+
+ std::string hotbar_image = "";
+ std::string hotbar_selected_image = "";
+
+ video::SColor light_color = video::SColor(255, 255, 255, 255);
+
+ float hurt_tilt_timer = 0.0f;
+ float hurt_tilt_strength = 0.0f;
+
+ GenericCAO *getCAO() const { return m_cao; }
+
+ void setCAO(GenericCAO *toset)
+ {
+ assert(!m_cao); // Pre-condition
+ m_cao = toset;
+ }
+
+ u32 maxHudId() const { return hud.size(); }
+
+ u16 getBreath() const { return m_breath; }
+ void setBreath(u16 breath) { m_breath = breath; }
+
+ v3s16 getLightPosition() const;
+
+ void setYaw(f32 yaw) { m_yaw = yaw; }
+ f32 getYaw() const { return m_yaw; }
+
+ void setPitch(f32 pitch) { m_pitch = pitch; }
+ f32 getPitch() const { return m_pitch; }
+
+ inline void setPosition(const v3f &position)
+ {
+ m_position = position;
+ m_sneak_node_exists = false;
+ }
+
+ v3f getPosition() const { return m_position; }
+ v3f getEyePosition() const { return m_position + getEyeOffset(); }
+ v3f getEyeOffset() const;
+ void setEyeHeight(float eye_height) { m_eye_height = eye_height; }
+
+ void setCollisionbox(const aabb3f &box) { m_collisionbox = box; }
+
+ float getZoomFOV() const { return m_zoom_fov; }
+ void setZoomFOV(float zoom_fov) { m_zoom_fov = zoom_fov; }
+
+ bool getAutojump() const { return m_autojump; }
+
+private:
+ void accelerateHorizontal(const v3f &target_speed, const f32 max_increase);
+ void accelerateVertical(const v3f &target_speed, const f32 max_increase);
+ bool updateSneakNode(Map *map, const v3f &position, const v3f &sneak_max);
+ float getSlipFactor(Environment *env, const v3f &speedH);
+ void handleAutojump(f32 dtime, Environment *env,
+ const collisionMoveResult &result,
+ const v3f &position_before_move, const v3f &speed_before_move,
+ f32 pos_max_d);
+
+ v3f m_position;
+ v3s16 m_standing_node;
+
+ v3s16 m_sneak_node = v3s16(32767, 32767, 32767);
+ // Stores the top bounding box of m_sneak_node
+ aabb3f m_sneak_node_bb_top = aabb3f(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f);
+ // Whether the player is allowed to sneak
+ bool m_sneak_node_exists = false;
+ // Whether a "sneak ladder" structure is detected at the players pos
+ // see detectSneakLadder() in the .cpp for more info (always false if disabled)
+ bool m_sneak_ladder_detected = false;
+
+ // ***** Variables for temporary option of the old move code *****
+ // Stores the max player uplift by m_sneak_node
+ f32 m_sneak_node_bb_ymax = 0.0f;
+ // Whether recalculation of m_sneak_node and its top bbox is needed
+ bool m_need_to_get_new_sneak_node = true;
+ // Node below player, used to determine whether it has been removed,
+ // and its old type
+ v3s16 m_old_node_below = v3s16(32767, 32767, 32767);
+ std::string m_old_node_below_type = "air";
+ // ***** End of variables for temporary option *****
+
+ bool m_can_jump = false;
+ u16 m_breath = PLAYER_MAX_BREATH_DEFAULT;
+ f32 m_yaw = 0.0f;
+ f32 m_pitch = 0.0f;
+ bool camera_barely_in_ceiling = false;
+ aabb3f m_collisionbox = aabb3f(-BS * 0.30f, 0.0f, -BS * 0.30f, BS * 0.30f,
+ BS * 1.75f, BS * 0.30f);
+ float m_eye_height = 1.625f;
+ float m_zoom_fov = 0.0f;
+ bool m_autojump = false;
+ float m_autojump_time = 0.0f;
+
+ GenericCAO *m_cao = nullptr;
+ Client *m_client;
+};
diff --git a/src/client/mapblock_mesh.cpp b/src/client/mapblock_mesh.cpp
new file mode 100644
index 000000000..ed8a073de
--- /dev/null
+++ b/src/client/mapblock_mesh.cpp
@@ -0,0 +1,1389 @@
+/*
+Minetest
+Copyright (C) 2010-2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+
+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 "mapblock_mesh.h"
+#include "client.h"
+#include "mapblock.h"
+#include "map.h"
+#include "profiler.h"
+#include "shader.h"
+#include "mesh.h"
+#include "minimap.h"
+#include "content_mapblock.h"
+#include "util/directiontables.h"
+#include "client/meshgen/collector.h"
+#include "client/renderingengine.h"
+#include <array>
+
+/*
+ MeshMakeData
+*/
+
+MeshMakeData::MeshMakeData(Client *client, bool use_shaders,
+ bool use_tangent_vertices):
+ m_client(client),
+ m_use_shaders(use_shaders),
+ m_use_tangent_vertices(use_tangent_vertices)
+{}
+
+void MeshMakeData::fillBlockDataBegin(const v3s16 &blockpos)
+{
+ m_blockpos = blockpos;
+
+ v3s16 blockpos_nodes = m_blockpos*MAP_BLOCKSIZE;
+
+ m_vmanip.clear();
+ VoxelArea voxel_area(blockpos_nodes - v3s16(1,1,1) * MAP_BLOCKSIZE,
+ blockpos_nodes + v3s16(1,1,1) * MAP_BLOCKSIZE*2-v3s16(1,1,1));
+ m_vmanip.addArea(voxel_area);
+}
+
+void MeshMakeData::fillBlockData(const v3s16 &block_offset, MapNode *data)
+{
+ v3s16 data_size(MAP_BLOCKSIZE, MAP_BLOCKSIZE, MAP_BLOCKSIZE);
+ VoxelArea data_area(v3s16(0,0,0), data_size - v3s16(1,1,1));
+
+ v3s16 bp = m_blockpos + block_offset;
+ v3s16 blockpos_nodes = bp * MAP_BLOCKSIZE;
+ m_vmanip.copyFrom(data, data_area, v3s16(0,0,0), blockpos_nodes, data_size);
+}
+
+void MeshMakeData::fill(MapBlock *block)
+{
+ fillBlockDataBegin(block->getPos());
+
+ fillBlockData(v3s16(0,0,0), block->getData());
+
+ // Get map for reading neighbor blocks
+ Map *map = block->getParent();
+
+ for (const v3s16 &dir : g_26dirs) {
+ v3s16 bp = m_blockpos + dir;
+ MapBlock *b = map->getBlockNoCreateNoEx(bp);
+ if(b)
+ fillBlockData(dir, b->getData());
+ }
+}
+
+void MeshMakeData::fillSingleNode(MapNode *node)
+{
+ m_blockpos = v3s16(0,0,0);
+
+ v3s16 blockpos_nodes = v3s16(0,0,0);
+ VoxelArea area(blockpos_nodes-v3s16(1,1,1)*MAP_BLOCKSIZE,
+ blockpos_nodes+v3s16(1,1,1)*MAP_BLOCKSIZE*2-v3s16(1,1,1));
+ s32 volume = area.getVolume();
+ s32 our_node_index = area.index(1,1,1);
+
+ // Allocate this block + neighbors
+ m_vmanip.clear();
+ m_vmanip.addArea(area);
+
+ // Fill in data
+ MapNode *data = new MapNode[volume];
+ for(s32 i = 0; i < volume; i++)
+ {
+ if (i == our_node_index)
+ data[i] = *node;
+ else
+ data[i] = MapNode(CONTENT_AIR, LIGHT_MAX, 0);
+ }
+ m_vmanip.copyFrom(data, area, area.MinEdge, area.MinEdge, area.getExtent());
+ delete[] data;
+}
+
+void MeshMakeData::setCrack(int crack_level, v3s16 crack_pos)
+{
+ if (crack_level >= 0)
+ m_crack_pos_relative = crack_pos - m_blockpos*MAP_BLOCKSIZE;
+}
+
+void MeshMakeData::setSmoothLighting(bool smooth_lighting)
+{
+ m_smooth_lighting = smooth_lighting;
+}
+
+/*
+ Light and vertex color functions
+*/
+
+/*
+ Calculate non-smooth lighting at interior of node.
+ Single light bank.
+*/
+static u8 getInteriorLight(enum LightBank bank, MapNode n, s32 increment,
+ const NodeDefManager *ndef)
+{
+ u8 light = n.getLight(bank, ndef);
+ if (light > 0)
+ light = rangelim(light + increment, 0, LIGHT_SUN);
+ return decode_light(light);
+}
+
+/*
+ Calculate non-smooth lighting at interior of node.
+ Both light banks.
+*/
+u16 getInteriorLight(MapNode n, s32 increment, const NodeDefManager *ndef)
+{
+ u16 day = getInteriorLight(LIGHTBANK_DAY, n, increment, ndef);
+ u16 night = getInteriorLight(LIGHTBANK_NIGHT, n, increment, ndef);
+ return day | (night << 8);
+}
+
+/*
+ Calculate non-smooth lighting at face of node.
+ Single light bank.
+*/
+static u8 getFaceLight(enum LightBank bank, MapNode n, MapNode n2,
+ v3s16 face_dir, const NodeDefManager *ndef)
+{
+ u8 light;
+ u8 l1 = n.getLight(bank, ndef);
+ u8 l2 = n2.getLight(bank, ndef);
+ if(l1 > l2)
+ light = l1;
+ else
+ light = l2;
+
+ // Boost light level for light sources
+ u8 light_source = MYMAX(ndef->get(n).light_source,
+ ndef->get(n2).light_source);
+ if(light_source > light)
+ light = light_source;
+
+ return decode_light(light);
+}
+
+/*
+ Calculate non-smooth lighting at face of node.
+ Both light banks.
+*/
+u16 getFaceLight(MapNode n, MapNode n2, const v3s16 &face_dir,
+ const NodeDefManager *ndef)
+{
+ u16 day = getFaceLight(LIGHTBANK_DAY, n, n2, face_dir, ndef);
+ u16 night = getFaceLight(LIGHTBANK_NIGHT, n, n2, face_dir, ndef);
+ return day | (night << 8);
+}
+
+/*
+ Calculate smooth lighting at the XYZ- corner of p.
+ Both light banks
+*/
+static u16 getSmoothLightCombined(const v3s16 &p,
+ const std::array<v3s16,8> &dirs, MeshMakeData *data)
+{
+ const NodeDefManager *ndef = data->m_client->ndef();
+
+ u16 ambient_occlusion = 0;
+ u16 light_count = 0;
+ u8 light_source_max = 0;
+ u16 light_day = 0;
+ u16 light_night = 0;
+ bool direct_sunlight = false;
+
+ auto add_node = [&] (u8 i, bool obstructed = false) -> bool {
+ if (obstructed) {
+ ambient_occlusion++;
+ return false;
+ }
+ MapNode n = data->m_vmanip.getNodeNoExNoEmerge(p + dirs[i]);
+ if (n.getContent() == CONTENT_IGNORE)
+ return true;
+ const ContentFeatures &f = ndef->get(n);
+ if (f.light_source > light_source_max)
+ light_source_max = f.light_source;
+ // Check f.solidness because fast-style leaves look better this way
+ if (f.param_type == CPT_LIGHT && f.solidness != 2) {
+ u8 light_level_day = n.getLightNoChecks(LIGHTBANK_DAY, &f);
+ u8 light_level_night = n.getLightNoChecks(LIGHTBANK_NIGHT, &f);
+ if (light_level_day == LIGHT_SUN)
+ direct_sunlight = true;
+ light_day += decode_light(light_level_day);
+ light_night += decode_light(light_level_night);
+ light_count++;
+ } else {
+ ambient_occlusion++;
+ }
+ return f.light_propagates;
+ };
+
+ std::array<bool, 4> obstructed = {{ 1, 1, 1, 1 }};
+ add_node(0);
+ bool opaque1 = !add_node(1);
+ bool opaque2 = !add_node(2);
+ bool opaque3 = !add_node(3);
+ obstructed[0] = opaque1 && opaque2;
+ obstructed[1] = opaque1 && opaque3;
+ obstructed[2] = opaque2 && opaque3;
+ for (u8 k = 0; k < 3; ++k)
+ if (add_node(k + 4, obstructed[k]))
+ obstructed[3] = false;
+ if (add_node(7, obstructed[3])) { // wrap light around nodes
+ ambient_occlusion -= 3;
+ for (u8 k = 0; k < 3; ++k)
+ add_node(k + 4, !obstructed[k]);
+ }
+
+ if (light_count == 0) {
+ light_day = light_night = 0;
+ } else {
+ light_day /= light_count;
+ light_night /= light_count;
+ }
+
+ // boost direct sunlight, if any
+ if (direct_sunlight)
+ light_day = 0xFF;
+
+ // Boost brightness around light sources
+ bool skip_ambient_occlusion_day = false;
+ if (decode_light(light_source_max) >= light_day) {
+ light_day = decode_light(light_source_max);
+ skip_ambient_occlusion_day = true;
+ }
+
+ bool skip_ambient_occlusion_night = false;
+ if(decode_light(light_source_max) >= light_night) {
+ light_night = decode_light(light_source_max);
+ skip_ambient_occlusion_night = true;
+ }
+
+ if (ambient_occlusion > 4) {
+ static thread_local const float ao_gamma = rangelim(
+ g_settings->getFloat("ambient_occlusion_gamma"), 0.25, 4.0);
+
+ // Table of gamma space multiply factors.
+ static thread_local const float light_amount[3] = {
+ powf(0.75, 1.0 / ao_gamma),
+ powf(0.5, 1.0 / ao_gamma),
+ powf(0.25, 1.0 / ao_gamma)
+ };
+
+ //calculate table index for gamma space multiplier
+ ambient_occlusion -= 5;
+
+ if (!skip_ambient_occlusion_day)
+ light_day = rangelim(core::round32(
+ light_day * light_amount[ambient_occlusion]), 0, 255);
+ if (!skip_ambient_occlusion_night)
+ light_night = rangelim(core::round32(
+ light_night * light_amount[ambient_occlusion]), 0, 255);
+ }
+
+ return light_day | (light_night << 8);
+}
+
+/*
+ Calculate smooth lighting at the given corner of p.
+ Both light banks.
+ Node at p is solid, and thus the lighting is face-dependent.
+*/
+u16 getSmoothLightSolid(const v3s16 &p, const v3s16 &face_dir, const v3s16 &corner, MeshMakeData *data)
+{
+ return getSmoothLightTransparent(p + face_dir, corner - 2 * face_dir, data);
+}
+
+/*
+ Calculate smooth lighting at the given corner of p.
+ Both light banks.
+ Node at p is not solid, and the lighting is not face-dependent.
+*/
+u16 getSmoothLightTransparent(const v3s16 &p, const v3s16 &corner, MeshMakeData *data)
+{
+ const std::array<v3s16,8> dirs = {{
+ // Always shine light
+ v3s16(0,0,0),
+ v3s16(corner.X,0,0),
+ v3s16(0,corner.Y,0),
+ v3s16(0,0,corner.Z),
+
+ // Can be obstructed
+ v3s16(corner.X,corner.Y,0),
+ v3s16(corner.X,0,corner.Z),
+ v3s16(0,corner.Y,corner.Z),
+ v3s16(corner.X,corner.Y,corner.Z)
+ }};
+ return getSmoothLightCombined(p, dirs, data);
+}
+
+void get_sunlight_color(video::SColorf *sunlight, u32 daynight_ratio){
+ f32 rg = daynight_ratio / 1000.0f - 0.04f;
+ f32 b = (0.98f * daynight_ratio) / 1000.0f + 0.078f;
+ sunlight->r = rg;
+ sunlight->g = rg;
+ sunlight->b = b;
+}
+
+void final_color_blend(video::SColor *result,
+ u16 light, u32 daynight_ratio)
+{
+ video::SColorf dayLight;
+ get_sunlight_color(&dayLight, daynight_ratio);
+ final_color_blend(result,
+ encode_light(light, 0), dayLight);
+}
+
+void final_color_blend(video::SColor *result,
+ const video::SColor &data, const video::SColorf &dayLight)
+{
+ static const video::SColorf artificialColor(1.04f, 1.04f, 1.04f);
+
+ video::SColorf c(data);
+ f32 n = 1 - c.a;
+
+ f32 r = c.r * (c.a * dayLight.r + n * artificialColor.r) * 2.0f;
+ f32 g = c.g * (c.a * dayLight.g + n * artificialColor.g) * 2.0f;
+ f32 b = c.b * (c.a * dayLight.b + n * artificialColor.b) * 2.0f;
+
+ // Emphase blue a bit in darker places
+ // Each entry of this array represents a range of 8 blue levels
+ static const u8 emphase_blue_when_dark[32] = {
+ 1, 4, 6, 6, 6, 5, 4, 3, 2, 1, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ };
+
+ b += emphase_blue_when_dark[irr::core::clamp((s32) ((r + g + b) / 3 * 255),
+ 0, 255) / 8] / 255.0f;
+
+ result->setRed(core::clamp((s32) (r * 255.0f), 0, 255));
+ result->setGreen(core::clamp((s32) (g * 255.0f), 0, 255));
+ result->setBlue(core::clamp((s32) (b * 255.0f), 0, 255));
+}
+
+/*
+ Mesh generation helpers
+*/
+
+/*
+ vertex_dirs: v3s16[4]
+*/
+static void getNodeVertexDirs(const v3s16 &dir, v3s16 *vertex_dirs)
+{
+ /*
+ If looked from outside the node towards the face, the corners are:
+ 0: bottom-right
+ 1: bottom-left
+ 2: top-left
+ 3: top-right
+ */
+ if (dir == v3s16(0, 0, 1)) {
+ // If looking towards z+, this is the face that is behind
+ // the center point, facing towards z+.
+ vertex_dirs[0] = v3s16(-1,-1, 1);
+ vertex_dirs[1] = v3s16( 1,-1, 1);
+ vertex_dirs[2] = v3s16( 1, 1, 1);
+ vertex_dirs[3] = v3s16(-1, 1, 1);
+ } else if (dir == v3s16(0, 0, -1)) {
+ // faces towards Z-
+ vertex_dirs[0] = v3s16( 1,-1,-1);
+ vertex_dirs[1] = v3s16(-1,-1,-1);
+ vertex_dirs[2] = v3s16(-1, 1,-1);
+ vertex_dirs[3] = v3s16( 1, 1,-1);
+ } else if (dir == v3s16(1, 0, 0)) {
+ // faces towards X+
+ vertex_dirs[0] = v3s16( 1,-1, 1);
+ vertex_dirs[1] = v3s16( 1,-1,-1);
+ vertex_dirs[2] = v3s16( 1, 1,-1);
+ vertex_dirs[3] = v3s16( 1, 1, 1);
+ } else if (dir == v3s16(-1, 0, 0)) {
+ // faces towards X-
+ vertex_dirs[0] = v3s16(-1,-1,-1);
+ vertex_dirs[1] = v3s16(-1,-1, 1);
+ vertex_dirs[2] = v3s16(-1, 1, 1);
+ vertex_dirs[3] = v3s16(-1, 1,-1);
+ } else if (dir == v3s16(0, 1, 0)) {
+ // faces towards Y+ (assume Z- as "down" in texture)
+ vertex_dirs[0] = v3s16( 1, 1,-1);
+ vertex_dirs[1] = v3s16(-1, 1,-1);
+ vertex_dirs[2] = v3s16(-1, 1, 1);
+ vertex_dirs[3] = v3s16( 1, 1, 1);
+ } else if (dir == v3s16(0, -1, 0)) {
+ // faces towards Y- (assume Z+ as "down" in texture)
+ vertex_dirs[0] = v3s16( 1,-1, 1);
+ vertex_dirs[1] = v3s16(-1,-1, 1);
+ vertex_dirs[2] = v3s16(-1,-1,-1);
+ vertex_dirs[3] = v3s16( 1,-1,-1);
+ }
+}
+
+static void getNodeTextureCoords(v3f base, const v3f &scale, const v3s16 &dir, float *u, float *v)
+{
+ if (dir.X > 0 || dir.Y > 0 || dir.Z < 0)
+ base -= scale;
+ if (dir == v3s16(0,0,1)) {
+ *u = -base.X - 1;
+ *v = -base.Y - 1;
+ } else if (dir == v3s16(0,0,-1)) {
+ *u = base.X + 1;
+ *v = -base.Y - 2;
+ } else if (dir == v3s16(1,0,0)) {
+ *u = base.Z + 1;
+ *v = -base.Y - 2;
+ } else if (dir == v3s16(-1,0,0)) {
+ *u = -base.Z - 1;
+ *v = -base.Y - 1;
+ } else if (dir == v3s16(0,1,0)) {
+ *u = base.X + 1;
+ *v = -base.Z - 2;
+ } else if (dir == v3s16(0,-1,0)) {
+ *u = base.X;
+ *v = base.Z;
+ }
+}
+
+struct FastFace
+{
+ TileSpec tile;
+ video::S3DVertex vertices[4]; // Precalculated vertices
+ /*!
+ * The face is divided into two triangles. If this is true,
+ * vertices 0 and 2 are connected, othervise vertices 1 and 3
+ * are connected.
+ */
+ bool vertex_0_2_connected;
+};
+
+static void makeFastFace(const TileSpec &tile, u16 li0, u16 li1, u16 li2, u16 li3,
+ const v3f &tp, const v3f &p, const v3s16 &dir, const v3f &scale, std::vector<FastFace> &dest)
+{
+ // Position is at the center of the cube.
+ v3f pos = p * BS;
+
+ float x0 = 0.0f;
+ float y0 = 0.0f;
+ float w = 1.0f;
+ float h = 1.0f;
+
+ v3f vertex_pos[4];
+ v3s16 vertex_dirs[4];
+ getNodeVertexDirs(dir, vertex_dirs);
+ if (tile.world_aligned)
+ getNodeTextureCoords(tp, scale, dir, &x0, &y0);
+
+ v3s16 t;
+ u16 t1;
+ switch (tile.rotation) {
+ case 0:
+ break;
+ case 1: //R90
+ t = vertex_dirs[0];
+ vertex_dirs[0] = vertex_dirs[3];
+ vertex_dirs[3] = vertex_dirs[2];
+ vertex_dirs[2] = vertex_dirs[1];
+ vertex_dirs[1] = t;
+ t1 = li0;
+ li0 = li3;
+ li3 = li2;
+ li2 = li1;
+ li1 = t1;
+ break;
+ case 2: //R180
+ t = vertex_dirs[0];
+ vertex_dirs[0] = vertex_dirs[2];
+ vertex_dirs[2] = t;
+ t = vertex_dirs[1];
+ vertex_dirs[1] = vertex_dirs[3];
+ vertex_dirs[3] = t;
+ t1 = li0;
+ li0 = li2;
+ li2 = t1;
+ t1 = li1;
+ li1 = li3;
+ li3 = t1;
+ break;
+ case 3: //R270
+ t = vertex_dirs[0];
+ vertex_dirs[0] = vertex_dirs[1];
+ vertex_dirs[1] = vertex_dirs[2];
+ vertex_dirs[2] = vertex_dirs[3];
+ vertex_dirs[3] = t;
+ t1 = li0;
+ li0 = li1;
+ li1 = li2;
+ li2 = li3;
+ li3 = t1;
+ break;
+ case 4: //FXR90
+ t = vertex_dirs[0];
+ vertex_dirs[0] = vertex_dirs[3];
+ vertex_dirs[3] = vertex_dirs[2];
+ vertex_dirs[2] = vertex_dirs[1];
+ vertex_dirs[1] = t;
+ t1 = li0;
+ li0 = li3;
+ li3 = li2;
+ li2 = li1;
+ li1 = t1;
+ y0 += h;
+ h *= -1;
+ break;
+ case 5: //FXR270
+ t = vertex_dirs[0];
+ vertex_dirs[0] = vertex_dirs[1];
+ vertex_dirs[1] = vertex_dirs[2];
+ vertex_dirs[2] = vertex_dirs[3];
+ vertex_dirs[3] = t;
+ t1 = li0;
+ li0 = li1;
+ li1 = li2;
+ li2 = li3;
+ li3 = t1;
+ y0 += h;
+ h *= -1;
+ break;
+ case 6: //FYR90
+ t = vertex_dirs[0];
+ vertex_dirs[0] = vertex_dirs[3];
+ vertex_dirs[3] = vertex_dirs[2];
+ vertex_dirs[2] = vertex_dirs[1];
+ vertex_dirs[1] = t;
+ t1 = li0;
+ li0 = li3;
+ li3 = li2;
+ li2 = li1;
+ li1 = t1;
+ x0 += w;
+ w *= -1;
+ break;
+ case 7: //FYR270
+ t = vertex_dirs[0];
+ vertex_dirs[0] = vertex_dirs[1];
+ vertex_dirs[1] = vertex_dirs[2];
+ vertex_dirs[2] = vertex_dirs[3];
+ vertex_dirs[3] = t;
+ t1 = li0;
+ li0 = li1;
+ li1 = li2;
+ li2 = li3;
+ li3 = t1;
+ x0 += w;
+ w *= -1;
+ break;
+ case 8: //FX
+ y0 += h;
+ h *= -1;
+ break;
+ case 9: //FY
+ x0 += w;
+ w *= -1;
+ break;
+ default:
+ break;
+ }
+
+ for (u16 i = 0; i < 4; i++) {
+ vertex_pos[i] = v3f(
+ BS / 2 * vertex_dirs[i].X,
+ BS / 2 * vertex_dirs[i].Y,
+ BS / 2 * vertex_dirs[i].Z
+ );
+ }
+
+ for (v3f &vpos : vertex_pos) {
+ vpos.X *= scale.X;
+ vpos.Y *= scale.Y;
+ vpos.Z *= scale.Z;
+ vpos += pos;
+ }
+
+ f32 abs_scale = 1.0f;
+ if (scale.X < 0.999f || scale.X > 1.001f) abs_scale = scale.X;
+ else if (scale.Y < 0.999f || scale.Y > 1.001f) abs_scale = scale.Y;
+ else if (scale.Z < 0.999f || scale.Z > 1.001f) abs_scale = scale.Z;
+
+ v3f normal(dir.X, dir.Y, dir.Z);
+
+ u16 li[4] = { li0, li1, li2, li3 };
+ u16 day[4];
+ u16 night[4];
+
+ for (u8 i = 0; i < 4; i++) {
+ day[i] = li[i] >> 8;
+ night[i] = li[i] & 0xFF;
+ }
+
+ bool vertex_0_2_connected = abs(day[0] - day[2]) + abs(night[0] - night[2])
+ < abs(day[1] - day[3]) + abs(night[1] - night[3]);
+
+ v2f32 f[4] = {
+ core::vector2d<f32>(x0 + w * abs_scale, y0 + h),
+ core::vector2d<f32>(x0, y0 + h),
+ core::vector2d<f32>(x0, y0),
+ core::vector2d<f32>(x0 + w * abs_scale, y0) };
+
+ // equivalent to dest.push_back(FastFace()) but faster
+ dest.emplace_back();
+ FastFace& face = *dest.rbegin();
+
+ for (u8 i = 0; i < 4; i++) {
+ video::SColor c = encode_light(li[i], tile.emissive_light);
+ if (!tile.emissive_light)
+ applyFacesShading(c, normal);
+
+ face.vertices[i] = video::S3DVertex(vertex_pos[i], normal, c, f[i]);
+ }
+
+ /*
+ Revert triangles for nicer looking gradient if the
+ brightness of vertices 1 and 3 differ less than
+ the brightness of vertices 0 and 2.
+ */
+ face.vertex_0_2_connected = vertex_0_2_connected;
+ face.tile = tile;
+}
+
+/*
+ Nodes make a face if contents differ and solidness differs.
+ Return value:
+ 0: No face
+ 1: Face uses m1's content
+ 2: Face uses m2's content
+ equivalent: Whether the blocks share the same face (eg. water and glass)
+
+ TODO: Add 3: Both faces drawn with backface culling, remove equivalent
+*/
+static u8 face_contents(content_t m1, content_t m2, bool *equivalent,
+ const NodeDefManager *ndef)
+{
+ *equivalent = false;
+
+ if (m1 == m2 || m1 == CONTENT_IGNORE || m2 == CONTENT_IGNORE)
+ return 0;
+
+ const ContentFeatures &f1 = ndef->get(m1);
+ const ContentFeatures &f2 = ndef->get(m2);
+
+ // Contents don't differ for different forms of same liquid
+ if (f1.sameLiquid(f2))
+ return 0;
+
+ u8 c1 = f1.solidness;
+ u8 c2 = f2.solidness;
+
+ if (c1 == c2)
+ return 0;
+
+ if (c1 == 0)
+ c1 = f1.visual_solidness;
+ else if (c2 == 0)
+ c2 = f2.visual_solidness;
+
+ if (c1 == c2) {
+ *equivalent = true;
+ // If same solidness, liquid takes precense
+ if (f1.isLiquid())
+ return 1;
+ if (f2.isLiquid())
+ return 2;
+ }
+
+ if (c1 > c2)
+ return 1;
+
+ return 2;
+}
+
+/*
+ Gets nth node tile (0 <= n <= 5).
+*/
+void getNodeTileN(MapNode mn, const v3s16 &p, u8 tileindex, MeshMakeData *data, TileSpec &tile)
+{
+ const NodeDefManager *ndef = data->m_client->ndef();
+ const ContentFeatures &f = ndef->get(mn);
+ tile = f.tiles[tileindex];
+ bool has_crack = p == data->m_crack_pos_relative;
+ for (TileLayer &layer : tile.layers) {
+ if (layer.texture_id == 0)
+ continue;
+ if (!layer.has_color)
+ mn.getColor(f, &(layer.color));
+ // Apply temporary crack
+ if (has_crack)
+ layer.material_flags |= MATERIAL_FLAG_CRACK;
+ }
+}
+
+/*
+ Gets node tile given a face direction.
+*/
+void getNodeTile(MapNode mn, const v3s16 &p, const v3s16 &dir, MeshMakeData *data, TileSpec &tile)
+{
+ const NodeDefManager *ndef = data->m_client->ndef();
+
+ // Direction must be (1,0,0), (-1,0,0), (0,1,0), (0,-1,0),
+ // (0,0,1), (0,0,-1) or (0,0,0)
+ assert(dir.X * dir.X + dir.Y * dir.Y + dir.Z * dir.Z <= 1);
+
+ // Convert direction to single integer for table lookup
+ // 0 = (0,0,0)
+ // 1 = (1,0,0)
+ // 2 = (0,1,0)
+ // 3 = (0,0,1)
+ // 4 = invalid, treat as (0,0,0)
+ // 5 = (0,0,-1)
+ // 6 = (0,-1,0)
+ // 7 = (-1,0,0)
+ u8 dir_i = ((dir.X + 2 * dir.Y + 3 * dir.Z) & 7) * 2;
+
+ // Get rotation for things like chests
+ u8 facedir = mn.getFaceDir(ndef);
+
+ static const u16 dir_to_tile[24 * 16] =
+ {
+ // 0 +X +Y +Z -Z -Y -X -> value=tile,rotation
+ 0,0, 2,0 , 0,0 , 4,0 , 0,0, 5,0 , 1,0 , 3,0 , // rotate around y+ 0 - 3
+ 0,0, 4,0 , 0,3 , 3,0 , 0,0, 2,0 , 1,1 , 5,0 ,
+ 0,0, 3,0 , 0,2 , 5,0 , 0,0, 4,0 , 1,2 , 2,0 ,
+ 0,0, 5,0 , 0,1 , 2,0 , 0,0, 3,0 , 1,3 , 4,0 ,
+
+ 0,0, 2,3 , 5,0 , 0,2 , 0,0, 1,0 , 4,2 , 3,1 , // rotate around z+ 4 - 7
+ 0,0, 4,3 , 2,0 , 0,1 , 0,0, 1,1 , 3,2 , 5,1 ,
+ 0,0, 3,3 , 4,0 , 0,0 , 0,0, 1,2 , 5,2 , 2,1 ,
+ 0,0, 5,3 , 3,0 , 0,3 , 0,0, 1,3 , 2,2 , 4,1 ,
+
+ 0,0, 2,1 , 4,2 , 1,2 , 0,0, 0,0 , 5,0 , 3,3 , // rotate around z- 8 - 11
+ 0,0, 4,1 , 3,2 , 1,3 , 0,0, 0,3 , 2,0 , 5,3 ,
+ 0,0, 3,1 , 5,2 , 1,0 , 0,0, 0,2 , 4,0 , 2,3 ,
+ 0,0, 5,1 , 2,2 , 1,1 , 0,0, 0,1 , 3,0 , 4,3 ,
+
+ 0,0, 0,3 , 3,3 , 4,1 , 0,0, 5,3 , 2,3 , 1,3 , // rotate around x+ 12 - 15
+ 0,0, 0,2 , 5,3 , 3,1 , 0,0, 2,3 , 4,3 , 1,0 ,
+ 0,0, 0,1 , 2,3 , 5,1 , 0,0, 4,3 , 3,3 , 1,1 ,
+ 0,0, 0,0 , 4,3 , 2,1 , 0,0, 3,3 , 5,3 , 1,2 ,
+
+ 0,0, 1,1 , 2,1 , 4,3 , 0,0, 5,1 , 3,1 , 0,1 , // rotate around x- 16 - 19
+ 0,0, 1,2 , 4,1 , 3,3 , 0,0, 2,1 , 5,1 , 0,0 ,
+ 0,0, 1,3 , 3,1 , 5,3 , 0,0, 4,1 , 2,1 , 0,3 ,
+ 0,0, 1,0 , 5,1 , 2,3 , 0,0, 3,1 , 4,1 , 0,2 ,
+
+ 0,0, 3,2 , 1,2 , 4,2 , 0,0, 5,2 , 0,2 , 2,2 , // rotate around y- 20 - 23
+ 0,0, 5,2 , 1,3 , 3,2 , 0,0, 2,2 , 0,1 , 4,2 ,
+ 0,0, 2,2 , 1,0 , 5,2 , 0,0, 4,2 , 0,0 , 3,2 ,
+ 0,0, 4,2 , 1,1 , 2,2 , 0,0, 3,2 , 0,3 , 5,2
+
+ };
+ u16 tile_index = facedir * 16 + dir_i;
+ getNodeTileN(mn, p, dir_to_tile[tile_index], data, tile);
+ tile.rotation = tile.world_aligned ? 0 : dir_to_tile[tile_index + 1];
+}
+
+static void getTileInfo(
+ // Input:
+ MeshMakeData *data,
+ const v3s16 &p,
+ const v3s16 &face_dir,
+ // Output:
+ bool &makes_face,
+ v3s16 &p_corrected,
+ v3s16 &face_dir_corrected,
+ u16 *lights,
+ TileSpec &tile
+ )
+{
+ VoxelManipulator &vmanip = data->m_vmanip;
+ const NodeDefManager *ndef = data->m_client->ndef();
+ v3s16 blockpos_nodes = data->m_blockpos * MAP_BLOCKSIZE;
+
+ const MapNode &n0 = vmanip.getNodeRefUnsafe(blockpos_nodes + p);
+
+ // Don't even try to get n1 if n0 is already CONTENT_IGNORE
+ if (n0.getContent() == CONTENT_IGNORE) {
+ makes_face = false;
+ return;
+ }
+
+ const MapNode &n1 = vmanip.getNodeRefUnsafeCheckFlags(blockpos_nodes + p + face_dir);
+
+ if (n1.getContent() == CONTENT_IGNORE) {
+ makes_face = false;
+ return;
+ }
+
+ // This is hackish
+ bool equivalent = false;
+ u8 mf = face_contents(n0.getContent(), n1.getContent(),
+ &equivalent, ndef);
+
+ if (mf == 0) {
+ makes_face = false;
+ return;
+ }
+
+ makes_face = true;
+
+ MapNode n = n0;
+
+ if (mf == 1) {
+ p_corrected = p;
+ face_dir_corrected = face_dir;
+ } else {
+ n = n1;
+ p_corrected = p + face_dir;
+ face_dir_corrected = -face_dir;
+ }
+
+ getNodeTile(n, p_corrected, face_dir_corrected, data, tile);
+ const ContentFeatures &f = ndef->get(n);
+ tile.emissive_light = f.light_source;
+
+ // eg. water and glass
+ if (equivalent) {
+ for (TileLayer &layer : tile.layers)
+ layer.material_flags |= MATERIAL_FLAG_BACKFACE_CULLING;
+ }
+
+ if (!data->m_smooth_lighting) {
+ lights[0] = lights[1] = lights[2] = lights[3] =
+ getFaceLight(n0, n1, face_dir, ndef);
+ } else {
+ v3s16 vertex_dirs[4];
+ getNodeVertexDirs(face_dir_corrected, vertex_dirs);
+
+ v3s16 light_p = blockpos_nodes + p_corrected;
+ for (u16 i = 0; i < 4; i++)
+ lights[i] = getSmoothLightSolid(light_p, face_dir_corrected, vertex_dirs[i], data);
+ }
+}
+
+/*
+ startpos:
+ translate_dir: unit vector with only one of x, y or z
+ face_dir: unit vector with only one of x, y or z
+*/
+static void updateFastFaceRow(
+ MeshMakeData *data,
+ const v3s16 &&startpos,
+ v3s16 translate_dir,
+ const v3f &&translate_dir_f,
+ const v3s16 &&face_dir,
+ std::vector<FastFace> &dest)
+{
+ v3s16 p = startpos;
+
+ u16 continuous_tiles_count = 1;
+
+ bool makes_face = false;
+ v3s16 p_corrected;
+ v3s16 face_dir_corrected;
+ u16 lights[4] = {0, 0, 0, 0};
+ TileSpec tile;
+ getTileInfo(data, p, face_dir,
+ makes_face, p_corrected, face_dir_corrected,
+ lights, tile);
+
+ // Unroll this variable which has a significant build cost
+ TileSpec next_tile;
+ for (u16 j = 0; j < MAP_BLOCKSIZE; j++) {
+ // If tiling can be done, this is set to false in the next step
+ bool next_is_different = true;
+
+ v3s16 p_next;
+
+ bool next_makes_face = false;
+ v3s16 next_p_corrected;
+ v3s16 next_face_dir_corrected;
+ u16 next_lights[4] = {0, 0, 0, 0};
+
+ // If at last position, there is nothing to compare to and
+ // the face must be drawn anyway
+ if (j != MAP_BLOCKSIZE - 1) {
+ p_next = p + translate_dir;
+
+ getTileInfo(data, p_next, face_dir,
+ next_makes_face, next_p_corrected,
+ next_face_dir_corrected, next_lights,
+ next_tile);
+
+ if (next_makes_face == makes_face
+ && next_p_corrected == p_corrected + translate_dir
+ && next_face_dir_corrected == face_dir_corrected
+ && memcmp(next_lights, lights, ARRLEN(lights) * sizeof(u16)) == 0
+ && next_tile.isTileable(tile)) {
+ next_is_different = false;
+ continuous_tiles_count++;
+ }
+ }
+ if (next_is_different) {
+ /*
+ Create a face if there should be one
+ */
+ if (makes_face) {
+ // Floating point conversion of the position vector
+ v3f pf(p_corrected.X, p_corrected.Y, p_corrected.Z);
+ // Center point of face (kind of)
+ v3f sp = pf - ((f32)continuous_tiles_count * 0.5f - 0.5f)
+ * translate_dir_f;
+ v3f scale(1, 1, 1);
+
+ if (translate_dir.X != 0)
+ scale.X = continuous_tiles_count;
+ if (translate_dir.Y != 0)
+ scale.Y = continuous_tiles_count;
+ if (translate_dir.Z != 0)
+ scale.Z = continuous_tiles_count;
+
+ makeFastFace(tile, lights[0], lights[1], lights[2], lights[3],
+ pf, sp, face_dir_corrected, scale, dest);
+
+ g_profiler->avg("Meshgen: faces drawn by tiling", 0);
+ for (int i = 1; i < continuous_tiles_count; i++)
+ g_profiler->avg("Meshgen: faces drawn by tiling", 1);
+ }
+
+ continuous_tiles_count = 1;
+ }
+
+ makes_face = next_makes_face;
+ p_corrected = next_p_corrected;
+ face_dir_corrected = next_face_dir_corrected;
+ std::memcpy(lights, next_lights, ARRLEN(lights) * sizeof(u16));
+ if (next_is_different)
+ tile = next_tile;
+ p = p_next;
+ }
+}
+
+static void updateAllFastFaceRows(MeshMakeData *data,
+ std::vector<FastFace> &dest)
+{
+ /*
+ Go through every y,z and get top(y+) faces in rows of x+
+ */
+ for (s16 y = 0; y < MAP_BLOCKSIZE; y++)
+ for (s16 z = 0; z < MAP_BLOCKSIZE; z++)
+ updateFastFaceRow(data,
+ v3s16(0, y, z),
+ v3s16(1, 0, 0), //dir
+ v3f (1, 0, 0),
+ v3s16(0, 1, 0), //face dir
+ dest);
+
+ /*
+ Go through every x,y and get right(x+) faces in rows of z+
+ */
+ for (s16 x = 0; x < MAP_BLOCKSIZE; x++)
+ for (s16 y = 0; y < MAP_BLOCKSIZE; y++)
+ updateFastFaceRow(data,
+ v3s16(x, y, 0),
+ v3s16(0, 0, 1), //dir
+ v3f (0, 0, 1),
+ v3s16(1, 0, 0), //face dir
+ dest);
+
+ /*
+ Go through every y,z and get back(z+) faces in rows of x+
+ */
+ for (s16 z = 0; z < MAP_BLOCKSIZE; z++)
+ for (s16 y = 0; y < MAP_BLOCKSIZE; y++)
+ updateFastFaceRow(data,
+ v3s16(0, y, z),
+ v3s16(1, 0, 0), //dir
+ v3f (1, 0, 0),
+ v3s16(0, 0, 1), //face dir
+ dest);
+}
+
+static void applyTileColor(PreMeshBuffer &pmb)
+{
+ video::SColor tc = pmb.layer.color;
+ if (tc == video::SColor(0xFFFFFFFF))
+ return;
+ for (video::S3DVertex &vertex : pmb.vertices) {
+ video::SColor *c = &vertex.Color;
+ c->set(c->getAlpha(),
+ c->getRed() * tc.getRed() / 255,
+ c->getGreen() * tc.getGreen() / 255,
+ c->getBlue() * tc.getBlue() / 255);
+ }
+}
+
+/*
+ MapBlockMesh
+*/
+
+MapBlockMesh::MapBlockMesh(MeshMakeData *data, v3s16 camera_offset):
+ m_minimap_mapblock(NULL),
+ m_tsrc(data->m_client->getTextureSource()),
+ m_shdrsrc(data->m_client->getShaderSource()),
+ m_animation_force_timer(0), // force initial animation
+ m_last_crack(-1),
+ m_last_daynight_ratio((u32) -1)
+{
+ for (auto &m : m_mesh)
+ m = new scene::SMesh();
+ m_enable_shaders = data->m_use_shaders;
+ m_use_tangent_vertices = data->m_use_tangent_vertices;
+ m_enable_vbo = g_settings->getBool("enable_vbo");
+
+ if (g_settings->getBool("enable_minimap")) {
+ m_minimap_mapblock = new MinimapMapblock;
+ m_minimap_mapblock->getMinimapNodes(
+ &data->m_vmanip, data->m_blockpos * MAP_BLOCKSIZE);
+ }
+
+ // 4-21ms for MAP_BLOCKSIZE=16 (NOTE: probably outdated)
+ // 24-155ms for MAP_BLOCKSIZE=32 (NOTE: probably outdated)
+ //TimeTaker timer1("MapBlockMesh()");
+
+ std::vector<FastFace> fastfaces_new;
+ fastfaces_new.reserve(512);
+
+ /*
+ We are including the faces of the trailing edges of the block.
+ This means that when something changes, the caller must
+ also update the meshes of the blocks at the leading edges.
+
+ NOTE: This is the slowest part of this method.
+ */
+ {
+ // 4-23ms for MAP_BLOCKSIZE=16 (NOTE: probably outdated)
+ //TimeTaker timer2("updateAllFastFaceRows()");
+ updateAllFastFaceRows(data, fastfaces_new);
+ }
+ // End of slow part
+
+ /*
+ Convert FastFaces to MeshCollector
+ */
+
+ MeshCollector collector;
+
+ {
+ // avg 0ms (100ms spikes when loading textures the first time)
+ // (NOTE: probably outdated)
+ //TimeTaker timer2("MeshCollector building");
+
+ for (const FastFace &f : fastfaces_new) {
+ static const u16 indices[] = {0, 1, 2, 2, 3, 0};
+ static const u16 indices_alternate[] = {0, 1, 3, 2, 3, 1};
+ const u16 *indices_p =
+ f.vertex_0_2_connected ? indices : indices_alternate;
+ collector.append(f.tile, f.vertices, 4, indices_p, 6);
+ }
+ }
+
+ /*
+ Add special graphics:
+ - torches
+ - flowing water
+ - fences
+ - whatever
+ */
+
+ {
+ MapblockMeshGenerator generator(data, &collector);
+ generator.generate();
+ }
+
+ /*
+ Convert MeshCollector to SMesh
+ */
+
+ for (int layer = 0; layer < MAX_TILE_LAYERS; layer++) {
+ for(u32 i = 0; i < collector.prebuffers[layer].size(); i++)
+ {
+ PreMeshBuffer &p = collector.prebuffers[layer][i];
+
+ applyTileColor(p);
+
+ // Generate animation data
+ // - Cracks
+ if (p.layer.material_flags & MATERIAL_FLAG_CRACK) {
+ // Find the texture name plus ^[crack:N:
+ std::ostringstream os(std::ios::binary);
+ os << m_tsrc->getTextureName(p.layer.texture_id) << "^[crack";
+ if (p.layer.material_flags & MATERIAL_FLAG_CRACK_OVERLAY)
+ os << "o"; // use ^[cracko
+ u8 tiles = p.layer.scale;
+ if (tiles > 1)
+ os << ":" << (u32)tiles;
+ os << ":" << (u32)p.layer.animation_frame_count << ":";
+ m_crack_materials.insert(std::make_pair(
+ std::pair<u8, u32>(layer, i), os.str()));
+ // Replace tile texture with the cracked one
+ p.layer.texture = m_tsrc->getTextureForMesh(
+ os.str() + "0",
+ &p.layer.texture_id);
+ }
+ // - Texture animation
+ if (p.layer.material_flags & MATERIAL_FLAG_ANIMATION) {
+ // Add to MapBlockMesh in order to animate these tiles
+ m_animation_tiles[std::pair<u8, u32>(layer, i)] = p.layer;
+ m_animation_frames[std::pair<u8, u32>(layer, i)] = 0;
+ if (g_settings->getBool(
+ "desynchronize_mapblock_texture_animation")) {
+ // Get starting position from noise
+ m_animation_frame_offsets[std::pair<u8, u32>(layer, i)] =
+ 100000 * (2.0 + noise3d(
+ data->m_blockpos.X, data->m_blockpos.Y,
+ data->m_blockpos.Z, 0));
+ } else {
+ // Play all synchronized
+ m_animation_frame_offsets[std::pair<u8, u32>(layer, i)] = 0;
+ }
+ // Replace tile texture with the first animation frame
+ p.layer.texture = (*p.layer.frames)[0].texture;
+ }
+
+ if (!m_enable_shaders) {
+ // Extract colors for day-night animation
+ // Dummy sunlight to handle non-sunlit areas
+ video::SColorf sunlight;
+ get_sunlight_color(&sunlight, 0);
+ u32 vertex_count = p.vertices.size();
+ for (u32 j = 0; j < vertex_count; j++) {
+ video::SColor *vc = &p.vertices[j].Color;
+ video::SColor copy = *vc;
+ if (vc->getAlpha() == 0) // No sunlight - no need to animate
+ final_color_blend(vc, copy, sunlight); // Finalize color
+ else // Record color to animate
+ m_daynight_diffs[std::pair<u8, u32>(layer, i)][j] = copy;
+
+ // The sunlight ratio has been stored,
+ // delete alpha (for the final rendering).
+ vc->setAlpha(255);
+ }
+ }
+
+ // Create material
+ video::SMaterial material;
+ material.setFlag(video::EMF_LIGHTING, false);
+ material.setFlag(video::EMF_BACK_FACE_CULLING, true);
+ material.setFlag(video::EMF_BILINEAR_FILTER, false);
+ material.setFlag(video::EMF_FOG_ENABLE, true);
+ material.setTexture(0, p.layer.texture);
+
+ if (m_enable_shaders) {
+ material.MaterialType = m_shdrsrc->getShaderInfo(
+ p.layer.shader_id).material;
+ p.layer.applyMaterialOptionsWithShaders(material);
+ if (p.layer.normal_texture)
+ material.setTexture(1, p.layer.normal_texture);
+ material.setTexture(2, p.layer.flags_texture);
+ } else {
+ p.layer.applyMaterialOptions(material);
+ }
+
+ scene::SMesh *mesh = (scene::SMesh *)m_mesh[layer];
+
+ // Create meshbuffer, add to mesh
+ if (m_use_tangent_vertices) {
+ scene::SMeshBufferTangents *buf =
+ new scene::SMeshBufferTangents();
+ buf->Material = material;
+ buf->Vertices.reallocate(p.vertices.size());
+ buf->Indices.reallocate(p.indices.size());
+ for (const video::S3DVertex &v: p.vertices)
+ buf->Vertices.push_back(video::S3DVertexTangents(v.Pos, v.Color, v.TCoords));
+ for (u16 i: p.indices)
+ buf->Indices.push_back(i);
+ buf->recalculateBoundingBox();
+ mesh->addMeshBuffer(buf);
+ buf->drop();
+ } else {
+ scene::SMeshBuffer *buf = new scene::SMeshBuffer();
+ buf->Material = material;
+ buf->append(&p.vertices[0], p.vertices.size(),
+ &p.indices[0], p.indices.size());
+ mesh->addMeshBuffer(buf);
+ buf->drop();
+ }
+ }
+
+ /*
+ Do some stuff to the mesh
+ */
+ m_camera_offset = camera_offset;
+ translateMesh(m_mesh[layer],
+ intToFloat(data->m_blockpos * MAP_BLOCKSIZE - camera_offset, BS));
+
+ if (m_use_tangent_vertices) {
+ scene::IMeshManipulator* meshmanip =
+ RenderingEngine::get_scene_manager()->getMeshManipulator();
+ meshmanip->recalculateTangents(m_mesh[layer], true, false, false);
+ }
+
+ if (m_mesh[layer]) {
+#if 0
+ // Usually 1-700 faces and 1-7 materials
+ std::cout << "Updated MapBlock has " << fastfaces_new.size()
+ << " faces and uses " << m_mesh[layer]->getMeshBufferCount()
+ << " materials (meshbuffers)" << std::endl;
+#endif
+
+ // Use VBO for mesh (this just would set this for ever buffer)
+ if (m_enable_vbo)
+ m_mesh[layer]->setHardwareMappingHint(scene::EHM_STATIC);
+ }
+ }
+
+ //std::cout<<"added "<<fastfaces.getSize()<<" faces."<<std::endl;
+
+ // Check if animation is required for this mesh
+ m_has_animation =
+ !m_crack_materials.empty() ||
+ !m_daynight_diffs.empty() ||
+ !m_animation_tiles.empty();
+}
+
+MapBlockMesh::~MapBlockMesh()
+{
+ for (scene::IMesh *m : m_mesh) {
+ if (m_enable_vbo && m)
+ for (u32 i = 0; i < m->getMeshBufferCount(); i++) {
+ scene::IMeshBuffer *buf = m->getMeshBuffer(i);
+ RenderingEngine::get_video_driver()->removeHardwareBuffer(buf);
+ }
+ m->drop();
+ m = NULL;
+ }
+ delete m_minimap_mapblock;
+}
+
+bool MapBlockMesh::animate(bool faraway, float time, int crack,
+ u32 daynight_ratio)
+{
+ if (!m_has_animation) {
+ m_animation_force_timer = 100000;
+ return false;
+ }
+
+ m_animation_force_timer = myrand_range(5, 100);
+
+ // Cracks
+ if (crack != m_last_crack) {
+ for (auto &crack_material : m_crack_materials) {
+ scene::IMeshBuffer *buf = m_mesh[crack_material.first.first]->
+ getMeshBuffer(crack_material.first.second);
+ std::string basename = crack_material.second;
+
+ // Create new texture name from original
+ std::ostringstream os;
+ os << basename << crack;
+ u32 new_texture_id = 0;
+ video::ITexture *new_texture =
+ m_tsrc->getTextureForMesh(os.str(), &new_texture_id);
+ buf->getMaterial().setTexture(0, new_texture);
+
+ // If the current material is also animated,
+ // update animation info
+ auto anim_iter = m_animation_tiles.find(crack_material.first);
+ if (anim_iter != m_animation_tiles.end()) {
+ TileLayer &tile = anim_iter->second;
+ tile.texture = new_texture;
+ tile.texture_id = new_texture_id;
+ // force animation update
+ m_animation_frames[crack_material.first] = -1;
+ }
+ }
+
+ m_last_crack = crack;
+ }
+
+ // Texture animation
+ for (auto &animation_tile : m_animation_tiles) {
+ const TileLayer &tile = animation_tile.second;
+ // Figure out current frame
+ int frameoffset = m_animation_frame_offsets[animation_tile.first];
+ int frame = (int)(time * 1000 / tile.animation_frame_length_ms
+ + frameoffset) % tile.animation_frame_count;
+ // If frame doesn't change, skip
+ if (frame == m_animation_frames[animation_tile.first])
+ continue;
+
+ m_animation_frames[animation_tile.first] = frame;
+
+ scene::IMeshBuffer *buf = m_mesh[animation_tile.first.first]->
+ getMeshBuffer(animation_tile.first.second);
+
+ const FrameSpec &animation_frame = (*tile.frames)[frame];
+ buf->getMaterial().setTexture(0, animation_frame.texture);
+ if (m_enable_shaders) {
+ if (animation_frame.normal_texture)
+ buf->getMaterial().setTexture(1,
+ animation_frame.normal_texture);
+ buf->getMaterial().setTexture(2, animation_frame.flags_texture);
+ }
+ }
+
+ // Day-night transition
+ if (!m_enable_shaders && (daynight_ratio != m_last_daynight_ratio)) {
+ // Force reload mesh to VBO
+ if (m_enable_vbo)
+ for (scene::IMesh *m : m_mesh)
+ m->setDirty();
+ video::SColorf day_color;
+ get_sunlight_color(&day_color, daynight_ratio);
+
+ for (auto &daynight_diff : m_daynight_diffs) {
+ scene::IMeshBuffer *buf = m_mesh[daynight_diff.first.first]->
+ getMeshBuffer(daynight_diff.first.second);
+ video::S3DVertex *vertices = (video::S3DVertex *)buf->getVertices();
+ for (const auto &j : daynight_diff.second)
+ final_color_blend(&(vertices[j.first].Color), j.second,
+ day_color);
+ }
+ m_last_daynight_ratio = daynight_ratio;
+ }
+
+ return true;
+}
+
+void MapBlockMesh::updateCameraOffset(v3s16 camera_offset)
+{
+ if (camera_offset != m_camera_offset) {
+ for (scene::IMesh *layer : m_mesh) {
+ translateMesh(layer,
+ intToFloat(m_camera_offset - camera_offset, BS));
+ if (m_enable_vbo)
+ layer->setDirty();
+ }
+ m_camera_offset = camera_offset;
+ }
+}
+
+video::SColor encode_light(u16 light, u8 emissive_light)
+{
+ // Get components
+ u32 day = (light & 0xff);
+ u32 night = (light >> 8);
+ // Add emissive light
+ night += emissive_light * 2.5f;
+ if (night > 255)
+ night = 255;
+ // Since we don't know if the day light is sunlight or
+ // artificial light, assume it is artificial when the night
+ // light bank is also lit.
+ if (day < night)
+ day = 0;
+ else
+ day = day - night;
+ u32 sum = day + night;
+ // Ratio of sunlight:
+ u32 r;
+ if (sum > 0)
+ r = day * 255 / sum;
+ else
+ r = 0;
+ // Average light:
+ float b = (day + night) / 2;
+ return video::SColor(r, b, b, b);
+}
diff --git a/src/client/mapblock_mesh.h b/src/client/mapblock_mesh.h
new file mode 100644
index 000000000..6af23a656
--- /dev/null
+++ b/src/client/mapblock_mesh.h
@@ -0,0 +1,228 @@
+/*
+Minetest
+Copyright (C) 2010-2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+
+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 "client/tile.h"
+#include "voxel.h"
+#include <array>
+#include <map>
+
+class Client;
+class IShaderSource;
+
+/*
+ Mesh making stuff
+*/
+
+
+class MapBlock;
+struct MinimapMapblock;
+
+struct MeshMakeData
+{
+ VoxelManipulator m_vmanip;
+ v3s16 m_blockpos = v3s16(-1337,-1337,-1337);
+ v3s16 m_crack_pos_relative = v3s16(-1337,-1337,-1337);
+ bool m_smooth_lighting = false;
+
+ Client *m_client;
+ bool m_use_shaders;
+ bool m_use_tangent_vertices;
+
+ MeshMakeData(Client *client, bool use_shaders,
+ bool use_tangent_vertices = false);
+
+ /*
+ Copy block data manually (to allow optimizations by the caller)
+ */
+ void fillBlockDataBegin(const v3s16 &blockpos);
+ void fillBlockData(const v3s16 &block_offset, MapNode *data);
+
+ /*
+ Copy central data directly from block, and other data from
+ parent of block.
+ */
+ void fill(MapBlock *block);
+
+ /*
+ Set up with only a single node at (1,1,1)
+ */
+ void fillSingleNode(MapNode *node);
+
+ /*
+ Set the (node) position of a crack
+ */
+ void setCrack(int crack_level, v3s16 crack_pos);
+
+ /*
+ Enable or disable smooth lighting
+ */
+ void setSmoothLighting(bool smooth_lighting);
+};
+
+/*
+ Holds a mesh for a mapblock.
+
+ Besides the SMesh*, this contains information used for animating
+ the vertex positions, colors and texture coordinates of the mesh.
+ For example:
+ - cracks [implemented]
+ - day/night transitions [implemented]
+ - animated flowing liquids [not implemented]
+ - animating vertex positions for e.g. axles [not implemented]
+*/
+class MapBlockMesh
+{
+public:
+ // Builds the mesh given
+ MapBlockMesh(MeshMakeData *data, v3s16 camera_offset);
+ ~MapBlockMesh();
+
+ // Main animation function, parameters:
+ // faraway: whether the block is far away from the camera (~50 nodes)
+ // time: the global animation time, 0 .. 60 (repeats every minute)
+ // daynight_ratio: 0 .. 1000
+ // crack: -1 .. CRACK_ANIMATION_LENGTH-1 (-1 for off)
+ // Returns true if anything has been changed.
+ bool animate(bool faraway, float time, int crack, u32 daynight_ratio);
+
+ scene::IMesh *getMesh()
+ {
+ return m_mesh[0];
+ }
+
+ scene::IMesh *getMesh(u8 layer)
+ {
+ return m_mesh[layer];
+ }
+
+ MinimapMapblock *moveMinimapMapblock()
+ {
+ MinimapMapblock *p = m_minimap_mapblock;
+ m_minimap_mapblock = NULL;
+ return p;
+ }
+
+ bool isAnimationForced() const
+ {
+ return m_animation_force_timer == 0;
+ }
+
+ void decreaseAnimationForceTimer()
+ {
+ if(m_animation_force_timer > 0)
+ m_animation_force_timer--;
+ }
+
+ void updateCameraOffset(v3s16 camera_offset);
+
+private:
+ scene::IMesh *m_mesh[MAX_TILE_LAYERS];
+ MinimapMapblock *m_minimap_mapblock;
+ ITextureSource *m_tsrc;
+ IShaderSource *m_shdrsrc;
+
+ bool m_enable_shaders;
+ bool m_use_tangent_vertices;
+ bool m_enable_vbo;
+
+ // Must animate() be called before rendering?
+ bool m_has_animation;
+ int m_animation_force_timer;
+
+ // Animation info: cracks
+ // Last crack value passed to animate()
+ int m_last_crack;
+ // Maps mesh and mesh buffer (i.e. material) indices to base texture names
+ std::map<std::pair<u8, u32>, std::string> m_crack_materials;
+
+ // Animation info: texture animationi
+ // Maps mesh and mesh buffer indices to TileSpecs
+ // Keys are pairs of (mesh index, buffer index in the mesh)
+ std::map<std::pair<u8, u32>, TileLayer> m_animation_tiles;
+ std::map<std::pair<u8, u32>, int> m_animation_frames; // last animation frame
+ std::map<std::pair<u8, u32>, int> m_animation_frame_offsets;
+
+ // Animation info: day/night transitions
+ // Last daynight_ratio value passed to animate()
+ u32 m_last_daynight_ratio;
+ // For each mesh and mesh buffer, stores pre-baked colors
+ // of sunlit vertices
+ // Keys are pairs of (mesh index, buffer index in the mesh)
+ std::map<std::pair<u8, u32>, std::map<u32, video::SColor > > m_daynight_diffs;
+
+ // Camera offset info -> do we have to translate the mesh?
+ v3s16 m_camera_offset;
+};
+
+/*!
+ * Encodes light of a node.
+ * The result is not the final color, but a
+ * half-baked vertex color.
+ * You have to multiply the resulting color
+ * with the node's color.
+ *
+ * \param light the first 8 bits are day light,
+ * the last 8 bits are night light
+ * \param emissive_light amount of light the surface emits,
+ * from 0 to LIGHT_SUN.
+ */
+video::SColor encode_light(u16 light, u8 emissive_light);
+
+// Compute light at node
+u16 getInteriorLight(MapNode n, s32 increment, const NodeDefManager *ndef);
+u16 getFaceLight(MapNode n, MapNode n2, const v3s16 &face_dir,
+ const NodeDefManager *ndef);
+u16 getSmoothLightSolid(const v3s16 &p, const v3s16 &face_dir, const v3s16 &corner, MeshMakeData *data);
+u16 getSmoothLightTransparent(const v3s16 &p, const v3s16 &corner, MeshMakeData *data);
+
+/*!
+ * Returns the sunlight's color from the current
+ * day-night ratio.
+ */
+void get_sunlight_color(video::SColorf *sunlight, u32 daynight_ratio);
+
+/*!
+ * Gives the final SColor shown on screen.
+ *
+ * \param result output color
+ * \param light first 8 bits are day light, second 8 bits are
+ * night light
+ */
+void final_color_blend(video::SColor *result,
+ u16 light, u32 daynight_ratio);
+
+/*!
+ * Gives the final SColor shown on screen.
+ *
+ * \param result output color
+ * \param data the half-baked vertex color
+ * \param dayLight color of the sunlight
+ */
+void final_color_blend(video::SColor *result,
+ const video::SColor &data, const video::SColorf &dayLight);
+
+// Retrieves the TileSpec of a face of a node
+// Adds MATERIAL_FLAG_CRACK if the node is cracked
+// TileSpec should be passed as reference due to the underlying TileFrame and its vector
+// TileFrame vector copy cost very much to client
+void getNodeTileN(MapNode mn, const v3s16 &p, u8 tileindex, MeshMakeData *data, TileSpec &tile);
+void getNodeTile(MapNode mn, const v3s16 &p, const v3s16 &dir, MeshMakeData *data, TileSpec &tile);
diff --git a/src/client/mesh.cpp b/src/client/mesh.cpp
new file mode 100644
index 000000000..7fc7531f2
--- /dev/null
+++ b/src/client/mesh.cpp
@@ -0,0 +1,1135 @@
+/*
+Minetest
+Copyright (C) 2010-2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+
+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 "mesh.h"
+#include "debug.h"
+#include "log.h"
+#include "irrMap.h"
+#include <iostream>
+#include <IAnimatedMesh.h>
+#include <SAnimatedMesh.h>
+#include <IAnimatedMeshSceneNode.h>
+
+// In Irrlicht 1.8 the signature of ITexture::lock was changed from
+// (bool, u32) to (E_TEXTURE_LOCK_MODE, u32).
+#if IRRLICHT_VERSION_MAJOR == 1 && IRRLICHT_VERSION_MINOR <= 7
+#define MY_ETLM_READ_ONLY true
+#else
+#define MY_ETLM_READ_ONLY video::ETLM_READ_ONLY
+#endif
+
+inline static void applyShadeFactor(video::SColor& color, float factor)
+{
+ color.setRed(core::clamp(core::round32(color.getRed()*factor), 0, 255));
+ color.setGreen(core::clamp(core::round32(color.getGreen()*factor), 0, 255));
+ color.setBlue(core::clamp(core::round32(color.getBlue()*factor), 0, 255));
+}
+
+void applyFacesShading(video::SColor &color, const v3f &normal)
+{
+ /*
+ Some drawtypes have normals set to (0, 0, 0), this must result in
+ maximum brightness: shade factor 1.0.
+ Shade factors for aligned cube faces are:
+ +Y 1.000000 sqrt(1.0)
+ -Y 0.447213 sqrt(0.2)
+ +-X 0.670820 sqrt(0.45)
+ +-Z 0.836660 sqrt(0.7)
+ */
+ float x2 = normal.X * normal.X;
+ float y2 = normal.Y * normal.Y;
+ float z2 = normal.Z * normal.Z;
+ if (normal.Y < 0)
+ applyShadeFactor(color, 0.670820f * x2 + 0.447213f * y2 + 0.836660f * z2);
+ else if ((x2 > 1e-3) || (z2 > 1e-3))
+ applyShadeFactor(color, 0.670820f * x2 + 1.000000f * y2 + 0.836660f * z2);
+}
+
+scene::IAnimatedMesh* createCubeMesh(v3f scale)
+{
+ video::SColor c(255,255,255,255);
+ video::S3DVertex vertices[24] =
+ {
+ // Up
+ video::S3DVertex(-0.5,+0.5,-0.5, 0,1,0, c, 0,1),
+ video::S3DVertex(-0.5,+0.5,+0.5, 0,1,0, c, 0,0),
+ video::S3DVertex(+0.5,+0.5,+0.5, 0,1,0, c, 1,0),
+ video::S3DVertex(+0.5,+0.5,-0.5, 0,1,0, c, 1,1),
+ // Down
+ video::S3DVertex(-0.5,-0.5,-0.5, 0,-1,0, c, 0,0),
+ video::S3DVertex(+0.5,-0.5,-0.5, 0,-1,0, c, 1,0),
+ video::S3DVertex(+0.5,-0.5,+0.5, 0,-1,0, c, 1,1),
+ video::S3DVertex(-0.5,-0.5,+0.5, 0,-1,0, c, 0,1),
+ // Right
+ video::S3DVertex(+0.5,-0.5,-0.5, 1,0,0, c, 0,1),
+ video::S3DVertex(+0.5,+0.5,-0.5, 1,0,0, c, 0,0),
+ video::S3DVertex(+0.5,+0.5,+0.5, 1,0,0, c, 1,0),
+ video::S3DVertex(+0.5,-0.5,+0.5, 1,0,0, c, 1,1),
+ // Left
+ video::S3DVertex(-0.5,-0.5,-0.5, -1,0,0, c, 1,1),
+ video::S3DVertex(-0.5,-0.5,+0.5, -1,0,0, c, 0,1),
+ video::S3DVertex(-0.5,+0.5,+0.5, -1,0,0, c, 0,0),
+ video::S3DVertex(-0.5,+0.5,-0.5, -1,0,0, c, 1,0),
+ // Back
+ video::S3DVertex(-0.5,-0.5,+0.5, 0,0,1, c, 1,1),
+ video::S3DVertex(+0.5,-0.5,+0.5, 0,0,1, c, 0,1),
+ video::S3DVertex(+0.5,+0.5,+0.5, 0,0,1, c, 0,0),
+ video::S3DVertex(-0.5,+0.5,+0.5, 0,0,1, c, 1,0),
+ // Front
+ video::S3DVertex(-0.5,-0.5,-0.5, 0,0,-1, c, 0,1),
+ video::S3DVertex(-0.5,+0.5,-0.5, 0,0,-1, c, 0,0),
+ video::S3DVertex(+0.5,+0.5,-0.5, 0,0,-1, c, 1,0),
+ video::S3DVertex(+0.5,-0.5,-0.5, 0,0,-1, c, 1,1),
+ };
+
+ u16 indices[6] = {0,1,2,2,3,0};
+
+ scene::SMesh *mesh = new scene::SMesh();
+ for (u32 i=0; i<6; ++i)
+ {
+ scene::IMeshBuffer *buf = new scene::SMeshBuffer();
+ buf->append(vertices + 4 * i, 4, indices, 6);
+ // Set default material
+ buf->getMaterial().setFlag(video::EMF_LIGHTING, false);
+ buf->getMaterial().setFlag(video::EMF_BILINEAR_FILTER, false);
+ buf->getMaterial().MaterialType = video::EMT_TRANSPARENT_ALPHA_CHANNEL_REF;
+ // Add mesh buffer to mesh
+ mesh->addMeshBuffer(buf);
+ buf->drop();
+ }
+
+ scene::SAnimatedMesh *anim_mesh = new scene::SAnimatedMesh(mesh);
+ mesh->drop();
+ scaleMesh(anim_mesh, scale); // also recalculates bounding box
+ return anim_mesh;
+}
+
+void scaleMesh(scene::IMesh *mesh, v3f scale)
+{
+ if (mesh == NULL)
+ return;
+
+ aabb3f bbox;
+ bbox.reset(0, 0, 0);
+
+ u32 mc = mesh->getMeshBufferCount();
+ for (u32 j = 0; j < mc; j++) {
+ scene::IMeshBuffer *buf = mesh->getMeshBuffer(j);
+ const u32 stride = getVertexPitchFromType(buf->getVertexType());
+ u32 vertex_count = buf->getVertexCount();
+ u8 *vertices = (u8 *)buf->getVertices();
+ for (u32 i = 0; i < vertex_count; i++)
+ ((video::S3DVertex *)(vertices + i * stride))->Pos *= scale;
+
+ buf->recalculateBoundingBox();
+
+ // calculate total bounding box
+ if (j == 0)
+ bbox = buf->getBoundingBox();
+ else
+ bbox.addInternalBox(buf->getBoundingBox());
+ }
+ mesh->setBoundingBox(bbox);
+}
+
+void translateMesh(scene::IMesh *mesh, v3f vec)
+{
+ if (mesh == NULL)
+ return;
+
+ aabb3f bbox;
+ bbox.reset(0, 0, 0);
+
+ u32 mc = mesh->getMeshBufferCount();
+ for (u32 j = 0; j < mc; j++) {
+ scene::IMeshBuffer *buf = mesh->getMeshBuffer(j);
+ const u32 stride = getVertexPitchFromType(buf->getVertexType());
+ u32 vertex_count = buf->getVertexCount();
+ u8 *vertices = (u8 *)buf->getVertices();
+ for (u32 i = 0; i < vertex_count; i++)
+ ((video::S3DVertex *)(vertices + i * stride))->Pos += vec;
+
+ buf->recalculateBoundingBox();
+
+ // calculate total bounding box
+ if (j == 0)
+ bbox = buf->getBoundingBox();
+ else
+ bbox.addInternalBox(buf->getBoundingBox());
+ }
+ mesh->setBoundingBox(bbox);
+}
+
+void setMeshBufferColor(scene::IMeshBuffer *buf, const video::SColor &color)
+{
+ const u32 stride = getVertexPitchFromType(buf->getVertexType());
+ u32 vertex_count = buf->getVertexCount();
+ u8 *vertices = (u8 *) buf->getVertices();
+ for (u32 i = 0; i < vertex_count; i++)
+ ((video::S3DVertex *) (vertices + i * stride))->Color = color;
+}
+
+void setAnimatedMeshColor(scene::IAnimatedMeshSceneNode *node, const video::SColor &color)
+{
+ for (u32 i = 0; i < node->getMaterialCount(); ++i) {
+ node->getMaterial(i).EmissiveColor = color;
+ }
+}
+
+void setMeshColor(scene::IMesh *mesh, const video::SColor &color)
+{
+ if (mesh == NULL)
+ return;
+
+ u32 mc = mesh->getMeshBufferCount();
+ for (u32 j = 0; j < mc; j++)
+ setMeshBufferColor(mesh->getMeshBuffer(j), color);
+}
+
+void colorizeMeshBuffer(scene::IMeshBuffer *buf, const video::SColor *buffercolor)
+{
+ const u32 stride = getVertexPitchFromType(buf->getVertexType());
+ u32 vertex_count = buf->getVertexCount();
+ u8 *vertices = (u8 *) buf->getVertices();
+ for (u32 i = 0; i < vertex_count; i++) {
+ video::S3DVertex *vertex = (video::S3DVertex *) (vertices + i * stride);
+ video::SColor *vc = &(vertex->Color);
+ // Reset color
+ *vc = *buffercolor;
+ // Apply shading
+ applyFacesShading(*vc, vertex->Normal);
+ }
+}
+
+void setMeshColorByNormalXYZ(scene::IMesh *mesh,
+ const video::SColor &colorX,
+ const video::SColor &colorY,
+ const video::SColor &colorZ)
+{
+ if (mesh == NULL)
+ return;
+
+ u16 mc = mesh->getMeshBufferCount();
+ for (u16 j = 0; j < mc; j++) {
+ scene::IMeshBuffer *buf = mesh->getMeshBuffer(j);
+ const u32 stride = getVertexPitchFromType(buf->getVertexType());
+ u32 vertex_count = buf->getVertexCount();
+ u8 *vertices = (u8 *)buf->getVertices();
+ for (u32 i = 0; i < vertex_count; i++) {
+ video::S3DVertex *vertex = (video::S3DVertex *)(vertices + i * stride);
+ f32 x = fabs(vertex->Normal.X);
+ f32 y = fabs(vertex->Normal.Y);
+ f32 z = fabs(vertex->Normal.Z);
+ if (x >= y && x >= z)
+ vertex->Color = colorX;
+ else if (y >= z)
+ vertex->Color = colorY;
+ else
+ vertex->Color = colorZ;
+ }
+ }
+}
+
+void setMeshColorByNormal(scene::IMesh *mesh, const v3f &normal,
+ const video::SColor &color)
+{
+ if (!mesh)
+ return;
+
+ u16 mc = mesh->getMeshBufferCount();
+ for (u16 j = 0; j < mc; j++) {
+ scene::IMeshBuffer *buf = mesh->getMeshBuffer(j);
+ const u32 stride = getVertexPitchFromType(buf->getVertexType());
+ u32 vertex_count = buf->getVertexCount();
+ u8 *vertices = (u8 *)buf->getVertices();
+ for (u32 i = 0; i < vertex_count; i++) {
+ video::S3DVertex *vertex = (video::S3DVertex *)(vertices + i * stride);
+ if (normal == vertex->Normal) {
+ vertex->Color = color;
+ }
+ }
+ }
+}
+
+void rotateMeshXYby(scene::IMesh *mesh, f64 degrees)
+{
+ u16 mc = mesh->getMeshBufferCount();
+ for (u16 j = 0; j < mc; j++) {
+ scene::IMeshBuffer *buf = mesh->getMeshBuffer(j);
+ const u32 stride = getVertexPitchFromType(buf->getVertexType());
+ u32 vertex_count = buf->getVertexCount();
+ u8 *vertices = (u8 *)buf->getVertices();
+ for (u32 i = 0; i < vertex_count; i++)
+ ((video::S3DVertex *)(vertices + i * stride))->Pos.rotateXYBy(degrees);
+ }
+}
+
+void rotateMeshXZby(scene::IMesh *mesh, f64 degrees)
+{
+ u16 mc = mesh->getMeshBufferCount();
+ for (u16 j = 0; j < mc; j++) {
+ scene::IMeshBuffer *buf = mesh->getMeshBuffer(j);
+ const u32 stride = getVertexPitchFromType(buf->getVertexType());
+ u32 vertex_count = buf->getVertexCount();
+ u8 *vertices = (u8 *)buf->getVertices();
+ for (u32 i = 0; i < vertex_count; i++)
+ ((video::S3DVertex *)(vertices + i * stride))->Pos.rotateXZBy(degrees);
+ }
+}
+
+void rotateMeshYZby(scene::IMesh *mesh, f64 degrees)
+{
+ u16 mc = mesh->getMeshBufferCount();
+ for (u16 j = 0; j < mc; j++) {
+ scene::IMeshBuffer *buf = mesh->getMeshBuffer(j);
+ const u32 stride = getVertexPitchFromType(buf->getVertexType());
+ u32 vertex_count = buf->getVertexCount();
+ u8 *vertices = (u8 *)buf->getVertices();
+ for (u32 i = 0; i < vertex_count; i++)
+ ((video::S3DVertex *)(vertices + i * stride))->Pos.rotateYZBy(degrees);
+ }
+}
+
+void rotateMeshBy6dFacedir(scene::IMesh *mesh, int facedir)
+{
+ int axisdir = facedir >> 2;
+ facedir &= 0x03;
+
+ u16 mc = mesh->getMeshBufferCount();
+ for (u16 j = 0; j < mc; j++) {
+ scene::IMeshBuffer *buf = mesh->getMeshBuffer(j);
+ const u32 stride = getVertexPitchFromType(buf->getVertexType());
+ u32 vertex_count = buf->getVertexCount();
+ u8 *vertices = (u8 *)buf->getVertices();
+ for (u32 i = 0; i < vertex_count; i++) {
+ video::S3DVertex *vertex = (video::S3DVertex *)(vertices + i * stride);
+ switch (axisdir) {
+ case 0:
+ if (facedir == 1)
+ vertex->Pos.rotateXZBy(-90);
+ else if (facedir == 2)
+ vertex->Pos.rotateXZBy(180);
+ else if (facedir == 3)
+ vertex->Pos.rotateXZBy(90);
+ break;
+ case 1: // z+
+ vertex->Pos.rotateYZBy(90);
+ if (facedir == 1)
+ vertex->Pos.rotateXYBy(90);
+ else if (facedir == 2)
+ vertex->Pos.rotateXYBy(180);
+ else if (facedir == 3)
+ vertex->Pos.rotateXYBy(-90);
+ break;
+ case 2: //z-
+ vertex->Pos.rotateYZBy(-90);
+ if (facedir == 1)
+ vertex->Pos.rotateXYBy(-90);
+ else if (facedir == 2)
+ vertex->Pos.rotateXYBy(180);
+ else if (facedir == 3)
+ vertex->Pos.rotateXYBy(90);
+ break;
+ case 3: //x+
+ vertex->Pos.rotateXYBy(-90);
+ if (facedir == 1)
+ vertex->Pos.rotateYZBy(90);
+ else if (facedir == 2)
+ vertex->Pos.rotateYZBy(180);
+ else if (facedir == 3)
+ vertex->Pos.rotateYZBy(-90);
+ break;
+ case 4: //x-
+ vertex->Pos.rotateXYBy(90);
+ if (facedir == 1)
+ vertex->Pos.rotateYZBy(-90);
+ else if (facedir == 2)
+ vertex->Pos.rotateYZBy(180);
+ else if (facedir == 3)
+ vertex->Pos.rotateYZBy(90);
+ break;
+ case 5:
+ vertex->Pos.rotateXYBy(-180);
+ if (facedir == 1)
+ vertex->Pos.rotateXZBy(90);
+ else if (facedir == 2)
+ vertex->Pos.rotateXZBy(180);
+ else if (facedir == 3)
+ vertex->Pos.rotateXZBy(-90);
+ break;
+ default:
+ break;
+ }
+ }
+ }
+}
+
+void recalculateBoundingBox(scene::IMesh *src_mesh)
+{
+ aabb3f bbox;
+ bbox.reset(0,0,0);
+ for (u16 j = 0; j < src_mesh->getMeshBufferCount(); j++) {
+ scene::IMeshBuffer *buf = src_mesh->getMeshBuffer(j);
+ buf->recalculateBoundingBox();
+ if (j == 0)
+ bbox = buf->getBoundingBox();
+ else
+ bbox.addInternalBox(buf->getBoundingBox());
+ }
+ src_mesh->setBoundingBox(bbox);
+}
+
+scene::IMeshBuffer* cloneMeshBuffer(scene::IMeshBuffer *mesh_buffer)
+{
+ switch (mesh_buffer->getVertexType()) {
+ case video::EVT_STANDARD: {
+ video::S3DVertex *v = (video::S3DVertex *) mesh_buffer->getVertices();
+ u16 *indices = mesh_buffer->getIndices();
+ scene::SMeshBuffer *cloned_buffer = new scene::SMeshBuffer();
+ cloned_buffer->append(v, mesh_buffer->getVertexCount(), indices,
+ mesh_buffer->getIndexCount());
+ return cloned_buffer;
+ }
+ case video::EVT_2TCOORDS: {
+ video::S3DVertex2TCoords *v =
+ (video::S3DVertex2TCoords *) mesh_buffer->getVertices();
+ u16 *indices = mesh_buffer->getIndices();
+ scene::SMeshBufferTangents *cloned_buffer =
+ new scene::SMeshBufferTangents();
+ cloned_buffer->append(v, mesh_buffer->getVertexCount(), indices,
+ mesh_buffer->getIndexCount());
+ return cloned_buffer;
+ }
+ case video::EVT_TANGENTS: {
+ video::S3DVertexTangents *v =
+ (video::S3DVertexTangents *) mesh_buffer->getVertices();
+ u16 *indices = mesh_buffer->getIndices();
+ scene::SMeshBufferTangents *cloned_buffer =
+ new scene::SMeshBufferTangents();
+ cloned_buffer->append(v, mesh_buffer->getVertexCount(), indices,
+ mesh_buffer->getIndexCount());
+ return cloned_buffer;
+ }
+ }
+ // This should not happen.
+ sanity_check(false);
+ return NULL;
+}
+
+scene::SMesh* cloneMesh(scene::IMesh *src_mesh)
+{
+ scene::SMesh* dst_mesh = new scene::SMesh();
+ for (u16 j = 0; j < src_mesh->getMeshBufferCount(); j++) {
+ scene::IMeshBuffer *temp_buf = cloneMeshBuffer(
+ src_mesh->getMeshBuffer(j));
+ dst_mesh->addMeshBuffer(temp_buf);
+ temp_buf->drop();
+
+ }
+ return dst_mesh;
+}
+
+scene::IMesh* convertNodeboxesToMesh(const std::vector<aabb3f> &boxes,
+ const f32 *uv_coords, float expand)
+{
+ scene::SMesh* dst_mesh = new scene::SMesh();
+
+ for (u16 j = 0; j < 6; j++)
+ {
+ scene::IMeshBuffer *buf = new scene::SMeshBuffer();
+ buf->getMaterial().setFlag(video::EMF_LIGHTING, false);
+ buf->getMaterial().setFlag(video::EMF_BILINEAR_FILTER, false);
+ dst_mesh->addMeshBuffer(buf);
+ buf->drop();
+ }
+
+ video::SColor c(255,255,255,255);
+
+ for (aabb3f box : boxes) {
+ box.repair();
+
+ box.MinEdge.X -= expand;
+ box.MinEdge.Y -= expand;
+ box.MinEdge.Z -= expand;
+ box.MaxEdge.X += expand;
+ box.MaxEdge.Y += expand;
+ box.MaxEdge.Z += expand;
+
+ // Compute texture UV coords
+ f32 tx1 = (box.MinEdge.X / BS) + 0.5;
+ f32 ty1 = (box.MinEdge.Y / BS) + 0.5;
+ f32 tz1 = (box.MinEdge.Z / BS) + 0.5;
+ f32 tx2 = (box.MaxEdge.X / BS) + 0.5;
+ f32 ty2 = (box.MaxEdge.Y / BS) + 0.5;
+ f32 tz2 = (box.MaxEdge.Z / BS) + 0.5;
+
+ f32 txc_default[24] = {
+ // up
+ tx1, 1 - tz2, tx2, 1 - tz1,
+ // down
+ tx1, tz1, tx2, tz2,
+ // right
+ tz1, 1 - ty2, tz2, 1 - ty1,
+ // left
+ 1 - tz2, 1 - ty2, 1 - tz1, 1 - ty1,
+ // back
+ 1 - tx2, 1 - ty2, 1 - tx1, 1 - ty1,
+ // front
+ tx1, 1 - ty2, tx2, 1 - ty1,
+ };
+
+ // use default texture UV mapping if not provided
+ const f32 *txc = uv_coords ? uv_coords : txc_default;
+
+ v3f min = box.MinEdge;
+ v3f max = box.MaxEdge;
+
+ video::S3DVertex vertices[24] =
+ {
+ // up
+ video::S3DVertex(min.X,max.Y,max.Z, 0,1,0, c, txc[0],txc[1]),
+ video::S3DVertex(max.X,max.Y,max.Z, 0,1,0, c, txc[2],txc[1]),
+ video::S3DVertex(max.X,max.Y,min.Z, 0,1,0, c, txc[2],txc[3]),
+ video::S3DVertex(min.X,max.Y,min.Z, 0,1,0, c, txc[0],txc[3]),
+ // down
+ video::S3DVertex(min.X,min.Y,min.Z, 0,-1,0, c, txc[4],txc[5]),
+ video::S3DVertex(max.X,min.Y,min.Z, 0,-1,0, c, txc[6],txc[5]),
+ video::S3DVertex(max.X,min.Y,max.Z, 0,-1,0, c, txc[6],txc[7]),
+ video::S3DVertex(min.X,min.Y,max.Z, 0,-1,0, c, txc[4],txc[7]),
+ // right
+ video::S3DVertex(max.X,max.Y,min.Z, 1,0,0, c, txc[ 8],txc[9]),
+ video::S3DVertex(max.X,max.Y,max.Z, 1,0,0, c, txc[10],txc[9]),
+ video::S3DVertex(max.X,min.Y,max.Z, 1,0,0, c, txc[10],txc[11]),
+ video::S3DVertex(max.X,min.Y,min.Z, 1,0,0, c, txc[ 8],txc[11]),
+ // left
+ video::S3DVertex(min.X,max.Y,max.Z, -1,0,0, c, txc[12],txc[13]),
+ video::S3DVertex(min.X,max.Y,min.Z, -1,0,0, c, txc[14],txc[13]),
+ video::S3DVertex(min.X,min.Y,min.Z, -1,0,0, c, txc[14],txc[15]),
+ video::S3DVertex(min.X,min.Y,max.Z, -1,0,0, c, txc[12],txc[15]),
+ // back
+ video::S3DVertex(max.X,max.Y,max.Z, 0,0,1, c, txc[16],txc[17]),
+ video::S3DVertex(min.X,max.Y,max.Z, 0,0,1, c, txc[18],txc[17]),
+ video::S3DVertex(min.X,min.Y,max.Z, 0,0,1, c, txc[18],txc[19]),
+ video::S3DVertex(max.X,min.Y,max.Z, 0,0,1, c, txc[16],txc[19]),
+ // front
+ video::S3DVertex(min.X,max.Y,min.Z, 0,0,-1, c, txc[20],txc[21]),
+ video::S3DVertex(max.X,max.Y,min.Z, 0,0,-1, c, txc[22],txc[21]),
+ video::S3DVertex(max.X,min.Y,min.Z, 0,0,-1, c, txc[22],txc[23]),
+ video::S3DVertex(min.X,min.Y,min.Z, 0,0,-1, c, txc[20],txc[23]),
+ };
+
+ u16 indices[] = {0,1,2,2,3,0};
+
+ for(u16 j = 0; j < 24; j += 4)
+ {
+ scene::IMeshBuffer *buf = dst_mesh->getMeshBuffer(j / 4);
+ buf->append(vertices + j, 4, indices, 6);
+ }
+ }
+ return dst_mesh;
+}
+
+struct vcache
+{
+ core::array<u32> tris;
+ float score;
+ s16 cachepos;
+ u16 NumActiveTris;
+};
+
+struct tcache
+{
+ u16 ind[3];
+ float score;
+ bool drawn;
+};
+
+const u16 cachesize = 32;
+
+float FindVertexScore(vcache *v)
+{
+ const float CacheDecayPower = 1.5f;
+ const float LastTriScore = 0.75f;
+ const float ValenceBoostScale = 2.0f;
+ const float ValenceBoostPower = 0.5f;
+ const float MaxSizeVertexCache = 32.0f;
+
+ if (v->NumActiveTris == 0)
+ {
+ // No tri needs this vertex!
+ return -1.0f;
+ }
+
+ float Score = 0.0f;
+ int CachePosition = v->cachepos;
+ if (CachePosition < 0)
+ {
+ // Vertex is not in FIFO cache - no score.
+ }
+ else
+ {
+ if (CachePosition < 3)
+ {
+ // This vertex was used in the last triangle,
+ // so it has a fixed score.
+ Score = LastTriScore;
+ }
+ else
+ {
+ // Points for being high in the cache.
+ const float Scaler = 1.0f / (MaxSizeVertexCache - 3);
+ Score = 1.0f - (CachePosition - 3) * Scaler;
+ Score = powf(Score, CacheDecayPower);
+ }
+ }
+
+ // Bonus points for having a low number of tris still to
+ // use the vert, so we get rid of lone verts quickly.
+ float ValenceBoost = powf(v->NumActiveTris,
+ -ValenceBoostPower);
+ Score += ValenceBoostScale * ValenceBoost;
+
+ return Score;
+}
+
+/*
+ A specialized LRU cache for the Forsyth algorithm.
+*/
+
+class f_lru
+{
+
+public:
+ f_lru(vcache *v, tcache *t): vc(v), tc(t)
+ {
+ for (int &i : cache) {
+ i = -1;
+ }
+ }
+
+ // Adds this vertex index and returns the highest-scoring triangle index
+ u32 add(u16 vert, bool updatetris = false)
+ {
+ bool found = false;
+
+ // Mark existing pos as empty
+ for (u16 i = 0; i < cachesize; i++)
+ {
+ if (cache[i] == vert)
+ {
+ // Move everything down
+ for (u16 j = i; j; j--)
+ {
+ cache[j] = cache[j - 1];
+ }
+
+ found = true;
+ break;
+ }
+ }
+
+ if (!found)
+ {
+ if (cache[cachesize-1] != -1)
+ vc[cache[cachesize-1]].cachepos = -1;
+
+ // Move everything down
+ for (u16 i = cachesize - 1; i; i--)
+ {
+ cache[i] = cache[i - 1];
+ }
+ }
+
+ cache[0] = vert;
+
+ u32 highest = 0;
+ float hiscore = 0;
+
+ if (updatetris)
+ {
+ // Update cache positions
+ for (u16 i = 0; i < cachesize; i++)
+ {
+ if (cache[i] == -1)
+ break;
+
+ vc[cache[i]].cachepos = i;
+ vc[cache[i]].score = FindVertexScore(&vc[cache[i]]);
+ }
+
+ // Update triangle scores
+ for (int i : cache) {
+ if (i == -1)
+ break;
+
+ const u16 trisize = vc[i].tris.size();
+ for (u16 t = 0; t < trisize; t++)
+ {
+ tcache *tri = &tc[vc[i].tris[t]];
+
+ tri->score =
+ vc[tri->ind[0]].score +
+ vc[tri->ind[1]].score +
+ vc[tri->ind[2]].score;
+
+ if (tri->score > hiscore)
+ {
+ hiscore = tri->score;
+ highest = vc[i].tris[t];
+ }
+ }
+ }
+ }
+
+ return highest;
+ }
+
+private:
+ s32 cache[cachesize];
+ vcache *vc;
+ tcache *tc;
+};
+
+/**
+Vertex cache optimization according to the Forsyth paper:
+http://home.comcast.net/~tom_forsyth/papers/fast_vert_cache_opt.html
+
+The function is thread-safe (read: you can optimize several meshes in different threads)
+
+\param mesh Source mesh for the operation. */
+scene::IMesh* createForsythOptimizedMesh(const scene::IMesh *mesh)
+{
+ if (!mesh)
+ return 0;
+
+ scene::SMesh *newmesh = new scene::SMesh();
+ newmesh->BoundingBox = mesh->getBoundingBox();
+
+ const u32 mbcount = mesh->getMeshBufferCount();
+
+ for (u32 b = 0; b < mbcount; ++b)
+ {
+ const scene::IMeshBuffer *mb = mesh->getMeshBuffer(b);
+
+ if (mb->getIndexType() != video::EIT_16BIT)
+ {
+ //os::Printer::log("Cannot optimize a mesh with 32bit indices", ELL_ERROR);
+ newmesh->drop();
+ return 0;
+ }
+
+ const u32 icount = mb->getIndexCount();
+ const u32 tcount = icount / 3;
+ const u32 vcount = mb->getVertexCount();
+ const u16 *ind = mb->getIndices();
+
+ vcache *vc = new vcache[vcount];
+ tcache *tc = new tcache[tcount];
+
+ f_lru lru(vc, tc);
+
+ // init
+ for (u16 i = 0; i < vcount; i++)
+ {
+ vc[i].score = 0;
+ vc[i].cachepos = -1;
+ vc[i].NumActiveTris = 0;
+ }
+
+ // First pass: count how many times a vert is used
+ for (u32 i = 0; i < icount; i += 3)
+ {
+ vc[ind[i]].NumActiveTris++;
+ vc[ind[i + 1]].NumActiveTris++;
+ vc[ind[i + 2]].NumActiveTris++;
+
+ const u32 tri_ind = i/3;
+ tc[tri_ind].ind[0] = ind[i];
+ tc[tri_ind].ind[1] = ind[i + 1];
+ tc[tri_ind].ind[2] = ind[i + 2];
+ }
+
+ // Second pass: list of each triangle
+ for (u32 i = 0; i < tcount; i++)
+ {
+ vc[tc[i].ind[0]].tris.push_back(i);
+ vc[tc[i].ind[1]].tris.push_back(i);
+ vc[tc[i].ind[2]].tris.push_back(i);
+
+ tc[i].drawn = false;
+ }
+
+ // Give initial scores
+ for (u16 i = 0; i < vcount; i++)
+ {
+ vc[i].score = FindVertexScore(&vc[i]);
+ }
+ for (u32 i = 0; i < tcount; i++)
+ {
+ tc[i].score =
+ vc[tc[i].ind[0]].score +
+ vc[tc[i].ind[1]].score +
+ vc[tc[i].ind[2]].score;
+ }
+
+ switch(mb->getVertexType())
+ {
+ case video::EVT_STANDARD:
+ {
+ video::S3DVertex *v = (video::S3DVertex *) mb->getVertices();
+
+ scene::SMeshBuffer *buf = new scene::SMeshBuffer();
+ buf->Material = mb->getMaterial();
+
+ buf->Vertices.reallocate(vcount);
+ buf->Indices.reallocate(icount);
+
+ core::map<const video::S3DVertex, const u16> sind; // search index for fast operation
+ typedef core::map<const video::S3DVertex, const u16>::Node snode;
+
+ // Main algorithm
+ u32 highest = 0;
+ u32 drawcalls = 0;
+ for (;;)
+ {
+ if (tc[highest].drawn)
+ {
+ bool found = false;
+ float hiscore = 0;
+ for (u32 t = 0; t < tcount; t++)
+ {
+ if (!tc[t].drawn)
+ {
+ if (tc[t].score > hiscore)
+ {
+ highest = t;
+ hiscore = tc[t].score;
+ found = true;
+ }
+ }
+ }
+ if (!found)
+ break;
+ }
+
+ // Output the best triangle
+ u16 newind = buf->Vertices.size();
+
+ snode *s = sind.find(v[tc[highest].ind[0]]);
+
+ if (!s)
+ {
+ buf->Vertices.push_back(v[tc[highest].ind[0]]);
+ buf->Indices.push_back(newind);
+ sind.insert(v[tc[highest].ind[0]], newind);
+ newind++;
+ }
+ else
+ {
+ buf->Indices.push_back(s->getValue());
+ }
+
+ s = sind.find(v[tc[highest].ind[1]]);
+
+ if (!s)
+ {
+ buf->Vertices.push_back(v[tc[highest].ind[1]]);
+ buf->Indices.push_back(newind);
+ sind.insert(v[tc[highest].ind[1]], newind);
+ newind++;
+ }
+ else
+ {
+ buf->Indices.push_back(s->getValue());
+ }
+
+ s = sind.find(v[tc[highest].ind[2]]);
+
+ if (!s)
+ {
+ buf->Vertices.push_back(v[tc[highest].ind[2]]);
+ buf->Indices.push_back(newind);
+ sind.insert(v[tc[highest].ind[2]], newind);
+ }
+ else
+ {
+ buf->Indices.push_back(s->getValue());
+ }
+
+ vc[tc[highest].ind[0]].NumActiveTris--;
+ vc[tc[highest].ind[1]].NumActiveTris--;
+ vc[tc[highest].ind[2]].NumActiveTris--;
+
+ tc[highest].drawn = true;
+
+ for (u16 j : tc[highest].ind) {
+ vcache *vert = &vc[j];
+ for (u16 t = 0; t < vert->tris.size(); t++)
+ {
+ if (highest == vert->tris[t])
+ {
+ vert->tris.erase(t);
+ break;
+ }
+ }
+ }
+
+ lru.add(tc[highest].ind[0]);
+ lru.add(tc[highest].ind[1]);
+ highest = lru.add(tc[highest].ind[2], true);
+ drawcalls++;
+ }
+
+ buf->setBoundingBox(mb->getBoundingBox());
+ newmesh->addMeshBuffer(buf);
+ buf->drop();
+ }
+ break;
+ case video::EVT_2TCOORDS:
+ {
+ video::S3DVertex2TCoords *v = (video::S3DVertex2TCoords *) mb->getVertices();
+
+ scene::SMeshBufferLightMap *buf = new scene::SMeshBufferLightMap();
+ buf->Material = mb->getMaterial();
+
+ buf->Vertices.reallocate(vcount);
+ buf->Indices.reallocate(icount);
+
+ core::map<const video::S3DVertex2TCoords, const u16> sind; // search index for fast operation
+ typedef core::map<const video::S3DVertex2TCoords, const u16>::Node snode;
+
+ // Main algorithm
+ u32 highest = 0;
+ u32 drawcalls = 0;
+ for (;;)
+ {
+ if (tc[highest].drawn)
+ {
+ bool found = false;
+ float hiscore = 0;
+ for (u32 t = 0; t < tcount; t++)
+ {
+ if (!tc[t].drawn)
+ {
+ if (tc[t].score > hiscore)
+ {
+ highest = t;
+ hiscore = tc[t].score;
+ found = true;
+ }
+ }
+ }
+ if (!found)
+ break;
+ }
+
+ // Output the best triangle
+ u16 newind = buf->Vertices.size();
+
+ snode *s = sind.find(v[tc[highest].ind[0]]);
+
+ if (!s)
+ {
+ buf->Vertices.push_back(v[tc[highest].ind[0]]);
+ buf->Indices.push_back(newind);
+ sind.insert(v[tc[highest].ind[0]], newind);
+ newind++;
+ }
+ else
+ {
+ buf->Indices.push_back(s->getValue());
+ }
+
+ s = sind.find(v[tc[highest].ind[1]]);
+
+ if (!s)
+ {
+ buf->Vertices.push_back(v[tc[highest].ind[1]]);
+ buf->Indices.push_back(newind);
+ sind.insert(v[tc[highest].ind[1]], newind);
+ newind++;
+ }
+ else
+ {
+ buf->Indices.push_back(s->getValue());
+ }
+
+ s = sind.find(v[tc[highest].ind[2]]);
+
+ if (!s)
+ {
+ buf->Vertices.push_back(v[tc[highest].ind[2]]);
+ buf->Indices.push_back(newind);
+ sind.insert(v[tc[highest].ind[2]], newind);
+ }
+ else
+ {
+ buf->Indices.push_back(s->getValue());
+ }
+
+ vc[tc[highest].ind[0]].NumActiveTris--;
+ vc[tc[highest].ind[1]].NumActiveTris--;
+ vc[tc[highest].ind[2]].NumActiveTris--;
+
+ tc[highest].drawn = true;
+
+ for (u16 j : tc[highest].ind) {
+ vcache *vert = &vc[j];
+ for (u16 t = 0; t < vert->tris.size(); t++)
+ {
+ if (highest == vert->tris[t])
+ {
+ vert->tris.erase(t);
+ break;
+ }
+ }
+ }
+
+ lru.add(tc[highest].ind[0]);
+ lru.add(tc[highest].ind[1]);
+ highest = lru.add(tc[highest].ind[2]);
+ drawcalls++;
+ }
+
+ buf->setBoundingBox(mb->getBoundingBox());
+ newmesh->addMeshBuffer(buf);
+ buf->drop();
+
+ }
+ break;
+ case video::EVT_TANGENTS:
+ {
+ video::S3DVertexTangents *v = (video::S3DVertexTangents *) mb->getVertices();
+
+ scene::SMeshBufferTangents *buf = new scene::SMeshBufferTangents();
+ buf->Material = mb->getMaterial();
+
+ buf->Vertices.reallocate(vcount);
+ buf->Indices.reallocate(icount);
+
+ core::map<const video::S3DVertexTangents, const u16> sind; // search index for fast operation
+ typedef core::map<const video::S3DVertexTangents, const u16>::Node snode;
+
+ // Main algorithm
+ u32 highest = 0;
+ u32 drawcalls = 0;
+ for (;;)
+ {
+ if (tc[highest].drawn)
+ {
+ bool found = false;
+ float hiscore = 0;
+ for (u32 t = 0; t < tcount; t++)
+ {
+ if (!tc[t].drawn)
+ {
+ if (tc[t].score > hiscore)
+ {
+ highest = t;
+ hiscore = tc[t].score;
+ found = true;
+ }
+ }
+ }
+ if (!found)
+ break;
+ }
+
+ // Output the best triangle
+ u16 newind = buf->Vertices.size();
+
+ snode *s = sind.find(v[tc[highest].ind[0]]);
+
+ if (!s)
+ {
+ buf->Vertices.push_back(v[tc[highest].ind[0]]);
+ buf->Indices.push_back(newind);
+ sind.insert(v[tc[highest].ind[0]], newind);
+ newind++;
+ }
+ else
+ {
+ buf->Indices.push_back(s->getValue());
+ }
+
+ s = sind.find(v[tc[highest].ind[1]]);
+
+ if (!s)
+ {
+ buf->Vertices.push_back(v[tc[highest].ind[1]]);
+ buf->Indices.push_back(newind);
+ sind.insert(v[tc[highest].ind[1]], newind);
+ newind++;
+ }
+ else
+ {
+ buf->Indices.push_back(s->getValue());
+ }
+
+ s = sind.find(v[tc[highest].ind[2]]);
+
+ if (!s)
+ {
+ buf->Vertices.push_back(v[tc[highest].ind[2]]);
+ buf->Indices.push_back(newind);
+ sind.insert(v[tc[highest].ind[2]], newind);
+ }
+ else
+ {
+ buf->Indices.push_back(s->getValue());
+ }
+
+ vc[tc[highest].ind[0]].NumActiveTris--;
+ vc[tc[highest].ind[1]].NumActiveTris--;
+ vc[tc[highest].ind[2]].NumActiveTris--;
+
+ tc[highest].drawn = true;
+
+ for (u16 j : tc[highest].ind) {
+ vcache *vert = &vc[j];
+ for (u16 t = 0; t < vert->tris.size(); t++)
+ {
+ if (highest == vert->tris[t])
+ {
+ vert->tris.erase(t);
+ break;
+ }
+ }
+ }
+
+ lru.add(tc[highest].ind[0]);
+ lru.add(tc[highest].ind[1]);
+ highest = lru.add(tc[highest].ind[2]);
+ drawcalls++;
+ }
+
+ buf->setBoundingBox(mb->getBoundingBox());
+ newmesh->addMeshBuffer(buf);
+ buf->drop();
+ }
+ break;
+ }
+
+ delete [] vc;
+ delete [] tc;
+
+ } // for each meshbuffer
+
+ return newmesh;
+}
diff --git a/src/client/mesh.h b/src/client/mesh.h
new file mode 100644
index 000000000..0c4094de2
--- /dev/null
+++ b/src/client/mesh.h
@@ -0,0 +1,129 @@
+/*
+Minetest
+Copyright (C) 2010-2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+
+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 "nodedef.h"
+
+/*!
+ * Applies shading to a color based on the surface's
+ * normal vector.
+ */
+void applyFacesShading(video::SColor &color, const v3f &normal);
+
+/*
+ Create a new cube mesh.
+ Vertices are at (+-scale.X/2, +-scale.Y/2, +-scale.Z/2).
+
+ The resulting mesh has 6 materials (up, down, right, left, back, front)
+ which must be defined by the caller.
+*/
+scene::IAnimatedMesh* createCubeMesh(v3f scale);
+
+/*
+ Multiplies each vertex coordinate by the specified scaling factors
+ (componentwise vector multiplication).
+*/
+void scaleMesh(scene::IMesh *mesh, v3f scale);
+
+/*
+ Translate each vertex coordinate by the specified vector.
+*/
+void translateMesh(scene::IMesh *mesh, v3f vec);
+
+/*!
+ * Sets a constant color for all vertices in the mesh buffer.
+ */
+void setMeshBufferColor(scene::IMeshBuffer *buf, const video::SColor &color);
+
+/*
+ Set a constant color for all vertices in the mesh
+*/
+void setMeshColor(scene::IMesh *mesh, const video::SColor &color);
+
+/*
+ Set a constant color for an animated mesh
+*/
+void setAnimatedMeshColor(scene::IAnimatedMeshSceneNode *node, const video::SColor &color);
+
+/*!
+ * Overwrites the color of a mesh buffer.
+ * The color is darkened based on the normal vector of the vertices.
+ */
+void colorizeMeshBuffer(scene::IMeshBuffer *buf, const video::SColor *buffercolor);
+
+/*
+ Set the color of all vertices in the mesh.
+ For each vertex, determine the largest absolute entry in
+ the normal vector, and choose one of colorX, colorY or
+ colorZ accordingly.
+*/
+void setMeshColorByNormalXYZ(scene::IMesh *mesh,
+ const video::SColor &colorX,
+ const video::SColor &colorY,
+ const video::SColor &colorZ);
+
+void setMeshColorByNormal(scene::IMesh *mesh, const v3f &normal,
+ const video::SColor &color);
+
+/*
+ Rotate the mesh by 6d facedir value.
+ Method only for meshnodes, not suitable for entities.
+*/
+void rotateMeshBy6dFacedir(scene::IMesh *mesh, int facedir);
+
+/*
+ Rotate the mesh around the axis and given angle in degrees.
+*/
+void rotateMeshXYby (scene::IMesh *mesh, f64 degrees);
+void rotateMeshXZby (scene::IMesh *mesh, f64 degrees);
+void rotateMeshYZby (scene::IMesh *mesh, f64 degrees);
+
+/*
+ * Clone the mesh buffer.
+ * The returned pointer should be dropped.
+ */
+scene::IMeshBuffer* cloneMeshBuffer(scene::IMeshBuffer *mesh_buffer);
+
+/*
+ Clone the mesh.
+*/
+scene::SMesh* cloneMesh(scene::IMesh *src_mesh);
+
+/*
+ Convert nodeboxes to mesh. Each tile goes into a different buffer.
+ boxes - set of nodeboxes to be converted into cuboids
+ uv_coords[24] - table of texture uv coords for each cuboid face
+ expand - factor by which cuboids will be resized
+*/
+scene::IMesh* convertNodeboxesToMesh(const std::vector<aabb3f> &boxes,
+ const f32 *uv_coords = NULL, float expand = 0);
+
+/*
+ Update bounding box for a mesh.
+*/
+void recalculateBoundingBox(scene::IMesh *src_mesh);
+
+/*
+ Vertex cache optimization according to the Forsyth paper:
+ http://home.comcast.net/~tom_forsyth/papers/fast_vert_cache_opt.html
+ Ported from irrlicht 1.8
+*/
+scene::IMesh* createForsythOptimizedMesh(const scene::IMesh *mesh);
diff --git a/src/client/mesh_generator_thread.cpp b/src/client/mesh_generator_thread.cpp
new file mode 100644
index 000000000..be4bcc1f4
--- /dev/null
+++ b/src/client/mesh_generator_thread.cpp
@@ -0,0 +1,308 @@
+/*
+Minetest
+Copyright (C) 2013, 2017 celeron55, Perttu Ahola <celeron55@gmail.com>
+
+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 "mesh_generator_thread.h"
+#include "settings.h"
+#include "profiler.h"
+#include "client.h"
+#include "mapblock.h"
+#include "map.h"
+
+/*
+ CachedMapBlockData
+*/
+
+CachedMapBlockData::~CachedMapBlockData()
+{
+ assert(refcount_from_queue == 0);
+
+ delete[] data;
+}
+
+/*
+ QueuedMeshUpdate
+*/
+
+QueuedMeshUpdate::~QueuedMeshUpdate()
+{
+ delete data;
+}
+
+/*
+ MeshUpdateQueue
+*/
+
+MeshUpdateQueue::MeshUpdateQueue(Client *client):
+ m_client(client)
+{
+ m_cache_enable_shaders = g_settings->getBool("enable_shaders");
+ m_cache_use_tangent_vertices = m_cache_enable_shaders && (
+ g_settings->getBool("enable_bumpmapping") ||
+ g_settings->getBool("enable_parallax_occlusion"));
+ m_cache_smooth_lighting = g_settings->getBool("smooth_lighting");
+ m_meshgen_block_cache_size = g_settings->getS32("meshgen_block_cache_size");
+}
+
+MeshUpdateQueue::~MeshUpdateQueue()
+{
+ MutexAutoLock lock(m_mutex);
+
+ for (auto &i : m_cache) {
+ delete i.second;
+ }
+
+ for (QueuedMeshUpdate *q : m_queue) {
+ delete q;
+ }
+}
+
+void MeshUpdateQueue::addBlock(Map *map, v3s16 p, bool ack_block_to_server, bool urgent)
+{
+ MutexAutoLock lock(m_mutex);
+
+ cleanupCache();
+
+ /*
+ Cache the block data (force-update the center block, don't update the
+ neighbors but get them if they aren't already cached)
+ */
+ std::vector<CachedMapBlockData*> cached_blocks;
+ size_t cache_hit_counter = 0;
+ cached_blocks.reserve(3*3*3);
+ v3s16 dp;
+ for (dp.X = -1; dp.X <= 1; dp.X++)
+ for (dp.Y = -1; dp.Y <= 1; dp.Y++)
+ for (dp.Z = -1; dp.Z <= 1; dp.Z++) {
+ v3s16 p1 = p + dp;
+ CachedMapBlockData *cached_block;
+ if (dp == v3s16(0, 0, 0))
+ cached_block = cacheBlock(map, p1, FORCE_UPDATE);
+ else
+ cached_block = cacheBlock(map, p1, SKIP_UPDATE_IF_ALREADY_CACHED,
+ &cache_hit_counter);
+ cached_blocks.push_back(cached_block);
+ }
+ g_profiler->avg("MeshUpdateQueue MapBlock cache hit %",
+ 100.0f * cache_hit_counter / cached_blocks.size());
+
+ /*
+ Mark the block as urgent if requested
+ */
+ if (urgent)
+ m_urgents.insert(p);
+
+ /*
+ Find if block is already in queue.
+ If it is, update the data and quit.
+ */
+ for (QueuedMeshUpdate *q : m_queue) {
+ if (q->p == p) {
+ // NOTE: We are not adding a new position to the queue, thus
+ // refcount_from_queue stays the same.
+ if(ack_block_to_server)
+ q->ack_block_to_server = true;
+ q->crack_level = m_client->getCrackLevel();
+ q->crack_pos = m_client->getCrackPos();
+ return;
+ }
+ }
+
+ /*
+ Add the block
+ */
+ QueuedMeshUpdate *q = new QueuedMeshUpdate;
+ q->p = p;
+ q->ack_block_to_server = ack_block_to_server;
+ q->crack_level = m_client->getCrackLevel();
+ q->crack_pos = m_client->getCrackPos();
+ m_queue.push_back(q);
+
+ // This queue entry is a new reference to the cached blocks
+ for (CachedMapBlockData *cached_block : cached_blocks) {
+ cached_block->refcount_from_queue++;
+ }
+}
+
+// Returned pointer must be deleted
+// Returns NULL if queue is empty
+QueuedMeshUpdate *MeshUpdateQueue::pop()
+{
+ MutexAutoLock lock(m_mutex);
+
+ bool must_be_urgent = !m_urgents.empty();
+ for (std::vector<QueuedMeshUpdate*>::iterator i = m_queue.begin();
+ i != m_queue.end(); ++i) {
+ QueuedMeshUpdate *q = *i;
+ if(must_be_urgent && m_urgents.count(q->p) == 0)
+ continue;
+ m_queue.erase(i);
+ m_urgents.erase(q->p);
+ fillDataFromMapBlockCache(q);
+ return q;
+ }
+ return NULL;
+}
+
+CachedMapBlockData* MeshUpdateQueue::cacheBlock(Map *map, v3s16 p, UpdateMode mode,
+ size_t *cache_hit_counter)
+{
+ std::map<v3s16, CachedMapBlockData*>::iterator it =
+ m_cache.find(p);
+ if (it != m_cache.end()) {
+ // Already in cache
+ CachedMapBlockData *cached_block = it->second;
+ if (mode == SKIP_UPDATE_IF_ALREADY_CACHED) {
+ if (cache_hit_counter)
+ (*cache_hit_counter)++;
+ return cached_block;
+ }
+ MapBlock *b = map->getBlockNoCreateNoEx(p);
+ if (b) {
+ if (cached_block->data == NULL)
+ cached_block->data =
+ new MapNode[MAP_BLOCKSIZE * MAP_BLOCKSIZE * MAP_BLOCKSIZE];
+ memcpy(cached_block->data, b->getData(),
+ MAP_BLOCKSIZE * MAP_BLOCKSIZE * MAP_BLOCKSIZE * sizeof(MapNode));
+ } else {
+ delete[] cached_block->data;
+ cached_block->data = NULL;
+ }
+ return cached_block;
+ }
+
+ // Not yet in cache
+ CachedMapBlockData *cached_block = new CachedMapBlockData();
+ m_cache[p] = cached_block;
+ MapBlock *b = map->getBlockNoCreateNoEx(p);
+ if (b) {
+ cached_block->data =
+ new MapNode[MAP_BLOCKSIZE * MAP_BLOCKSIZE * MAP_BLOCKSIZE];
+ memcpy(cached_block->data, b->getData(),
+ MAP_BLOCKSIZE * MAP_BLOCKSIZE * MAP_BLOCKSIZE * sizeof(MapNode));
+ }
+ return cached_block;
+}
+
+CachedMapBlockData* MeshUpdateQueue::getCachedBlock(const v3s16 &p)
+{
+ std::map<v3s16, CachedMapBlockData*>::iterator it = m_cache.find(p);
+ if (it != m_cache.end()) {
+ return it->second;
+ }
+ return NULL;
+}
+
+void MeshUpdateQueue::fillDataFromMapBlockCache(QueuedMeshUpdate *q)
+{
+ MeshMakeData *data = new MeshMakeData(m_client, m_cache_enable_shaders,
+ m_cache_use_tangent_vertices);
+ q->data = data;
+
+ data->fillBlockDataBegin(q->p);
+
+ std::time_t t_now = std::time(0);
+
+ // Collect data for 3*3*3 blocks from cache
+ v3s16 dp;
+ for (dp.X = -1; dp.X <= 1; dp.X++)
+ for (dp.Y = -1; dp.Y <= 1; dp.Y++)
+ for (dp.Z = -1; dp.Z <= 1; dp.Z++) {
+ v3s16 p = q->p + dp;
+ CachedMapBlockData *cached_block = getCachedBlock(p);
+ if (cached_block) {
+ cached_block->refcount_from_queue--;
+ cached_block->last_used_timestamp = t_now;
+ if (cached_block->data)
+ data->fillBlockData(dp, cached_block->data);
+ }
+ }
+
+ data->setCrack(q->crack_level, q->crack_pos);
+ data->setSmoothLighting(m_cache_smooth_lighting);
+}
+
+void MeshUpdateQueue::cleanupCache()
+{
+ const int mapblock_kB = MAP_BLOCKSIZE * MAP_BLOCKSIZE * MAP_BLOCKSIZE *
+ sizeof(MapNode) / 1000;
+ g_profiler->avg("MeshUpdateQueue MapBlock cache size kB",
+ mapblock_kB * m_cache.size());
+
+ // The cache size is kept roughly below cache_soft_max_size, not letting
+ // anything get older than cache_seconds_max or deleted before 2 seconds.
+ const int cache_seconds_max = 10;
+ const int cache_soft_max_size = m_meshgen_block_cache_size * 1000 / mapblock_kB;
+ int cache_seconds = MYMAX(2, cache_seconds_max -
+ m_cache.size() / (cache_soft_max_size / cache_seconds_max));
+
+ int t_now = time(0);
+
+ for (std::map<v3s16, CachedMapBlockData*>::iterator it = m_cache.begin();
+ it != m_cache.end(); ) {
+ CachedMapBlockData *cached_block = it->second;
+ if (cached_block->refcount_from_queue == 0 &&
+ cached_block->last_used_timestamp < t_now - cache_seconds) {
+ m_cache.erase(it++);
+ delete cached_block;
+ } else {
+ ++it;
+ }
+ }
+}
+
+/*
+ MeshUpdateThread
+*/
+
+MeshUpdateThread::MeshUpdateThread(Client *client):
+ UpdateThread("Mesh"),
+ m_queue_in(client)
+{
+ m_generation_interval = g_settings->getU16("mesh_generation_interval");
+ m_generation_interval = rangelim(m_generation_interval, 0, 50);
+}
+
+void MeshUpdateThread::updateBlock(Map *map, v3s16 p, bool ack_block_to_server,
+ bool urgent)
+{
+ // Allow the MeshUpdateQueue to do whatever it wants
+ m_queue_in.addBlock(map, p, ack_block_to_server, urgent);
+ deferUpdate();
+}
+
+void MeshUpdateThread::doUpdate()
+{
+ QueuedMeshUpdate *q;
+ while ((q = m_queue_in.pop())) {
+ if (m_generation_interval)
+ sleep_ms(m_generation_interval);
+ ScopeProfiler sp(g_profiler, "Client: Mesh making");
+
+ MapBlockMesh *mesh_new = new MapBlockMesh(q->data, m_camera_offset);
+
+ MeshUpdateResult r;
+ r.p = q->p;
+ r.mesh = mesh_new;
+ r.ack_block_to_server = q->ack_block_to_server;
+
+ m_queue_out.push_back(r);
+
+ delete q;
+ }
+}
diff --git a/src/client/mesh_generator_thread.h b/src/client/mesh_generator_thread.h
new file mode 100644
index 000000000..9a42852a3
--- /dev/null
+++ b/src/client/mesh_generator_thread.h
@@ -0,0 +1,131 @@
+/*
+Minetest
+Copyright (C) 2013, 2017 celeron55, Perttu Ahola <celeron55@gmail.com>
+
+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 <ctime>
+#include <mutex>
+#include "mapblock_mesh.h"
+#include "threading/mutex_auto_lock.h"
+#include "util/thread.h"
+
+struct CachedMapBlockData
+{
+ v3s16 p = v3s16(-1337, -1337, -1337);
+ MapNode *data = nullptr; // A copy of the MapBlock's data member
+ int refcount_from_queue = 0;
+ std::time_t last_used_timestamp = std::time(0);
+
+ CachedMapBlockData() = default;
+ ~CachedMapBlockData();
+};
+
+struct QueuedMeshUpdate
+{
+ v3s16 p = v3s16(-1337, -1337, -1337);
+ bool ack_block_to_server = false;
+ bool urgent = false;
+ int crack_level = -1;
+ v3s16 crack_pos;
+ MeshMakeData *data = nullptr; // This is generated in MeshUpdateQueue::pop()
+
+ QueuedMeshUpdate() = default;
+ ~QueuedMeshUpdate();
+};
+
+/*
+ A thread-safe queue of mesh update tasks and a cache of MapBlock data
+*/
+class MeshUpdateQueue
+{
+ enum UpdateMode
+ {
+ FORCE_UPDATE,
+ SKIP_UPDATE_IF_ALREADY_CACHED,
+ };
+
+public:
+ MeshUpdateQueue(Client *client);
+
+ ~MeshUpdateQueue();
+
+ // Caches the block at p and its neighbors (if needed) and queues a mesh
+ // update for the block at p
+ void addBlock(Map *map, v3s16 p, bool ack_block_to_server, bool urgent);
+
+ // Returned pointer must be deleted
+ // Returns NULL if queue is empty
+ QueuedMeshUpdate *pop();
+
+ u32 size()
+ {
+ MutexAutoLock lock(m_mutex);
+ return m_queue.size();
+ }
+
+private:
+ Client *m_client;
+ std::vector<QueuedMeshUpdate *> m_queue;
+ std::set<v3s16> m_urgents;
+ std::map<v3s16, CachedMapBlockData *> m_cache;
+ std::mutex m_mutex;
+
+ // TODO: Add callback to update these when g_settings changes
+ bool m_cache_enable_shaders;
+ bool m_cache_use_tangent_vertices;
+ bool m_cache_smooth_lighting;
+ int m_meshgen_block_cache_size;
+
+ CachedMapBlockData *cacheBlock(Map *map, v3s16 p, UpdateMode mode,
+ size_t *cache_hit_counter = NULL);
+ CachedMapBlockData *getCachedBlock(const v3s16 &p);
+ void fillDataFromMapBlockCache(QueuedMeshUpdate *q);
+ void cleanupCache();
+};
+
+struct MeshUpdateResult
+{
+ v3s16 p = v3s16(-1338, -1338, -1338);
+ MapBlockMesh *mesh = nullptr;
+ bool ack_block_to_server = false;
+
+ MeshUpdateResult() = default;
+};
+
+class MeshUpdateThread : public UpdateThread
+{
+public:
+ MeshUpdateThread(Client *client);
+
+ // Caches the block at p and its neighbors (if needed) and queues a mesh
+ // update for the block at p
+ void updateBlock(Map *map, v3s16 p, bool ack_block_to_server, bool urgent);
+
+ v3s16 m_camera_offset;
+ MutexedQueue<MeshUpdateResult> m_queue_out;
+
+private:
+ MeshUpdateQueue m_queue_in;
+
+ // TODO: Add callback to update these when g_settings changes
+ int m_generation_interval;
+
+protected:
+ virtual void doUpdate();
+};
diff --git a/src/client/meshgen/collector.cpp b/src/client/meshgen/collector.cpp
index c317a0772..25457c868 100644
--- a/src/client/meshgen/collector.cpp
+++ b/src/client/meshgen/collector.cpp
@@ -20,7 +20,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "collector.h"
#include <stdexcept>
#include "log.h"
-#include "mesh.h"
+#include "client/mesh.h"
void MeshCollector::append(const TileSpec &tile, const video::S3DVertex *vertices,
u32 numVertices, const u16 *indices, u32 numIndices)
diff --git a/src/client/minimap.cpp b/src/client/minimap.cpp
new file mode 100644
index 000000000..4d83c088a
--- /dev/null
+++ b/src/client/minimap.cpp
@@ -0,0 +1,624 @@
+/*
+Minetest
+Copyright (C) 2010-2015 celeron55, Perttu Ahola <celeron55@gmail.com>
+
+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 <cmath>
+#include "client.h"
+#include "clientmap.h"
+#include "settings.h"
+#include "shader.h"
+#include "mapblock.h"
+#include "client/renderingengine.h"
+
+
+////
+//// MinimapUpdateThread
+////
+
+MinimapUpdateThread::~MinimapUpdateThread()
+{
+ for (auto &it : m_blocks_cache) {
+ delete it.second;
+ }
+
+ for (auto &q : m_update_queue) {
+ delete q.data;
+ }
+}
+
+bool MinimapUpdateThread::pushBlockUpdate(v3s16 pos, MinimapMapblock *data)
+{
+ MutexAutoLock lock(m_queue_mutex);
+
+ // Find if block is already in queue.
+ // If it is, update the data and quit.
+ for (QueuedMinimapUpdate &q : m_update_queue) {
+ if (q.pos == pos) {
+ delete q.data;
+ q.data = data;
+ return false;
+ }
+ }
+
+ // Add the block
+ QueuedMinimapUpdate q;
+ q.pos = pos;
+ q.data = data;
+ m_update_queue.push_back(q);
+
+ return true;
+}
+
+bool MinimapUpdateThread::popBlockUpdate(QueuedMinimapUpdate *update)
+{
+ MutexAutoLock lock(m_queue_mutex);
+
+ if (m_update_queue.empty())
+ return false;
+
+ *update = m_update_queue.front();
+ m_update_queue.pop_front();
+
+ return true;
+}
+
+void MinimapUpdateThread::enqueueBlock(v3s16 pos, MinimapMapblock *data)
+{
+ pushBlockUpdate(pos, data);
+ deferUpdate();
+}
+
+
+void MinimapUpdateThread::doUpdate()
+{
+ QueuedMinimapUpdate update;
+
+ while (popBlockUpdate(&update)) {
+ if (update.data) {
+ // Swap two values in the map using single lookup
+ std::pair<std::map<v3s16, MinimapMapblock*>::iterator, bool>
+ result = m_blocks_cache.insert(std::make_pair(update.pos, update.data));
+ if (!result.second) {
+ delete result.first->second;
+ result.first->second = update.data;
+ }
+ } else {
+ std::map<v3s16, MinimapMapblock *>::iterator it;
+ it = m_blocks_cache.find(update.pos);
+ if (it != m_blocks_cache.end()) {
+ delete it->second;
+ m_blocks_cache.erase(it);
+ }
+ }
+ }
+
+ if (data->map_invalidated && data->mode != MINIMAP_MODE_OFF) {
+ getMap(data->pos, data->map_size, data->scan_height);
+ data->map_invalidated = false;
+ }
+}
+
+void MinimapUpdateThread::getMap(v3s16 pos, s16 size, s16 height)
+{
+ v3s16 pos_min(pos.X - size / 2, pos.Y - height / 2, pos.Z - size / 2);
+ v3s16 pos_max(pos_min.X + size - 1, pos.Y + height / 2, pos_min.Z + size - 1);
+ v3s16 blockpos_min = getNodeBlockPos(pos_min);
+ v3s16 blockpos_max = getNodeBlockPos(pos_max);
+
+// clear the map
+ for (int z = 0; z < size; z++)
+ for (int x = 0; x < size; x++) {
+ MinimapPixel &mmpixel = data->minimap_scan[x + z * size];
+ mmpixel.air_count = 0;
+ mmpixel.height = 0;
+ mmpixel.n = MapNode(CONTENT_AIR);
+ }
+
+// draw the map
+ v3s16 blockpos;
+ for (blockpos.Z = blockpos_min.Z; blockpos.Z <= blockpos_max.Z; ++blockpos.Z)
+ for (blockpos.Y = blockpos_min.Y; blockpos.Y <= blockpos_max.Y; ++blockpos.Y)
+ for (blockpos.X = blockpos_min.X; blockpos.X <= blockpos_max.X; ++blockpos.X) {
+ std::map<v3s16, MinimapMapblock *>::const_iterator pblock =
+ m_blocks_cache.find(blockpos);
+ if (pblock == m_blocks_cache.end())
+ continue;
+ const MinimapMapblock &block = *pblock->second;
+
+ v3s16 block_node_min(blockpos * MAP_BLOCKSIZE);
+ v3s16 block_node_max(block_node_min + MAP_BLOCKSIZE - 1);
+ // clip
+ v3s16 range_min = componentwise_max(block_node_min, pos_min);
+ v3s16 range_max = componentwise_min(block_node_max, pos_max);
+
+ v3s16 pos;
+ pos.Y = range_min.Y;
+ for (pos.Z = range_min.Z; pos.Z <= range_max.Z; ++pos.Z)
+ for (pos.X = range_min.X; pos.X <= range_max.X; ++pos.X) {
+ v3s16 inblock_pos = pos - block_node_min;
+ const MinimapPixel &in_pixel =
+ block.data[inblock_pos.Z * MAP_BLOCKSIZE + inblock_pos.X];
+
+ v3s16 inmap_pos = pos - pos_min;
+ MinimapPixel &out_pixel =
+ data->minimap_scan[inmap_pos.X + inmap_pos.Z * size];
+
+ out_pixel.air_count += in_pixel.air_count;
+ if (in_pixel.n.param0 != CONTENT_AIR) {
+ out_pixel.n = in_pixel.n;
+ out_pixel.height = inmap_pos.Y + in_pixel.height;
+ }
+ }
+ }
+}
+
+////
+//// Mapper
+////
+
+Minimap::Minimap(Client *client)
+{
+ this->client = client;
+ this->driver = RenderingEngine::get_video_driver();
+ this->m_tsrc = client->getTextureSource();
+ this->m_shdrsrc = client->getShaderSource();
+ this->m_ndef = client->getNodeDefManager();
+
+ m_angle = 0.f;
+
+ // Initialize static settings
+ m_enable_shaders = g_settings->getBool("enable_shaders");
+ m_surface_mode_scan_height =
+ g_settings->getBool("minimap_double_scan_height") ? 256 : 128;
+
+ // Initialize minimap data
+ data = new MinimapData;
+ data->mode = MINIMAP_MODE_OFF;
+ data->is_radar = false;
+ data->map_invalidated = true;
+ data->texture = NULL;
+ data->heightmap_texture = NULL;
+ data->minimap_shape_round = g_settings->getBool("minimap_shape_round");
+
+ // Get round minimap textures
+ data->minimap_mask_round = driver->createImage(
+ m_tsrc->getTexture("minimap_mask_round.png"),
+ core::position2d<s32>(0, 0),
+ core::dimension2d<u32>(MINIMAP_MAX_SX, MINIMAP_MAX_SY));
+ data->minimap_overlay_round = m_tsrc->getTexture("minimap_overlay_round.png");
+
+ // Get square minimap textures
+ data->minimap_mask_square = driver->createImage(
+ m_tsrc->getTexture("minimap_mask_square.png"),
+ core::position2d<s32>(0, 0),
+ core::dimension2d<u32>(MINIMAP_MAX_SX, MINIMAP_MAX_SY));
+ data->minimap_overlay_square = m_tsrc->getTexture("minimap_overlay_square.png");
+
+ // Create player marker texture
+ data->player_marker = m_tsrc->getTexture("player_marker.png");
+ // Create object marker texture
+ data->object_marker_red = m_tsrc->getTexture("object_marker_red.png");
+
+ // Create mesh buffer for minimap
+ m_meshbuffer = getMinimapMeshBuffer();
+
+ // Initialize and start thread
+ m_minimap_update_thread = new MinimapUpdateThread();
+ m_minimap_update_thread->data = data;
+ m_minimap_update_thread->start();
+}
+
+Minimap::~Minimap()
+{
+ m_minimap_update_thread->stop();
+ m_minimap_update_thread->wait();
+
+ 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);
+ driver->removeTexture(data->object_marker_red);
+
+ delete data;
+ delete m_minimap_update_thread;
+}
+
+void Minimap::addBlock(v3s16 pos, MinimapMapblock *data)
+{
+ m_minimap_update_thread->enqueueBlock(pos, data);
+}
+
+void Minimap::toggleMinimapShape()
+{
+ MutexAutoLock lock(m_mutex);
+
+ data->minimap_shape_round = !data->minimap_shape_round;
+ g_settings->setBool("minimap_shape_round", data->minimap_shape_round);
+ m_minimap_update_thread->deferUpdate();
+}
+
+void Minimap::setMinimapShape(MinimapShape shape)
+{
+ MutexAutoLock lock(m_mutex);
+
+ if (shape == MINIMAP_SHAPE_SQUARE)
+ data->minimap_shape_round = false;
+ else if (shape == MINIMAP_SHAPE_ROUND)
+ data->minimap_shape_round = true;
+
+ g_settings->setBool("minimap_shape_round", data->minimap_shape_round);
+ m_minimap_update_thread->deferUpdate();
+}
+
+MinimapShape Minimap::getMinimapShape()
+{
+ if (data->minimap_shape_round) {
+ return MINIMAP_SHAPE_ROUND;
+ }
+
+ return MINIMAP_SHAPE_SQUARE;
+}
+
+void Minimap::setMinimapMode(MinimapMode mode)
+{
+ static const MinimapModeDef modedefs[MINIMAP_MODE_COUNT] = {
+ {false, 0, 0},
+ {false, m_surface_mode_scan_height, 256},
+ {false, m_surface_mode_scan_height, 128},
+ {false, m_surface_mode_scan_height, 64},
+ {true, 32, 128},
+ {true, 32, 64},
+ {true, 32, 32}
+ };
+
+ if (mode >= MINIMAP_MODE_COUNT)
+ return;
+
+ MutexAutoLock lock(m_mutex);
+
+ data->is_radar = modedefs[mode].is_radar;
+ data->scan_height = modedefs[mode].scan_height;
+ data->map_size = modedefs[mode].map_size;
+ data->mode = mode;
+
+ m_minimap_update_thread->deferUpdate();
+}
+
+void Minimap::setPos(v3s16 pos)
+{
+ bool do_update = false;
+
+ {
+ MutexAutoLock lock(m_mutex);
+
+ if (pos != data->old_pos) {
+ data->old_pos = data->pos;
+ data->pos = pos;
+ do_update = true;
+ }
+ }
+
+ if (do_update)
+ m_minimap_update_thread->deferUpdate();
+}
+
+void Minimap::setAngle(f32 angle)
+{
+ m_angle = angle;
+}
+
+void Minimap::blitMinimapPixelsToImageRadar(video::IImage *map_image)
+{
+ video::SColor c(240, 0, 0, 0);
+ for (s16 x = 0; x < data->map_size; x++)
+ for (s16 z = 0; z < data->map_size; z++) {
+ MinimapPixel *mmpixel = &data->minimap_scan[x + z * data->map_size];
+
+ if (mmpixel->air_count > 0)
+ c.setGreen(core::clamp(core::round32(32 + mmpixel->air_count * 8), 0, 255));
+ else
+ c.setGreen(0);
+
+ map_image->setPixel(x, data->map_size - z - 1, c);
+ }
+}
+
+void Minimap::blitMinimapPixelsToImageSurface(
+ video::IImage *map_image, video::IImage *heightmap_image)
+{
+ // This variable creation/destruction has a 1% cost on rendering minimap
+ video::SColor tilecolor;
+ for (s16 x = 0; x < data->map_size; x++)
+ for (s16 z = 0; z < data->map_size; z++) {
+ MinimapPixel *mmpixel = &data->minimap_scan[x + z * data->map_size];
+
+ const ContentFeatures &f = m_ndef->get(mmpixel->n);
+ const TileDef *tile = &f.tiledef[0];
+
+ // Color of the 0th tile (mostly this is the topmost)
+ if(tile->has_color)
+ tilecolor = tile->color;
+ else
+ mmpixel->n.getColor(f, &tilecolor);
+
+ tilecolor.setRed(tilecolor.getRed() * f.minimap_color.getRed() / 255);
+ tilecolor.setGreen(tilecolor.getGreen() * f.minimap_color.getGreen() / 255);
+ tilecolor.setBlue(tilecolor.getBlue() * f.minimap_color.getBlue() / 255);
+ tilecolor.setAlpha(240);
+
+ map_image->setPixel(x, data->map_size - z - 1, tilecolor);
+
+ u32 h = mmpixel->height;
+ heightmap_image->setPixel(x,data->map_size - z - 1,
+ video::SColor(255, h, h, h));
+ }
+}
+
+video::ITexture *Minimap::getMinimapTexture()
+{
+ // update minimap textures when new scan is ready
+ if (data->map_invalidated)
+ return data->texture;
+
+ // create minimap and heightmap images in memory
+ core::dimension2d<u32> 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<u32>(MINIMAP_MAX_SX, MINIMAP_MAX_SY));
+
+ // Blit MinimapPixels to images
+ if (data->is_radar)
+ blitMinimapPixelsToImageRadar(map_image);
+ else
+ blitMinimapPixelsToImageSurface(map_image, heightmap_image);
+
+ map_image->copyToScaling(minimap_image);
+ map_image->drop();
+
+ video::IImage *minimap_mask = data->minimap_shape_round ?
+ data->minimap_mask_round : data->minimap_mask_square;
+
+ if (minimap_mask) {
+ for (s16 y = 0; y < MINIMAP_MAX_SY; y++)
+ for (s16 x = 0; x < MINIMAP_MAX_SX; x++) {
+ const 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 Minimap::getYawVec()
+{
+ if (data->minimap_shape_round) {
+ return v3f(
+ std::cos(m_angle * core::DEGTORAD),
+ std::sin(m_angle * core::DEGTORAD),
+ 1.0);
+ }
+
+ return v3f(1.0, 0.0, 1.0);
+}
+
+scene::SMeshBuffer *Minimap::getMinimapMeshBuffer()
+{
+ scene::SMeshBuffer *buf = new scene::SMeshBuffer();
+ buf->Vertices.set_used(4);
+ buf->Indices.set_used(6);
+ static const 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 Minimap::drawMinimap()
+{
+ video::ITexture *minimap_texture = getMinimapTexture();
+ if (!minimap_texture)
+ return;
+
+ updateActiveMarkers();
+ v2u32 screensize = RenderingEngine::get_instance()->getWindowSize();
+ const u32 size = 0.25 * screensize.Y;
+
+ core::rect<s32> oldViewPort = driver->getViewPort();
+ core::matrix4 oldProjMat = driver->getTransform(video::ETS_PROJECTION);
+ core::matrix4 oldViewMat = driver->getTransform(video::ETS_VIEW);
+
+ driver->setViewPort(core::rect<s32>(
+ screensize.X - size - 10, 10,
+ screensize.X - 10, size + 10));
+ driver->setTransform(video::ETS_PROJECTION, core::matrix4());
+ driver->setTransform(video::ETS_VIEW, core::matrix4());
+
+ core::matrix4 matrix;
+ matrix.makeIdentity();
+
+ 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->is_radar) {
+ u16 sid = m_shdrsrc->getShader("minimap_shader", 1, 1);
+ material.MaterialType = m_shdrsrc->getShaderInfo(sid).material;
+ } else {
+ material.MaterialType = video::EMT_TRANSPARENT_ALPHA_CHANNEL;
+ }
+
+ if (data->minimap_shape_round)
+ matrix.setRotationDegrees(core::vector3df(0, 0, 360 - m_angle));
+
+ // Draw minimap
+ driver->setTransform(video::ETS_WORLD, matrix);
+ driver->setMaterial(material);
+ driver->drawMeshBuffer(m_meshbuffer);
+
+ // Draw overlay
+ video::ITexture *minimap_overlay = data->minimap_shape_round ?
+ data->minimap_overlay_round : data->minimap_overlay_square;
+ material.TextureLayer[0].Texture = minimap_overlay;
+ material.MaterialType = video::EMT_TRANSPARENT_ALPHA_CHANNEL;
+ driver->setMaterial(material);
+ driver->drawMeshBuffer(m_meshbuffer);
+
+ // Draw player marker on minimap
+ if (data->minimap_shape_round) {
+ matrix.setRotationDegrees(core::vector3df(0, 0, 0));
+ } else {
+ matrix.setRotationDegrees(core::vector3df(0, 0, m_angle));
+ }
+
+ material.TextureLayer[0].Texture = data->player_marker;
+ driver->setTransform(video::ETS_WORLD, matrix);
+ driver->setMaterial(material);
+ driver->drawMeshBuffer(m_meshbuffer);
+
+ // Reset transformations
+ driver->setTransform(video::ETS_VIEW, oldViewMat);
+ driver->setTransform(video::ETS_PROJECTION, oldProjMat);
+ driver->setViewPort(oldViewPort);
+
+ // Draw player markers
+ v2s32 s_pos(screensize.X - size - 10, 10);
+ core::dimension2di imgsize(data->object_marker_red->getOriginalSize());
+ core::rect<s32> img_rect(0, 0, imgsize.Width, imgsize.Height);
+ static const video::SColor col(255, 255, 255, 255);
+ static const video::SColor c[4] = {col, col, col, col};
+ f32 sin_angle = std::sin(m_angle * core::DEGTORAD);
+ f32 cos_angle = std::cos(m_angle * core::DEGTORAD);
+ s32 marker_size2 = 0.025 * (float)size;
+ for (std::list<v2f>::const_iterator
+ i = m_active_markers.begin();
+ i != m_active_markers.end(); ++i) {
+ v2f posf = *i;
+ if (data->minimap_shape_round) {
+ f32 t1 = posf.X * cos_angle - posf.Y * sin_angle;
+ f32 t2 = posf.X * sin_angle + posf.Y * cos_angle;
+ posf.X = t1;
+ posf.Y = t2;
+ }
+ posf.X = (posf.X + 0.5) * (float)size;
+ posf.Y = (posf.Y + 0.5) * (float)size;
+ core::rect<s32> dest_rect(
+ s_pos.X + posf.X - marker_size2,
+ s_pos.Y + posf.Y - marker_size2,
+ s_pos.X + posf.X + marker_size2,
+ s_pos.Y + posf.Y + marker_size2);
+ driver->draw2DImage(data->object_marker_red, dest_rect,
+ img_rect, &dest_rect, &c[0], true);
+ }
+}
+
+void Minimap::updateActiveMarkers()
+{
+ video::IImage *minimap_mask = data->minimap_shape_round ?
+ data->minimap_mask_round : data->minimap_mask_square;
+
+ const std::list<Nametag *> &nametags = client->getCamera()->getNametags();
+
+ m_active_markers.clear();
+
+ for (Nametag *nametag : nametags) {
+ v3s16 pos = floatToInt(nametag->parent_node->getPosition() +
+ intToFloat(client->getCamera()->getOffset(), BS), BS);
+ pos -= data->pos - v3s16(data->map_size / 2,
+ data->scan_height / 2,
+ data->map_size / 2);
+ if (pos.X < 0 || pos.X > data->map_size ||
+ pos.Y < 0 || pos.Y > data->scan_height ||
+ pos.Z < 0 || pos.Z > data->map_size) {
+ continue;
+ }
+ pos.X = ((float)pos.X / data->map_size) * MINIMAP_MAX_SX;
+ pos.Z = ((float)pos.Z / data->map_size) * MINIMAP_MAX_SY;
+ const video::SColor &mask_col = minimap_mask->getPixel(pos.X, pos.Z);
+ if (!mask_col.getAlpha()) {
+ continue;
+ }
+
+ m_active_markers.emplace_back(((float)pos.X / (float)MINIMAP_MAX_SX) - 0.5,
+ (1.0 - (float)pos.Z / (float)MINIMAP_MAX_SY) - 0.5);
+ }
+}
+
+////
+//// MinimapMapblock
+////
+
+void MinimapMapblock::getMinimapNodes(VoxelManipulator *vmanip, const v3s16 &pos)
+{
+
+ 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 *mmpixel = &data[z * MAP_BLOCKSIZE + x];
+
+ for (s16 y = MAP_BLOCKSIZE -1; y >= 0; y--) {
+ v3s16 p(x, y, z);
+ MapNode n = vmanip->getNodeNoEx(pos + p);
+ if (!surface_found && n.getContent() != CONTENT_AIR) {
+ mmpixel->height = y;
+ mmpixel->n = n;
+ surface_found = true;
+ } else if (n.getContent() == CONTENT_AIR) {
+ air_count++;
+ }
+ }
+
+ if (!surface_found)
+ mmpixel->n = MapNode(CONTENT_AIR);
+
+ mmpixel->air_count = air_count;
+ }
+}
diff --git a/src/client/minimap.h b/src/client/minimap.h
new file mode 100644
index 000000000..258d5330d
--- /dev/null
+++ b/src/client/minimap.h
@@ -0,0 +1,163 @@
+/*
+Minetest
+Copyright (C) 2010-2015 celeron55, Perttu Ahola <celeron55@gmail.com>
+
+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 "util/thread.h"
+#include "voxel.h"
+#include <map>
+#include <string>
+#include <vector>
+
+class Client;
+class ITextureSource;
+class IShaderSource;
+
+#define MINIMAP_MAX_SX 512
+#define MINIMAP_MAX_SY 512
+
+enum MinimapMode {
+ MINIMAP_MODE_OFF,
+ MINIMAP_MODE_SURFACEx1,
+ MINIMAP_MODE_SURFACEx2,
+ MINIMAP_MODE_SURFACEx4,
+ MINIMAP_MODE_RADARx1,
+ MINIMAP_MODE_RADARx2,
+ MINIMAP_MODE_RADARx4,
+ MINIMAP_MODE_COUNT,
+};
+
+enum MinimapShape {
+ MINIMAP_SHAPE_SQUARE,
+ MINIMAP_SHAPE_ROUND,
+};
+
+struct MinimapModeDef {
+ bool is_radar;
+ u16 scan_height;
+ u16 map_size;
+};
+
+struct MinimapPixel {
+ //! The topmost node that the minimap displays.
+ MapNode n;
+ u16 height;
+ u16 air_count;
+};
+
+struct MinimapMapblock {
+ void getMinimapNodes(VoxelManipulator *vmanip, const v3s16 &pos);
+
+ MinimapPixel data[MAP_BLOCKSIZE * MAP_BLOCKSIZE];
+};
+
+struct MinimapData {
+ bool is_radar;
+ MinimapMode mode;
+ v3s16 pos;
+ v3s16 old_pos;
+ u16 scan_height;
+ u16 map_size;
+ MinimapPixel minimap_scan[MINIMAP_MAX_SX * MINIMAP_MAX_SY];
+ bool map_invalidated;
+ bool minimap_shape_round;
+ video::IImage *minimap_mask_round = nullptr;
+ video::IImage *minimap_mask_square = nullptr;
+ video::ITexture *texture = nullptr;
+ video::ITexture *heightmap_texture = nullptr;
+ video::ITexture *minimap_overlay_round = nullptr;
+ video::ITexture *minimap_overlay_square = nullptr;
+ video::ITexture *player_marker = nullptr;
+ video::ITexture *object_marker_red = nullptr;
+};
+
+struct QueuedMinimapUpdate {
+ v3s16 pos;
+ MinimapMapblock *data = nullptr;
+};
+
+class MinimapUpdateThread : public UpdateThread {
+public:
+ MinimapUpdateThread() : UpdateThread("Minimap") {}
+ virtual ~MinimapUpdateThread();
+
+ void getMap(v3s16 pos, s16 size, s16 height);
+ void enqueueBlock(v3s16 pos, MinimapMapblock *data);
+ bool pushBlockUpdate(v3s16 pos, MinimapMapblock *data);
+ bool popBlockUpdate(QueuedMinimapUpdate *update);
+
+ MinimapData *data = nullptr;
+
+protected:
+ virtual void doUpdate();
+
+private:
+ std::mutex m_queue_mutex;
+ std::deque<QueuedMinimapUpdate> m_update_queue;
+ std::map<v3s16, MinimapMapblock *> m_blocks_cache;
+};
+
+class Minimap {
+public:
+ Minimap(Client *client);
+ ~Minimap();
+
+ void addBlock(v3s16 pos, MinimapMapblock *data);
+
+ v3f getYawVec();
+
+ void setPos(v3s16 pos);
+ v3s16 getPos() const { return data->pos; }
+ void setAngle(f32 angle);
+ f32 getAngle() const { return m_angle; }
+ void setMinimapMode(MinimapMode mode);
+ MinimapMode getMinimapMode() const { return data->mode; }
+ void toggleMinimapShape();
+ void setMinimapShape(MinimapShape shape);
+ MinimapShape getMinimapShape();
+
+
+ video::ITexture *getMinimapTexture();
+
+ void blitMinimapPixelsToImageRadar(video::IImage *map_image);
+ void blitMinimapPixelsToImageSurface(video::IImage *map_image,
+ video::IImage *heightmap_image);
+
+ scene::SMeshBuffer *getMinimapMeshBuffer();
+
+ void updateActiveMarkers();
+ void drawMinimap();
+
+ video::IVideoDriver *driver;
+ Client* client;
+ MinimapData *data;
+
+private:
+ ITextureSource *m_tsrc;
+ IShaderSource *m_shdrsrc;
+ const NodeDefManager *m_ndef;
+ MinimapUpdateThread *m_minimap_update_thread;
+ scene::SMeshBuffer *m_meshbuffer;
+ bool m_enable_shaders;
+ u16 m_surface_mode_scan_height;
+ f32 m_angle;
+ std::mutex m_mutex;
+ std::list<v2f> m_active_markers;
+};
diff --git a/src/client/particles.cpp b/src/client/particles.cpp
new file mode 100644
index 000000000..25cfa081e
--- /dev/null
+++ b/src/client/particles.cpp
@@ -0,0 +1,684 @@
+/*
+Minetest
+Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+
+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 "particles.h"
+#include <cmath>
+#include "client.h"
+#include "collision.h"
+#include "client/clientevent.h"
+#include "client/renderingengine.h"
+#include "util/numeric.h"
+#include "light.h"
+#include "environment.h"
+#include "clientmap.h"
+#include "mapnode.h"
+#include "nodedef.h"
+#include "client.h"
+#include "settings.h"
+
+/*
+ Utility
+*/
+
+v3f random_v3f(v3f min, v3f max)
+{
+ return v3f( rand()/(float)RAND_MAX*(max.X-min.X)+min.X,
+ rand()/(float)RAND_MAX*(max.Y-min.Y)+min.Y,
+ rand()/(float)RAND_MAX*(max.Z-min.Z)+min.Z);
+}
+
+Particle::Particle(
+ IGameDef *gamedef,
+ LocalPlayer *player,
+ ClientEnvironment *env,
+ v3f pos,
+ v3f velocity,
+ v3f acceleration,
+ float expirationtime,
+ float size,
+ bool collisiondetection,
+ bool collision_removal,
+ bool object_collision,
+ bool vertical,
+ video::ITexture *texture,
+ v2f texpos,
+ v2f texsize,
+ const struct TileAnimationParams &anim,
+ u8 glow,
+ video::SColor color
+):
+ scene::ISceneNode(RenderingEngine::get_scene_manager()->getRootSceneNode(),
+ RenderingEngine::get_scene_manager())
+{
+ // Misc
+ m_gamedef = gamedef;
+ m_env = env;
+
+ // Texture
+ m_material.setFlag(video::EMF_LIGHTING, false);
+ m_material.setFlag(video::EMF_BACK_FACE_CULLING, false);
+ m_material.setFlag(video::EMF_BILINEAR_FILTER, false);
+ m_material.setFlag(video::EMF_FOG_ENABLE, true);
+ m_material.MaterialType = video::EMT_TRANSPARENT_ALPHA_CHANNEL;
+ m_material.setTexture(0, texture);
+ m_texpos = texpos;
+ m_texsize = texsize;
+ m_animation = anim;
+
+ // Color
+ m_base_color = color;
+ m_color = color;
+
+ // Particle related
+ m_pos = pos;
+ m_velocity = velocity;
+ m_acceleration = acceleration;
+ m_expiration = expirationtime;
+ m_player = player;
+ m_size = size;
+ m_collisiondetection = collisiondetection;
+ m_collision_removal = collision_removal;
+ m_object_collision = object_collision;
+ m_vertical = vertical;
+ m_glow = glow;
+
+ // Irrlicht stuff
+ m_collisionbox = aabb3f
+ (-size/2,-size/2,-size/2,size/2,size/2,size/2);
+ this->setAutomaticCulling(scene::EAC_OFF);
+
+ // Init lighting
+ updateLight();
+
+ // Init model
+ updateVertices();
+}
+
+void Particle::OnRegisterSceneNode()
+{
+ if (IsVisible)
+ SceneManager->registerNodeForRendering(this, scene::ESNRP_TRANSPARENT_EFFECT);
+
+ ISceneNode::OnRegisterSceneNode();
+}
+
+void Particle::render()
+{
+ video::IVideoDriver* driver = SceneManager->getVideoDriver();
+ driver->setMaterial(m_material);
+ driver->setTransform(video::ETS_WORLD, AbsoluteTransformation);
+
+ u16 indices[] = {0,1,2, 2,3,0};
+ driver->drawVertexPrimitiveList(m_vertices, 4,
+ indices, 2, video::EVT_STANDARD,
+ scene::EPT_TRIANGLES, video::EIT_16BIT);
+}
+
+void Particle::step(float dtime)
+{
+ m_time += dtime;
+ if (m_collisiondetection) {
+ aabb3f box = m_collisionbox;
+ v3f p_pos = m_pos * BS;
+ v3f p_velocity = m_velocity * BS;
+ collisionMoveResult r = collisionMoveSimple(m_env, m_gamedef, BS * 0.5f,
+ box, 0.0f, dtime, &p_pos, &p_velocity, m_acceleration * BS, nullptr,
+ m_object_collision);
+ if (m_collision_removal && r.collides) {
+ // force expiration of the particle
+ m_expiration = -1.0;
+ } else {
+ m_pos = p_pos / BS;
+ m_velocity = p_velocity / BS;
+ }
+ } else {
+ m_velocity += m_acceleration * dtime;
+ m_pos += m_velocity * dtime;
+ }
+ if (m_animation.type != TAT_NONE) {
+ m_animation_time += dtime;
+ int frame_length_i, frame_count;
+ m_animation.determineParams(
+ m_material.getTexture(0)->getSize(),
+ &frame_count, &frame_length_i, NULL);
+ float frame_length = frame_length_i / 1000.0;
+ while (m_animation_time > frame_length) {
+ m_animation_frame++;
+ m_animation_time -= frame_length;
+ }
+ }
+
+ // Update lighting
+ updateLight();
+
+ // Update model
+ updateVertices();
+}
+
+void Particle::updateLight()
+{
+ u8 light = 0;
+ bool pos_ok;
+
+ v3s16 p = v3s16(
+ floor(m_pos.X+0.5),
+ floor(m_pos.Y+0.5),
+ floor(m_pos.Z+0.5)
+ );
+ MapNode n = m_env->getClientMap().getNodeNoEx(p, &pos_ok);
+ if (pos_ok)
+ light = n.getLightBlend(m_env->getDayNightRatio(), m_gamedef->ndef());
+ else
+ light = blend_light(m_env->getDayNightRatio(), LIGHT_SUN, 0);
+
+ u8 m_light = decode_light(light + m_glow);
+ m_color.set(255,
+ m_light * m_base_color.getRed() / 255,
+ m_light * m_base_color.getGreen() / 255,
+ m_light * m_base_color.getBlue() / 255);
+}
+
+void Particle::updateVertices()
+{
+ f32 tx0, tx1, ty0, ty1;
+
+ if (m_animation.type != TAT_NONE) {
+ const v2u32 texsize = m_material.getTexture(0)->getSize();
+ v2f texcoord, framesize_f;
+ v2u32 framesize;
+ texcoord = m_animation.getTextureCoords(texsize, m_animation_frame);
+ m_animation.determineParams(texsize, NULL, NULL, &framesize);
+ framesize_f = v2f(framesize.X / (float) texsize.X, framesize.Y / (float) texsize.Y);
+
+ tx0 = m_texpos.X + texcoord.X;
+ tx1 = m_texpos.X + texcoord.X + framesize_f.X * m_texsize.X;
+ ty0 = m_texpos.Y + texcoord.Y;
+ ty1 = m_texpos.Y + texcoord.Y + framesize_f.Y * m_texsize.Y;
+ } else {
+ tx0 = m_texpos.X;
+ tx1 = m_texpos.X + m_texsize.X;
+ ty0 = m_texpos.Y;
+ ty1 = m_texpos.Y + m_texsize.Y;
+ }
+
+ m_vertices[0] = video::S3DVertex(-m_size / 2, -m_size / 2,
+ 0, 0, 0, 0, m_color, tx0, ty1);
+ m_vertices[1] = video::S3DVertex(m_size / 2, -m_size / 2,
+ 0, 0, 0, 0, m_color, tx1, ty1);
+ m_vertices[2] = video::S3DVertex(m_size / 2, m_size / 2,
+ 0, 0, 0, 0, m_color, tx1, ty0);
+ m_vertices[3] = video::S3DVertex(-m_size / 2, m_size / 2,
+ 0, 0, 0, 0, m_color, tx0, ty0);
+
+ v3s16 camera_offset = m_env->getCameraOffset();
+ for (video::S3DVertex &vertex : m_vertices) {
+ if (m_vertical) {
+ v3f ppos = m_player->getPosition()/BS;
+ vertex.Pos.rotateXZBy(std::atan2(ppos.Z - m_pos.Z, ppos.X - m_pos.X) /
+ core::DEGTORAD + 90);
+ } else {
+ vertex.Pos.rotateYZBy(m_player->getPitch());
+ vertex.Pos.rotateXZBy(m_player->getYaw());
+ }
+ m_box.addInternalPoint(vertex.Pos);
+ vertex.Pos += m_pos*BS - intToFloat(camera_offset, BS);
+ }
+}
+
+/*
+ ParticleSpawner
+*/
+
+ParticleSpawner::ParticleSpawner(
+ IGameDef *gamedef,
+ LocalPlayer *player,
+ u16 amount,
+ float time,
+ v3f minpos, v3f maxpos,
+ v3f minvel, v3f maxvel,
+ v3f minacc, v3f maxacc,
+ float minexptime, float maxexptime,
+ float minsize, float maxsize,
+ bool collisiondetection,
+ bool collision_removal,
+ bool object_collision,
+ u16 attached_id,
+ bool vertical,
+ video::ITexture *texture,
+ u32 id,
+ const struct TileAnimationParams &anim,
+ u8 glow,
+ ParticleManager *p_manager
+):
+ m_particlemanager(p_manager)
+{
+ m_gamedef = gamedef;
+ m_player = player;
+ m_amount = amount;
+ m_spawntime = time;
+ m_minpos = minpos;
+ m_maxpos = maxpos;
+ m_minvel = minvel;
+ m_maxvel = maxvel;
+ m_minacc = minacc;
+ m_maxacc = maxacc;
+ m_minexptime = minexptime;
+ m_maxexptime = maxexptime;
+ m_minsize = minsize;
+ m_maxsize = maxsize;
+ m_collisiondetection = collisiondetection;
+ m_collision_removal = collision_removal;
+ m_object_collision = object_collision;
+ m_attached_id = attached_id;
+ m_vertical = vertical;
+ m_texture = texture;
+ m_time = 0;
+ m_animation = anim;
+ m_glow = glow;
+
+ for (u16 i = 0; i<=m_amount; i++)
+ {
+ float spawntime = (float)rand()/(float)RAND_MAX*m_spawntime;
+ m_spawntimes.push_back(spawntime);
+ }
+}
+
+void ParticleSpawner::spawnParticle(ClientEnvironment *env, float radius,
+ bool is_attached, const v3f &attached_pos, float attached_yaw)
+{
+ v3f ppos = m_player->getPosition() / BS;
+ v3f pos = random_v3f(m_minpos, m_maxpos);
+
+ // Need to apply this first or the following check
+ // will be wrong for attached spawners
+ if (is_attached) {
+ pos.rotateXZBy(attached_yaw);
+ pos += attached_pos;
+ }
+
+ if (pos.getDistanceFrom(ppos) > radius)
+ return;
+
+ v3f vel = random_v3f(m_minvel, m_maxvel);
+ v3f acc = random_v3f(m_minacc, m_maxacc);
+
+ if (is_attached) {
+ // Apply attachment yaw
+ vel.rotateXZBy(attached_yaw);
+ acc.rotateXZBy(attached_yaw);
+ }
+
+ float exptime = rand() / (float)RAND_MAX
+ * (m_maxexptime - m_minexptime)
+ + m_minexptime;
+ float size = rand() / (float)RAND_MAX
+ * (m_maxsize - m_minsize)
+ + m_minsize;
+
+ m_particlemanager->addParticle(new Particle(
+ m_gamedef,
+ m_player,
+ env,
+ pos,
+ vel,
+ acc,
+ exptime,
+ size,
+ m_collisiondetection,
+ m_collision_removal,
+ m_object_collision,
+ m_vertical,
+ m_texture,
+ v2f(0.0, 0.0),
+ v2f(1.0, 1.0),
+ m_animation,
+ m_glow
+ ));
+}
+
+void ParticleSpawner::step(float dtime, ClientEnvironment* env)
+{
+ m_time += dtime;
+
+ static thread_local const float radius =
+ g_settings->getS16("max_block_send_distance") * MAP_BLOCKSIZE;
+
+ bool unloaded = false;
+ bool is_attached = false;
+ v3f attached_pos = v3f(0,0,0);
+ float attached_yaw = 0;
+ if (m_attached_id != 0) {
+ if (ClientActiveObject *attached = env->getActiveObject(m_attached_id)) {
+ attached_pos = attached->getPosition() / BS;
+ attached_yaw = attached->getYaw();
+ is_attached = true;
+ } else {
+ unloaded = true;
+ }
+ }
+
+ if (m_spawntime != 0) {
+ // Spawner exists for a predefined timespan
+ for (std::vector<float>::iterator i = m_spawntimes.begin();
+ i != m_spawntimes.end();) {
+ if ((*i) <= m_time && m_amount > 0) {
+ m_amount--;
+
+ // Pretend to, but don't actually spawn a particle if it is
+ // attached to an unloaded object or distant from player.
+ if (!unloaded)
+ spawnParticle(env, radius, is_attached, attached_pos, attached_yaw);
+
+ i = m_spawntimes.erase(i);
+ } else {
+ ++i;
+ }
+ }
+ } else {
+ // Spawner exists for an infinity timespan, spawn on a per-second base
+
+ // Skip this step if attached to an unloaded object
+ if (unloaded)
+ return;
+
+ for (int i = 0; i <= m_amount; i++) {
+ if (rand() / (float)RAND_MAX < dtime)
+ spawnParticle(env, radius, is_attached, attached_pos, attached_yaw);
+ }
+ }
+}
+
+
+ParticleManager::ParticleManager(ClientEnvironment* env) :
+ m_env(env)
+{}
+
+ParticleManager::~ParticleManager()
+{
+ clearAll();
+}
+
+void ParticleManager::step(float dtime)
+{
+ stepParticles (dtime);
+ stepSpawners (dtime);
+}
+
+void ParticleManager::stepSpawners (float dtime)
+{
+ MutexAutoLock lock(m_spawner_list_lock);
+ for (std::map<u32, ParticleSpawner*>::iterator i =
+ m_particle_spawners.begin();
+ i != m_particle_spawners.end();)
+ {
+ if (i->second->get_expired())
+ {
+ delete i->second;
+ m_particle_spawners.erase(i++);
+ }
+ else
+ {
+ i->second->step(dtime, m_env);
+ ++i;
+ }
+ }
+}
+
+void ParticleManager::stepParticles (float dtime)
+{
+ MutexAutoLock lock(m_particle_list_lock);
+ for(std::vector<Particle*>::iterator i = m_particles.begin();
+ i != m_particles.end();)
+ {
+ if ((*i)->get_expired())
+ {
+ (*i)->remove();
+ delete *i;
+ i = m_particles.erase(i);
+ }
+ else
+ {
+ (*i)->step(dtime);
+ ++i;
+ }
+ }
+}
+
+void ParticleManager::clearAll ()
+{
+ MutexAutoLock lock(m_spawner_list_lock);
+ MutexAutoLock lock2(m_particle_list_lock);
+ for(std::map<u32, ParticleSpawner*>::iterator i =
+ m_particle_spawners.begin();
+ i != m_particle_spawners.end();)
+ {
+ delete i->second;
+ m_particle_spawners.erase(i++);
+ }
+
+ for(std::vector<Particle*>::iterator i =
+ m_particles.begin();
+ i != m_particles.end();)
+ {
+ (*i)->remove();
+ delete *i;
+ i = m_particles.erase(i);
+ }
+}
+
+void ParticleManager::handleParticleEvent(ClientEvent *event, Client *client,
+ LocalPlayer *player)
+{
+ switch (event->type) {
+ case CE_DELETE_PARTICLESPAWNER: {
+ MutexAutoLock lock(m_spawner_list_lock);
+ if (m_particle_spawners.find(event->delete_particlespawner.id) !=
+ m_particle_spawners.end()) {
+ delete m_particle_spawners.find(event->delete_particlespawner.id)->second;
+ m_particle_spawners.erase(event->delete_particlespawner.id);
+ }
+ // no allocated memory in delete event
+ break;
+ }
+ case CE_ADD_PARTICLESPAWNER: {
+ {
+ MutexAutoLock lock(m_spawner_list_lock);
+ if (m_particle_spawners.find(event->add_particlespawner.id) !=
+ m_particle_spawners.end()) {
+ delete m_particle_spawners.find(event->add_particlespawner.id)->second;
+ m_particle_spawners.erase(event->add_particlespawner.id);
+ }
+ }
+
+ video::ITexture *texture =
+ client->tsrc()->getTextureForMesh(*(event->add_particlespawner.texture));
+
+ ParticleSpawner *toadd = new ParticleSpawner(client, player,
+ event->add_particlespawner.amount,
+ event->add_particlespawner.spawntime,
+ *event->add_particlespawner.minpos,
+ *event->add_particlespawner.maxpos,
+ *event->add_particlespawner.minvel,
+ *event->add_particlespawner.maxvel,
+ *event->add_particlespawner.minacc,
+ *event->add_particlespawner.maxacc,
+ event->add_particlespawner.minexptime,
+ event->add_particlespawner.maxexptime,
+ event->add_particlespawner.minsize,
+ event->add_particlespawner.maxsize,
+ event->add_particlespawner.collisiondetection,
+ event->add_particlespawner.collision_removal,
+ event->add_particlespawner.object_collision,
+ event->add_particlespawner.attached_id,
+ event->add_particlespawner.vertical,
+ texture,
+ event->add_particlespawner.id,
+ event->add_particlespawner.animation,
+ event->add_particlespawner.glow,
+ this);
+
+ /* delete allocated content of event */
+ delete event->add_particlespawner.minpos;
+ delete event->add_particlespawner.maxpos;
+ delete event->add_particlespawner.minvel;
+ delete event->add_particlespawner.maxvel;
+ delete event->add_particlespawner.minacc;
+ delete event->add_particlespawner.texture;
+ delete event->add_particlespawner.maxacc;
+
+ {
+ MutexAutoLock lock(m_spawner_list_lock);
+ m_particle_spawners.insert(
+ std::pair<u32, ParticleSpawner*>(
+ event->add_particlespawner.id,
+ toadd));
+ }
+ break;
+ }
+ case CE_SPAWN_PARTICLE: {
+ video::ITexture *texture =
+ client->tsrc()->getTextureForMesh(*(event->spawn_particle.texture));
+
+ Particle *toadd = new Particle(client, player, m_env,
+ *event->spawn_particle.pos,
+ *event->spawn_particle.vel,
+ *event->spawn_particle.acc,
+ event->spawn_particle.expirationtime,
+ event->spawn_particle.size,
+ event->spawn_particle.collisiondetection,
+ event->spawn_particle.collision_removal,
+ event->spawn_particle.object_collision,
+ event->spawn_particle.vertical,
+ texture,
+ v2f(0.0, 0.0),
+ v2f(1.0, 1.0),
+ event->spawn_particle.animation,
+ event->spawn_particle.glow);
+
+ addParticle(toadd);
+
+ delete event->spawn_particle.pos;
+ delete event->spawn_particle.vel;
+ delete event->spawn_particle.acc;
+ delete event->spawn_particle.texture;
+
+ break;
+ }
+ default: break;
+ }
+}
+
+// The final burst of particles when a node is finally dug, *not* particles
+// spawned during the digging of a node.
+
+void ParticleManager::addDiggingParticles(IGameDef* gamedef,
+ LocalPlayer *player, v3s16 pos, const MapNode &n, const ContentFeatures &f)
+{
+ // No particles for "airlike" nodes
+ if (f.drawtype == NDT_AIRLIKE)
+ return;
+
+ for (u16 j = 0; j < 16; j++) {
+ addNodeParticle(gamedef, player, pos, n, f);
+ }
+}
+
+// During the digging of a node particles are spawned individually by this
+// function, called from Game::handleDigging() in game.cpp.
+
+void ParticleManager::addNodeParticle(IGameDef* gamedef,
+ LocalPlayer *player, v3s16 pos, const MapNode &n, const ContentFeatures &f)
+{
+ // No particles for "airlike" nodes
+ if (f.drawtype == NDT_AIRLIKE)
+ return;
+
+ // Texture
+ u8 texid = myrand_range(0, 5);
+ const TileLayer &tile = f.tiles[texid].layers[0];
+ video::ITexture *texture;
+ struct TileAnimationParams anim;
+ anim.type = TAT_NONE;
+
+ // Only use first frame of animated texture
+ if (tile.material_flags & MATERIAL_FLAG_ANIMATION)
+ texture = (*tile.frames)[0].texture;
+ else
+ texture = tile.texture;
+
+ float size = (rand() % 8) / 64.0f;
+ float visual_size = BS * size;
+ if (tile.scale)
+ size /= tile.scale;
+ v2f texsize(size * 2.0f, size * 2.0f);
+ v2f texpos;
+ texpos.X = (rand() % 64) / 64.0f - texsize.X;
+ texpos.Y = (rand() % 64) / 64.0f - texsize.Y;
+
+ // Physics
+ v3f velocity(
+ (rand() % 150) / 50.0f - 1.5f,
+ (rand() % 150) / 50.0f,
+ (rand() % 150) / 50.0f - 1.5f
+ );
+ v3f acceleration(
+ 0.0f,
+ -player->movement_gravity * player->physics_override_gravity / BS,
+ 0.0f
+ );
+ v3f particlepos = v3f(
+ (f32)pos.X + (rand() % 100) / 200.0f - 0.25f,
+ (f32)pos.Y + (rand() % 100) / 200.0f - 0.25f,
+ (f32)pos.Z + (rand() % 100) / 200.0f - 0.25f
+ );
+
+ video::SColor color;
+ if (tile.has_color)
+ color = tile.color;
+ else
+ n.getColor(f, &color);
+
+ Particle* toadd = new Particle(
+ gamedef,
+ player,
+ m_env,
+ particlepos,
+ velocity,
+ acceleration,
+ (rand() % 100) / 100.0f, // expiration time
+ visual_size,
+ true,
+ false,
+ false,
+ false,
+ texture,
+ texpos,
+ texsize,
+ anim,
+ 0,
+ color);
+
+ addParticle(toadd);
+}
+
+void ParticleManager::addParticle(Particle* toadd)
+{
+ MutexAutoLock lock(m_particle_list_lock);
+ m_particles.push_back(toadd);
+}
diff --git a/src/client/particles.h b/src/client/particles.h
new file mode 100644
index 000000000..3392e7e95
--- /dev/null
+++ b/src/client/particles.h
@@ -0,0 +1,223 @@
+/*
+Minetest
+Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+
+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 <iostream>
+#include "irrlichttypes_extrabloated.h"
+#include "client/tile.h"
+#include "localplayer.h"
+#include "tileanimation.h"
+
+struct ClientEvent;
+class ParticleManager;
+class ClientEnvironment;
+struct MapNode;
+struct ContentFeatures;
+
+class Particle : public scene::ISceneNode
+{
+ public:
+ Particle(
+ IGameDef* gamedef,
+ LocalPlayer *player,
+ ClientEnvironment *env,
+ v3f pos,
+ v3f velocity,
+ v3f acceleration,
+ float expirationtime,
+ float size,
+ bool collisiondetection,
+ bool collision_removal,
+ bool object_collision,
+ bool vertical,
+ video::ITexture *texture,
+ v2f texpos,
+ v2f texsize,
+ const struct TileAnimationParams &anim,
+ u8 glow,
+ video::SColor color = video::SColor(0xFFFFFFFF)
+ );
+ ~Particle() = default;
+
+ virtual const aabb3f &getBoundingBox() const
+ {
+ return m_box;
+ }
+
+ virtual u32 getMaterialCount() const
+ {
+ return 1;
+ }
+
+ virtual video::SMaterial& getMaterial(u32 i)
+ {
+ return m_material;
+ }
+
+ virtual void OnRegisterSceneNode();
+ virtual void render();
+
+ void step(float dtime);
+
+ bool get_expired ()
+ { return m_expiration < m_time; }
+
+private:
+ void updateLight();
+ void updateVertices();
+
+ video::S3DVertex m_vertices[4];
+ float m_time = 0.0f;
+ float m_expiration;
+
+ ClientEnvironment *m_env;
+ IGameDef *m_gamedef;
+ aabb3f m_box;
+ aabb3f m_collisionbox;
+ video::SMaterial m_material;
+ v2f m_texpos;
+ v2f m_texsize;
+ v3f m_pos;
+ v3f m_velocity;
+ v3f m_acceleration;
+ LocalPlayer *m_player;
+ float m_size;
+ //! Color without lighting
+ video::SColor m_base_color;
+ //! Final rendered color
+ video::SColor m_color;
+ bool m_collisiondetection;
+ bool m_collision_removal;
+ bool m_object_collision;
+ bool m_vertical;
+ v3s16 m_camera_offset;
+ struct TileAnimationParams m_animation;
+ float m_animation_time = 0.0f;
+ int m_animation_frame = 0;
+ u8 m_glow;
+};
+
+class ParticleSpawner
+{
+public:
+ ParticleSpawner(IGameDef* gamedef,
+ LocalPlayer *player,
+ u16 amount,
+ float time,
+ v3f minp, v3f maxp,
+ v3f minvel, v3f maxvel,
+ v3f minacc, v3f maxacc,
+ float minexptime, float maxexptime,
+ float minsize, float maxsize,
+ bool collisiondetection,
+ bool collision_removal,
+ bool object_collision,
+ u16 attached_id,
+ bool vertical,
+ video::ITexture *texture,
+ u32 id,
+ const struct TileAnimationParams &anim, u8 glow,
+ ParticleManager* p_manager);
+
+ ~ParticleSpawner() = default;
+
+ void step(float dtime, ClientEnvironment *env);
+
+ bool get_expired ()
+ { return (m_amount <= 0) && m_spawntime != 0; }
+
+private:
+ void spawnParticle(ClientEnvironment *env, float radius,
+ bool is_attached, const v3f &attached_pos,
+ float attached_yaw);
+
+ ParticleManager *m_particlemanager;
+ float m_time;
+ IGameDef *m_gamedef;
+ LocalPlayer *m_player;
+ u16 m_amount;
+ float m_spawntime;
+ v3f m_minpos;
+ v3f m_maxpos;
+ v3f m_minvel;
+ v3f m_maxvel;
+ v3f m_minacc;
+ v3f m_maxacc;
+ float m_minexptime;
+ float m_maxexptime;
+ float m_minsize;
+ float m_maxsize;
+ video::ITexture *m_texture;
+ std::vector<float> m_spawntimes;
+ bool m_collisiondetection;
+ bool m_collision_removal;
+ bool m_object_collision;
+ bool m_vertical;
+ u16 m_attached_id;
+ struct TileAnimationParams m_animation;
+ u8 m_glow;
+};
+
+/**
+ * Class doing particle as well as their spawners handling
+ */
+class ParticleManager
+{
+friend class ParticleSpawner;
+public:
+ ParticleManager(ClientEnvironment* env);
+ ~ParticleManager();
+
+ void step (float dtime);
+
+ void handleParticleEvent(ClientEvent *event, Client *client,
+ LocalPlayer *player);
+
+ void addDiggingParticles(IGameDef *gamedef, LocalPlayer *player, v3s16 pos,
+ const MapNode &n, const ContentFeatures &f);
+
+ void addNodeParticle(IGameDef *gamedef, LocalPlayer *player, v3s16 pos,
+ const MapNode &n, const ContentFeatures &f);
+
+ u32 getSpawnerId() const
+ {
+ for (u32 id = 0;; ++id) { // look for unused particlespawner id
+ if (m_particle_spawners.find(id) == m_particle_spawners.end())
+ return id;
+ }
+ }
+
+protected:
+ void addParticle(Particle* toadd);
+
+private:
+
+ void stepParticles (float dtime);
+ void stepSpawners (float dtime);
+
+ void clearAll ();
+
+ std::vector<Particle*> m_particles;
+ std::map<u32, ParticleSpawner*> m_particle_spawners;
+
+ ClientEnvironment* m_env;
+ std::mutex m_particle_list_lock;
+ std::mutex m_spawner_list_lock;
+};
diff --git a/src/client/render/core.cpp b/src/client/render/core.cpp
index bd280bc73..8c70b36c6 100644
--- a/src/client/render/core.cpp
+++ b/src/client/render/core.cpp
@@ -19,11 +19,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,
*/
#include "core.h"
-#include "camera.h"
-#include "client.h"
-#include "clientmap.h"
-#include "hud.h"
-#include "minimap.h"
+#include "client/camera.h"
+#include "client/client.h"
+#include "client/clientmap.h"
+#include "client/hud.h"
+#include "client/minimap.h"
RenderingCore::RenderingCore(IrrlichtDevice *_device, Client *_client, Hud *_hud)
: device(_device), driver(device->getVideoDriver()), smgr(device->getSceneManager()),
diff --git a/src/client/render/interlaced.cpp b/src/client/render/interlaced.cpp
index 87d75de30..2aadadc17 100644
--- a/src/client/render/interlaced.cpp
+++ b/src/client/render/interlaced.cpp
@@ -19,8 +19,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
*/
#include "interlaced.h"
-#include "client.h"
-#include "shader.h"
+#include "client/client.h"
+#include "client/shader.h"
#include "client/tile.h"
RenderingCoreInterlaced::RenderingCoreInterlaced(
diff --git a/src/client/render/stereo.cpp b/src/client/render/stereo.cpp
index 4a5761bb7..967b5a78f 100644
--- a/src/client/render/stereo.cpp
+++ b/src/client/render/stereo.cpp
@@ -19,7 +19,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
*/
#include "stereo.h"
-#include "camera.h"
+#include "client/camera.h"
#include "constants.h"
#include "settings.h"
diff --git a/src/client/shader.cpp b/src/client/shader.cpp
new file mode 100644
index 000000000..3b49a36ba
--- /dev/null
+++ b/src/client/shader.cpp
@@ -0,0 +1,873 @@
+/*
+Minetest
+Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+Copyright (C) 2013 Kahrl <kahrl@gmx.net>
+
+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 <fstream>
+#include <iterator>
+#include "shader.h"
+#include "irrlichttypes_extrabloated.h"
+#include "debug.h"
+#include "filesys.h"
+#include "util/container.h"
+#include "util/thread.h"
+#include "settings.h"
+#include <ICameraSceneNode.h>
+#include <IGPUProgrammingServices.h>
+#include <IMaterialRenderer.h>
+#include <IMaterialRendererServices.h>
+#include <IShaderConstantSetCallBack.h>
+#include "client/renderingengine.h"
+#include "EShaderTypes.h"
+#include "log.h"
+#include "gamedef.h"
+#include "client/tile.h"
+
+/*
+ A cache from shader name to shader path
+*/
+MutexedMap<std::string, std::string> g_shadername_to_path_cache;
+
+/*
+ Gets the path to a shader by first checking if the file
+ name_of_shader/filename
+ exists in shader_path and if not, using the data path.
+
+ If not found, returns "".
+
+ Utilizes a thread-safe cache.
+*/
+std::string getShaderPath(const std::string &name_of_shader,
+ const std::string &filename)
+{
+ std::string combined = name_of_shader + DIR_DELIM + filename;
+ std::string fullpath;
+ /*
+ Check from cache
+ */
+ bool incache = g_shadername_to_path_cache.get(combined, &fullpath);
+ if(incache)
+ return fullpath;
+
+ /*
+ Check from shader_path
+ */
+ std::string shader_path = g_settings->get("shader_path");
+ if (!shader_path.empty()) {
+ std::string testpath = shader_path + DIR_DELIM + combined;
+ if(fs::PathExists(testpath))
+ fullpath = testpath;
+ }
+
+ /*
+ Check from default data directory
+ */
+ if (fullpath.empty()) {
+ std::string rel_path = std::string("client") + DIR_DELIM
+ + "shaders" + DIR_DELIM
+ + name_of_shader + DIR_DELIM
+ + filename;
+ std::string testpath = porting::path_share + DIR_DELIM + rel_path;
+ if(fs::PathExists(testpath))
+ fullpath = testpath;
+ }
+
+ // Add to cache (also an empty result is cached)
+ g_shadername_to_path_cache.set(combined, fullpath);
+
+ // Finally return it
+ return fullpath;
+}
+
+/*
+ SourceShaderCache: A cache used for storing source shaders.
+*/
+
+class SourceShaderCache
+{
+public:
+ void insert(const std::string &name_of_shader, const std::string &filename,
+ const std::string &program, bool prefer_local)
+ {
+ std::string combined = name_of_shader + DIR_DELIM + filename;
+ // Try to use local shader instead if asked to
+ if(prefer_local){
+ std::string path = getShaderPath(name_of_shader, filename);
+ if(!path.empty()){
+ std::string p = readFile(path);
+ if (!p.empty()) {
+ m_programs[combined] = p;
+ return;
+ }
+ }
+ }
+ m_programs[combined] = program;
+ }
+
+ std::string get(const std::string &name_of_shader,
+ const std::string &filename)
+ {
+ std::string combined = name_of_shader + DIR_DELIM + filename;
+ StringMap::iterator n = m_programs.find(combined);
+ if (n != m_programs.end())
+ return n->second;
+ return "";
+ }
+
+ // Primarily fetches from cache, secondarily tries to read from filesystem
+ std::string getOrLoad(const std::string &name_of_shader,
+ const std::string &filename)
+ {
+ std::string combined = name_of_shader + DIR_DELIM + filename;
+ StringMap::iterator n = m_programs.find(combined);
+ if (n != m_programs.end())
+ return n->second;
+ std::string path = getShaderPath(name_of_shader, filename);
+ if (path.empty()) {
+ infostream << "SourceShaderCache::getOrLoad(): No path found for \""
+ << combined << "\"" << std::endl;
+ return "";
+ }
+ infostream << "SourceShaderCache::getOrLoad(): Loading path \""
+ << path << "\"" << std::endl;
+ std::string p = readFile(path);
+ if (!p.empty()) {
+ m_programs[combined] = p;
+ return p;
+ }
+ return "";
+ }
+private:
+ StringMap m_programs;
+
+ std::string readFile(const std::string &path)
+ {
+ std::ifstream is(path.c_str(), std::ios::binary);
+ if(!is.is_open())
+ return "";
+ std::ostringstream tmp_os;
+ tmp_os << is.rdbuf();
+ return tmp_os.str();
+ }
+};
+
+
+/*
+ ShaderCallback: Sets constants that can be used in shaders
+*/
+
+class ShaderCallback : public video::IShaderConstantSetCallBack
+{
+ std::vector<IShaderConstantSetter*> m_setters;
+
+public:
+ ShaderCallback(const std::vector<IShaderConstantSetterFactory *> &factories)
+ {
+ for (IShaderConstantSetterFactory *factory : factories)
+ m_setters.push_back(factory->create());
+ }
+
+ ~ShaderCallback()
+ {
+ for (IShaderConstantSetter *setter : m_setters)
+ delete setter;
+ }
+
+ virtual void OnSetConstants(video::IMaterialRendererServices *services, s32 userData)
+ {
+ video::IVideoDriver *driver = services->getVideoDriver();
+ sanity_check(driver != NULL);
+
+ bool is_highlevel = userData;
+
+ for (IShaderConstantSetter *setter : m_setters)
+ setter->onSetConstants(services, is_highlevel);
+ }
+};
+
+
+/*
+ MainShaderConstantSetter: Set basic constants required for almost everything
+*/
+
+class MainShaderConstantSetter : public IShaderConstantSetter
+{
+ CachedVertexShaderSetting<float, 16> m_world_view_proj;
+ CachedVertexShaderSetting<float, 16> m_world;
+
+public:
+ MainShaderConstantSetter() :
+ m_world_view_proj("mWorldViewProj"),
+ m_world("mWorld")
+ {}
+ ~MainShaderConstantSetter() = default;
+
+ virtual void onSetConstants(video::IMaterialRendererServices *services,
+ bool is_highlevel)
+ {
+ video::IVideoDriver *driver = services->getVideoDriver();
+ sanity_check(driver);
+
+ // Set clip matrix
+ core::matrix4 worldViewProj;
+ worldViewProj = driver->getTransform(video::ETS_PROJECTION);
+ worldViewProj *= driver->getTransform(video::ETS_VIEW);
+ worldViewProj *= driver->getTransform(video::ETS_WORLD);
+ if (is_highlevel)
+ m_world_view_proj.set(*reinterpret_cast<float(*)[16]>(worldViewProj.pointer()), services);
+ else
+ services->setVertexShaderConstant(worldViewProj.pointer(), 0, 4);
+
+ // Set world matrix
+ core::matrix4 world = driver->getTransform(video::ETS_WORLD);
+ if (is_highlevel)
+ m_world.set(*reinterpret_cast<float(*)[16]>(world.pointer()), services);
+ else
+ services->setVertexShaderConstant(world.pointer(), 4, 4);
+
+ }
+};
+
+
+class MainShaderConstantSetterFactory : public IShaderConstantSetterFactory
+{
+public:
+ virtual IShaderConstantSetter* create()
+ { return new MainShaderConstantSetter(); }
+};
+
+
+/*
+ ShaderSource
+*/
+
+class ShaderSource : public IWritableShaderSource
+{
+public:
+ ShaderSource();
+ ~ShaderSource();
+
+ /*
+ - If shader material specified by name is found from cache,
+ return the cached id.
+ - Otherwise generate the shader material, add to cache and return id.
+
+ The id 0 points to a null shader. Its material is EMT_SOLID.
+ */
+ u32 getShaderIdDirect(const std::string &name,
+ const u8 material_type, const u8 drawtype);
+
+ /*
+ If shader specified by the name pointed by the id doesn't
+ exist, create it, then return id.
+
+ Can be called from any thread. If called from some other thread
+ and not found in cache, the call is queued to the main thread
+ for processing.
+ */
+
+ u32 getShader(const std::string &name,
+ const u8 material_type, const u8 drawtype);
+
+ ShaderInfo getShaderInfo(u32 id);
+
+ // Processes queued shader requests from other threads.
+ // Shall be called from the main thread.
+ void processQueue();
+
+ // Insert a shader program into the cache without touching the
+ // filesystem. Shall be called from the main thread.
+ void insertSourceShader(const std::string &name_of_shader,
+ const std::string &filename, const std::string &program);
+
+ // Rebuild shaders from the current set of source shaders
+ // Shall be called from the main thread.
+ void rebuildShaders();
+
+ void addShaderConstantSetterFactory(IShaderConstantSetterFactory *setter)
+ {
+ m_setter_factories.push_back(setter);
+ }
+
+private:
+
+ // The id of the thread that is allowed to use irrlicht directly
+ std::thread::id m_main_thread;
+
+ // Cache of source shaders
+ // This should be only accessed from the main thread
+ SourceShaderCache m_sourcecache;
+
+ // A shader id is index in this array.
+ // The first position contains a dummy shader.
+ std::vector<ShaderInfo> m_shaderinfo_cache;
+ // The former container is behind this mutex
+ std::mutex m_shaderinfo_cache_mutex;
+
+ // Queued shader fetches (to be processed by the main thread)
+ RequestQueue<std::string, u32, u8, u8> m_get_shader_queue;
+
+ // Global constant setter factories
+ std::vector<IShaderConstantSetterFactory *> m_setter_factories;
+
+ // Shader callbacks
+ std::vector<ShaderCallback *> m_callbacks;
+};
+
+IWritableShaderSource *createShaderSource()
+{
+ return new ShaderSource();
+}
+
+/*
+ Generate shader given the shader name.
+*/
+ShaderInfo generate_shader(const std::string &name,
+ u8 material_type, u8 drawtype, std::vector<ShaderCallback *> &callbacks,
+ const std::vector<IShaderConstantSetterFactory *> &setter_factories,
+ SourceShaderCache *sourcecache);
+
+/*
+ Load shader programs
+*/
+void load_shaders(const std::string &name, SourceShaderCache *sourcecache,
+ video::E_DRIVER_TYPE drivertype, bool enable_shaders,
+ std::string &vertex_program, std::string &pixel_program,
+ std::string &geometry_program, bool &is_highlevel);
+
+ShaderSource::ShaderSource()
+{
+ m_main_thread = std::this_thread::get_id();
+
+ // Add a dummy ShaderInfo as the first index, named ""
+ m_shaderinfo_cache.emplace_back();
+
+ // Add main global constant setter
+ addShaderConstantSetterFactory(new MainShaderConstantSetterFactory());
+}
+
+ShaderSource::~ShaderSource()
+{
+ for (ShaderCallback *callback : m_callbacks) {
+ delete callback;
+ }
+ for (IShaderConstantSetterFactory *setter_factorie : m_setter_factories) {
+ delete setter_factorie;
+ }
+}
+
+u32 ShaderSource::getShader(const std::string &name,
+ const u8 material_type, const u8 drawtype)
+{
+ /*
+ Get shader
+ */
+
+ if (std::this_thread::get_id() == m_main_thread) {
+ return getShaderIdDirect(name, material_type, drawtype);
+ }
+
+ /*errorstream<<"getShader(): Queued: name=\""<<name<<"\""<<std::endl;*/
+
+ // We're gonna ask the result to be put into here
+
+ static ResultQueue<std::string, u32, u8, u8> result_queue;
+
+ // Throw a request in
+ m_get_shader_queue.add(name, 0, 0, &result_queue);
+
+ /* infostream<<"Waiting for shader from main thread, name=\""
+ <<name<<"\""<<std::endl;*/
+
+ while(true) {
+ GetResult<std::string, u32, u8, u8>
+ result = result_queue.pop_frontNoEx();
+
+ if (result.key == name) {
+ return result.item;
+ }
+
+ errorstream << "Got shader with invalid name: " << result.key << std::endl;
+ }
+
+ infostream << "getShader(): Failed" << std::endl;
+
+ return 0;
+}
+
+/*
+ This method generates all the shaders
+*/
+u32 ShaderSource::getShaderIdDirect(const std::string &name,
+ const u8 material_type, const u8 drawtype)
+{
+ //infostream<<"getShaderIdDirect(): name=\""<<name<<"\""<<std::endl;
+
+ // Empty name means shader 0
+ if (name.empty()) {
+ infostream<<"getShaderIdDirect(): name is empty"<<std::endl;
+ return 0;
+ }
+
+ // Check if already have such instance
+ for(u32 i=0; i<m_shaderinfo_cache.size(); i++){
+ ShaderInfo *info = &m_shaderinfo_cache[i];
+ if(info->name == name && info->material_type == material_type &&
+ info->drawtype == drawtype)
+ return i;
+ }
+
+ /*
+ Calling only allowed from main thread
+ */
+ if (std::this_thread::get_id() != m_main_thread) {
+ errorstream<<"ShaderSource::getShaderIdDirect() "
+ "called not from main thread"<<std::endl;
+ return 0;
+ }
+
+ ShaderInfo info = generate_shader(name, material_type, drawtype,
+ m_callbacks, m_setter_factories, &m_sourcecache);
+
+ /*
+ Add shader to caches (add dummy shaders too)
+ */
+
+ MutexAutoLock lock(m_shaderinfo_cache_mutex);
+
+ u32 id = m_shaderinfo_cache.size();
+ m_shaderinfo_cache.push_back(info);
+
+ infostream<<"getShaderIdDirect(): "
+ <<"Returning id="<<id<<" for name \""<<name<<"\""<<std::endl;
+
+ return id;
+}
+
+
+ShaderInfo ShaderSource::getShaderInfo(u32 id)
+{
+ MutexAutoLock lock(m_shaderinfo_cache_mutex);
+
+ if(id >= m_shaderinfo_cache.size())
+ return ShaderInfo();
+
+ return m_shaderinfo_cache[id];
+}
+
+void ShaderSource::processQueue()
+{
+
+
+}
+
+void ShaderSource::insertSourceShader(const std::string &name_of_shader,
+ const std::string &filename, const std::string &program)
+{
+ /*infostream<<"ShaderSource::insertSourceShader(): "
+ "name_of_shader=\""<<name_of_shader<<"\", "
+ "filename=\""<<filename<<"\""<<std::endl;*/
+
+ sanity_check(std::this_thread::get_id() == m_main_thread);
+
+ m_sourcecache.insert(name_of_shader, filename, program, true);
+}
+
+void ShaderSource::rebuildShaders()
+{
+ MutexAutoLock lock(m_shaderinfo_cache_mutex);
+
+ /*// Oh well... just clear everything, they'll load sometime.
+ m_shaderinfo_cache.clear();
+ m_name_to_id.clear();*/
+
+ /*
+ FIXME: Old shader materials can't be deleted in Irrlicht,
+ or can they?
+ (This would be nice to do in the destructor too)
+ */
+
+ // Recreate shaders
+ for (ShaderInfo &i : m_shaderinfo_cache) {
+ ShaderInfo *info = &i;
+ if (!info->name.empty()) {
+ *info = generate_shader(info->name, info->material_type,
+ info->drawtype, m_callbacks,
+ m_setter_factories, &m_sourcecache);
+ }
+ }
+}
+
+
+ShaderInfo generate_shader(const std::string &name, u8 material_type, u8 drawtype,
+ std::vector<ShaderCallback *> &callbacks,
+ const std::vector<IShaderConstantSetterFactory *> &setter_factories,
+ SourceShaderCache *sourcecache)
+{
+ ShaderInfo shaderinfo;
+ shaderinfo.name = name;
+ shaderinfo.material_type = material_type;
+ shaderinfo.drawtype = drawtype;
+ shaderinfo.material = video::EMT_SOLID;
+ switch (material_type) {
+ case TILE_MATERIAL_OPAQUE:
+ case TILE_MATERIAL_LIQUID_OPAQUE:
+ shaderinfo.base_material = video::EMT_SOLID;
+ break;
+ case TILE_MATERIAL_ALPHA:
+ case TILE_MATERIAL_LIQUID_TRANSPARENT:
+ shaderinfo.base_material = video::EMT_TRANSPARENT_ALPHA_CHANNEL;
+ break;
+ case TILE_MATERIAL_BASIC:
+ case TILE_MATERIAL_WAVING_LEAVES:
+ case TILE_MATERIAL_WAVING_PLANTS:
+ shaderinfo.base_material = video::EMT_TRANSPARENT_ALPHA_CHANNEL_REF;
+ break;
+ }
+
+ bool enable_shaders = g_settings->getBool("enable_shaders");
+ if (!enable_shaders)
+ return shaderinfo;
+
+ video::IVideoDriver *driver = RenderingEngine::get_video_driver();
+
+ video::IGPUProgrammingServices *gpu = driver->getGPUProgrammingServices();
+ if(!gpu){
+ errorstream<<"generate_shader(): "
+ "failed to generate \""<<name<<"\", "
+ "GPU programming not supported."
+ <<std::endl;
+ return shaderinfo;
+ }
+
+ // Choose shader language depending on driver type and settings
+ // Then load shaders
+ std::string vertex_program;
+ std::string pixel_program;
+ std::string geometry_program;
+ bool is_highlevel;
+ load_shaders(name, sourcecache, driver->getDriverType(),
+ enable_shaders, vertex_program, pixel_program,
+ geometry_program, is_highlevel);
+ // Check hardware/driver support
+ if (!vertex_program.empty() &&
+ !driver->queryFeature(video::EVDF_VERTEX_SHADER_1_1) &&
+ !driver->queryFeature(video::EVDF_ARB_VERTEX_PROGRAM_1)){
+ infostream<<"generate_shader(): vertex shaders disabled "
+ "because of missing driver/hardware support."
+ <<std::endl;
+ vertex_program = "";
+ }
+ if (!pixel_program.empty() &&
+ !driver->queryFeature(video::EVDF_PIXEL_SHADER_1_1) &&
+ !driver->queryFeature(video::EVDF_ARB_FRAGMENT_PROGRAM_1)){
+ infostream<<"generate_shader(): pixel shaders disabled "
+ "because of missing driver/hardware support."
+ <<std::endl;
+ pixel_program = "";
+ }
+ if (!geometry_program.empty() &&
+ !driver->queryFeature(video::EVDF_GEOMETRY_SHADER)){
+ infostream<<"generate_shader(): geometry shaders disabled "
+ "because of missing driver/hardware support."
+ <<std::endl;
+ geometry_program = "";
+ }
+
+ // If no shaders are used, don't make a separate material type
+ if (vertex_program.empty() && pixel_program.empty() && geometry_program.empty())
+ return shaderinfo;
+
+ // Create shaders header
+ std::string shaders_header = "#version 120\n";
+
+ static const char* drawTypes[] = {
+ "NDT_NORMAL",
+ "NDT_AIRLIKE",
+ "NDT_LIQUID",
+ "NDT_FLOWINGLIQUID",
+ "NDT_GLASSLIKE",
+ "NDT_ALLFACES",
+ "NDT_ALLFACES_OPTIONAL",
+ "NDT_TORCHLIKE",
+ "NDT_SIGNLIKE",
+ "NDT_PLANTLIKE",
+ "NDT_FENCELIKE",
+ "NDT_RAILLIKE",
+ "NDT_NODEBOX",
+ "NDT_GLASSLIKE_FRAMED",
+ "NDT_FIRELIKE",
+ "NDT_GLASSLIKE_FRAMED_OPTIONAL",
+ "NDT_PLANTLIKE_ROOTED",
+ };
+
+ for (int i = 0; i < 14; i++){
+ shaders_header += "#define ";
+ shaders_header += drawTypes[i];
+ shaders_header += " ";
+ shaders_header += itos(i);
+ shaders_header += "\n";
+ }
+
+ static const char* materialTypes[] = {
+ "TILE_MATERIAL_BASIC",
+ "TILE_MATERIAL_ALPHA",
+ "TILE_MATERIAL_LIQUID_TRANSPARENT",
+ "TILE_MATERIAL_LIQUID_OPAQUE",
+ "TILE_MATERIAL_WAVING_LEAVES",
+ "TILE_MATERIAL_WAVING_PLANTS",
+ "TILE_MATERIAL_OPAQUE"
+ };
+
+ for (int i = 0; i < 7; i++){
+ shaders_header += "#define ";
+ shaders_header += materialTypes[i];
+ shaders_header += " ";
+ shaders_header += itos(i);
+ shaders_header += "\n";
+ }
+
+ shaders_header += "#define MATERIAL_TYPE ";
+ shaders_header += itos(material_type);
+ shaders_header += "\n";
+ shaders_header += "#define DRAW_TYPE ";
+ shaders_header += itos(drawtype);
+ shaders_header += "\n";
+
+ if (g_settings->getBool("generate_normalmaps")) {
+ shaders_header += "#define GENERATE_NORMALMAPS 1\n";
+ } else {
+ shaders_header += "#define GENERATE_NORMALMAPS 0\n";
+ }
+ shaders_header += "#define NORMALMAPS_STRENGTH ";
+ shaders_header += ftos(g_settings->getFloat("normalmaps_strength"));
+ shaders_header += "\n";
+ float sample_step;
+ int smooth = (int)g_settings->getFloat("normalmaps_smooth");
+ switch (smooth){
+ case 0:
+ sample_step = 0.0078125; // 1.0 / 128.0
+ break;
+ case 1:
+ sample_step = 0.00390625; // 1.0 / 256.0
+ break;
+ case 2:
+ sample_step = 0.001953125; // 1.0 / 512.0
+ break;
+ default:
+ sample_step = 0.0078125;
+ break;
+ }
+ shaders_header += "#define SAMPLE_STEP ";
+ shaders_header += ftos(sample_step);
+ shaders_header += "\n";
+
+ if (g_settings->getBool("enable_bumpmapping"))
+ shaders_header += "#define ENABLE_BUMPMAPPING\n";
+
+ if (g_settings->getBool("enable_parallax_occlusion")){
+ int mode = g_settings->getFloat("parallax_occlusion_mode");
+ float scale = g_settings->getFloat("parallax_occlusion_scale");
+ float bias = g_settings->getFloat("parallax_occlusion_bias");
+ int iterations = g_settings->getFloat("parallax_occlusion_iterations");
+ shaders_header += "#define ENABLE_PARALLAX_OCCLUSION\n";
+ shaders_header += "#define PARALLAX_OCCLUSION_MODE ";
+ shaders_header += itos(mode);
+ shaders_header += "\n";
+ shaders_header += "#define PARALLAX_OCCLUSION_SCALE ";
+ shaders_header += ftos(scale);
+ shaders_header += "\n";
+ shaders_header += "#define PARALLAX_OCCLUSION_BIAS ";
+ shaders_header += ftos(bias);
+ shaders_header += "\n";
+ shaders_header += "#define PARALLAX_OCCLUSION_ITERATIONS ";
+ shaders_header += itos(iterations);
+ shaders_header += "\n";
+ }
+
+ shaders_header += "#define USE_NORMALMAPS ";
+ if (g_settings->getBool("enable_bumpmapping") || g_settings->getBool("enable_parallax_occlusion"))
+ shaders_header += "1\n";
+ else
+ shaders_header += "0\n";
+
+ if (g_settings->getBool("enable_waving_water")){
+ shaders_header += "#define ENABLE_WAVING_WATER 1\n";
+ shaders_header += "#define WATER_WAVE_HEIGHT ";
+ shaders_header += ftos(g_settings->getFloat("water_wave_height"));
+ shaders_header += "\n";
+ shaders_header += "#define WATER_WAVE_LENGTH ";
+ shaders_header += ftos(g_settings->getFloat("water_wave_length"));
+ shaders_header += "\n";
+ shaders_header += "#define WATER_WAVE_SPEED ";
+ shaders_header += ftos(g_settings->getFloat("water_wave_speed"));
+ shaders_header += "\n";
+ } else{
+ shaders_header += "#define ENABLE_WAVING_WATER 0\n";
+ }
+
+ shaders_header += "#define ENABLE_WAVING_LEAVES ";
+ if (g_settings->getBool("enable_waving_leaves"))
+ shaders_header += "1\n";
+ else
+ shaders_header += "0\n";
+
+ shaders_header += "#define ENABLE_WAVING_PLANTS ";
+ if (g_settings->getBool("enable_waving_plants"))
+ shaders_header += "1\n";
+ else
+ shaders_header += "0\n";
+
+ if (g_settings->getBool("tone_mapping"))
+ shaders_header += "#define ENABLE_TONE_MAPPING\n";
+
+ shaders_header += "#define FOG_START ";
+ shaders_header += ftos(rangelim(g_settings->getFloat("fog_start"), 0.0f, 0.99f));
+ shaders_header += "\n";
+
+ // Call addHighLevelShaderMaterial() or addShaderMaterial()
+ const c8* vertex_program_ptr = 0;
+ const c8* pixel_program_ptr = 0;
+ const c8* geometry_program_ptr = 0;
+ if (!vertex_program.empty()) {
+ vertex_program = shaders_header + vertex_program;
+ vertex_program_ptr = vertex_program.c_str();
+ }
+ if (!pixel_program.empty()) {
+ pixel_program = shaders_header + pixel_program;
+ pixel_program_ptr = pixel_program.c_str();
+ }
+ if (!geometry_program.empty()) {
+ geometry_program = shaders_header + geometry_program;
+ geometry_program_ptr = geometry_program.c_str();
+ }
+ ShaderCallback *cb = new ShaderCallback(setter_factories);
+ s32 shadermat = -1;
+ if(is_highlevel){
+ infostream<<"Compiling high level shaders for "<<name<<std::endl;
+ shadermat = gpu->addHighLevelShaderMaterial(
+ vertex_program_ptr, // Vertex shader program
+ "vertexMain", // Vertex shader entry point
+ video::EVST_VS_1_1, // Vertex shader version
+ pixel_program_ptr, // Pixel shader program
+ "pixelMain", // Pixel shader entry point
+ video::EPST_PS_1_2, // Pixel shader version
+ geometry_program_ptr, // Geometry shader program
+ "geometryMain", // Geometry shader entry point
+ video::EGST_GS_4_0, // Geometry shader version
+ scene::EPT_TRIANGLES, // Geometry shader input
+ scene::EPT_TRIANGLE_STRIP, // Geometry shader output
+ 0, // Support maximum number of vertices
+ cb, // Set-constant callback
+ shaderinfo.base_material, // Base material
+ 1 // Userdata passed to callback
+ );
+ if(shadermat == -1){
+ errorstream<<"generate_shader(): "
+ "failed to generate \""<<name<<"\", "
+ "addHighLevelShaderMaterial failed."
+ <<std::endl;
+ dumpShaderProgram(warningstream, "Vertex", vertex_program);
+ dumpShaderProgram(warningstream, "Pixel", pixel_program);
+ dumpShaderProgram(warningstream, "Geometry", geometry_program);
+ delete cb;
+ return shaderinfo;
+ }
+ }
+ else{
+ infostream<<"Compiling assembly shaders for "<<name<<std::endl;
+ shadermat = gpu->addShaderMaterial(
+ vertex_program_ptr, // Vertex shader program
+ pixel_program_ptr, // Pixel shader program
+ cb, // Set-constant callback
+ shaderinfo.base_material, // Base material
+ 0 // Userdata passed to callback
+ );
+
+ if(shadermat == -1){
+ errorstream<<"generate_shader(): "
+ "failed to generate \""<<name<<"\", "
+ "addShaderMaterial failed."
+ <<std::endl;
+ dumpShaderProgram(warningstream, "Vertex", vertex_program);
+ dumpShaderProgram(warningstream,"Pixel", pixel_program);
+ delete cb;
+ return shaderinfo;
+ }
+ }
+ callbacks.push_back(cb);
+
+ // HACK, TODO: investigate this better
+ // Grab the material renderer once more so minetest doesn't crash on exit
+ driver->getMaterialRenderer(shadermat)->grab();
+
+ // Apply the newly created material type
+ shaderinfo.material = (video::E_MATERIAL_TYPE) shadermat;
+ return shaderinfo;
+}
+
+void load_shaders(const std::string &name, SourceShaderCache *sourcecache,
+ video::E_DRIVER_TYPE drivertype, bool enable_shaders,
+ std::string &vertex_program, std::string &pixel_program,
+ std::string &geometry_program, bool &is_highlevel)
+{
+ vertex_program = "";
+ pixel_program = "";
+ geometry_program = "";
+ is_highlevel = false;
+
+ if(enable_shaders){
+ // Look for high level shaders
+ if(drivertype == video::EDT_DIRECT3D9){
+ // Direct3D 9: HLSL
+ // (All shaders in one file)
+ vertex_program = sourcecache->getOrLoad(name, "d3d9.hlsl");
+ pixel_program = vertex_program;
+ geometry_program = vertex_program;
+ }
+ else if(drivertype == video::EDT_OPENGL){
+ // OpenGL: GLSL
+ vertex_program = sourcecache->getOrLoad(name, "opengl_vertex.glsl");
+ pixel_program = sourcecache->getOrLoad(name, "opengl_fragment.glsl");
+ geometry_program = sourcecache->getOrLoad(name, "opengl_geometry.glsl");
+ }
+ if (!vertex_program.empty() || !pixel_program.empty() || !geometry_program.empty()){
+ is_highlevel = true;
+ return;
+ }
+ }
+
+}
+
+void dumpShaderProgram(std::ostream &output_stream,
+ const std::string &program_type, const std::string &program)
+{
+ output_stream << program_type << " shader program:" << std::endl <<
+ "----------------------------------" << std::endl;
+ size_t pos = 0;
+ size_t prev = 0;
+ s16 line = 1;
+ while ((pos = program.find('\n', prev)) != std::string::npos) {
+ output_stream << line++ << ": "<< program.substr(prev, pos - prev) <<
+ std::endl;
+ prev = pos + 1;
+ }
+ output_stream << line << ": " << program.substr(prev) << std::endl <<
+ "End of " << program_type << " shader program." << std::endl <<
+ " " << std::endl;
+}
diff --git a/src/client/shader.h b/src/client/shader.h
new file mode 100644
index 000000000..583c776f4
--- /dev/null
+++ b/src/client/shader.h
@@ -0,0 +1,156 @@
+/*
+Minetest
+Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+Copyright (C) 2013 Kahrl <kahrl@gmx.net>
+
+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 <IMaterialRendererServices.h>
+#include "irrlichttypes_bloated.h"
+#include <string>
+
+class IGameDef;
+
+/*
+ shader.{h,cpp}: Shader handling stuff.
+*/
+
+/*
+ Gets the path to a shader by first checking if the file
+ name_of_shader/filename
+ exists in shader_path and if not, using the data path.
+
+ If not found, returns "".
+
+ Utilizes a thread-safe cache.
+*/
+std::string getShaderPath(const std::string &name_of_shader,
+ const std::string &filename);
+
+struct ShaderInfo {
+ std::string name = "";
+ video::E_MATERIAL_TYPE base_material = video::EMT_SOLID;
+ video::E_MATERIAL_TYPE material = video::EMT_SOLID;
+ u8 drawtype = 0;
+ u8 material_type = 0;
+
+ ShaderInfo() = default;
+ virtual ~ShaderInfo() = default;
+};
+
+/*
+ Setter of constants for shaders
+*/
+
+namespace irr { namespace video {
+ class IMaterialRendererServices;
+} }
+
+
+class IShaderConstantSetter {
+public:
+ virtual ~IShaderConstantSetter() = default;
+ virtual void onSetConstants(video::IMaterialRendererServices *services,
+ bool is_highlevel) = 0;
+};
+
+
+class IShaderConstantSetterFactory {
+public:
+ virtual ~IShaderConstantSetterFactory() = default;
+ virtual IShaderConstantSetter* create() = 0;
+};
+
+
+template <typename T, std::size_t count=1>
+class CachedShaderSetting {
+ const char *m_name;
+ T m_sent[count];
+ bool has_been_set = false;
+ bool is_pixel;
+protected:
+ CachedShaderSetting(const char *name, bool is_pixel) :
+ m_name(name), is_pixel(is_pixel)
+ {}
+public:
+ void set(const T value[count], video::IMaterialRendererServices *services)
+ {
+ if (has_been_set && std::equal(m_sent, m_sent + count, value))
+ return;
+ if (is_pixel)
+ services->setPixelShaderConstant(m_name, value, count);
+ else
+ services->setVertexShaderConstant(m_name, value, count);
+ std::copy(value, value + count, m_sent);
+ has_been_set = true;
+ }
+};
+
+template <typename T, std::size_t count = 1>
+class CachedPixelShaderSetting : public CachedShaderSetting<T, count> {
+public:
+ CachedPixelShaderSetting(const char *name) :
+ CachedShaderSetting<T, count>(name, true){}
+};
+
+template <typename T, std::size_t count = 1>
+class CachedVertexShaderSetting : public CachedShaderSetting<T, count> {
+public:
+ CachedVertexShaderSetting(const char *name) :
+ CachedShaderSetting<T, count>(name, false){}
+};
+
+
+/*
+ ShaderSource creates and caches shaders.
+*/
+
+class IShaderSource {
+public:
+ IShaderSource() = default;
+ virtual ~IShaderSource() = default;
+
+ virtual u32 getShaderIdDirect(const std::string &name,
+ const u8 material_type, const u8 drawtype){return 0;}
+ virtual ShaderInfo getShaderInfo(u32 id){return ShaderInfo();}
+ virtual u32 getShader(const std::string &name,
+ const u8 material_type, const u8 drawtype){return 0;}
+};
+
+class IWritableShaderSource : public IShaderSource {
+public:
+ IWritableShaderSource() = default;
+ virtual ~IWritableShaderSource() = default;
+
+ virtual u32 getShaderIdDirect(const std::string &name,
+ const u8 material_type, const u8 drawtype){return 0;}
+ virtual ShaderInfo getShaderInfo(u32 id){return ShaderInfo();}
+ virtual u32 getShader(const std::string &name,
+ const u8 material_type, const u8 drawtype){return 0;}
+
+ virtual void processQueue()=0;
+ virtual void insertSourceShader(const std::string &name_of_shader,
+ const std::string &filename, const std::string &program)=0;
+ virtual void rebuildShaders()=0;
+ virtual void addShaderConstantSetterFactory(IShaderConstantSetterFactory *setter) = 0;
+};
+
+IWritableShaderSource *createShaderSource();
+
+void dumpShaderProgram(std::ostream &output_stream,
+ const std::string &program_type, const std::string &program);
diff --git a/src/client/sky.cpp b/src/client/sky.cpp
new file mode 100644
index 000000000..faf12ba92
--- /dev/null
+++ b/src/client/sky.cpp
@@ -0,0 +1,755 @@
+/*
+Minetest
+Copyright (C) 2010-2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+
+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 "sky.h"
+#include "IVideoDriver.h"
+#include "ISceneManager.h"
+#include "ICameraSceneNode.h"
+#include "S3DVertex.h"
+#include "client/tile.h"
+#include "noise.h" // easeCurve
+#include "profiler.h"
+#include "util/numeric.h"
+#include <cmath>
+#include "client/renderingengine.h"
+#include "settings.h"
+#include "camera.h" // CameraModes
+
+
+Sky::Sky(s32 id, ITextureSource *tsrc):
+ scene::ISceneNode(RenderingEngine::get_scene_manager()->getRootSceneNode(),
+ RenderingEngine::get_scene_manager(), id)
+{
+ setAutomaticCulling(scene::EAC_OFF);
+ m_box.MaxEdge.set(0, 0, 0);
+ m_box.MinEdge.set(0, 0, 0);
+
+ // Create material
+
+ video::SMaterial mat;
+ mat.Lighting = false;
+#ifdef __ANDROID__
+ mat.ZBuffer = video::ECFN_DISABLED;
+#else
+ mat.ZBuffer = video::ECFN_NEVER;
+#endif
+ mat.ZWriteEnable = false;
+ mat.AntiAliasing = 0;
+ mat.TextureLayer[0].TextureWrapU = video::ETC_CLAMP_TO_EDGE;
+ mat.TextureLayer[0].TextureWrapV = video::ETC_CLAMP_TO_EDGE;
+ mat.BackfaceCulling = false;
+
+ m_materials[0] = mat;
+
+ m_materials[1] = mat;
+ //m_materials[1].MaterialType = video::EMT_TRANSPARENT_VERTEX_ALPHA;
+ m_materials[1].MaterialType = video::EMT_TRANSPARENT_ALPHA_CHANNEL;
+
+ m_materials[2] = mat;
+ m_materials[2].setTexture(0, tsrc->getTextureForMesh("sunrisebg.png"));
+ m_materials[2].MaterialType = video::EMT_TRANSPARENT_ALPHA_CHANNEL;
+ //m_materials[2].MaterialType = video::EMT_TRANSPARENT_ADD_COLOR;
+
+ m_sun_texture = tsrc->isKnownSourceImage("sun.png") ?
+ tsrc->getTextureForMesh("sun.png") : NULL;
+ m_moon_texture = tsrc->isKnownSourceImage("moon.png") ?
+ tsrc->getTextureForMesh("moon.png") : NULL;
+ m_sun_tonemap = tsrc->isKnownSourceImage("sun_tonemap.png") ?
+ tsrc->getTexture("sun_tonemap.png") : NULL;
+ m_moon_tonemap = tsrc->isKnownSourceImage("moon_tonemap.png") ?
+ tsrc->getTexture("moon_tonemap.png") : NULL;
+
+ if (m_sun_texture) {
+ m_materials[3] = mat;
+ m_materials[3].setTexture(0, m_sun_texture);
+ m_materials[3].MaterialType = video::EMT_TRANSPARENT_ALPHA_CHANNEL;
+ if (m_sun_tonemap)
+ m_materials[3].Lighting = true;
+ }
+
+ if (m_moon_texture) {
+ m_materials[4] = mat;
+ m_materials[4].setTexture(0, m_moon_texture);
+ m_materials[4].MaterialType = video::EMT_TRANSPARENT_ALPHA_CHANNEL;
+ if (m_moon_tonemap)
+ m_materials[4].Lighting = true;
+ }
+
+ for (v3f &star : m_stars) {
+ star = v3f(
+ myrand_range(-10000, 10000),
+ myrand_range(-10000, 10000),
+ myrand_range(-10000, 10000)
+ );
+ star.normalize();
+ }
+
+ m_directional_colored_fog = g_settings->getBool("directional_colored_fog");
+}
+
+
+void Sky::OnRegisterSceneNode()
+{
+ if (IsVisible)
+ SceneManager->registerNodeForRendering(this, scene::ESNRP_SKY_BOX);
+
+ scene::ISceneNode::OnRegisterSceneNode();
+}
+
+
+void Sky::render()
+{
+ if (!m_visible)
+ return;
+
+ video::IVideoDriver* driver = SceneManager->getVideoDriver();
+ scene::ICameraSceneNode* camera = SceneManager->getActiveCamera();
+
+ if (!camera || !driver)
+ return;
+
+ ScopeProfiler sp(g_profiler, "Sky::render()", SPT_AVG);
+
+ // Draw perspective skybox
+
+ core::matrix4 translate(AbsoluteTransformation);
+ translate.setTranslation(camera->getAbsolutePosition());
+
+ // Draw the sky box between the near and far clip plane
+ const f32 viewDistance = (camera->getNearValue() + camera->getFarValue()) * 0.5f;
+ core::matrix4 scale;
+ scale.setScale(core::vector3df(viewDistance, viewDistance, viewDistance));
+
+ driver->setTransform(video::ETS_WORLD, translate * scale);
+
+ if (m_sunlight_seen) {
+ float sunsize = 0.07;
+ video::SColorf suncolor_f(1, 1, 0, 1);
+ //suncolor_f.r = 1;
+ //suncolor_f.g = MYMAX(0.3, MYMIN(1.0, 0.7 + m_time_brightness * 0.5));
+ //suncolor_f.b = MYMAX(0.0, m_brightness * 0.95);
+ video::SColorf suncolor2_f(1, 1, 1, 1);
+ // The values below were probably meant to be suncolor2_f instead of a
+ // reassignment of suncolor_f. However, the resulting colour was chosen
+ // and is our long-running classic colour. So preserve, but comment-out
+ // the unnecessary first assignments above.
+ suncolor_f.r = 1;
+ suncolor_f.g = MYMAX(0.3, MYMIN(1.0, 0.85 + m_time_brightness * 0.5));
+ suncolor_f.b = MYMAX(0.0, m_brightness);
+
+ float moonsize = 0.04;
+ video::SColorf mooncolor_f(0.50, 0.57, 0.65, 1);
+ video::SColorf mooncolor2_f(0.85, 0.875, 0.9, 1);
+
+ float nightlength = 0.415;
+ float wn = nightlength / 2;
+ float wicked_time_of_day = 0;
+ if (m_time_of_day > wn && m_time_of_day < 1.0 - wn)
+ wicked_time_of_day = (m_time_of_day - wn) / (1.0 - wn * 2) * 0.5 + 0.25;
+ else if (m_time_of_day < 0.5)
+ wicked_time_of_day = m_time_of_day / wn * 0.25;
+ else
+ wicked_time_of_day = 1.0 - ((1.0 - m_time_of_day) / wn * 0.25);
+ /*std::cerr<<"time_of_day="<<m_time_of_day<<" -> "
+ <<"wicked_time_of_day="<<wicked_time_of_day<<std::endl;*/
+
+ video::SColor suncolor = suncolor_f.toSColor();
+ video::SColor suncolor2 = suncolor2_f.toSColor();
+ video::SColor mooncolor = mooncolor_f.toSColor();
+ video::SColor mooncolor2 = mooncolor2_f.toSColor();
+
+ // Calculate offset normalized to the X dimension of a 512x1 px tonemap
+ float offset = (1.0 - fabs(sin((m_time_of_day - 0.5) * irr::core::PI))) * 511;
+
+ if (m_sun_tonemap) {
+ u8 * texels = (u8 *)m_sun_tonemap->lock();
+ video::SColor* texel = (video::SColor *)(texels + (u32)offset * 4);
+ video::SColor texel_color (255, texel->getRed(),
+ texel->getGreen(), texel->getBlue());
+ m_sun_tonemap->unlock();
+ m_materials[3].EmissiveColor = texel_color;
+ }
+
+ if (m_moon_tonemap) {
+ u8 * texels = (u8 *)m_moon_tonemap->lock();
+ video::SColor* texel = (video::SColor *)(texels + (u32)offset * 4);
+ video::SColor texel_color (255, texel->getRed(),
+ texel->getGreen(), texel->getBlue());
+ m_moon_tonemap->unlock();
+ m_materials[4].EmissiveColor = texel_color;
+ }
+
+ const f32 t = 1.0f;
+ const f32 o = 0.0f;
+ static const u16 indices[4] = {0, 1, 2, 3};
+ video::S3DVertex vertices[4];
+
+ driver->setMaterial(m_materials[1]);
+
+ video::SColor cloudyfogcolor = m_bgcolor;
+
+ // Draw far cloudy fog thing blended with skycolor
+ for (u32 j = 0; j < 4; j++) {
+ video::SColor c = cloudyfogcolor.getInterpolated(m_skycolor, 0.45);
+ vertices[0] = video::S3DVertex(-1, 0.08, -1, 0, 0, 1, c, t, t);
+ vertices[1] = video::S3DVertex( 1, 0.08, -1, 0, 0, 1, c, o, t);
+ vertices[2] = video::S3DVertex( 1, 0.12, -1, 0, 0, 1, c, o, o);
+ vertices[3] = video::S3DVertex(-1, 0.12, -1, 0, 0, 1, c, t, o);
+ for (video::S3DVertex &vertex : vertices) {
+ if (j == 0)
+ // Don't switch
+ {}
+ else if (j == 1)
+ // Switch from -Z (south) to +X (east)
+ vertex.Pos.rotateXZBy(90);
+ else if (j == 2)
+ // Switch from -Z (south) to -X (west)
+ vertex.Pos.rotateXZBy(-90);
+ else
+ // Switch from -Z (south) to +Z (north)
+ vertex.Pos.rotateXZBy(-180);
+ }
+ driver->drawIndexedTriangleFan(&vertices[0], 4, indices, 2);
+ }
+
+ // Draw far cloudy fog thing
+ for (u32 j = 0; j < 4; j++) {
+ video::SColor c = cloudyfogcolor;
+ vertices[0] = video::S3DVertex(-1, -1.0, -1, 0, 0, 1, c, t, t);
+ vertices[1] = video::S3DVertex( 1, -1.0, -1, 0, 0, 1, c, o, t);
+ vertices[2] = video::S3DVertex( 1, 0.08, -1, 0, 0, 1, c, o, o);
+ vertices[3] = video::S3DVertex(-1, 0.08, -1, 0, 0, 1, c, t, o);
+ for (video::S3DVertex &vertex : vertices) {
+ if (j == 0)
+ // Don't switch
+ {}
+ else if (j == 1)
+ // Switch from -Z (south) to +X (east)
+ vertex.Pos.rotateXZBy(90);
+ else if (j == 2)
+ // Switch from -Z (south) to -X (west)
+ vertex.Pos.rotateXZBy(-90);
+ else
+ // Switch from -Z (south) to +Z (north)
+ vertex.Pos.rotateXZBy(-180);
+ }
+ driver->drawIndexedTriangleFan(&vertices[0], 4, indices, 2);
+ }
+
+ // Draw bottom far cloudy fog thing
+ video::SColor c = cloudyfogcolor;
+ vertices[0] = video::S3DVertex(-1, -1.0, -1, 0, 1, 0, c, t, t);
+ vertices[1] = video::S3DVertex( 1, -1.0, -1, 0, 1, 0, c, o, t);
+ vertices[2] = video::S3DVertex( 1, -1.0, 1, 0, 1, 0, c, o, o);
+ vertices[3] = video::S3DVertex(-1, -1.0, 1, 0, 1, 0, c, t, o);
+ driver->drawIndexedTriangleFan(&vertices[0], 4, indices, 2);
+
+ // If sun, moon and stars are (temporarily) disabled, abort here
+ if (!m_bodies_visible)
+ return;
+
+ driver->setMaterial(m_materials[2]);
+
+ // Draw sunrise/sunset horizon glow texture (textures/base/pack/sunrisebg.png)
+ {
+ float mid1 = 0.25;
+ float mid = wicked_time_of_day < 0.5 ? mid1 : (1.0 - mid1);
+ float a_ = 1.0f - std::fabs(wicked_time_of_day - mid) * 35.0f;
+ float a = easeCurve(MYMAX(0, MYMIN(1, a_)));
+ //std::cerr<<"a_="<<a_<<" a="<<a<<std::endl;
+ video::SColor c(255, 255, 255, 255);
+ float y = -(1.0 - a) * 0.22;
+ vertices[0] = video::S3DVertex(-1, -0.05 + y, -1, 0, 0, 1, c, t, t);
+ vertices[1] = video::S3DVertex( 1, -0.05 + y, -1, 0, 0, 1, c, o, t);
+ vertices[2] = video::S3DVertex( 1, 0.2 + y, -1, 0, 0, 1, c, o, o);
+ vertices[3] = video::S3DVertex(-1, 0.2 + y, -1, 0, 0, 1, c, t, o);
+ for (video::S3DVertex &vertex : vertices) {
+ if (wicked_time_of_day < 0.5)
+ // Switch from -Z (south) to +X (east)
+ vertex.Pos.rotateXZBy(90);
+ else
+ // Switch from -Z (south) to -X (west)
+ vertex.Pos.rotateXZBy(-90);
+ }
+ driver->drawIndexedTriangleFan(&vertices[0], 4, indices, 2);
+ }
+
+ // Draw stars
+ do {
+ driver->setMaterial(m_materials[1]);
+ float starbrightness = MYMAX(0, MYMIN(1,
+ (0.285 - fabs(wicked_time_of_day < 0.5 ?
+ wicked_time_of_day : (1.0 - wicked_time_of_day))) * 10));
+ float f = starbrightness;
+ float d = 0.007/2;
+ video::SColor starcolor(255, f * 90, f * 90, f * 90);
+ if (starcolor.getBlue() < m_skycolor.getBlue())
+ break;
+#ifdef __ANDROID__
+ u16 indices[SKY_STAR_COUNT * 3];
+ video::S3DVertex vertices[SKY_STAR_COUNT * 3];
+ for (u32 i = 0; i < SKY_STAR_COUNT; i++) {
+ indices[i * 3 + 0] = i * 3 + 0;
+ indices[i * 3 + 1] = i * 3 + 1;
+ indices[i * 3 + 2] = i * 3 + 2;
+ v3f r = m_stars[i];
+ core::CMatrix4<f32> a;
+ a.buildRotateFromTo(v3f(0, 1, 0), r);
+ v3f p = v3f(-d, 1, -d);
+ v3f p1 = v3f(d, 1, 0);
+ v3f p2 = v3f(-d, 1, d);
+ a.rotateVect(p);
+ a.rotateVect(p1);
+ a.rotateVect(p2);
+ p.rotateXYBy(wicked_time_of_day * 360 - 90);
+ p1.rotateXYBy(wicked_time_of_day * 360 - 90);
+ p2.rotateXYBy(wicked_time_of_day * 360 - 90);
+ vertices[i * 3 + 0].Pos = p;
+ vertices[i * 3 + 0].Color = starcolor;
+ vertices[i * 3 + 1].Pos = p1;
+ vertices[i * 3 + 1].Color = starcolor;
+ vertices[i * 3 + 2].Pos = p2;
+ vertices[i * 3 + 2].Color = starcolor;
+ }
+ driver->drawIndexedTriangleList(vertices, SKY_STAR_COUNT * 3,
+ indices, SKY_STAR_COUNT);
+#else
+ u16 indices[SKY_STAR_COUNT * 4];
+ video::S3DVertex vertices[SKY_STAR_COUNT * 4];
+ for (u32 i = 0; i < SKY_STAR_COUNT; i++) {
+ indices[i * 4 + 0] = i * 4 + 0;
+ indices[i * 4 + 1] = i * 4 + 1;
+ indices[i * 4 + 2] = i * 4 + 2;
+ indices[i * 4 + 3] = i * 4 + 3;
+ v3f r = m_stars[i];
+ core::CMatrix4<f32> a;
+ a.buildRotateFromTo(v3f(0, 1, 0), r);
+ v3f p = v3f(-d, 1, -d);
+ v3f p1 = v3f( d, 1, -d);
+ v3f p2 = v3f( d, 1, d);
+ v3f p3 = v3f(-d, 1, d);
+ a.rotateVect(p);
+ a.rotateVect(p1);
+ a.rotateVect(p2);
+ a.rotateVect(p3);
+ p.rotateXYBy(wicked_time_of_day * 360 - 90);
+ p1.rotateXYBy(wicked_time_of_day * 360 - 90);
+ p2.rotateXYBy(wicked_time_of_day * 360 - 90);
+ p3.rotateXYBy(wicked_time_of_day * 360 - 90);
+ vertices[i * 4 + 0].Pos = p;
+ vertices[i * 4 + 0].Color = starcolor;
+ vertices[i * 4 + 1].Pos = p1;
+ vertices[i * 4 + 1].Color = starcolor;
+ vertices[i * 4 + 2].Pos = p2;
+ vertices[i * 4 + 2].Color = starcolor;
+ vertices[i * 4 + 3].Pos = p3;
+ vertices[i * 4 + 3].Color = starcolor;
+ }
+ driver->drawVertexPrimitiveList(vertices, SKY_STAR_COUNT * 4,
+ indices, SKY_STAR_COUNT, video::EVT_STANDARD,
+ scene::EPT_QUADS, video::EIT_16BIT);
+#endif
+ } while(false);
+
+ // Draw sun
+ if (wicked_time_of_day > 0.15 && wicked_time_of_day < 0.85) {
+ if (!m_sun_texture) {
+ driver->setMaterial(m_materials[1]);
+ float d = sunsize * 1.7;
+ video::SColor c = suncolor;
+ c.setAlpha(0.05 * 255);
+ vertices[0] = video::S3DVertex(-d, -d, -1, 0, 0, 1, c, t, t);
+ vertices[1] = video::S3DVertex( d, -d, -1, 0, 0, 1, c, o, t);
+ vertices[2] = video::S3DVertex( d, d, -1, 0, 0, 1, c, o, o);
+ vertices[3] = video::S3DVertex(-d, d, -1, 0, 0, 1, c, t, o);
+ for (video::S3DVertex &vertex : vertices) {
+ // Switch from -Z (south) to +X (east)
+ vertex.Pos.rotateXZBy(90);
+ vertex.Pos.rotateXYBy(wicked_time_of_day * 360 - 90);
+ }
+ driver->drawIndexedTriangleFan(&vertices[0], 4, indices, 2);
+
+ d = sunsize * 1.2;
+ c = suncolor;
+ c.setAlpha(0.15 * 255);
+ vertices[0] = video::S3DVertex(-d, -d, -1, 0, 0, 1, c, t, t);
+ vertices[1] = video::S3DVertex( d, -d, -1, 0, 0, 1, c, o, t);
+ vertices[2] = video::S3DVertex( d, d, -1, 0, 0, 1, c, o, o);
+ vertices[3] = video::S3DVertex(-d, d, -1, 0, 0, 1, c, t, o);
+ for (video::S3DVertex &vertex : vertices) {
+ // Switch from -Z (south) to +X (east)
+ vertex.Pos.rotateXZBy(90);
+ vertex.Pos.rotateXYBy(wicked_time_of_day * 360 - 90);
+ }
+ driver->drawIndexedTriangleFan(&vertices[0], 4, indices, 2);
+
+ d = sunsize;
+ vertices[0] = video::S3DVertex(-d, -d, -1, 0, 0, 1, suncolor, t, t);
+ vertices[1] = video::S3DVertex( d, -d, -1, 0, 0, 1, suncolor, o, t);
+ vertices[2] = video::S3DVertex( d, d, -1, 0, 0, 1, suncolor, o, o);
+ vertices[3] = video::S3DVertex(-d, d, -1, 0, 0, 1, suncolor, t, o);
+ for (video::S3DVertex &vertex : vertices) {
+ // Switch from -Z (south) to +X (east)
+ vertex.Pos.rotateXZBy(90);
+ vertex.Pos.rotateXYBy(wicked_time_of_day * 360 - 90);
+ }
+ driver->drawIndexedTriangleFan(&vertices[0], 4, indices, 2);
+
+ d = sunsize * 0.7;
+ vertices[0] = video::S3DVertex(-d, -d, -1, 0, 0, 1, suncolor2, t, t);
+ vertices[1] = video::S3DVertex( d, -d, -1, 0, 0, 1, suncolor2, o, t);
+ vertices[2] = video::S3DVertex( d, d, -1, 0, 0, 1, suncolor2, o, o);
+ vertices[3] = video::S3DVertex(-d, d, -1, 0, 0, 1, suncolor2, t, o);
+ for (video::S3DVertex &vertex : vertices) {
+ // Switch from -Z (south) to +X (east)
+ vertex.Pos.rotateXZBy(90);
+ vertex.Pos.rotateXYBy(wicked_time_of_day * 360 - 90);
+ }
+ driver->drawIndexedTriangleFan(&vertices[0], 4, indices, 2);
+ } else {
+ driver->setMaterial(m_materials[3]);
+ float d = sunsize * 1.7;
+ video::SColor c;
+ if (m_sun_tonemap)
+ c = video::SColor (0, 0, 0, 0);
+ else
+ c = video::SColor (255, 255, 255, 255);
+ vertices[0] = video::S3DVertex(-d, -d, -1, 0, 0, 1, c, t, t);
+ vertices[1] = video::S3DVertex( d, -d, -1, 0, 0, 1, c, o, t);
+ vertices[2] = video::S3DVertex( d, d, -1, 0, 0, 1, c, o, o);
+ vertices[3] = video::S3DVertex(-d, d, -1, 0, 0, 1, c, t, o);
+ for (video::S3DVertex &vertex : vertices) {
+ // Switch from -Z (south) to +X (east)
+ vertex.Pos.rotateXZBy(90);
+ vertex.Pos.rotateXYBy(wicked_time_of_day * 360 - 90);
+ }
+ driver->drawIndexedTriangleFan(&vertices[0], 4, indices, 2);
+ }
+ }
+
+ // Draw moon
+ if (wicked_time_of_day < 0.3 || wicked_time_of_day > 0.7) {
+ if (!m_moon_texture) {
+ driver->setMaterial(m_materials[1]);
+ float d = moonsize * 1.9;
+ video::SColor c = mooncolor;
+ c.setAlpha(0.05 * 255);
+ vertices[0] = video::S3DVertex(-d, -d, -1, 0, 0, 1, c, t, t);
+ vertices[1] = video::S3DVertex( d, -d, -1, 0, 0, 1, c, o, t);
+ vertices[2] = video::S3DVertex( d, d, -1, 0, 0, 1, c, o, o);
+ vertices[3] = video::S3DVertex(-d, d, -1, 0, 0, 1, c, t, o);
+ for (video::S3DVertex &vertex : vertices) {
+ // Switch from -Z (south) to -X (west)
+ vertex.Pos.rotateXZBy(-90);
+ vertex.Pos.rotateXYBy(wicked_time_of_day * 360 - 90);
+ }
+ driver->drawIndexedTriangleFan(&vertices[0], 4, indices, 2);
+
+ d = moonsize * 1.3;
+ c = mooncolor;
+ c.setAlpha(0.15 * 255);
+ vertices[0] = video::S3DVertex(-d, -d, -1, 0, 0, 1, c, t, t);
+ vertices[1] = video::S3DVertex( d, -d, -1, 0, 0, 1, c, o, t);
+ vertices[2] = video::S3DVertex( d, d, -1, 0, 0, 1, c, o, o);
+ vertices[3] = video::S3DVertex(-d, d, -1, 0, 0, 1, c, t, o);
+ for (video::S3DVertex &vertex : vertices) {
+ // Switch from -Z (south) to -X (west)
+ vertex.Pos.rotateXZBy(-90);
+ vertex.Pos.rotateXYBy(wicked_time_of_day * 360 - 90);
+ }
+ driver->drawIndexedTriangleFan(&vertices[0], 4, indices, 2);
+
+ d = moonsize;
+ vertices[0] = video::S3DVertex(-d, -d, -1, 0, 0, 1, mooncolor, t, t);
+ vertices[1] = video::S3DVertex( d, -d, -1, 0, 0, 1, mooncolor, o, t);
+ vertices[2] = video::S3DVertex( d, d, -1, 0, 0, 1, mooncolor, o, o);
+ vertices[3] = video::S3DVertex(-d, d, -1, 0, 0, 1, mooncolor, t, o);
+ for (video::S3DVertex &vertex : vertices) {
+ // Switch from -Z (south) to -X (west)
+ vertex.Pos.rotateXZBy(-90);
+ vertex.Pos.rotateXYBy(wicked_time_of_day * 360 - 90);
+ }
+ driver->drawIndexedTriangleFan(&vertices[0], 4, indices, 2);
+
+ float d2 = moonsize * 0.6;
+ vertices[0] = video::S3DVertex(-d, -d, -1, 0, 0, 1, mooncolor2, t, t);
+ vertices[1] = video::S3DVertex( d2,-d, -1, 0, 0, 1, mooncolor2, o, t);
+ vertices[2] = video::S3DVertex( d2, d2, -1, 0, 0, 1, mooncolor2, o, o);
+ vertices[3] = video::S3DVertex(-d, d2, -1, 0, 0, 1, mooncolor2, t, o);
+ for (video::S3DVertex &vertex : vertices) {
+ // Switch from -Z (south) to -X (west)
+ vertex.Pos.rotateXZBy(-90);
+ vertex.Pos.rotateXYBy(wicked_time_of_day * 360 - 90);
+ }
+ driver->drawIndexedTriangleFan(&vertices[0], 4, indices, 2);
+ } else {
+ driver->setMaterial(m_materials[4]);
+ float d = moonsize * 1.9;
+ video::SColor c;
+ if (m_moon_tonemap)
+ c = video::SColor (0, 0, 0, 0);
+ else
+ c = video::SColor (255, 255, 255, 255);
+ vertices[0] = video::S3DVertex(-d, -d, -1, 0, 0, 1, c, t, t);
+ vertices[1] = video::S3DVertex( d, -d, -1, 0, 0, 1, c, o, t);
+ vertices[2] = video::S3DVertex( d, d, -1, 0, 0, 1, c, o, o);
+ vertices[3] = video::S3DVertex(-d, d, -1, 0, 0, 1, c, t, o);
+ for (video::S3DVertex &vertex : vertices) {
+ // Switch from -Z (south) to -X (west)
+ vertex.Pos.rotateXZBy(-90);
+ vertex.Pos.rotateXYBy(wicked_time_of_day * 360 - 90);
+ }
+ driver->drawIndexedTriangleFan(&vertices[0], 4, indices, 2);
+ }
+ }
+
+ // Draw far cloudy fog thing below east and west horizons
+ for (u32 j = 0; j < 2; j++) {
+ video::SColor c = cloudyfogcolor;
+ vertices[0] = video::S3DVertex(-1, -1.0, -1, 0, 0, 1, c, t, t);
+ vertices[1] = video::S3DVertex( 1, -1.0, -1, 0, 0, 1, c, o, t);
+ vertices[2] = video::S3DVertex( 1, -0.02, -1, 0, 0, 1, c, o, o);
+ vertices[3] = video::S3DVertex(-1, -0.02, -1, 0, 0, 1, c, t, o);
+ for (video::S3DVertex &vertex : vertices) {
+ //if (wicked_time_of_day < 0.5)
+ if (j == 0)
+ // Switch from -Z (south) to +X (east)
+ vertex.Pos.rotateXZBy(90);
+ else
+ // Switch from -Z (south) to -X (west)
+ vertex.Pos.rotateXZBy(-90);
+ }
+ driver->drawIndexedTriangleFan(&vertices[0], 4, indices, 2);
+ }
+ }
+}
+
+
+void Sky::update(float time_of_day, float time_brightness,
+ float direct_brightness, bool sunlight_seen,
+ CameraMode cam_mode, float yaw, float pitch)
+{
+ // Stabilize initial brightness and color values by flooding updates
+ if (m_first_update) {
+ /*dstream<<"First update with time_of_day="<<time_of_day
+ <<" time_brightness="<<time_brightness
+ <<" direct_brightness="<<direct_brightness
+ <<" sunlight_seen="<<sunlight_seen<<std::endl;*/
+ m_first_update = false;
+ for (u32 i = 0; i < 100; i++) {
+ update(time_of_day, time_brightness, direct_brightness,
+ sunlight_seen, cam_mode, yaw, pitch);
+ }
+ return;
+ }
+
+ m_time_of_day = time_of_day;
+ m_time_brightness = time_brightness;
+ m_sunlight_seen = sunlight_seen;
+ m_bodies_visible = true;
+
+ bool is_dawn = (time_brightness >= 0.20 && time_brightness < 0.35);
+
+ /*
+ Development colours
+
+ video::SColorf bgcolor_bright_normal_f(170. / 255, 200. / 255, 230. / 255, 1.0);
+ video::SColorf bgcolor_bright_dawn_f(0.666, 200. / 255 * 0.7, 230. / 255 * 0.5, 1.0);
+ video::SColorf bgcolor_bright_dawn_f(0.666, 0.549, 0.220, 1.0);
+ video::SColorf bgcolor_bright_dawn_f(0.666 * 1.2, 0.549 * 1.0, 0.220 * 1.0, 1.0);
+ video::SColorf bgcolor_bright_dawn_f(0.666 * 1.2, 0.549 * 1.0, 0.220 * 1.2, 1.0);
+
+ video::SColorf cloudcolor_bright_dawn_f(1.0, 0.591, 0.4);
+ video::SColorf cloudcolor_bright_dawn_f(1.0, 0.65, 0.44);
+ video::SColorf cloudcolor_bright_dawn_f(1.0, 0.7, 0.5);
+ */
+
+ video::SColorf bgcolor_bright_normal_f = video::SColor(255, 155, 193, 240);
+ video::SColorf bgcolor_bright_indoor_f = video::SColor(255, 100, 100, 100);
+ video::SColorf bgcolor_bright_dawn_f = video::SColor(255, 186, 193, 240);
+ video::SColorf bgcolor_bright_night_f = video::SColor(255, 64, 144, 255);
+
+ video::SColorf skycolor_bright_normal_f = video::SColor(255, 140, 186, 250);
+ video::SColorf skycolor_bright_dawn_f = video::SColor(255, 180, 186, 250);
+ video::SColorf skycolor_bright_night_f = video::SColor(255, 0, 107, 255);
+
+ // pure white: becomes "diffuse light component" for clouds
+ video::SColorf cloudcolor_bright_normal_f = video::SColor(255, 255, 255, 255);
+ // dawn-factoring version of pure white (note: R is above 1.0)
+ video::SColorf cloudcolor_bright_dawn_f(255.0f/240.0f, 223.0f/240.0f, 191.0f/255.0f);
+
+ float cloud_color_change_fraction = 0.95;
+ if (sunlight_seen) {
+ if (std::fabs(time_brightness - m_brightness) < 0.2f) {
+ m_brightness = m_brightness * 0.95 + time_brightness * 0.05;
+ } else {
+ m_brightness = m_brightness * 0.80 + time_brightness * 0.20;
+ cloud_color_change_fraction = 0.0;
+ }
+ } else {
+ if (direct_brightness < m_brightness)
+ m_brightness = m_brightness * 0.95 + direct_brightness * 0.05;
+ else
+ m_brightness = m_brightness * 0.98 + direct_brightness * 0.02;
+ }
+
+ m_clouds_visible = true;
+ float color_change_fraction = 0.98f;
+ if (sunlight_seen) {
+ if (is_dawn) { // Dawn
+ m_bgcolor_bright_f = m_bgcolor_bright_f.getInterpolated(
+ bgcolor_bright_dawn_f, color_change_fraction);
+ m_skycolor_bright_f = m_skycolor_bright_f.getInterpolated(
+ skycolor_bright_dawn_f, color_change_fraction);
+ m_cloudcolor_bright_f = m_cloudcolor_bright_f.getInterpolated(
+ cloudcolor_bright_dawn_f, color_change_fraction);
+ } else {
+ if (time_brightness < 0.13f) { // Night
+ m_bgcolor_bright_f = m_bgcolor_bright_f.getInterpolated(
+ bgcolor_bright_night_f, color_change_fraction);
+ m_skycolor_bright_f = m_skycolor_bright_f.getInterpolated(
+ skycolor_bright_night_f, color_change_fraction);
+ } else { // Day
+ m_bgcolor_bright_f = m_bgcolor_bright_f.getInterpolated(
+ bgcolor_bright_normal_f, color_change_fraction);
+ m_skycolor_bright_f = m_skycolor_bright_f.getInterpolated(
+ skycolor_bright_normal_f, color_change_fraction);
+ }
+
+ m_cloudcolor_bright_f = m_cloudcolor_bright_f.getInterpolated(
+ cloudcolor_bright_normal_f, color_change_fraction);
+ }
+ } else {
+ m_bgcolor_bright_f = m_bgcolor_bright_f.getInterpolated(
+ bgcolor_bright_indoor_f, color_change_fraction);
+ m_skycolor_bright_f = m_skycolor_bright_f.getInterpolated(
+ bgcolor_bright_indoor_f, color_change_fraction);
+ m_cloudcolor_bright_f = m_cloudcolor_bright_f.getInterpolated(
+ cloudcolor_bright_normal_f, color_change_fraction);
+ m_clouds_visible = false;
+ }
+
+ video::SColor bgcolor_bright = m_bgcolor_bright_f.toSColor();
+ m_bgcolor = video::SColor(
+ 255,
+ bgcolor_bright.getRed() * m_brightness,
+ bgcolor_bright.getGreen() * m_brightness,
+ bgcolor_bright.getBlue() * m_brightness
+ );
+
+ video::SColor skycolor_bright = m_skycolor_bright_f.toSColor();
+ m_skycolor = video::SColor(
+ 255,
+ skycolor_bright.getRed() * m_brightness,
+ skycolor_bright.getGreen() * m_brightness,
+ skycolor_bright.getBlue() * m_brightness
+ );
+
+ // Horizon coloring based on sun and moon direction during sunset and sunrise
+ video::SColor pointcolor = video::SColor(m_bgcolor.getAlpha(), 255, 255, 255);
+ if (m_directional_colored_fog) {
+ if (m_horizon_blend() != 0) {
+ // Calculate hemisphere value from yaw, (inverted in third person front view)
+ s8 dir_factor = 1;
+ if (cam_mode > CAMERA_MODE_THIRD)
+ dir_factor = -1;
+ f32 pointcolor_blend = wrapDegrees_0_360(yaw * dir_factor + 90);
+ if (pointcolor_blend > 180)
+ pointcolor_blend = 360 - pointcolor_blend;
+ pointcolor_blend /= 180;
+ // Bound view angle to determine where transition starts and ends
+ pointcolor_blend = rangelim(1 - pointcolor_blend * 1.375, 0, 1 / 1.375) *
+ 1.375;
+ // Combine the colors when looking up or down, otherwise turning looks weird
+ pointcolor_blend += (0.5 - pointcolor_blend) *
+ (1 - MYMIN((90 - std::fabs(pitch)) / 90 * 1.5, 1));
+ // Invert direction to match where the sun and moon are rising
+ if (m_time_of_day > 0.5)
+ pointcolor_blend = 1 - pointcolor_blend;
+ // Horizon colors of sun and moon
+ f32 pointcolor_light = rangelim(m_time_brightness * 3, 0.2, 1);
+
+ video::SColorf pointcolor_sun_f(1, 1, 1, 1);
+ if (m_sun_tonemap) {
+ pointcolor_sun_f.r = pointcolor_light *
+ (float)m_materials[3].EmissiveColor.getRed() / 255;
+ pointcolor_sun_f.b = pointcolor_light *
+ (float)m_materials[3].EmissiveColor.getBlue() / 255;
+ pointcolor_sun_f.g = pointcolor_light *
+ (float)m_materials[3].EmissiveColor.getGreen() / 255;
+ } else {
+ pointcolor_sun_f.r = pointcolor_light * 1;
+ pointcolor_sun_f.b = pointcolor_light *
+ (0.25 + (rangelim(m_time_brightness, 0.25, 0.75) - 0.25) * 2 * 0.75);
+ pointcolor_sun_f.g = pointcolor_light * (pointcolor_sun_f.b * 0.375 +
+ (rangelim(m_time_brightness, 0.05, 0.15) - 0.05) * 10 * 0.625);
+ }
+
+ video::SColorf pointcolor_moon_f(0.5 * pointcolor_light,
+ 0.6 * pointcolor_light, 0.8 * pointcolor_light, 1);
+ if (m_moon_tonemap) {
+ pointcolor_moon_f.r = pointcolor_light *
+ (float)m_materials[4].EmissiveColor.getRed() / 255;
+ pointcolor_moon_f.b = pointcolor_light *
+ (float)m_materials[4].EmissiveColor.getBlue() / 255;
+ pointcolor_moon_f.g = pointcolor_light *
+ (float)m_materials[4].EmissiveColor.getGreen() / 255;
+ }
+
+ video::SColor pointcolor_sun = pointcolor_sun_f.toSColor();
+ video::SColor pointcolor_moon = pointcolor_moon_f.toSColor();
+ // Calculate the blend color
+ pointcolor = m_mix_scolor(pointcolor_moon, pointcolor_sun, pointcolor_blend);
+ }
+ m_bgcolor = m_mix_scolor(m_bgcolor, pointcolor, m_horizon_blend() * 0.5);
+ m_skycolor = m_mix_scolor(m_skycolor, pointcolor, m_horizon_blend() * 0.25);
+ }
+
+ float cloud_direct_brightness = 0.0f;
+ if (sunlight_seen) {
+ if (!m_directional_colored_fog) {
+ cloud_direct_brightness = time_brightness;
+ // Boost cloud brightness relative to sky, at dawn, dusk and at night
+ if (time_brightness < 0.7f)
+ cloud_direct_brightness *= 1.3f;
+ } else {
+ cloud_direct_brightness = std::fmin(m_horizon_blend() * 0.15f +
+ m_time_brightness, 1.0f);
+ // Set the same minimum cloud brightness at night
+ if (time_brightness < 0.5f)
+ cloud_direct_brightness = std::fmax(cloud_direct_brightness,
+ time_brightness * 1.3f);
+ }
+ } else {
+ cloud_direct_brightness = direct_brightness;
+ }
+
+ m_cloud_brightness = m_cloud_brightness * cloud_color_change_fraction +
+ cloud_direct_brightness * (1.0 - cloud_color_change_fraction);
+ m_cloudcolor_f = video::SColorf(
+ m_cloudcolor_bright_f.r * m_cloud_brightness,
+ m_cloudcolor_bright_f.g * m_cloud_brightness,
+ m_cloudcolor_bright_f.b * m_cloud_brightness,
+ 1.0
+ );
+ if (m_directional_colored_fog) {
+ m_cloudcolor_f = m_mix_scolorf(m_cloudcolor_f,
+ video::SColorf(pointcolor), m_horizon_blend() * 0.25);
+ }
+}
diff --git a/src/client/sky.h b/src/client/sky.h
new file mode 100644
index 000000000..b66a4990f
--- /dev/null
+++ b/src/client/sky.h
@@ -0,0 +1,148 @@
+/*
+Minetest
+Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+
+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 <ISceneNode.h>
+#include "camera.h"
+#include "irrlichttypes_extrabloated.h"
+
+#pragma once
+
+#define SKY_MATERIAL_COUNT 5
+#define SKY_STAR_COUNT 200
+
+class ITextureSource;
+
+// Skybox, rendered with zbuffer turned off, before all other nodes.
+class Sky : public scene::ISceneNode
+{
+public:
+ //! constructor
+ Sky(s32 id, ITextureSource *tsrc);
+
+ virtual void OnRegisterSceneNode();
+
+ //! renders the node.
+ virtual void render();
+
+ virtual const aabb3f &getBoundingBox() const { return m_box; }
+
+ // Used by Irrlicht for optimizing rendering
+ virtual video::SMaterial &getMaterial(u32 i) { return m_materials[i]; }
+
+ // Used by Irrlicht for optimizing rendering
+ virtual u32 getMaterialCount() const { return SKY_MATERIAL_COUNT; }
+
+ void update(float m_time_of_day, float time_brightness, float direct_brightness,
+ bool sunlight_seen, CameraMode cam_mode, float yaw, float pitch);
+
+ float getBrightness() { return m_brightness; }
+
+ const video::SColor &getBgColor() const
+ {
+ return m_visible ? m_bgcolor : m_fallback_bg_color;
+ }
+
+ const video::SColor &getSkyColor() const
+ {
+ return m_visible ? m_skycolor : m_fallback_bg_color;
+ }
+
+ bool getCloudsVisible() const { return m_clouds_visible && m_clouds_enabled; }
+ const video::SColorf &getCloudColor() const { return m_cloudcolor_f; }
+
+ void setVisible(bool visible) { m_visible = visible; }
+ // Set only from set_sky API
+ void setCloudsEnabled(bool clouds_enabled) { m_clouds_enabled = clouds_enabled; }
+ void setFallbackBgColor(const video::SColor &fallback_bg_color)
+ {
+ m_fallback_bg_color = fallback_bg_color;
+ }
+ void overrideColors(const video::SColor &bgcolor, const video::SColor &skycolor)
+ {
+ m_bgcolor = bgcolor;
+ m_skycolor = skycolor;
+ }
+ void setBodiesVisible(bool visible) { m_bodies_visible = visible; }
+
+private:
+ aabb3f m_box;
+ video::SMaterial m_materials[SKY_MATERIAL_COUNT];
+
+ // How much sun & moon transition should affect horizon color
+ float m_horizon_blend()
+ {
+ if (!m_sunlight_seen)
+ return 0;
+ float x = m_time_of_day >= 0.5 ? (1 - m_time_of_day) * 2
+ : m_time_of_day * 2;
+
+ if (x <= 0.3)
+ return 0;
+ if (x <= 0.4) // when the sun and moon are aligned
+ return (x - 0.3) * 10;
+ if (x <= 0.5)
+ return (0.5 - x) * 10;
+ return 0;
+ }
+
+ // Mix two colors by a given amount
+ video::SColor m_mix_scolor(video::SColor col1, video::SColor col2, f32 factor)
+ {
+ video::SColor result = video::SColor(
+ col1.getAlpha() * (1 - factor) + col2.getAlpha() * factor,
+ col1.getRed() * (1 - factor) + col2.getRed() * factor,
+ col1.getGreen() * (1 - factor) + col2.getGreen() * factor,
+ col1.getBlue() * (1 - factor) + col2.getBlue() * factor);
+ return result;
+ }
+ video::SColorf m_mix_scolorf(video::SColorf col1, video::SColorf col2, f32 factor)
+ {
+ video::SColorf result =
+ video::SColorf(col1.r * (1 - factor) + col2.r * factor,
+ col1.g * (1 - factor) + col2.g * factor,
+ col1.b * (1 - factor) + col2.b * factor,
+ col1.a * (1 - factor) + col2.a * factor);
+ return result;
+ }
+
+ bool m_visible = true;
+ // Used when m_visible=false
+ video::SColor m_fallback_bg_color = video::SColor(255, 255, 255, 255);
+ bool m_first_update = true;
+ float m_time_of_day;
+ float m_time_brightness;
+ bool m_sunlight_seen;
+ float m_brightness = 0.5f;
+ float m_cloud_brightness = 0.5f;
+ bool m_clouds_visible; // Whether clouds are disabled due to player underground
+ bool m_clouds_enabled = true; // Initialised to true, reset only by set_sky API
+ bool m_directional_colored_fog;
+ bool m_bodies_visible = true; // sun, moon, stars
+ video::SColorf m_bgcolor_bright_f = video::SColorf(1.0f, 1.0f, 1.0f, 1.0f);
+ video::SColorf m_skycolor_bright_f = video::SColorf(1.0f, 1.0f, 1.0f, 1.0f);
+ video::SColorf m_cloudcolor_bright_f = video::SColorf(1.0f, 1.0f, 1.0f, 1.0f);
+ video::SColor m_bgcolor;
+ video::SColor m_skycolor;
+ video::SColorf m_cloudcolor_f;
+ v3f m_stars[SKY_STAR_COUNT];
+ video::ITexture *m_sun_texture;
+ video::ITexture *m_moon_texture;
+ video::ITexture *m_sun_tonemap;
+ video::ITexture *m_moon_tonemap;
+};
diff --git a/src/client/wieldmesh.cpp b/src/client/wieldmesh.cpp
new file mode 100644
index 000000000..7791a5a92
--- /dev/null
+++ b/src/client/wieldmesh.cpp
@@ -0,0 +1,685 @@
+/*
+Minetest
+Copyright (C) 2010-2014 celeron55, Perttu Ahola <celeron55@gmail.com>
+
+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 "wieldmesh.h"
+#include "settings.h"
+#include "shader.h"
+#include "inventory.h"
+#include "client.h"
+#include "itemdef.h"
+#include "nodedef.h"
+#include "mesh.h"
+#include "content_mapblock.h"
+#include "mapblock_mesh.h"
+#include "client/meshgen/collector.h"
+#include "client/tile.h"
+#include "log.h"
+#include "util/numeric.h"
+#include <map>
+#include <IMeshManipulator.h>
+
+#define WIELD_SCALE_FACTOR 30.0
+#define WIELD_SCALE_FACTOR_EXTRUDED 40.0
+
+#define MIN_EXTRUSION_MESH_RESOLUTION 16
+#define MAX_EXTRUSION_MESH_RESOLUTION 512
+
+static scene::IMesh *createExtrusionMesh(int resolution_x, int resolution_y)
+{
+ const f32 r = 0.5;
+
+ scene::IMeshBuffer *buf = new scene::SMeshBuffer();
+ video::SColor c(255,255,255,255);
+ v3f scale(1.0, 1.0, 0.1);
+
+ // Front and back
+ {
+ video::S3DVertex vertices[8] = {
+ // z-
+ video::S3DVertex(-r,+r,-r, 0,0,-1, c, 0,0),
+ video::S3DVertex(+r,+r,-r, 0,0,-1, c, 1,0),
+ video::S3DVertex(+r,-r,-r, 0,0,-1, c, 1,1),
+ video::S3DVertex(-r,-r,-r, 0,0,-1, c, 0,1),
+ // z+
+ video::S3DVertex(-r,+r,+r, 0,0,+1, c, 0,0),
+ video::S3DVertex(-r,-r,+r, 0,0,+1, c, 0,1),
+ video::S3DVertex(+r,-r,+r, 0,0,+1, c, 1,1),
+ video::S3DVertex(+r,+r,+r, 0,0,+1, c, 1,0),
+ };
+ u16 indices[12] = {0,1,2,2,3,0,4,5,6,6,7,4};
+ buf->append(vertices, 8, indices, 12);
+ }
+
+ f32 pixelsize_x = 1 / (f32) resolution_x;
+ f32 pixelsize_y = 1 / (f32) resolution_y;
+
+ for (int i = 0; i < resolution_x; ++i) {
+ f32 pixelpos_x = i * pixelsize_x - 0.5;
+ f32 x0 = pixelpos_x;
+ f32 x1 = pixelpos_x + pixelsize_x;
+ f32 tex0 = (i + 0.1) * pixelsize_x;
+ f32 tex1 = (i + 0.9) * pixelsize_x;
+ video::S3DVertex vertices[8] = {
+ // x-
+ video::S3DVertex(x0,-r,-r, -1,0,0, c, tex0,1),
+ video::S3DVertex(x0,-r,+r, -1,0,0, c, tex1,1),
+ video::S3DVertex(x0,+r,+r, -1,0,0, c, tex1,0),
+ video::S3DVertex(x0,+r,-r, -1,0,0, c, tex0,0),
+ // x+
+ video::S3DVertex(x1,-r,-r, +1,0,0, c, tex0,1),
+ video::S3DVertex(x1,+r,-r, +1,0,0, c, tex0,0),
+ video::S3DVertex(x1,+r,+r, +1,0,0, c, tex1,0),
+ video::S3DVertex(x1,-r,+r, +1,0,0, c, tex1,1),
+ };
+ u16 indices[12] = {0,1,2,2,3,0,4,5,6,6,7,4};
+ buf->append(vertices, 8, indices, 12);
+ }
+ for (int i = 0; i < resolution_y; ++i) {
+ f32 pixelpos_y = i * pixelsize_y - 0.5;
+ f32 y0 = -pixelpos_y - pixelsize_y;
+ f32 y1 = -pixelpos_y;
+ f32 tex0 = (i + 0.1) * pixelsize_y;
+ f32 tex1 = (i + 0.9) * pixelsize_y;
+ video::S3DVertex vertices[8] = {
+ // y-
+ video::S3DVertex(-r,y0,-r, 0,-1,0, c, 0,tex0),
+ video::S3DVertex(+r,y0,-r, 0,-1,0, c, 1,tex0),
+ video::S3DVertex(+r,y0,+r, 0,-1,0, c, 1,tex1),
+ video::S3DVertex(-r,y0,+r, 0,-1,0, c, 0,tex1),
+ // y+
+ video::S3DVertex(-r,y1,-r, 0,+1,0, c, 0,tex0),
+ video::S3DVertex(-r,y1,+r, 0,+1,0, c, 0,tex1),
+ video::S3DVertex(+r,y1,+r, 0,+1,0, c, 1,tex1),
+ video::S3DVertex(+r,y1,-r, 0,+1,0, c, 1,tex0),
+ };
+ u16 indices[12] = {0,1,2,2,3,0,4,5,6,6,7,4};
+ buf->append(vertices, 8, indices, 12);
+ }
+
+ // Create mesh object
+ scene::SMesh *mesh = new scene::SMesh();
+ mesh->addMeshBuffer(buf);
+ buf->drop();
+ scaleMesh(mesh, scale); // also recalculates bounding box
+ return mesh;
+}
+
+/*
+ Caches extrusion meshes so that only one of them per resolution
+ is needed. Also caches one cube (for convenience).
+
+ E.g. there is a single extrusion mesh that is used for all
+ 16x16 px images, another for all 256x256 px images, and so on.
+
+ WARNING: Not thread safe. This should not be a problem since
+ rendering related classes (such as WieldMeshSceneNode) will be
+ used from the rendering thread only.
+*/
+class ExtrusionMeshCache: public IReferenceCounted
+{
+public:
+ // Constructor
+ ExtrusionMeshCache()
+ {
+ for (int resolution = MIN_EXTRUSION_MESH_RESOLUTION;
+ resolution <= MAX_EXTRUSION_MESH_RESOLUTION;
+ resolution *= 2) {
+ m_extrusion_meshes[resolution] =
+ createExtrusionMesh(resolution, resolution);
+ }
+ m_cube = createCubeMesh(v3f(1.0, 1.0, 1.0));
+ }
+ // Destructor
+ virtual ~ExtrusionMeshCache()
+ {
+ for (auto &extrusion_meshe : m_extrusion_meshes) {
+ extrusion_meshe.second->drop();
+ }
+ m_cube->drop();
+ }
+ // Get closest extrusion mesh for given image dimensions
+ // Caller must drop the returned pointer
+ scene::IMesh* create(core::dimension2d<u32> dim)
+ {
+ // handle non-power of two textures inefficiently without cache
+ if (!is_power_of_two(dim.Width) || !is_power_of_two(dim.Height)) {
+ return createExtrusionMesh(dim.Width, dim.Height);
+ }
+
+ int maxdim = MYMAX(dim.Width, dim.Height);
+
+ std::map<int, scene::IMesh*>::iterator
+ it = m_extrusion_meshes.lower_bound(maxdim);
+
+ if (it == m_extrusion_meshes.end()) {
+ // no viable resolution found; use largest one
+ it = m_extrusion_meshes.find(MAX_EXTRUSION_MESH_RESOLUTION);
+ sanity_check(it != m_extrusion_meshes.end());
+ }
+
+ scene::IMesh *mesh = it->second;
+ mesh->grab();
+ return mesh;
+ }
+ // Returns a 1x1x1 cube mesh with one meshbuffer (material) per face
+ // Caller must drop the returned pointer
+ scene::IMesh* createCube()
+ {
+ m_cube->grab();
+ return m_cube;
+ }
+
+private:
+ std::map<int, scene::IMesh*> m_extrusion_meshes;
+ scene::IMesh *m_cube;
+};
+
+ExtrusionMeshCache *g_extrusion_mesh_cache = NULL;
+
+
+WieldMeshSceneNode::WieldMeshSceneNode(scene::ISceneManager *mgr, s32 id, bool lighting):
+ scene::ISceneNode(mgr->getRootSceneNode(), mgr, id),
+ m_material_type(video::EMT_TRANSPARENT_ALPHA_CHANNEL_REF),
+ m_lighting(lighting)
+{
+ m_enable_shaders = g_settings->getBool("enable_shaders");
+ m_anisotropic_filter = g_settings->getBool("anisotropic_filter");
+ m_bilinear_filter = g_settings->getBool("bilinear_filter");
+ m_trilinear_filter = g_settings->getBool("trilinear_filter");
+
+ // If this is the first wield mesh scene node, create a cache
+ // for extrusion meshes (and a cube mesh), otherwise reuse it
+ if (!g_extrusion_mesh_cache)
+ g_extrusion_mesh_cache = new ExtrusionMeshCache();
+ else
+ g_extrusion_mesh_cache->grab();
+
+ // Disable bounding box culling for this scene node
+ // since we won't calculate the bounding box.
+ setAutomaticCulling(scene::EAC_OFF);
+
+ // Create the child scene node
+ scene::IMesh *dummymesh = g_extrusion_mesh_cache->createCube();
+ m_meshnode = SceneManager->addMeshSceneNode(dummymesh, this, -1);
+ m_meshnode->setReadOnlyMaterials(false);
+ m_meshnode->setVisible(false);
+ dummymesh->drop(); // m_meshnode grabbed it
+}
+
+WieldMeshSceneNode::~WieldMeshSceneNode()
+{
+ sanity_check(g_extrusion_mesh_cache);
+ if (g_extrusion_mesh_cache->drop())
+ g_extrusion_mesh_cache = nullptr;
+}
+
+void WieldMeshSceneNode::setCube(const ContentFeatures &f,
+ v3f wield_scale)
+{
+ scene::IMesh *cubemesh = g_extrusion_mesh_cache->createCube();
+ scene::SMesh *copy = cloneMesh(cubemesh);
+ cubemesh->drop();
+ postProcessNodeMesh(copy, f, false, true, &m_material_type, &m_colors, true);
+ changeToMesh(copy);
+ copy->drop();
+ m_meshnode->setScale(wield_scale * WIELD_SCALE_FACTOR);
+}
+
+void WieldMeshSceneNode::setExtruded(const std::string &imagename,
+ const std::string &overlay_name, v3f wield_scale, ITextureSource *tsrc,
+ u8 num_frames)
+{
+ video::ITexture *texture = tsrc->getTexture(imagename);
+ if (!texture) {
+ changeToMesh(nullptr);
+ return;
+ }
+ video::ITexture *overlay_texture =
+ overlay_name.empty() ? NULL : tsrc->getTexture(overlay_name);
+
+ core::dimension2d<u32> dim = texture->getSize();
+ // Detect animation texture and pull off top frame instead of using entire thing
+ if (num_frames > 1) {
+ u32 frame_height = dim.Height / num_frames;
+ dim = core::dimension2d<u32>(dim.Width, frame_height);
+ }
+ scene::IMesh *original = g_extrusion_mesh_cache->create(dim);
+ scene::SMesh *mesh = cloneMesh(original);
+ original->drop();
+ //set texture
+ mesh->getMeshBuffer(0)->getMaterial().setTexture(0,
+ tsrc->getTexture(imagename));
+ if (overlay_texture) {
+ scene::IMeshBuffer *copy = cloneMeshBuffer(mesh->getMeshBuffer(0));
+ copy->getMaterial().setTexture(0, overlay_texture);
+ mesh->addMeshBuffer(copy);
+ copy->drop();
+ }
+ changeToMesh(mesh);
+ mesh->drop();
+
+ m_meshnode->setScale(wield_scale * WIELD_SCALE_FACTOR_EXTRUDED);
+
+ // Customize materials
+ for (u32 layer = 0; layer < m_meshnode->getMaterialCount(); layer++) {
+ video::SMaterial &material = m_meshnode->getMaterial(layer);
+ material.TextureLayer[0].TextureWrapU = video::ETC_CLAMP_TO_EDGE;
+ material.TextureLayer[0].TextureWrapV = video::ETC_CLAMP_TO_EDGE;
+ material.MaterialType = m_material_type;
+ material.setFlag(video::EMF_BACK_FACE_CULLING, true);
+ // Enable bi/trilinear filtering only for high resolution textures
+ if (dim.Width > 32) {
+ material.setFlag(video::EMF_BILINEAR_FILTER, m_bilinear_filter);
+ material.setFlag(video::EMF_TRILINEAR_FILTER, m_trilinear_filter);
+ } else {
+ material.setFlag(video::EMF_BILINEAR_FILTER, false);
+ material.setFlag(video::EMF_TRILINEAR_FILTER, false);
+ }
+ material.setFlag(video::EMF_ANISOTROPIC_FILTER, m_anisotropic_filter);
+ // mipmaps cause "thin black line" artifacts
+#if (IRRLICHT_VERSION_MAJOR >= 1 && IRRLICHT_VERSION_MINOR >= 8) || IRRLICHT_VERSION_MAJOR >= 2
+ material.setFlag(video::EMF_USE_MIP_MAPS, false);
+#endif
+ if (m_enable_shaders) {
+ material.setTexture(2, tsrc->getShaderFlagsTexture(false));
+ }
+ }
+}
+
+scene::SMesh *createSpecialNodeMesh(Client *client, content_t id, std::vector<ItemPartColor> *colors)
+{
+ MeshMakeData mesh_make_data(client, false, false);
+ MeshCollector collector;
+ mesh_make_data.setSmoothLighting(false);
+ MapblockMeshGenerator gen(&mesh_make_data, &collector);
+ gen.renderSingle(id);
+ colors->clear();
+ scene::SMesh *mesh = new scene::SMesh();
+ for (auto &prebuffers : collector.prebuffers)
+ for (PreMeshBuffer &p : prebuffers) {
+ if (p.layer.material_flags & MATERIAL_FLAG_ANIMATION) {
+ const FrameSpec &frame = (*p.layer.frames)[0];
+ p.layer.texture = frame.texture;
+ p.layer.normal_texture = frame.normal_texture;
+ }
+ for (video::S3DVertex &v : p.vertices)
+ v.Color.setAlpha(255);
+ scene::SMeshBuffer *buf = new scene::SMeshBuffer();
+ buf->Material.setTexture(0, p.layer.texture);
+ p.layer.applyMaterialOptions(buf->Material);
+ mesh->addMeshBuffer(buf);
+ buf->append(&p.vertices[0], p.vertices.size(),
+ &p.indices[0], p.indices.size());
+ buf->drop();
+ colors->push_back(
+ ItemPartColor(p.layer.has_color, p.layer.color));
+ }
+ return mesh;
+}
+
+void WieldMeshSceneNode::setItem(const ItemStack &item, Client *client)
+{
+ ITextureSource *tsrc = client->getTextureSource();
+ IItemDefManager *idef = client->getItemDefManager();
+ IShaderSource *shdrsrc = client->getShaderSource();
+ const NodeDefManager *ndef = client->getNodeDefManager();
+ const ItemDefinition &def = item.getDefinition(idef);
+ const ContentFeatures &f = ndef->get(def.name);
+ content_t id = ndef->getId(def.name);
+
+ scene::SMesh *mesh = nullptr;
+
+ if (m_enable_shaders) {
+ u32 shader_id = shdrsrc->getShader("wielded_shader", TILE_MATERIAL_BASIC, NDT_NORMAL);
+ m_material_type = shdrsrc->getShaderInfo(shader_id).material;
+ }
+
+ // Color-related
+ m_colors.clear();
+ m_base_color = idef->getItemstackColor(item, client);
+
+ // If wield_image is defined, it overrides everything else
+ if (!def.wield_image.empty()) {
+ setExtruded(def.wield_image, def.wield_overlay, def.wield_scale, tsrc,
+ 1);
+ m_colors.emplace_back();
+ // overlay is white, if present
+ m_colors.emplace_back(true, video::SColor(0xFFFFFFFF));
+ return;
+ }
+
+ // Handle nodes
+ // See also CItemDefManager::createClientCached()
+ if (def.type == ITEM_NODE) {
+ if (f.mesh_ptr[0]) {
+ // e.g. mesh nodes and nodeboxes
+ mesh = cloneMesh(f.mesh_ptr[0]);
+ postProcessNodeMesh(mesh, f, m_enable_shaders, true,
+ &m_material_type, &m_colors);
+ changeToMesh(mesh);
+ mesh->drop();
+ // mesh is pre-scaled by BS * f->visual_scale
+ m_meshnode->setScale(
+ def.wield_scale * WIELD_SCALE_FACTOR
+ / (BS * f.visual_scale));
+ } else {
+ switch (f.drawtype) {
+ case NDT_AIRLIKE: {
+ changeToMesh(nullptr);
+ break;
+ }
+ case NDT_PLANTLIKE: {
+ setExtruded(tsrc->getTextureName(f.tiles[0].layers[0].texture_id),
+ tsrc->getTextureName(f.tiles[0].layers[1].texture_id),
+ def.wield_scale, tsrc,
+ f.tiles[0].layers[0].animation_frame_count);
+ // Add color
+ const TileLayer &l0 = f.tiles[0].layers[0];
+ m_colors.emplace_back(l0.has_color, l0.color);
+ const TileLayer &l1 = f.tiles[0].layers[1];
+ m_colors.emplace_back(l1.has_color, l1.color);
+ break;
+ }
+ case NDT_PLANTLIKE_ROOTED: {
+ setExtruded(tsrc->getTextureName(f.special_tiles[0].layers[0].texture_id),
+ "", def.wield_scale, tsrc,
+ f.special_tiles[0].layers[0].animation_frame_count);
+ // Add color
+ const TileLayer &l0 = f.special_tiles[0].layers[0];
+ m_colors.emplace_back(l0.has_color, l0.color);
+ break;
+ }
+ case NDT_NORMAL:
+ case NDT_ALLFACES:
+ case NDT_LIQUID:
+ case NDT_FLOWINGLIQUID: {
+ setCube(f, def.wield_scale);
+ break;
+ }
+ default: {
+ mesh = createSpecialNodeMesh(client, id, &m_colors);
+ changeToMesh(mesh);
+ mesh->drop();
+ m_meshnode->setScale(
+ def.wield_scale * WIELD_SCALE_FACTOR
+ / (BS * f.visual_scale));
+ }
+ }
+ }
+ u32 material_count = m_meshnode->getMaterialCount();
+ for (u32 i = 0; i < material_count; ++i) {
+ video::SMaterial &material = m_meshnode->getMaterial(i);
+ material.MaterialType = m_material_type;
+ material.setFlag(video::EMF_BACK_FACE_CULLING, true);
+ material.setFlag(video::EMF_BILINEAR_FILTER, m_bilinear_filter);
+ material.setFlag(video::EMF_TRILINEAR_FILTER, m_trilinear_filter);
+ }
+ return;
+ }
+ else if (!def.inventory_image.empty()) {
+ setExtruded(def.inventory_image, def.inventory_overlay, def.wield_scale,
+ tsrc, 1);
+ m_colors.emplace_back();
+ // overlay is white, if present
+ m_colors.emplace_back(true, video::SColor(0xFFFFFFFF));
+ return;
+ }
+
+ // no wield mesh found
+ changeToMesh(nullptr);
+}
+
+void WieldMeshSceneNode::setColor(video::SColor c)
+{
+ assert(!m_lighting);
+ scene::IMesh *mesh = m_meshnode->getMesh();
+ if (!mesh)
+ return;
+
+ u8 red = c.getRed();
+ u8 green = c.getGreen();
+ u8 blue = c.getBlue();
+ u32 mc = mesh->getMeshBufferCount();
+ for (u32 j = 0; j < mc; j++) {
+ video::SColor bc(m_base_color);
+ if ((m_colors.size() > j) && (m_colors[j].override_base))
+ bc = m_colors[j].color;
+ video::SColor buffercolor(255,
+ bc.getRed() * red / 255,
+ bc.getGreen() * green / 255,
+ bc.getBlue() * blue / 255);
+ scene::IMeshBuffer *buf = mesh->getMeshBuffer(j);
+ colorizeMeshBuffer(buf, &buffercolor);
+ }
+}
+
+void WieldMeshSceneNode::render()
+{
+ // note: if this method is changed to actually do something,
+ // you probably should implement OnRegisterSceneNode as well
+}
+
+void WieldMeshSceneNode::changeToMesh(scene::IMesh *mesh)
+{
+ if (!mesh) {
+ scene::IMesh *dummymesh = g_extrusion_mesh_cache->createCube();
+ m_meshnode->setVisible(false);
+ m_meshnode->setMesh(dummymesh);
+ dummymesh->drop(); // m_meshnode grabbed it
+ } else {
+ m_meshnode->setMesh(mesh);
+ }
+
+ m_meshnode->setMaterialFlag(video::EMF_LIGHTING, m_lighting);
+ // need to normalize normals when lighting is enabled (because of setScale())
+ m_meshnode->setMaterialFlag(video::EMF_NORMALIZE_NORMALS, m_lighting);
+ m_meshnode->setVisible(true);
+}
+
+void getItemMesh(Client *client, const ItemStack &item, ItemMesh *result)
+{
+ ITextureSource *tsrc = client->getTextureSource();
+ IItemDefManager *idef = client->getItemDefManager();
+ const NodeDefManager *ndef = client->getNodeDefManager();
+ const ItemDefinition &def = item.getDefinition(idef);
+ const ContentFeatures &f = ndef->get(def.name);
+ content_t id = ndef->getId(def.name);
+
+ FATAL_ERROR_IF(!g_extrusion_mesh_cache, "Extrusion mesh cache is not yet initialized");
+
+ scene::SMesh *mesh = nullptr;
+
+ // Shading is on by default
+ result->needs_shading = true;
+
+ // If inventory_image is defined, it overrides everything else
+ if (!def.inventory_image.empty()) {
+ mesh = getExtrudedMesh(tsrc, def.inventory_image,
+ def.inventory_overlay);
+ result->buffer_colors.emplace_back();
+ // overlay is white, if present
+ result->buffer_colors.emplace_back(true, video::SColor(0xFFFFFFFF));
+ // Items with inventory images do not need shading
+ result->needs_shading = false;
+ } else if (def.type == ITEM_NODE) {
+ if (f.mesh_ptr[0]) {
+ mesh = cloneMesh(f.mesh_ptr[0]);
+ scaleMesh(mesh, v3f(0.12, 0.12, 0.12));
+ postProcessNodeMesh(mesh, f, false, false, nullptr,
+ &result->buffer_colors);
+ } else {
+ switch (f.drawtype) {
+ case NDT_PLANTLIKE: {
+ mesh = getExtrudedMesh(tsrc,
+ tsrc->getTextureName(f.tiles[0].layers[0].texture_id),
+ tsrc->getTextureName(f.tiles[0].layers[1].texture_id));
+ // Add color
+ const TileLayer &l0 = f.tiles[0].layers[0];
+ result->buffer_colors.emplace_back(l0.has_color, l0.color);
+ const TileLayer &l1 = f.tiles[0].layers[1];
+ result->buffer_colors.emplace_back(l1.has_color, l1.color);
+ break;
+ }
+ case NDT_PLANTLIKE_ROOTED: {
+ mesh = getExtrudedMesh(tsrc,
+ tsrc->getTextureName(f.special_tiles[0].layers[0].texture_id), "");
+ // Add color
+ const TileLayer &l0 = f.special_tiles[0].layers[0];
+ result->buffer_colors.emplace_back(l0.has_color, l0.color);
+ break;
+ }
+ case NDT_NORMAL:
+ case NDT_ALLFACES:
+ case NDT_LIQUID:
+ case NDT_FLOWINGLIQUID: {
+ scene::IMesh *cube = g_extrusion_mesh_cache->createCube();
+ mesh = cloneMesh(cube);
+ cube->drop();
+ scaleMesh(mesh, v3f(1.2, 1.2, 1.2));
+ // add overlays
+ postProcessNodeMesh(mesh, f, false, false, nullptr,
+ &result->buffer_colors);
+ break;
+ }
+ default: {
+ mesh = createSpecialNodeMesh(client, id, &result->buffer_colors);
+ scaleMesh(mesh, v3f(0.12, 0.12, 0.12));
+ }
+ }
+ }
+
+ u32 mc = mesh->getMeshBufferCount();
+ for (u32 i = 0; i < mc; ++i) {
+ scene::IMeshBuffer *buf = mesh->getMeshBuffer(i);
+ video::SMaterial &material = buf->getMaterial();
+ material.MaterialType = video::EMT_TRANSPARENT_ALPHA_CHANNEL;
+ material.setFlag(video::EMF_BILINEAR_FILTER, false);
+ material.setFlag(video::EMF_TRILINEAR_FILTER, false);
+ material.setFlag(video::EMF_BACK_FACE_CULLING, true);
+ material.setFlag(video::EMF_LIGHTING, false);
+ }
+
+ rotateMeshXZby(mesh, -45);
+ rotateMeshYZby(mesh, -30);
+ }
+ result->mesh = mesh;
+}
+
+
+
+scene::SMesh *getExtrudedMesh(ITextureSource *tsrc,
+ const std::string &imagename, const std::string &overlay_name)
+{
+ // check textures
+ video::ITexture *texture = tsrc->getTextureForMesh(imagename);
+ if (!texture) {
+ return NULL;
+ }
+ video::ITexture *overlay_texture =
+ (overlay_name.empty()) ? NULL : tsrc->getTexture(overlay_name);
+
+ // get mesh
+ core::dimension2d<u32> dim = texture->getSize();
+ scene::IMesh *original = g_extrusion_mesh_cache->create(dim);
+ scene::SMesh *mesh = cloneMesh(original);
+ original->drop();
+
+ //set texture
+ mesh->getMeshBuffer(0)->getMaterial().setTexture(0,
+ tsrc->getTexture(imagename));
+ if (overlay_texture) {
+ scene::IMeshBuffer *copy = cloneMeshBuffer(mesh->getMeshBuffer(0));
+ copy->getMaterial().setTexture(0, overlay_texture);
+ mesh->addMeshBuffer(copy);
+ copy->drop();
+ }
+ // Customize materials
+ for (u32 layer = 0; layer < mesh->getMeshBufferCount(); layer++) {
+ video::SMaterial &material = mesh->getMeshBuffer(layer)->getMaterial();
+ material.TextureLayer[0].TextureWrapU = video::ETC_CLAMP_TO_EDGE;
+ material.TextureLayer[0].TextureWrapV = video::ETC_CLAMP_TO_EDGE;
+ material.setFlag(video::EMF_BILINEAR_FILTER, false);
+ material.setFlag(video::EMF_TRILINEAR_FILTER, false);
+ material.setFlag(video::EMF_BACK_FACE_CULLING, true);
+ material.setFlag(video::EMF_LIGHTING, false);
+ material.MaterialType = video::EMT_TRANSPARENT_ALPHA_CHANNEL;
+ }
+ scaleMesh(mesh, v3f(2.0, 2.0, 2.0));
+
+ return mesh;
+}
+
+void postProcessNodeMesh(scene::SMesh *mesh, const ContentFeatures &f,
+ bool use_shaders, bool set_material, const video::E_MATERIAL_TYPE *mattype,
+ std::vector<ItemPartColor> *colors, bool apply_scale)
+{
+ u32 mc = mesh->getMeshBufferCount();
+ // Allocate colors for existing buffers
+ colors->clear();
+ for (u32 i = 0; i < mc; ++i)
+ colors->push_back(ItemPartColor());
+
+ for (u32 i = 0; i < mc; ++i) {
+ const TileSpec *tile = &(f.tiles[i]);
+ scene::IMeshBuffer *buf = mesh->getMeshBuffer(i);
+ for (int layernum = 0; layernum < MAX_TILE_LAYERS; layernum++) {
+ const TileLayer *layer = &tile->layers[layernum];
+ if (layer->texture_id == 0)
+ continue;
+ if (layernum != 0) {
+ scene::IMeshBuffer *copy = cloneMeshBuffer(buf);
+ copy->getMaterial() = buf->getMaterial();
+ mesh->addMeshBuffer(copy);
+ copy->drop();
+ buf = copy;
+ colors->push_back(
+ ItemPartColor(layer->has_color, layer->color));
+ } else {
+ (*colors)[i] = ItemPartColor(layer->has_color, layer->color);
+ }
+ video::SMaterial &material = buf->getMaterial();
+ if (set_material)
+ layer->applyMaterialOptions(material);
+ if (mattype) {
+ material.MaterialType = *mattype;
+ }
+ if (layer->animation_frame_count > 1) {
+ const FrameSpec &animation_frame = (*layer->frames)[0];
+ material.setTexture(0, animation_frame.texture);
+ } else {
+ material.setTexture(0, layer->texture);
+ }
+ if (use_shaders) {
+ if (layer->normal_texture) {
+ if (layer->animation_frame_count > 1) {
+ const FrameSpec &animation_frame = (*layer->frames)[0];
+ material.setTexture(1, animation_frame.normal_texture);
+ } else
+ material.setTexture(1, layer->normal_texture);
+ }
+ material.setTexture(2, layer->flags_texture);
+ }
+ if (apply_scale && tile->world_aligned) {
+ u32 n = buf->getVertexCount();
+ for (u32 k = 0; k != n; ++k)
+ buf->getTCoords(k) /= layer->scale;
+ }
+ }
+ }
+}
diff --git a/src/client/wieldmesh.h b/src/client/wieldmesh.h
new file mode 100644
index 000000000..0908d3ac2
--- /dev/null
+++ b/src/client/wieldmesh.h
@@ -0,0 +1,140 @@
+/*
+Minetest
+Copyright (C) 2010-2014 celeron55, Perttu Ahola <celeron55@gmail.com>
+
+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 <string>
+#include <vector>
+#include "irrlichttypes_extrabloated.h"
+
+struct ItemStack;
+class Client;
+class ITextureSource;
+struct ContentFeatures;
+
+/*!
+ * Holds color information of an item mesh's buffer.
+ */
+struct ItemPartColor
+{
+ /*!
+ * If this is false, the global base color of the item
+ * will be used instead of the specific color of the
+ * buffer.
+ */
+ bool override_base = false;
+ /*!
+ * The color of the buffer.
+ */
+ video::SColor color = 0;
+
+ ItemPartColor() = default;
+
+ ItemPartColor(bool override, video::SColor color) :
+ override_base(override), color(color)
+ {
+ }
+};
+
+struct ItemMesh
+{
+ scene::IMesh *mesh = nullptr;
+ /*!
+ * Stores the color of each mesh buffer.
+ */
+ std::vector<ItemPartColor> buffer_colors;
+ /*!
+ * If false, all faces of the item should have the same brightness.
+ * Disables shading based on normal vectors.
+ */
+ bool needs_shading = true;
+
+ ItemMesh() = default;
+};
+
+/*
+ Wield item scene node, renders the wield mesh of some item
+*/
+class WieldMeshSceneNode : public scene::ISceneNode
+{
+public:
+ WieldMeshSceneNode(scene::ISceneManager *mgr, s32 id = -1, bool lighting = false);
+ virtual ~WieldMeshSceneNode();
+
+ void setCube(const ContentFeatures &f, v3f wield_scale);
+ void setExtruded(const std::string &imagename, const std::string &overlay_image,
+ v3f wield_scale, ITextureSource *tsrc, u8 num_frames);
+ void setItem(const ItemStack &item, Client *client);
+
+ // Sets the vertex color of the wield mesh.
+ // Must only be used if the constructor was called with lighting = false
+ void setColor(video::SColor color);
+
+ scene::IMesh *getMesh() { return m_meshnode->getMesh(); }
+
+ virtual void render();
+
+ virtual const aabb3f &getBoundingBox() const { return m_bounding_box; }
+
+private:
+ void changeToMesh(scene::IMesh *mesh);
+
+ // Child scene node with the current wield mesh
+ scene::IMeshSceneNode *m_meshnode = nullptr;
+ video::E_MATERIAL_TYPE m_material_type;
+
+ // True if EMF_LIGHTING should be enabled.
+ bool m_lighting;
+
+ bool m_enable_shaders;
+ bool m_anisotropic_filter;
+ bool m_bilinear_filter;
+ bool m_trilinear_filter;
+ /*!
+ * Stores the colors of the mesh's mesh buffers.
+ * This does not include lighting.
+ */
+ std::vector<ItemPartColor> m_colors;
+ /*!
+ * The base color of this mesh. This is the default
+ * for all mesh buffers.
+ */
+ video::SColor m_base_color;
+
+ // Bounding box culling is disabled for this type of scene node,
+ // so this variable is just required so we can implement
+ // getBoundingBox() and is set to an empty box.
+ aabb3f m_bounding_box;
+};
+
+void getItemMesh(Client *client, const ItemStack &item, ItemMesh *result);
+
+scene::SMesh *getExtrudedMesh(ITextureSource *tsrc, const std::string &imagename,
+ const std::string &overlay_name);
+
+/*!
+ * Applies overlays, textures and optionally materials to the given mesh and
+ * extracts tile colors for colorization.
+ * \param mattype overrides the buffer's material type, but can also
+ * be NULL to leave the original material.
+ * \param colors returns the colors of the mesh buffers in the mesh.
+ */
+void postProcessNodeMesh(scene::SMesh *mesh, const ContentFeatures &f, bool use_shaders,
+ bool set_material, const video::E_MATERIAL_TYPE *mattype,
+ std::vector<ItemPartColor> *colors, bool apply_scale = false);