From 404a063fdfbbddcba32d22dd1c34dd73922bc12a Mon Sep 17 00:00:00 2001 From: grorp Date: Sun, 21 Jan 2024 17:44:08 +0100 Subject: [PATCH] Touchscreen: Allow mods to swap the meaning of short and long taps (punch with single tap) (#14087) This works through a new field "touch_interaction" in item definitions. The two most important use cases are: - Punching players/entities with short tap instead of long tap (enabled by default) - Making items usable that require holding the place button (e.g. bows and shields in MC-like games) --- doc/android.md | 4 +- doc/lua_api.md | 14 ++ src/client/game.cpp | 9 +- src/gui/touchscreengui.cpp | 228 ++++++++++++++++++-------------- src/gui/touchscreengui.h | 31 ++++- src/itemdef.cpp | 56 ++++++++ src/itemdef.h | 22 +++ src/script/common/c_content.cpp | 24 ++++ src/script/common/c_types.cpp | 7 + src/script/common/c_types.h | 1 + 10 files changed, 288 insertions(+), 108 deletions(-) diff --git a/doc/android.md b/doc/android.md index 843d842d3..44e66d7c1 100644 --- a/doc/android.md +++ b/doc/android.md @@ -9,8 +9,8 @@ due to limited capabilities of common devices. What can be done is described bel While you're playing the game normally (that is, no menu or inventory is shown), the following controls are available: * Look around: touch screen and slide finger -* Tap: Place a node -* Long tap: Dig node or use the held item +* Tap: Place a node, punch an object or use the selected item (default) +* Long tap: Dig a node or use the selected item (default) * Press back: Pause menu * Touch buttons: Press button * Buttons: diff --git a/doc/lua_api.md b/doc/lua_api.md index 1f62e46a0..ab2602043 100644 --- a/doc/lua_api.md +++ b/doc/lua_api.md @@ -8787,6 +8787,20 @@ Used by `minetest.register_node`, `minetest.register_craftitem`, and -- Otherwise should be name of node which the client immediately places -- upon digging. Server will always update with actual result shortly. + touch_interaction = { + -- Only affects touchscreen clients. + -- Defines the meaning of short and long taps with the item in hand. + -- The fields in this table have two valid values: + -- * "long_dig_short_place" (long tap = dig, short tap = place) + -- * "short_dig_long_place" (short tap = dig, long tap = place) + -- The field to be used is selected according to the current + -- `pointed_thing`. + + pointed_nothing = "long_dig_short_place", + pointed_node = "long_dig_short_place", + pointed_object = "short_dig_long_place", + }, + sound = { -- Definition of item sounds to be played at various events. -- All fields in this table are optional. diff --git a/src/client/game.cpp b/src/client/game.cpp index 6916cefdc..b740f43a1 100644 --- a/src/client/game.cpp +++ b/src/client/game.cpp @@ -3349,6 +3349,11 @@ void Game::processPlayerInteraction(f32 dtime, bool show_hud) if (pointed != runData.pointed_old) infostream << "Pointing at " << pointed.dump() << std::endl; +#ifdef HAVE_TOUCHSCREENGUI + if (g_touchscreengui) + g_touchscreengui->applyContextControls(selected_def.touch_interaction.getMode(pointed)); +#endif + // Note that updating the selection mesh every frame is not particularly efficient, // but the halo rendering code is already inefficient so there's no point in optimizing it here hud->updateSelectionMesh(camera_offset); @@ -4484,8 +4489,8 @@ void Game::showPauseMenu() static const std::string control_text = strgettext("Controls:\n" "No menu open:\n" "- slide finger: look around\n" - "- tap: place/use\n" - "- long tap: dig/punch/use\n" + "- tap: place/punch/use (default)\n" + "- long tap: dig/use (default)\n" "Menu/inventory open:\n" "- double tap (outside):\n" " --> close\n" diff --git a/src/gui/touchscreengui.cpp b/src/gui/touchscreengui.cpp index 104dcbf7b..c9fe5569e 100644 --- a/src/gui/touchscreengui.cpp +++ b/src/gui/touchscreengui.cpp @@ -2,6 +2,8 @@ Copyright (C) 2014 sapier Copyright (C) 2018 srifqi, Muhammad Rifqi Priyo Susanto +Copyright (C) 2024 grorp, Gregor Parzefall + 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 @@ -657,23 +659,13 @@ void TouchScreenGUI::handleReleaseEvent(size_t evt_id) // handle the point used for moving view m_has_move_id = false; - // if this pointer issued a mouse event issue symmetric release here - if (m_move_sent_as_mouse_event) { - SEvent translated {}; - translated.EventType = EET_MOUSE_INPUT_EVENT; - translated.MouseInput.X = m_move_downlocation.X; - translated.MouseInput.Y = m_move_downlocation.Y; - translated.MouseInput.Shift = false; - translated.MouseInput.Control = false; - translated.MouseInput.ButtonStates = 0; - translated.MouseInput.Event = EMIE_LMOUSE_LEFT_UP; - if (m_draw_crosshair) { - translated.MouseInput.X = m_screensize.X / 2; - translated.MouseInput.Y = m_screensize.Y / 2; - } - m_receiver->OnEvent(translated); - } else if (!m_move_has_really_moved) { - doRightClick(); + // If m_tap_state is already set to TapState::ShortTap, we must keep + // that value. Otherwise, many short taps will be ignored if you tap + // very fast. + if (!m_move_has_really_moved && m_tap_state != TapState::LongTap) { + m_tap_state = TapState::ShortTap; + } else { + m_tap_state = TapState::None; } } @@ -794,10 +786,8 @@ void TouchScreenGUI::translateEvent(const SEvent &event) m_move_id = event.TouchInput.ID; m_move_has_really_moved = false; m_move_downtime = porting::getTimeMs(); - m_move_downlocation = touch_pos; - m_move_sent_as_mouse_event = false; - if (m_draw_crosshair) - m_move_downlocation = v2s32(m_screensize.X / 2, m_screensize.Y / 2); + // DON'T reset m_tap_state here, otherwise many short taps + // will be ignored if you tap very fast. } } } @@ -820,33 +810,20 @@ void TouchScreenGUI::translateEvent(const SEvent &event) const double touch_threshold_sq = m_touchscreen_threshold * m_touchscreen_threshold; - if (m_has_move_id) { - if (event.TouchInput.ID == m_move_id && - (!m_move_sent_as_mouse_event || m_draw_crosshair)) { - if (dir_free.getLengthSQ() > touch_threshold_sq || m_move_has_really_moved) { - m_move_has_really_moved = true; + if (m_has_move_id && event.TouchInput.ID == m_move_id) { + if (dir_free.getLengthSQ() > touch_threshold_sq || m_move_has_really_moved) { + m_move_has_really_moved = true; - // update camera_yaw and camera_pitch - m_pointer_pos[event.TouchInput.ID] = touch_pos; + m_pointer_pos[event.TouchInput.ID] = touch_pos; + if (m_tap_state == TapState::None || m_draw_crosshair) { // adapt to similar behavior as pc screen const double d = g_settings->getFloat("touchscreen_sensitivity", 0.001f, 10.0f) * 3.0f; + // update camera_yaw and camera_pitch m_camera_yaw_change -= dir_free.X * d; m_camera_pitch_change += dir_free.Y * d; - - // update shootline - // no need to update (X, Y) when using crosshair since the shootline is not used - m_shootline = m_device - ->getSceneManager() - ->getSceneCollisionManager() - ->getRayFromScreenCoordinates(touch_pos); } - } else if (event.TouchInput.ID == m_move_id && m_move_sent_as_mouse_event) { - m_shootline = m_device - ->getSceneManager() - ->getSceneCollisionManager() - ->getRayFromScreenCoordinates(touch_pos); } } @@ -931,40 +908,6 @@ void TouchScreenGUI::handleChangedButton(const SEvent &event) handleButtonEvent((touch_gui_button_id) current_button_id, event.TouchInput.ID, true); } -bool TouchScreenGUI::doRightClick() -{ - v2s32 mPos = v2s32(m_move_downlocation.X, m_move_downlocation.Y); - if (m_draw_crosshair) { - mPos.X = m_screensize.X / 2; - mPos.Y = m_screensize.Y / 2; - } - - SEvent translated {}; - translated.EventType = EET_MOUSE_INPUT_EVENT; - translated.MouseInput.X = mPos.X; - translated.MouseInput.Y = mPos.Y; - translated.MouseInput.Shift = false; - translated.MouseInput.Control = false; - translated.MouseInput.ButtonStates = EMBSM_RIGHT; - - // update shootline - m_shootline = m_device - ->getSceneManager() - ->getSceneCollisionManager() - ->getRayFromScreenCoordinates(mPos); - - translated.MouseInput.Event = EMIE_RMOUSE_PRESSED_DOWN; - verbosestream << "TouchScreenGUI::translateEvent right click press" << std::endl; - m_receiver->OnEvent(translated); - - translated.MouseInput.ButtonStates = 0; - translated.MouseInput.Event = EMIE_RMOUSE_LEFT_UP; - verbosestream << "TouchScreenGUI::translateEvent right click release" << std::endl; - m_receiver->OnEvent(translated); - - return true; -} - void TouchScreenGUI::applyJoystickStatus() { if (m_joystick_triggers_aux1) { @@ -1038,37 +981,27 @@ void TouchScreenGUI::step(float dtime) applyJoystickStatus(); // if a new placed pointer isn't moved for some time start digging - if (m_has_move_id && - (!m_move_has_really_moved) && - (!m_move_sent_as_mouse_event)) { + if (m_has_move_id && !m_move_has_really_moved && m_tap_state == TapState::None) { u64 delta = porting::getDeltaMs(m_move_downtime, porting::getTimeMs()); if (delta > MIN_DIG_TIME_MS) { - s32 mX = m_move_downlocation.X; - s32 mY = m_move_downlocation.Y; - if (m_draw_crosshair) { - mX = m_screensize.X / 2; - mY = m_screensize.Y / 2; - } - m_shootline = m_device - ->getSceneManager() - ->getSceneCollisionManager() - ->getRayFromScreenCoordinates(v2s32(mX, mY)); - - SEvent translated {}; - translated.EventType = EET_MOUSE_INPUT_EVENT; - translated.MouseInput.X = mX; - translated.MouseInput.Y = mY; - translated.MouseInput.Shift = false; - translated.MouseInput.Control = false; - translated.MouseInput.ButtonStates = EMBSM_LEFT; - translated.MouseInput.Event = EMIE_LMOUSE_PRESSED_DOWN; - verbosestream << "TouchScreenGUI::step left click press" << std::endl; - m_receiver->OnEvent(translated); - m_move_sent_as_mouse_event = true; + m_tap_state = TapState::LongTap; } } + // Update the shootline. + // Since not only the pointer position, but also the player position and + // thus the camera position can change, it doesn't suffice to update the + // shootline when a touch event occurs. + // Note that the shootline isn't used if touch_use_crosshair is enabled. + if (!m_draw_crosshair) { + v2s32 pointer_pos = getPointerPos(); + m_shootline = m_device + ->getSceneManager() + ->getSceneCollisionManager() + ->getRayFromScreenCoordinates(pointer_pos); + } + m_settings_bar.step(dtime); m_rare_controls_bar.step(dtime); } @@ -1125,3 +1058,100 @@ void TouchScreenGUI::show() setVisible(true); } + +v2s32 TouchScreenGUI::getPointerPos() +{ + if (m_draw_crosshair) + return v2s32(m_screensize.X / 2, m_screensize.Y / 2); + return m_pointer_pos[m_move_id]; +} + +void TouchScreenGUI::emitMouseEvent(EMOUSE_INPUT_EVENT type) +{ + v2s32 pointer_pos = getPointerPos(); + + SEvent event{}; + event.EventType = EET_MOUSE_INPUT_EVENT; + event.MouseInput.X = pointer_pos.X; + event.MouseInput.Y = pointer_pos.Y; + event.MouseInput.Shift = false; + event.MouseInput.Control = false; + event.MouseInput.ButtonStates = 0; + event.MouseInput.Event = type; + m_receiver->OnEvent(event); +} + +void TouchScreenGUI::applyContextControls(const TouchInteractionMode &mode) +{ + // Since the pointed thing has already been determined when this function + // is called, we cannot use this function to update the shootline. + + bool target_dig_pressed = false; + bool target_place_pressed = false; + + u64 now = porting::getTimeMs(); + + switch (m_tap_state) { + case TapState::ShortTap: + if (mode == SHORT_DIG_LONG_PLACE) { + if (!m_dig_pressed) { + // The button isn't currently pressed, we can press it. + m_dig_pressed_until = now + SIMULATED_CLICK_DURATION_MS; + // We're done with this short tap. + m_tap_state = TapState::None; + } else { + // The button is already pressed, perhaps due to another short tap. + // Release it now, press it again during the next client step. + // We can't release and press during the same client step because + // the digging code simply ignores that. + m_dig_pressed_until = 0; + } + } else { + if (!m_place_pressed) { + // The button isn't currently pressed, we can press it. + m_place_pressed_until = now + SIMULATED_CLICK_DURATION_MS; + // We're done with this short tap. + m_tap_state = TapState::None; + } else { + // The button is already pressed, perhaps due to another short tap. + // Release it now, press it again during the next client step. + // We can't release and press during the same client step because + // the digging code simply ignores that. + m_place_pressed_until = 0; + } + } + break; + + case TapState::LongTap: + if (mode == SHORT_DIG_LONG_PLACE) + target_place_pressed = true; + else + target_dig_pressed = true; + break; + + case TapState::None: + break; + } + + // Apply short taps. + target_dig_pressed |= now < m_dig_pressed_until; + target_place_pressed |= now < m_place_pressed_until; + + if (target_dig_pressed && !m_dig_pressed) { + emitMouseEvent(EMIE_LMOUSE_PRESSED_DOWN); + m_dig_pressed = true; + + } else if (!target_dig_pressed && m_dig_pressed) { + emitMouseEvent(EMIE_LMOUSE_LEFT_UP); + m_dig_pressed = false; + } + + if (target_place_pressed && !m_place_pressed) { + emitMouseEvent(EMIE_RMOUSE_PRESSED_DOWN); + m_place_pressed = true; + + } else if (!target_place_pressed && m_place_pressed) { + emitMouseEvent(irr::EMIE_RMOUSE_LEFT_UP); + m_place_pressed = false; + } +} diff --git a/src/gui/touchscreengui.h b/src/gui/touchscreengui.h index b60edac79..2e48a71cf 100644 --- a/src/gui/touchscreengui.h +++ b/src/gui/touchscreengui.h @@ -1,5 +1,7 @@ /* Copyright (C) 2014 sapier +Copyright (C) 2024 grorp, Gregor Parzefall + 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 @@ -29,6 +31,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include #include +#include "itemdef.h" #include "client/tile.h" #include "client/game.h" @@ -36,6 +39,13 @@ using namespace irr; using namespace irr::core; using namespace irr::gui; +enum class TapState +{ + None, + ShortTap, + LongTap, +}; + typedef enum { jump_id = 0, @@ -75,6 +85,11 @@ typedef enum #define SETTINGS_BAR_Y_OFFSET 5 #define RARE_CONTROLS_BAR_Y_OFFSET 5 +// Our simulated clicks last some milliseconds so that server-side mods have a +// chance to detect them via l_get_player_control. +// If you tap faster than this value, the simulated clicks are of course shorter. +#define SIMULATED_CLICK_DURATION_MS 50 + extern const std::string button_image_names[]; extern const std::string joystick_image_names[]; @@ -161,6 +176,7 @@ public: ~TouchScreenGUI(); void translateEvent(const SEvent &event); + void applyContextControls(const TouchInteractionMode &mode); void init(ISimpleTextureSource *tsrc); @@ -230,8 +246,6 @@ private: size_t m_move_id; bool m_move_has_really_moved = false; u64 m_move_downtime = 0; - bool m_move_sent_as_mouse_event = false; - v2s32 m_move_downlocation = v2s32(-10000, -10000); // off-screen bool m_has_joystick_id = false; size_t m_joystick_id; @@ -283,9 +297,6 @@ private: // handle pressing hotbar items bool isHotbarButton(const SEvent &event); - // do a right-click - bool doRightClick(); - // handle release event void handleReleaseEvent(size_t evt_id); @@ -300,6 +311,16 @@ private: // rare controls bar AutoHideButtonBar m_rare_controls_bar; + + v2s32 getPointerPos(); + void emitMouseEvent(EMOUSE_INPUT_EVENT type); + TapState m_tap_state = TapState::None; + + bool m_dig_pressed = false; + u64 m_dig_pressed_until = 0; + + bool m_place_pressed = false; + u64 m_place_pressed_until = 0; }; extern TouchScreenGUI *g_touchscreengui; diff --git a/src/itemdef.cpp b/src/itemdef.cpp index 21406e52e..143612afd 100644 --- a/src/itemdef.cpp +++ b/src/itemdef.cpp @@ -35,9 +35,60 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "util/serialize.h" #include "util/container.h" #include "util/thread.h" +#include "util/pointedthing.h" #include #include +TouchInteraction::TouchInteraction() +{ + pointed_nothing = LONG_DIG_SHORT_PLACE; + pointed_node = LONG_DIG_SHORT_PLACE; + // Map punching to single tap by default. + pointed_object = SHORT_DIG_LONG_PLACE; +} + +TouchInteractionMode TouchInteraction::getMode(const PointedThing &pointed) const +{ + switch (pointed.type) { + case POINTEDTHING_NOTHING: + return pointed_nothing; + case POINTEDTHING_NODE: + return pointed_node; + case POINTEDTHING_OBJECT: + return pointed_object; + default: + FATAL_ERROR("Invalid PointedThingType given to TouchInteraction::getMode"); + } +} + +void TouchInteraction::serialize(std::ostream &os) const +{ + writeU8(os, pointed_nothing); + writeU8(os, pointed_node); + writeU8(os, pointed_object); +} + +void TouchInteraction::deSerialize(std::istream &is) +{ + u8 tmp = readU8(is); + if (is.eof()) + throw SerializationError(""); + if (tmp < TouchInteractionMode_END) + pointed_nothing = (TouchInteractionMode)tmp; + + tmp = readU8(is); + if (is.eof()) + throw SerializationError(""); + if (tmp < TouchInteractionMode_END) + pointed_node = (TouchInteractionMode)tmp; + + tmp = readU8(is); + if (is.eof()) + throw SerializationError(""); + if (tmp < TouchInteractionMode_END) + pointed_object = (TouchInteractionMode)tmp; +} + /* ItemDefinition */ @@ -84,6 +135,7 @@ ItemDefinition& ItemDefinition::operator=(const ItemDefinition &def) range = def.range; palette_image = def.palette_image; color = def.color; + touch_interaction = def.touch_interaction; return *this; } @@ -126,6 +178,7 @@ void ItemDefinition::reset() node_placement_prediction.clear(); place_param2.reset(); wallmounted_rotate_vertical = false; + touch_interaction = TouchInteraction(); } void ItemDefinition::serialize(std::ostream &os, u16 protocol_version) const @@ -185,7 +238,9 @@ void ItemDefinition::serialize(std::ostream &os, u16 protocol_version) const os << (u8)place_param2.has_value(); // protocol_version >= 43 if (place_param2) os << *place_param2; + writeU8(os, wallmounted_rotate_vertical); + touch_interaction.serialize(os); } void ItemDefinition::deSerialize(std::istream &is, u16 protocol_version) @@ -260,6 +315,7 @@ void ItemDefinition::deSerialize(std::istream &is, u16 protocol_version) place_param2 = readU8(is); wallmounted_rotate_vertical = readU8(is); // 0 if missing + touch_interaction.deSerialize(is); } catch(SerializationError &e) {}; } diff --git a/src/itemdef.h b/src/itemdef.h index c7a579555..361848911 100644 --- a/src/itemdef.h +++ b/src/itemdef.h @@ -31,6 +31,7 @@ with this program; if not, write to the Free Software Foundation, Inc., class IGameDef; class Client; struct ToolCapabilities; +struct PointedThing; #ifndef SERVER #include "client/tile.h" struct ItemMesh; @@ -50,6 +51,25 @@ enum ItemType : u8 ItemType_END // Dummy for validity check }; +enum TouchInteractionMode : u8 +{ + LONG_DIG_SHORT_PLACE, + SHORT_DIG_LONG_PLACE, + TouchInteractionMode_END, // Dummy for validity check +}; + +struct TouchInteraction +{ + TouchInteractionMode pointed_nothing; + TouchInteractionMode pointed_node; + TouchInteractionMode pointed_object; + + TouchInteraction(); + TouchInteractionMode getMode(const PointedThing &pointed) const; + void serialize(std::ostream &os) const; + void deSerialize(std::istream &is); +}; + struct ItemDefinition { /* @@ -92,6 +112,8 @@ struct ItemDefinition std::optional place_param2; bool wallmounted_rotate_vertical; + TouchInteraction touch_interaction; + /* Some helpful methods */ diff --git a/src/script/common/c_content.cpp b/src/script/common/c_content.cpp index 430ae8e72..8e499a7bd 100644 --- a/src/script/common/c_content.cpp +++ b/src/script/common/c_content.cpp @@ -138,6 +138,20 @@ void read_item_definition(lua_State* L, int index, def.place_param2 = rangelim(place_param2, 0, U8_MAX); getboolfield(L, index, "wallmounted_rotate_vertical", def.wallmounted_rotate_vertical); + + lua_getfield(L, index, "touch_interaction"); + if (!lua_isnil(L, -1)) { + luaL_checktype(L, -1, LUA_TTABLE); + + TouchInteraction &inter = def.touch_interaction; + inter.pointed_nothing = (TouchInteractionMode)getenumfield(L, -1, "pointed_nothing", + es_TouchInteractionMode, inter.pointed_nothing); + inter.pointed_node = (TouchInteractionMode)getenumfield(L, -1, "pointed_node", + es_TouchInteractionMode, inter.pointed_node); + inter.pointed_object = (TouchInteractionMode)getenumfield(L, -1, "pointed_object", + es_TouchInteractionMode, inter.pointed_object); + } + lua_pop(L, 1); } /******************************************************************************/ @@ -199,6 +213,16 @@ void push_item_definition_full(lua_State *L, const ItemDefinition &i) lua_setfield(L, -2, "node_placement_prediction"); lua_pushboolean(L, i.wallmounted_rotate_vertical); lua_setfield(L, -2, "wallmounted_rotate_vertical"); + + lua_createtable(L, 0, 3); + const TouchInteraction &inter = i.touch_interaction; + lua_pushstring(L, es_TouchInteractionMode[inter.pointed_nothing].str); + lua_setfield(L, -2,"pointed_nothing"); + lua_pushstring(L, es_TouchInteractionMode[inter.pointed_node].str); + lua_setfield(L, -2,"pointed_node"); + lua_pushstring(L, es_TouchInteractionMode[inter.pointed_object].str); + lua_setfield(L, -2,"pointed_object"); + lua_setfield(L, -2, "touch_interaction"); } /******************************************************************************/ diff --git a/src/script/common/c_types.cpp b/src/script/common/c_types.cpp index e832ff2ab..7338834f7 100644 --- a/src/script/common/c_types.cpp +++ b/src/script/common/c_types.cpp @@ -32,3 +32,10 @@ struct EnumString es_ItemType[] = {ITEM_TOOL, "tool"}, {0, NULL}, }; + +struct EnumString es_TouchInteractionMode[] = + { + {LONG_DIG_SHORT_PLACE, "long_dig_short_place"}, + {SHORT_DIG_LONG_PLACE, "short_dig_long_place"}, + {0, NULL}, + }; diff --git a/src/script/common/c_types.h b/src/script/common/c_types.h index 86bfb0b6b..88d88703e 100644 --- a/src/script/common/c_types.h +++ b/src/script/common/c_types.h @@ -59,3 +59,4 @@ public: extern EnumString es_ItemType[]; +extern EnumString es_TouchInteractionMode[];