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)
This commit is contained in:
grorp 2024-01-21 17:44:08 +01:00 committed by GitHub
parent 8cbd629010
commit 404a063fdf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 288 additions and 108 deletions

@ -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:

@ -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.

@ -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"

@ -2,6 +2,8 @@
Copyright (C) 2014 sapier
Copyright (C) 2018 srifqi, Muhammad Rifqi Priyo Susanto
<muhammadrifqipriyosusanto@gmail.com>
Copyright (C) 2024 grorp, Gregor Parzefall
<gregor.parzefall@posteo.de>
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 (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;
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,35 +981,25 @@ 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_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(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;
}
->getRayFromScreenCoordinates(pointer_pos);
}
m_settings_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;
}
}

@ -1,5 +1,7 @@
/*
Copyright (C) 2014 sapier
Copyright (C) 2024 grorp, Gregor Parzefall
<gregor.parzefall@posteo.de>
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 <unordered_map>
#include <vector>
#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;

@ -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 <map>
#include <set>
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) {};
}

@ -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<u8> place_param2;
bool wallmounted_rotate_vertical;
TouchInteraction touch_interaction;
/*
Some helpful methods
*/

@ -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");
}
/******************************************************************************/

@ -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},
};

@ -59,3 +59,4 @@ public:
extern EnumString es_ItemType[];
extern EnumString es_TouchInteractionMode[];