diff options
author | Quentin Bazin <quent42340@gmail.com> | 2018-11-28 20:01:49 +0100 |
---|---|---|
committer | SmallJoker <SmallJoker@users.noreply.github.com> | 2018-11-28 20:01:49 +0100 |
commit | 5f1cd555cd9d1c64426e173b30b5b792d211c835 (patch) | |
tree | 2c8508467d3bf28d549cce2d25144fa8ef42beae /src/client | |
parent | ddd9317b733857630499972179caebc236b4d991 (diff) | |
download | minetest-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')
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, + ¤t_intersection, ¤t_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 §or_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> ¤t_screen_size = + RenderingEngine::get_video_driver()->getScreenSize(); + // Verify if window size has changed and save it if it's the case + // Ensure evaluating settings->getBool after verifying screensize + // First condition is cheaper + if (previous_screen_size != current_screen_size && + current_screen_size != irr::core::dimension2d<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); |