From bec080be8d4b6c6d765249ce771483609120897a Mon Sep 17 00:00:00 2001 From: Jaidyn Ann <10477760+JadedCtrl@users.noreply.github.com> Date: Sat, 20 Jan 2024 14:36:42 +0000 Subject: [PATCH 01/19] Add Esperanto builtin translation (#14215) --- builtin/locale/__builtin.eo.tr | 246 +++++++++++++++++++++++++++++++++ 1 file changed, 246 insertions(+) create mode 100644 builtin/locale/__builtin.eo.tr diff --git a/builtin/locale/__builtin.eo.tr b/builtin/locale/__builtin.eo.tr new file mode 100644 index 0000000000..c3fec6c37b --- /dev/null +++ b/builtin/locale/__builtin.eo.tr @@ -0,0 +1,246 @@ +# textdomain: __builtin +Invalid parameters (see /help @1).=Nevalidaj parametroj (kontrolu /help @1) +Too many arguments, try using just /help =Tro da parametroj, eble provu /help +Available commands: @1=Nunaj ordonoj: @1 +Use '/help ' to get more information, or '/help all' to list everything.=Uzu «/help » por specifa informo, aŭ «/help all» por listigi ĉion. +Available commands:=Nunaj ordonoj: +Command not available: @1=Ordono neuzebla: @1 +[all | privs | ] [-t]=[all | privs | ] +Get help for commands or list privileges (-t: output in chat)=Listigi helpon pri ordonoj («all» aŭ ) aŭ rajtoj («privs») (kun -t: presu en babilejo) +Available privileges:=Nunaj rajtoj: +Command=Ordono +Parameters=Parametroj +For more information, click on any entry in the list.=Por pliaj informoj, klaku ajnan listeron. +Double-click to copy the entry to the chat history.=Dufoje-klaku por kopii listeron al la babilejo. +Command: @1 @2=Ordono: @1 @2 +Available commands: (see also: /help )=Nunaj ordonoj: (vidu ankaŭ: /help ) +Close=Fermi +Privilege=Rajto +Description=Priskribo +Empty command.=Malplena ordono. +Invalid command: @1=Nevalida ordono: @1 +Invalid command usage.=Nevalida ordonouzo. + (@1 s)= (@1 s) +Command execution took @1 s=Ruliĝo de ordono postulis @1 s +You don't have permission to run this command (missing privileges: @1).=Vi ne rajtas uzi tiun ĉi ordonon (mankataj rajtoj: @1) +Unable to get position of player @1.=Ne povis akiri pozicion de ludanto @1. +Incorrect area format. Expected: (x1,y1,z1) (x2,y2,z2)=Nevalida loko-formo. Atendis: (x1,y1,z1) (x2,y2,z2) += +Show chat action (e.g., '/me orders a pizza' displays ' orders a pizza')=Roli agon en la babilejo (ekz., «/me mendas picon» montras « mendas picon») +Show the name of the server owner=Montri nomon de la serviladministranto +The administrator of this server is @1.=La adminstranto de tiu ĉi servilo estas @1. +There's no administrator named in the config file.=Estas neniu agordita administranto en la agordodosiero. +@1 does not have any privileges.=@1 havas neniun rajton. +Privileges of @1: @2=Rajtoj de @1: @2 +[]=[] +Show privileges of yourself or another player=Montri rajtojn de vi aŭ alia ludanto +Player @1 does not exist.=Ludanto @1 ne ekzistas. += +Return list of all online players with privilege=Listi ĉeretan ludanton kun specifa rajto +Invalid parameters (see /help haspriv).=Nevalidaj parametroj (vidu /help haspriv). +Unknown privilege!=Nekonata rajto! +No online player has the "@1" privilege.=Neniu ĉeretulo havas la rajton «@1». +Players online with the "@1" privilege: @2=Ĉeretuloj kun la rajto «@1»: @2 +Your privileges are insufficient.=Viaj rajtoj ne sufiĉas. +Your privileges are insufficient. '@1' only allows you to grant: @2=Viaj rajtoj ne sufiĉas. «@1» sole lasas vin doni: @2 +Unknown privilege: @1=Nekonata rajto: @1 +@1 granted you privileges: @2=@1 donis al vi rajtojn: @2 + ( [, [<...>]] | all)= ( [, [<...>]] | all) +Give privileges to player=Doni rajtojn al ludanto, aŭ aparte, aŭ ĉiun kune («all») +Invalid parameters (see /help grant).=Nevalidaj parametroj (vidu /help grant). + [, [<...>]] | all= [, [<...>]] | all +Grant privileges to yourself=Doni rajtojn al vi mem, aŭ aparte, aŭ ĉiun kune («all») +Invalid parameters (see /help grantme).=Nevalidaj parametroj (vidu /help grantme) +Your privileges are insufficient. '@1' only allows you to revoke: @2=Viaj rajtoj ne sufiĉas. «@1» sole lasas vin repreni: @2 +Note: Cannot revoke in singleplayer: @1=Rimarko: Neeblas repreni rajton sole: @1 +Note: Cannot revoke from admin: @1=Rimarko: Neeblas repreni rajnton de administranto: @1 +No privileges were revoked.=Neniu rajto reprenita. +@1 revoked privileges from you: @2=@1 reprenis rajtojn de vi: @2 +Remove privileges from player=Repreni rajtojn de ludanto, aŭ aparte, aŭ ĉiun kune («all») +Invalid parameters (see /help revoke).=Nevalidaj parametroj (vidu /help revoke) +Revoke privileges from yourself=Repreni rajtojn de vi mem, aŭ aparte, aŭ ĉiun kune («all») +Invalid parameters (see /help revokeme).=Nevalidaj parametroj (vidu /help revokeme). + = +Set player's password (sent unencrypted, thus insecure)=Agordi pasvorton de ludanto (sendute senĉifre, kaj tiel malsekure) +Name field required.=Nomo bezonata. +Your password was cleared by @1.=Via pasvorto forviŝiĝis de @1. +Password of player "@1" cleared.=Pasvorto de ludanto «@1» forviŝita. +Your password was set by @1.=Via pasvorto agordiĝis de @1. +Password of player "@1" set.=Pasvorto de ludanto «@1» agordita. += +Set empty password for a player=Forviŝi pasvorton de ludanto +Reload authentication data=Reenlegi aŭtentigajn datumojn +Done.=Finite. +Failed.=Malsukcese.. +Remove a player's data=Forviŝi datumojn de ludanto +Player "@1" removed.=Ludanto «@1» forigita. +No such player "@1" to remove.=Neniu ekzistanta ludanto «@1» forigebla. +Player "@1" is connected, cannot remove.=Ludanto «@1» ĉeretas, do ne forigeblas. +Unhandled remove_player return code @1.=Netraktata remove_player redonkodo @1. +Cannot teleport out of map bounds!=Neeblas teleporti ekstermonden! +Cannot get player with name @1.=Neeblas trovi ludanton nomitan «@1». +Cannot teleport, @1 is attached to an object!=Ne povas teleporti @1, ĝi estas ligita al objekto! +Teleporting @1 to @2.=Teleportante @1 al @2. +One does not teleport to oneself.=Oni kutimas ne teleportu al si mem. +Cannot get teleportee with name @1.=Ne trovis alteleportaton nomitan @1. +Cannot get target player with name @1.=Ne trovis celatan ludanton nomitan @1. +Teleporting @1 to @2 at @3.=Teleportante @1 al @2 ĉe @3. +,, | | ,, | =,, | | ,, | +Teleport to position or player=Teleportiĝi al pozicio aŭ ludanto +You don't have permission to teleport other players (missing privilege: @1).=Vi ne rajtas teleporti aliajn ludantojn (mankas rajto: @1). +([-n] ) | =([-n] ) | +Set or read server configuration setting=Agordi aŭ vidi servilan agordon +Failed. Cannot modify secure settings. Edit the settings file manually.=Malsukcese. Neeblas redakti sekurajn agordojn. Redaktu la agordodosieron permane. +Failed. Use '/set -n ' to create a new setting.=Malsukcese. Uzu «/set -n » por krei novan agordon. +@1 @= @2=@1 @= @2 += +Invalid parameters (see /help set).=Nevalidaj parametroj (vidu /help set). +Finished emerging @1 blocks in @2ms.=Finenlegis @1 monderojn dum @2ms. +emergeblocks update: @1/@2 blocks emerged (@3%)=emergeblocks ĝisdatigo: @1/@2 monderoj enlegitaj (@3%) +(here []) | ( )=(here []) | ( ) +Load (or, if nonexistent, generate) map blocks contained in area pos1 to pos2 ( and must be in parentheses)=Enlegi (aŭ, laŭnecese, krei) mondopecojn inter la pozicioj poz1 kaj pos2 ( kaj devas esti inter krampoj) +Started emerge of area ranging from @1 to @2.=Ekenlegis mondpecojn inter @1 kaj @2. +Delete map blocks contained in area pos1 to pos2 ( and must be in parentheses)=Forigi mondpecojn inter la pozicioj poz1 kaj poz2 ( kaj devas esti inter krampoj) +Successfully cleared area ranging from @1 to @2.=Sukcese forigis mondpecojn inter @1 kaj @2. +Failed to clear one or more blocks in area.=Malsukcesis forigi unu aŭ pli da mondpecoj. +Resets lighting in the area between pos1 and pos2 ( and must be in parentheses)=Reŝarĝas lumojn inter la pozicioj poz1 kaj poz2 ( kaj devas esti inter krampoj) +Successfully reset light in the area ranging from @1 to @2.=Sukcesis reŝarĝi lumojn inter @1 kaj @2. +Failed to load one or more blocks in area.=Malsukcesis enlegante unu aŭ pli da monderoj. +List mods installed on the server=Listigi modifaĵojn instalitajn de la servilo. +No mods installed.=Neniu modifaĵ instalita. +Cannot give an empty item.=Neeblas doni neniun portaĵon. +Cannot give an unknown item.=Neeblas doni nekonatan portaĵon. +Giving 'ignore' is not allowed.=Doni «ignore» estas ne permesita. +@1 is not a known player.=@1 ne estas konata ludanto. +@1 partially added to inventory.=@1 parte enmetiĝis al portaĵujon. +@1 could not be added to inventory.=@1 ne enmetiĝis al portaĵujon. +@1 added to inventory.=@1 enmetiĝis al portaĵujon. +@1 partially added to inventory of @2.=@1 parte enmetiĝis al portaĵujon de @2. +@1 could not be added to inventory of @2.=@1 ne enmetiĝis al portaĵujon de @2. +@1 added to inventory of @2.=@1 enmetiĝis al portaĵujon de @2. + [ []]= [ ]] +Give item to player=Doni portaĵon al ludanto +Name and ItemString required.=Nomo kaj PortaĵNomo postualtaj. + [ []]= [ []] +Give item to yourself=Doni portaĵon al vi mem. +ItemString required.=PortaĵNomo postulata. + [,,]= [, , ] +Spawn entity at given (or your) position=Estigi estaĵon ĉe la donita (aŭ la via) pozicio +EntityName required.=EstaĵNomo postulata. +Unable to spawn entity, player is nil.=Ne povis estigi estaĵon, ĉar ludanto estas nil. +Cannot spawn an unknown entity.=Neeblas estigi nekonatan estaĵon. +Invalid parameters (@1).=Nevalidaj parametroj (@1). +@1 spawned.=@1 naskiĝis. +@1 failed to spawn.=@1 ne naskiĝis. +Destroy item in hand=Detrui portaĵon enmanan +Unable to pulverize, no player.=Ne povis detrui, neniu ludanto. +Unable to pulverize, no item in hand.=Ne povis detrui, ĉar nenio estas tenata. +An item was pulverized.=Portaĵo detruiĝis. +[] [] []=[] [] [] +Check who last touched a node or a node near it within the time specified by . Default: range @= 0, seconds @= 86400 @= 24h, limit @= 5. Set to inf for no time limit=Kontroli kiu lastafoje tuŝis monderon aŭ monderon proksiman al ĝi dum +Rollback functions are disabled.=Malfaraj funkcioj estas malŝaltitaj. +That limit is too high!=Tiu limo troaltas! +Checking @1 ...=Kontrolas @1… +Nobody has touched the specified location in @1 seconds.=Neniu tuŝis tiun lokon dum @1 sekundoj. +@1 @2 @3 -> @4 @5 seconds ago.=@1 @2 @3 -> @4 @5 sekundoj antaŭe. +Punch a node (range@=@1, seconds@=@2, limit@=@3).=Frapi monderon (intertempo@=@1, sekundoj@=@2, limo@=@3). +( []) | (: [])=( []) | (: []) +Revert actions of a player. Default for is 60. Set to inf for no time limit=Malfari agojn de ludanto. Implicita valoro de estas 60. Metu kiel «inf» por neniu tempolimo +Invalid parameters. See /help rollback and /help rollback_check.=Nevalidaj parametroj. Vidu /help rollback kaj /help rollback_check. +Reverting actions of player '@1' since @2 seconds.=Malfaras agojn de ludanto «@1» ekde @2 sekundoj antaŭ nun. +Reverting actions of @1 since @2 seconds.=Malfaras agojn de @1 ekde @2 sekundoj antaŭ nun. +(log is too long to show)=(protokolo trolongas por montri) +Reverting actions succeeded.=Sukcesis malfari agojn. +Reverting actions FAILED.=MALsukcesis malfari agojn. +Show server status=Montri staton de servilon. +This command was disabled by a mod or game.=Tiun ordonon malŝaltis modifaĵo aŭ ludo. +[<0..23>:<0..59> | <0..24000>]=[<0..23>:<0..59> | <0..24000>] +Show or set time of day=Montri aŭ ŝanĝi la horon +Current time is @1:@2.=Nun estas @1:@2. +You don't have permission to run this command (missing privilege: @1).=Vi ne rajtas rulu tiun orodnon (mankata rajto: @1). +Invalid time (must be between 0 and 24000).=Nevalida tempo (estu inter 0 kaj 24000) +Time of day changed.=Horo ŝanĝita. +Invalid hour (must be between 0 and 23 inclusive).=Nevalida horo (estu inter 0 kaj 23, inkluzive). +Invalid minute (must be between 0 and 59 inclusive).=Nevalida minuto (estu inter 0 kaj 59, inkluzive). +Show day count since world creation=Montri pasitajn tagojn ekde mondokreo +Current day is @1.=Nuna tago estas @1. +[ | -1] [-r] []=[ | -1] [-r] [] +Shutdown server (-1 cancels a delayed shutdown, -r allows players to reconnect)=Malŝalti servilon (-1 nuligas planitan malŝalton, -r lasas ludantojn rekonektiĝi) +Server shutting down (operator request).=Servilo malŝaltiĝas (laŭ prizorganta peto) +Ban the IP of a player or show the ban list=Forbari la IP-adreson de ludanto, aŭ listigi forbaritojn +The ban list is empty.=La forbaritolisto malplenas. +Ban list: @1=Forbaritoj: @1 +You cannot ban players in singleplayer!=Vi ne povas forbari ludantojn en unuludanta reĝimo! +Player is not online.=Ludanto ne ĉeretas. +Failed to ban player.=Malsukcesis forbari ludanton. +Banned @1.=Forbaris @1. + | = | +Remove IP ban belonging to a player/IP=Nuligi IP-forbaron de ludanto/IP +Failed to unban player/IP.=Malsukcesis nuligi forbaron de ludanto/IP-adreso +Unbanned @1.=Malforbaris @1. + []= [] +Kick a player=Elĵeti ludanton +Failed to kick player @1.=Malsukcesis elĵeti ludanton @1. +Kicked @1.=Elĵetis @1. +[full | quick]=[full | quick] +Clear all objects in world=Forigu ĉiujn lasitajn portaĵojn en la mondo +Invalid usage, see /help clearobjects.=Nevalida uzo, vidu /help clearobjects. +Clearing all objects. This may take a long time. You may experience a timeout. (by @1)=Forigante ĉiun lasitan portaĵon. Tio ĉi eble postulos longan tempon. Vi eble malkonektiĝos pro tempo-elĉerpo. (de @1) +Cleared all objects.=Forigis ĉiun lasitan portaĵon. + = +Send a direct message to a player=Sendu rekte privatan mesaĝon al ludanto +Invalid usage, see /help msg.=Nevalida uzo, vidu /help msg. +The player @1 is not online.=La ludanto @1 ne ĉeretas. +DM from @1: @2=Privata mesaĝo de @1: @2 +Message sent.=Mesaĝo sendita. +Get the last login time of a player or yourself=Vidi la lastan salutotempon de ludanto, aŭ vi mem +@1's last login time was @2.=Lasta salutotempo de @1 estas @2. +@1's last login time is unknown.=Lasta salutotempo de @1 estas nesciata. +Clear the inventory of yourself or another player=Malplenigi la portaĵujon de vi aŭ alia ludanto. +You don't have permission to clear another player's inventory (missing privilege: @1).=Vi ne rajtas malplenigi portaĵujon de alia ludanto (mankata rajto: @1). +@1 cleared your inventory.=@1 malplenigis vian portaĵujon. +Cleared @1's inventory.=Malplenigis portaĵujon de @1. +Player must be online to clear inventory!=Por malplenigi onian portaĵujon, tiu devas ĉereti! +Players can't be killed, damage has been disabled.=Nemortigeblas ludantoj, ĉar vundado estas malŝaltita. +Player @1 is not online.=Ludanto @1 ne ĉeretas. +You are already dead.=Vi jam estas mortinta. +@1 is already dead.=@1 estas mortinta. +@1 has been killed.=@1 estas murdita. +Kill player or yourself=Mortigi ludanton aŭ vin mem +@1 joined the game.=@1 aliĝis la ludon. +@1 left the game.=@1 foriris de la ludo. +@1 left the game (timed out).=@1 foriris de la ludo (tempo-elĉerpo) +(no description)=(neniu priskribo) +Can interact with things and modify the world=Povas interfaci kaj redakti la mondon +Can speak in chat=Povas paroli babileje +Can modify basic privileges (@1)=Povas redakti bazajn rajtojn (@1) +Can modify privileges=Povas redakti rajtojn +Can teleport self=Povas teleporti sin +Can teleport other players=Povas teleporti aliajn ludantojn +Can set the time of day using /time=Povas ŝanĝi la tempon per /time +Can do server maintenance stuff=Povas fari servilestrajn aferojn +Can bypass node protection in the world=Povas malatenti monderajn protektojn de la mondo +Can ban and unban players=Povas forbari kaj malforbari ludantojn +Can kick players=Povas elĵeti ludantojn +Can use /give and /giveme=Povas uzi /give kaj /giveme +Can use /setpassword and /clearpassword=Povas uzi /setpassword kaj /clearpassword +Can use fly mode=Povas ŝalti flugan reĝimon +Can use fast mode=Povas ŝalti rapidegan reĝimon +Can fly through solid nodes using noclip mode=Povas traflugi monderojn per trapasa reĝimo +Can use the rollback functionality=Povas uzi malfarajn funkciojn +Can enable wireframe=Povas ŝalti ĉirkaŭkadron +Unknown Item=Nekonata portaĵo +Air=Aero +Ignore=Malatenti +You can't place 'ignore' nodes!=Vi ne povas meti «malatentajn» monderojn! +print [] | dump [] | save [ []] | reset=print [] | dump [] | save [ []] | reset +Handle the profiler and profiling data=Trakti la analizilon kaj analizajn datumojn +Statistics written to action log.=Analizoj skribitaj al agoprotokolo. +Statistics were reset.=Analizoj forviŝitaj. +Usage: @1=Uzado: @1 +Format can be one of txt, csv, lua, json, json_pretty (structures may be subject to change).=Formo povas estas txt, csv, lua, json, aŭ json_pretty (struktuoj eble iam ŝanĝiĝos). +Values below show absolute/relative times spend per server step by the instrumented function.=Valoroj subaj montras la malrelativan/relativan tempon pasigitan de la servilo je ĉiu paŝo de la funkcio. +A total of @1 sample(s) were taken.=Sume @1 ekzemplero(j) konserviĝis. +The output is limited to '@1'.=La eligo estas limigita al «@1». +Saving of profile failed: @1=Konservado de profilo malsukcesis: @1 +Profile saved to @1=Profilo konservita al @1 From e9233bc16921c68182cdcae99bcdecf3e864d0be Mon Sep 17 00:00:00 2001 From: DS Date: Sat, 20 Jan 2024 15:36:53 +0100 Subject: [PATCH 02/19] Fix typo in minimap bumpmapping shader (#14280) --- client/shaders/minimap_shader/opengl_fragment.glsl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/shaders/minimap_shader/opengl_fragment.glsl b/client/shaders/minimap_shader/opengl_fragment.glsl index cef359e8a7..3f6b041dab 100644 --- a/client/shaders/minimap_shader/opengl_fragment.glsl +++ b/client/shaders/minimap_shader/opengl_fragment.glsl @@ -12,13 +12,13 @@ void main (void) //texture sampling rate const float step = 1.0 / 256.0; float tl = texture2D(normalTexture, vec2(uv.x - step, uv.y + step)).r; - float t = texture2D(normalTexture, vec2(uv.x - step, uv.y - step)).r; + float t = texture2D(normalTexture, vec2(uv.x, uv.y + step)).r; float tr = texture2D(normalTexture, vec2(uv.x + step, uv.y + step)).r; - float r = texture2D(normalTexture, vec2(uv.x + step, uv.y)).r; + float r = texture2D(normalTexture, vec2(uv.x + step, uv.y )).r; float br = texture2D(normalTexture, vec2(uv.x + step, uv.y - step)).r; - float b = texture2D(normalTexture, vec2(uv.x, uv.y - step)).r; + float b = texture2D(normalTexture, vec2(uv.x, uv.y - step)).r; float bl = texture2D(normalTexture, vec2(uv.x - step, uv.y - step)).r; - float l = texture2D(normalTexture, vec2(uv.x - step, uv.y)).r; + float l = texture2D(normalTexture, vec2(uv.x - step, uv.y )).r; float dX = (tr + 2.0 * r + br) - (tl + 2.0 * l + bl); float dY = (bl + 2.0 * b + br) - (tl + 2.0 * t + tr); vec4 bump = vec4 (normalize(vec3 (dX, dY, 0.1)),1.0); From 8cbd629010b22eb735c7bb95f4c4cd396dae1039 Mon Sep 17 00:00:00 2001 From: sfan5 Date: Sat, 20 Jan 2024 15:37:30 +0100 Subject: [PATCH 03/19] Fix bugs in ModifySafeMap (#14276) --- src/unittest/CMakeLists.txt | 1 + src/unittest/test_datastructures.cpp | 181 +++++++++++++++++++++++++++ src/util/container.h | 34 +++-- 3 files changed, 206 insertions(+), 10 deletions(-) create mode 100644 src/unittest/test_datastructures.cpp diff --git a/src/unittest/CMakeLists.txt b/src/unittest/CMakeLists.txt index 84c545459f..8466902df7 100644 --- a/src/unittest/CMakeLists.txt +++ b/src/unittest/CMakeLists.txt @@ -9,6 +9,7 @@ set (UNITTEST_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/test_compression.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_connection.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_craft.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/test_datastructures.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_filesys.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_inventory.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_irrptr.cpp diff --git a/src/unittest/test_datastructures.cpp b/src/unittest/test_datastructures.cpp new file mode 100644 index 0000000000..a7e24b989a --- /dev/null +++ b/src/unittest/test_datastructures.cpp @@ -0,0 +1,181 @@ +/* +Minetest +Copyright (C) 2024 sfan5 + +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 "test.h" + +#include "util/container.h" + +class TestDataStructures : public TestBase +{ +public: + TestDataStructures() { TestManager::registerTestModule(this); } + const char *getName() { return "TestDataStructures"; } + + void runTests(IGameDef *gamedef); + + void testMap1(); + void testMap2(); + void testMap3(); + void testMap4(); + void testMap5(); +}; + +static TestDataStructures g_test_instance; + +void TestDataStructures::runTests(IGameDef *gamedef) +{ + rawstream << "-------- ModifySafeMap" << std::endl; + TEST(testMap1); + TEST(testMap2); + TEST(testMap3); + TEST(testMap4); + TEST(testMap5); +} + +namespace { + +struct TrackerState { + bool copied = false; + bool deleted = false; +}; + +class Tracker { + TrackerState *res = nullptr; + + inline void trackDeletion() { res && (res->deleted = true); } + +public: + Tracker() {} + Tracker(TrackerState &res) : res(&res) {} + + operator bool() const { return !!res; } + + Tracker(const Tracker &other) { *this = other; } + Tracker &operator=(const Tracker &other) { + trackDeletion(); + res = other.res; + res && (res->copied = true); + return *this; + } + Tracker(Tracker &&other) { *this = std::move(other); } + Tracker &operator=(Tracker &&other) { + trackDeletion(); + res = other.res; + other.res = nullptr; + return *this; + } + + ~Tracker() { trackDeletion(); } +}; + +} + +void TestDataStructures::testMap1() +{ + ModifySafeMap map; + TrackerState t0, t1; + + // no-copy put + map.put(1, Tracker(t0)); + UASSERT(!t0.copied); + UASSERT(!t0.deleted); + + // overwrite during iter + bool once = false; + for (auto &it : map.iter()) { + (void)it; + map.put(1, Tracker(t1)); + UASSERT(t0.deleted); + UASSERT(!t1.copied); + UASSERT(!t1.deleted); + if (once |= 1) + break; + } + UASSERT(once); +} + +void TestDataStructures::testMap2() +{ + ModifySafeMap map; + TrackerState t0, t1; + + // overwrite + map.put(1, Tracker(t0)); + map.put(1, Tracker(t1)); + UASSERT(t0.deleted); + UASSERT(!t1.copied); + UASSERT(!t1.deleted); +} + +void TestDataStructures::testMap3() +{ + ModifySafeMap map; + TrackerState t0, t1; + + // take + map.put(1, Tracker(t0)); + { + auto v = map.take(1); + UASSERT(!t0.copied); + UASSERT(!t0.deleted); + } + UASSERT(t0.deleted); + + // remove during iter + map.put(1, Tracker(t1)); + for (auto &it : map.iter()) { + (void)it; + map.remove(1); + UASSERT(t1.deleted); + break; + } +} + +void TestDataStructures::testMap4() +{ + ModifySafeMap map; + + // overwrite + take during iter + map.put(1, 100); + for (auto &it : map.iter()) { + (void)it; + map.put(1, 200); + u32 taken = map.take(1); + UASSERTEQ(u32, taken, 200); + break; + } + + UASSERT(map.get(1) == u32()); + UASSERTEQ(size_t, map.size(), 0); +} + +void TestDataStructures::testMap5() +{ + ModifySafeMap map; + + // overwrite 2x during iter + map.put(9001, 9001); + for (auto &it : map.iter()) { + (void)it; + map.put(1, 100); + map.put(1, 200); + UASSERTEQ(u32, map.get(1), 200); + break; + } +} diff --git a/src/util/container.h b/src/util/container.h index 985a9a4477..9a52778d46 100644 --- a/src/util/container.h +++ b/src/util/container.h @@ -376,10 +376,16 @@ public: assert(false); return; } - if (m_iterating) - m_new.emplace(key, value); - else - m_values.emplace(key, value); + if (m_iterating) { + auto it = m_values.find(key); + if (it != m_values.end()) { + it->second = V(); + m_garbage++; + } + m_new[key] = value; + } else { + m_values[key] = value; + } } void put(const K &key, V &&value) { @@ -387,10 +393,16 @@ public: assert(false); return; } - if (m_iterating) - m_new.emplace(key, std::move(value)); - else - m_values.emplace(key, std::move(value)); + if (m_iterating) { + auto it = m_values.find(key); + if (it != m_values.end()) { + it->second = V(); + m_garbage++; + } + m_new[key] = std::move(value); + } else { + m_values[key] = std::move(value); + } } V take(const K &key) { @@ -405,7 +417,8 @@ public: auto it = m_values.find(key); if (it == m_values.end()) return ret; - ret = std::move(it->second); + if (!ret) + ret = std::move(it->second); if (m_iterating) { it->second = V(); m_garbage++; @@ -516,7 +529,8 @@ private: std::map m_values; std::map m_new; unsigned int m_iterating = 0; - size_t m_garbage = 0; // upper bound for null-placeholders in m_values + // approximate amount of null-placeholders in m_values, reliable for != 0 tests + size_t m_garbage = 0; static constexpr size_t GC_MIN_SIZE = 30; }; From 404a063fdfbbddcba32d22dd1c34dd73922bc12a Mon Sep 17 00:00:00 2001 From: grorp Date: Sun, 21 Jan 2024 17:44:08 +0100 Subject: [PATCH 04/19] 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 843d842d3d..44e66d7c16 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 1f62e46a01..ab2602043f 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 6916cefdcb..b740f43a14 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 104dcbf7ba..c9fe5569e6 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 b60edac797..2e48a71cf3 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 21406e52e5..143612afdb 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 c7a5795552..3618489110 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 430ae8e725..8e499a7bdc 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 e832ff2ab8..7338834f79 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 86bfb0b6b0..88d88703e9 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[]; From fb461d21a502a3758e0b103c7d193fe88f01cc7e Mon Sep 17 00:00:00 2001 From: Lars Mueller Date: Thu, 11 Jan 2024 22:07:03 +0100 Subject: [PATCH 05/19] Fix waypoint precision wraparound, add bounds check --- src/client/hud.cpp | 4 +++- src/script/common/c_content.cpp | 19 +++++++++++++------ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/client/hud.cpp b/src/client/hud.cpp index 5d3de7bfbf..1618b757f7 100644 --- a/src/client/hud.cpp +++ b/src/client/hud.cpp @@ -416,7 +416,9 @@ void Hud::drawLuaElements(const v3s16 &camera_offset) (e->number >> 0) & 0xFF); std::wstring text = unescape_translate(utf8_to_wide(e->name)); const std::string &unit = e->text; - // waypoints reuse the item field to store precision, item = precision + 1 + // Waypoints reuse the item field to store precision, + // item = precision + 1 and item = 0 <=> precision = 10 for backwards compatibility. + // Also see `push_hud_element`. u32 item = e->item; float precision = (item == 0) ? 10.0f : (item - 1.f); bool draw_precision = precision > 0; diff --git a/src/script/common/c_content.cpp b/src/script/common/c_content.cpp index 8e499a7bdc..b50e02a80b 100644 --- a/src/script/common/c_content.cpp +++ b/src/script/common/c_content.cpp @@ -2025,11 +2025,16 @@ void read_hud_element(lua_State *L, HudElement *elem) elem->name = getstringfield_default(L, 2, "name", ""); elem->text = getstringfield_default(L, 2, "text", ""); elem->number = getintfield_default(L, 2, "number", 0); - if (elem->type == HUD_ELEM_WAYPOINT) - // waypoints reuse the item field to store precision, item = precision + 1 - elem->item = getintfield_default(L, 2, "precision", -1) + 1; - else + if (elem->type == HUD_ELEM_WAYPOINT) { + // Waypoints reuse the item field to store precision, + // item = precision + 1 and item = 0 <=> precision = 10 for backwards compatibility. + int precision = getintfield_default(L, 2, "precision", 10); + if (precision < 0) + throw LuaError("Waypoint precision must be non-negative"); + elem->item = precision + 1; + } else { elem->item = getintfield_default(L, 2, "item", 0); + } elem->dir = getintfield_default(L, 2, "direction", 0); elem->z_index = MYMAX(S16_MIN, MYMIN(S16_MAX, getintfield_default(L, 2, "z_index", 0))); @@ -2081,8 +2086,10 @@ void push_hud_element(lua_State *L, HudElement *elem) lua_setfield(L, -2, "number"); if (elem->type == HUD_ELEM_WAYPOINT) { - // waypoints reuse the item field to store precision, precision = item - 1 - lua_pushnumber(L, elem->item - 1); + // Waypoints reuse the item field to store precision, + // item = precision + 1 and item = 0 <=> precision = 10 for backwards compatibility. + // See `Hud::drawLuaElements`, case `HUD_ELEM_WAYPOINT`. + lua_pushnumber(L, (elem->item == 0) ? 10 : (elem->item - 1)); lua_setfield(L, -2, "precision"); } // push the item field for waypoints as well for backwards compatibility From 59587143091a150ac496b20c2cddab9352c6d3fe Mon Sep 17 00:00:00 2001 From: cx384 Date: Mon, 22 Jan 2024 18:27:08 +0100 Subject: [PATCH 06/19] Tool specific pointing and blocking pointable type (#13992) --- builtin/game/features.lua | 2 + doc/lua_api.md | 35 +++- games/devtest/mods/testentities/init.lua | 1 + games/devtest/mods/testentities/pointable.lua | 23 +++ games/devtest/mods/testnodes/properties.lua | 20 +++ .../textures/testnodes_blocking_pointable.png | Bin 0 -> 150 bytes .../textures/testnodes_not_pointable.png | Bin 0 -> 152 bytes .../textures/testnodes_pointable.png | Bin 0 -> 144 bytes games/devtest/mods/testtools/init.lua | 66 ++++++++ .../testtools_blocked_pointing_staff.png | Bin 0 -> 136 bytes .../testtools_ultimate_pointing_staff.png | Bin 0 -> 136 bytes src/client/clientenvironment.cpp | 23 ++- src/client/clientenvironment.h | 3 +- src/client/content_cao.cpp | 3 +- src/client/content_cao.h | 7 + src/client/game.cpp | 6 +- src/environment.cpp | 45 ++++-- src/environment.h | 5 +- src/itemdef.cpp | 17 ++ src/itemdef.h | 6 +- src/nodedef.cpp | 10 +- src/nodedef.h | 5 +- src/object_properties.cpp | 6 +- src/object_properties.h | 3 +- src/raycast.cpp | 6 +- src/raycast.h | 3 +- src/script/common/c_content.cpp | 150 +++++++++++++++++- src/script/common/c_content.h | 6 + src/script/lua_api/l_env.cpp | 2 +- src/script/lua_api/l_env.h | 5 +- src/server/luaentity_sao.cpp | 4 +- src/server/player_sao.cpp | 4 +- src/serverenvironment.cpp | 27 +++- src/serverenvironment.h | 3 +- src/util/CMakeLists.txt | 1 + src/util/pointabilities.cpp | 147 +++++++++++++++++ src/util/pointabilities.h | 70 ++++++++ src/util/pointedthing.cpp | 17 +- src/util/pointedthing.h | 12 +- 39 files changed, 676 insertions(+), 67 deletions(-) create mode 100644 games/devtest/mods/testentities/pointable.lua create mode 100644 games/devtest/mods/testnodes/textures/testnodes_blocking_pointable.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_not_pointable.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_pointable.png create mode 100644 games/devtest/mods/testtools/textures/testtools_blocked_pointing_staff.png create mode 100644 games/devtest/mods/testtools/textures/testtools_ultimate_pointing_staff.png create mode 100644 src/util/pointabilities.cpp create mode 100644 src/util/pointabilities.h diff --git a/builtin/game/features.lua b/builtin/game/features.lua index 727d90174d..510d0461f3 100644 --- a/builtin/game/features.lua +++ b/builtin/game/features.lua @@ -33,6 +33,8 @@ core.features = { random_state_restore = true, after_order_expiry_registration = true, wallmounted_rotate = true, + item_specific_pointabilities = true, + blocking_pointability_type = true, } function core.has_feature(arg) diff --git a/doc/lua_api.md b/doc/lua_api.md index ab2602043f..7de57aff74 100644 --- a/doc/lua_api.md +++ b/doc/lua_api.md @@ -5298,6 +5298,10 @@ Utilities -- wallmounted nodes mounted at floor or ceiling may additionally -- be rotated by 90° with special param2 values (5.9.0) wallmounted_rotate = true, + -- Availability of the `pointabilities` property in the item definition (5.9.0) + item_specific_pointabilities = true, + -- Nodes `pointable` property can be `"blocking"` (5.9.0) + blocking_pointability_type = true, } ``` @@ -8382,7 +8386,9 @@ Player properties need to be saved manually. pointable = true, - -- Whether the object can be pointed at + -- Can be `true` if it is pointable, `false` if it can be pointed through, + -- or `"blocking"` if it is pointable but not selectable. + -- Can be overridden by the `pointabilities` of the held item. visual = "cube" / "sprite" / "upright_sprite" / "mesh" / "wielditem" / "item", -- "cube" is a node-sized cube. @@ -8746,6 +8752,27 @@ Used by `minetest.register_node`, `minetest.register_craftitem`, and -- If true, item can point to all liquid nodes (`liquidtype ~= "none"`), -- even those for which `pointable = false` + pointabilities = { + nodes = { + ["default:stone"] = "blocking", + ["group:leaves"] = false, + }, + objects = { + ["modname:entityname"] = true, + ["group:ghosty"] = true, -- (an armor group) + } + }, + -- Contains lists to override the `pointable` property of pointed nodes and objects. + -- The index can be a node/entity name or a group with the prefix `"group:"`. + -- (For objects `armor_groups` are used and for players the entity name is irrelevant.) + -- If multiple fields fit, the following priority order is applied: + -- value of matching node/entity name + -- `true` for any group + -- `false` for any group + -- `"blocking"` for any group + -- `liquids_pointable` if it is a liquid node + -- `pointable` property of the node or object + light_source = 0, -- When used for nodes: Defines amount of light emitted by node. -- Otherwise: Defines texture glow when viewed as a dropped item @@ -8971,7 +8998,11 @@ Used by `minetest.register_node`. walkable = true, -- If true, objects collide with node - pointable = true, -- If true, can be pointed at + pointable = true, + -- Can be `true` if it is pointable, `false` if it can be pointed through, + -- or `"blocking"` if it is pointable but not selectable. + -- Can be overridden by the `pointabilities` of the held item. + -- A client may be able to point non-pointable nodes, since it isn't checked server-side. diggable = true, -- If false, can never be dug diff --git a/games/devtest/mods/testentities/init.lua b/games/devtest/mods/testentities/init.lua index 6e0f8acb8d..9ab54f5ab6 100644 --- a/games/devtest/mods/testentities/init.lua +++ b/games/devtest/mods/testentities/init.lua @@ -1,3 +1,4 @@ dofile(minetest.get_modpath("testentities").."/visuals.lua") dofile(minetest.get_modpath("testentities").."/selectionbox.lua") dofile(minetest.get_modpath("testentities").."/armor.lua") +dofile(minetest.get_modpath("testentities").."/pointable.lua") diff --git a/games/devtest/mods/testentities/pointable.lua b/games/devtest/mods/testentities/pointable.lua new file mode 100644 index 0000000000..be7ce4ba8b --- /dev/null +++ b/games/devtest/mods/testentities/pointable.lua @@ -0,0 +1,23 @@ +-- Pointability test Entities + +-- Register wrapper for compactness +local function register_pointable_testentity(name, pointable) + local texture = "testnodes_"..name..".png" + minetest.register_entity("testentities:"..name, { + initial_properties = { + visual = "cube", + visual_size = {x = 0.6, y = 0.6, z = 0.6}, + textures = { + texture, texture, texture, texture, texture, texture + }, + pointable = pointable, + }, + on_activate = function(self) + self.object:set_armor_groups({[name.."_test"] = 1}) + end + }) +end + +register_pointable_testentity("pointable", true) +register_pointable_testentity("not_pointable", false) +register_pointable_testentity("blocking_pointable", "blocking") diff --git a/games/devtest/mods/testnodes/properties.lua b/games/devtest/mods/testnodes/properties.lua index b93d0c73b9..a688d3814d 100644 --- a/games/devtest/mods/testnodes/properties.lua +++ b/games/devtest/mods/testnodes/properties.lua @@ -663,3 +663,23 @@ minetest.register_node("testnodes:post_effect_color_shaded_true", { is_ground_content = false, groups = {dig_immediate=3}, }) + +-- Pointability + +-- Register wrapper for compactness +local function register_pointable_test_node(name, description, pointable) + local texture = "testnodes_"..name..".png" + minetest.register_node("testnodes:"..name, { + description = S(description), + tiles = {texture}, + drawtype = "glasslike_framed", + paramtype = "light", + walkable = false, + pointable = pointable, + groups = {dig_immediate=3, [name.."_test"]=1}, + }) +end + +register_pointable_test_node("pointable", "Pointable Node", true) +register_pointable_test_node("not_pointable", "Not Pointable Node", false) +register_pointable_test_node("blocking_pointable", "Blocking Pointable Node", "blocking") diff --git a/games/devtest/mods/testnodes/textures/testnodes_blocking_pointable.png b/games/devtest/mods/testnodes/textures/testnodes_blocking_pointable.png new file mode 100644 index 0000000000000000000000000000000000000000..fa021041a6fb026f97bdca163edc8f39b11eaaf7 GIT binary patch literal 150 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!93?!50ihlx9JOMr-u0T36GBPc~;Q#;shUrFU zfPBW1AirP+hi5m^fE)`?7sn6|+2n+c|CJ9Y9Aa=`\" for players."), inventory_image = "testtools_branding_iron.png", groups = { testtool = 1, disable_repair = 1 }, + pointabilities = pointabilities_objects, on_use = function(_itemstack, user, pointed_thing) local obj local msg @@ -499,6 +524,7 @@ minetest.register_tool("testtools:object_editor", { S("Punch air: Edit yourself"), inventory_image = "testtools_object_editor.png", groups = { testtool = 1, disable_repair = 1 }, + pointabilities = pointabilities_objects, on_use = function(itemstack, user, pointed_thing) if user and user:is_player() then local name = user:get_player_name() @@ -586,6 +612,7 @@ minetest.register_tool("testtools:object_attacher", { S("Aux1+Sneak+Place: Decrease attachment rotation"), inventory_image = "testtools_object_attacher.png", groups = { testtool = 1, disable_repair = 1 }, + pointabilities = pointabilities_objects, on_place = attacher_config, on_secondary_use = attacher_config, on_use = function(itemstack, user, pointed_thing) @@ -679,6 +706,7 @@ minetest.register_tool("testtools:children_getter", { S("Punch air to show your own 'children'"), inventory_image = "testtools_children_getter.png", groups = { testtool = 1, disable_repair = 1 }, + pointabilities = pointabilities_objects, on_use = function(itemstack, user, pointed_thing) if user and user:is_player() then local name = user:get_player_name() @@ -998,3 +1026,41 @@ minetest.register_on_leaveplayer(function(player) meta_latest_keylist[name] = nil node_meta_posses[name] = nil end) + +-- Pointing Staffs + +minetest.register_tool("testtools:blocked_pointing_staff", { + description = S("Blocked Pointing Staff").."\n".. + S("Can point the Blocking Pointable Node/Object and ".. + "the Pointable Node/Object is point blocking."), + inventory_image = "testtools_blocked_pointing_staff.png", + pointabilities = { + nodes = { + ["testnodes:blocking_pointable"] = true, + ["group:pointable_test"] = "blocking" + }, + objects = { + ["testentities:blocking_pointable"] = true, + ["group:pointable_test"] = "blocking" + } + } +}) + +minetest.register_tool("testtools:ultimate_pointing_staff", { + description = S("Ultimate Pointing Staff").."\n".. + S("Can point all pointable test nodes, objects and liquids."), + inventory_image = "testtools_ultimate_pointing_staff.png", + liquids_pointable = true, + pointabilities = { + nodes = { + ["group:blocking_pointable_test"] = true, + ["group:pointable_test"] = true, + ["testnodes:not_pointable"] = true + }, + objects = { + ["group:blocking_pointable_test"] = true, + ["group:pointable_test"] = true, + ["testentities:not_pointable"] = true + } + } +}) diff --git a/games/devtest/mods/testtools/textures/testtools_blocked_pointing_staff.png b/games/devtest/mods/testtools/textures/testtools_blocked_pointing_staff.png new file mode 100644 index 0000000000000000000000000000000000000000..aa1cdb97aa1c8393e2272e4608406afc829a6370 GIT binary patch literal 136 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!93?!50ihlx9JOMr-u0T36GBPc~;Q#;shUrFU zfPBW1AirP+hi5m^fE+DP7sn8e=;Q*M=|9Gif;7(8A5T-G@yGywoL(jtfe literal 0 HcmV?d00001 diff --git a/games/devtest/mods/testtools/textures/testtools_ultimate_pointing_staff.png b/games/devtest/mods/testtools/textures/testtools_ultimate_pointing_staff.png new file mode 100644 index 0000000000000000000000000000000000000000..e4d80fb4d1c1f92609082c9667103a7b9ec2fc61 GIT binary patch literal 136 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!93?!50ihlx9JOMr-u0Yx-%^)%|^8f$;${!qs zfqcf2AirP+hi5m^fE+DP7sn8e=;Q &shootline_on_map, - std::vector &objects) + std::vector &objects, + const std::optional &pointabilities) { auto allObjects = m_ao_manager.getActiveSelectableObjects(shootline_on_map); const v3f line_vector = shootline_on_map.getVector(); @@ -516,9 +517,23 @@ void ClientEnvironment::getSelectedActiveObjects( current_raw_normal = current_normal; } if (collision) { - current_intersection += obj->getPosition(); - objects.emplace_back(obj->getId(), current_intersection, current_normal, current_raw_normal, - (current_intersection - shootline_on_map.start).getLengthSQ()); + PointabilityType pointable; + if (pointabilities) { + if (gcao->isPlayer()) { + pointable = pointabilities->matchPlayer(gcao->getGroups()).value_or( + gcao->getProperties().pointable); + } else { + pointable = pointabilities->matchObject(gcao->getName(), + gcao->getGroups()).value_or(gcao->getProperties().pointable); + } + } else { + pointable = gcao->getProperties().pointable; + } + if (pointable != PointabilityType::POINTABLE_NOT) { + current_intersection += obj->getPosition(); + objects.emplace_back(obj->getId(), current_intersection, current_normal, current_raw_normal, + (current_intersection - shootline_on_map.start).getLengthSQ(), pointable); + } } } } diff --git a/src/client/clientenvironment.h b/src/client/clientenvironment.h index ba0fdb50aa..407473be75 100644 --- a/src/client/clientenvironment.h +++ b/src/client/clientenvironment.h @@ -131,7 +131,8 @@ public: virtual void getSelectedActiveObjects( const core::line3d &shootline_on_map, - std::vector &objects + std::vector &objects, + const std::optional &pointabilities ); const std::set &getPlayerNames() { return m_player_names; } diff --git a/src/client/content_cao.cpp b/src/client/content_cao.cpp index 728906bb89..d80869d9c6 100644 --- a/src/client/content_cao.cpp +++ b/src/client/content_cao.cpp @@ -411,8 +411,7 @@ GenericCAO::~GenericCAO() bool GenericCAO::getSelectionBox(aabb3f *toset) const { - if (!m_prop.is_visible || !m_is_visible || m_is_local_player - || !m_prop.pointable) { + if (!m_prop.is_visible || !m_is_visible || m_is_local_player) { return false; } *toset = m_selection_box; diff --git a/src/client/content_cao.h b/src/client/content_cao.h index 08d42220e7..7fdcb73da2 100644 --- a/src/client/content_cao.h +++ b/src/client/content_cao.h @@ -174,6 +174,8 @@ public: inline const ObjectProperties &getProperties() const { return m_prop; } + inline const std::string &getName() const { return m_name; } + scene::ISceneNode *getSceneNode() const override; scene::IAnimatedMeshSceneNode *getAnimatedMeshSceneNode() const override; @@ -208,6 +210,11 @@ public: return m_is_local_player; } + inline bool isPlayer() const + { + return m_is_player; + } + inline bool isVisible() const { return m_is_visible; diff --git a/src/client/game.cpp b/src/client/game.cpp index b740f43a14..54bff75573 100644 --- a/src/client/game.cpp +++ b/src/client/game.cpp @@ -841,6 +841,7 @@ protected: * the camera position. This also gives the maximal distance * of the search. * @param[in] liquids_pointable if false, liquids are ignored + * @param[in] pointabilities item specific pointable overriding * @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 @@ -848,6 +849,7 @@ protected: */ PointedThing updatePointedThing( const core::line3d &shootline, bool liquids_pointable, + const std::optional &pointabilities, bool look_for_object, const v3s16 &camera_offset); void handlePointingAtNothing(const ItemStack &playerItem); void handlePointingAtNode(const PointedThing &pointed, @@ -3343,6 +3345,7 @@ void Game::processPlayerInteraction(f32 dtime, bool show_hud) PointedThing pointed = updatePointedThing(shootline, selected_def.liquids_pointable, + selected_def.pointabilities, !runData.btn_down_for_dig, camera_offset); @@ -3454,6 +3457,7 @@ void Game::processPlayerInteraction(f32 dtime, bool show_hud) PointedThing Game::updatePointedThing( const core::line3d &shootline, bool liquids_pointable, + const std::optional &pointabilities, bool look_for_object, const v3s16 &camera_offset) { @@ -3470,7 +3474,7 @@ PointedThing Game::updatePointedThing( runData.selected_object = NULL; hud->pointing_at_object = false; - RaycastState s(shootline, look_for_object, liquids_pointable); + RaycastState s(shootline, look_for_object, liquids_pointable, pointabilities); PointedThing result; env.continueRaycast(&s, &result); if (result.type == POINTEDTHING_OBJECT) { diff --git a/src/environment.cpp b/src/environment.cpp index 0f74ae6bb6..44ab8a6bfe 100644 --- a/src/environment.cpp +++ b/src/environment.cpp @@ -102,24 +102,33 @@ bool Environment::line_of_sight(v3f pos1, v3f pos2, v3s16 *p) } /* - Check if a node is pointable + Check how a node can be pointed at */ -inline static bool isPointableNode(const MapNode &n, - const NodeDefManager *nodedef , bool liquids_pointable) +inline static PointabilityType isPointableNode(const MapNode &n, + const NodeDefManager *nodedef, bool liquids_pointable, + const std::optional &pointabilities) { const ContentFeatures &features = nodedef->get(n); - return features.pointable || - (liquids_pointable && features.isLiquid()); + if (pointabilities) { + std::optional match = + pointabilities->matchNode(features.name, features.groups); + if (match) + return match.value(); + } + + if (features.isLiquid() && liquids_pointable) + return PointabilityType::POINTABLE; + return features.pointable; } -void Environment::continueRaycast(RaycastState *state, PointedThing *result) +void Environment::continueRaycast(RaycastState *state, PointedThing *result_p) { const NodeDefManager *nodedef = getMap().getNodeDefManager(); if (state->m_initialization_needed) { // Add objects if (state->m_objects_pointable) { std::vector found; - getSelectedActiveObjects(state->m_shootline, found); + getSelectedActiveObjects(state->m_shootline, found, state->m_pointabilities); for (const PointedThing &pointed : found) { state->m_found.push(pointed); } @@ -184,10 +193,15 @@ void Environment::continueRaycast(RaycastState *state, PointedThing *result) bool is_valid_position; n = map.getNode(np, &is_valid_position); - if (!(is_valid_position && isPointableNode(n, nodedef, - state->m_liquids_pointable))) { + if (!is_valid_position) + continue; + + PointabilityType pointable = isPointableNode(n, nodedef, + state->m_liquids_pointable, + state->m_pointabilities); + // If it can be pointed through skip + if (pointable == PointabilityType::POINTABLE_NOT) continue; - } PointedThing result; @@ -234,6 +248,7 @@ void Environment::continueRaycast(RaycastState *state, PointedThing *result) if (!is_colliding) { continue; } + result.pointability = pointable; result.type = POINTEDTHING_NODE; result.node_undersurface = np; result.distanceSq = min_distance_sq; @@ -275,12 +290,16 @@ void Environment::continueRaycast(RaycastState *state, PointedThing *result) state->m_previous_node = state->m_iterator.m_current_node_pos; state->m_iterator.next(); } - // Return empty PointedThing if nothing left on the ray + + // Return empty PointedThing if nothing left on the ray or it is blocking pointable if (state->m_found.empty()) { - result->type = POINTEDTHING_NOTHING; + result_p->type = POINTEDTHING_NOTHING; } else { - *result = state->m_found.top(); + *result_p = state->m_found.top(); state->m_found.pop(); + if (result_p->pointability == PointabilityType::POINTABLE_BLOCKING) { + result_p->type = POINTEDTHING_NOTHING; + } } } diff --git a/src/environment.h b/src/environment.h index 19c30b4d5a..62e52a8a4b 100644 --- a/src/environment.h +++ b/src/environment.h @@ -34,6 +34,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include #include #include +#include #include "irr_v3d.h" #include "util/basic_macros.h" #include "line3d.h" @@ -42,6 +43,7 @@ class IGameDef; class Map; struct PointedThing; class RaycastState; +struct Pointabilities; class Environment { @@ -97,7 +99,8 @@ public: * @param[out] objects found objects */ virtual void getSelectedActiveObjects(const core::line3d &shootline_on_map, - std::vector &objects) = 0; + std::vector &objects, + const std::optional &pointabilities) = 0; /*! * Returns the next node or object the shootline meets. diff --git a/src/itemdef.cpp b/src/itemdef.cpp index 143612afdb..ee3e3829d9 100644 --- a/src/itemdef.cpp +++ b/src/itemdef.cpp @@ -122,6 +122,7 @@ ItemDefinition& ItemDefinition::operator=(const ItemDefinition &def) stack_max = def.stack_max; usable = def.usable; liquids_pointable = def.liquids_pointable; + pointabilities = def.pointabilities; if (def.tool_capabilities) tool_capabilities = new ToolCapabilities(*def.tool_capabilities); groups = def.groups; @@ -167,6 +168,7 @@ void ItemDefinition::reset() stack_max = 99; usable = false; liquids_pointable = false; + pointabilities = std::nullopt; delete tool_capabilities; tool_capabilities = NULL; groups.clear(); @@ -241,6 +243,14 @@ void ItemDefinition::serialize(std::ostream &os, u16 protocol_version) const writeU8(os, wallmounted_rotate_vertical); touch_interaction.serialize(os); + + std::string pointabilities_s; + if (pointabilities) { + std::ostringstream tmp_os(std::ios::binary); + pointabilities->serialize(tmp_os); + pointabilities_s = tmp_os.str(); + } + os << serializeString16(pointabilities_s); } void ItemDefinition::deSerialize(std::istream &is, u16 protocol_version) @@ -316,6 +326,13 @@ void ItemDefinition::deSerialize(std::istream &is, u16 protocol_version) wallmounted_rotate_vertical = readU8(is); // 0 if missing touch_interaction.deSerialize(is); + + std::string pointabilities_s = deSerializeString16(is); + if (!pointabilities_s.empty()) { + std::istringstream tmp_is(pointabilities_s, std::ios::binary); + pointabilities = std::make_optional(); + pointabilities->deSerialize(tmp_is); + } } catch(SerializationError &e) {}; } diff --git a/src/itemdef.h b/src/itemdef.h index 3618489110..192e900952 100644 --- a/src/itemdef.h +++ b/src/itemdef.h @@ -28,6 +28,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "itemgroup.h" #include "sound.h" #include "texture_override.h" // TextureOverride +#include "util/pointabilities.h" class IGameDef; class Client; struct ToolCapabilities; @@ -97,8 +98,11 @@ struct ItemDefinition u16 stack_max; bool usable; bool liquids_pointable; - // May be NULL. If non-NULL, deleted by destructor + std::optional pointabilities; + + // They may be NULL. If non-NULL, deleted by destructor ToolCapabilities *tool_capabilities; + ItemGroupList groups; SoundSpec sound_place; SoundSpec sound_place_failed; diff --git a/src/nodedef.cpp b/src/nodedef.cpp index 3979fc5751..830ee1ff42 100644 --- a/src/nodedef.cpp +++ b/src/nodedef.cpp @@ -386,7 +386,7 @@ void ContentFeatures::reset() light_propagates = false; sunlight_propagates = false; walkable = true; - pointable = true; + pointable = PointabilityType::POINTABLE; diggable = true; climbable = false; buildable_to = false; @@ -504,7 +504,7 @@ void ContentFeatures::serialize(std::ostream &os, u16 protocol_version) const // interaction writeU8(os, walkable); - writeU8(os, pointable); + Pointabilities::serializePointabilityType(os, pointable); writeU8(os, diggable); writeU8(os, climbable); writeU8(os, buildable_to); @@ -617,7 +617,7 @@ void ContentFeatures::deSerialize(std::istream &is, u16 protocol_version) // interaction walkable = readU8(is); - pointable = readU8(is); + pointable = Pointabilities::deSerializePointabilityType(is); diggable = readU8(is); climbable = readU8(is); buildable_to = readU8(is); @@ -1083,7 +1083,7 @@ void NodeDefManager::clear() f.light_propagates = true; f.sunlight_propagates = true; f.walkable = false; - f.pointable = false; + f.pointable = PointabilityType::POINTABLE_NOT; f.diggable = false; f.buildable_to = true; f.floodable = true; @@ -1104,7 +1104,7 @@ void NodeDefManager::clear() f.light_propagates = false; f.sunlight_propagates = false; f.walkable = false; - f.pointable = false; + f.pointable = PointabilityType::POINTABLE_NOT; f.diggable = false; f.buildable_to = true; // A way to remove accidental CONTENT_IGNOREs f.is_ground_content = true; diff --git a/src/nodedef.h b/src/nodedef.h index 8d111f67e0..f493d48c42 100644 --- a/src/nodedef.h +++ b/src/nodedef.h @@ -35,6 +35,7 @@ class Client; #include "constants.h" // BS #include "texture_override.h" // TextureOverride #include "tileanimation.h" +#include "util/pointabilities.h" class IItemDefManager; class ITextureSource; @@ -395,8 +396,8 @@ struct ContentFeatures // This is used for collision detection. // Also for general solidness queries. bool walkable; - // Player can point to these - bool pointable; + // Player can point to these, point through or it is blocking + PointabilityType pointable; // Player can dig these bool diggable; // Player can climb these diff --git a/src/object_properties.cpp b/src/object_properties.cpp index 00f538d291..6f66fbaee5 100644 --- a/src/object_properties.cpp +++ b/src/object_properties.cpp @@ -73,7 +73,7 @@ std::string ObjectProperties::dump() os << ", selectionbox=" << selectionbox.MinEdge << "," << selectionbox.MaxEdge; os << ", rotate_selectionbox=" << rotate_selectionbox; - os << ", pointable=" << pointable; + os << ", pointable=" << Pointabilities::toStringPointabilityType(pointable); os << ", static_save=" << static_save; os << ", eye_height=" << eye_height; os << ", zoom_fov=" << zoom_fov; @@ -127,7 +127,7 @@ void ObjectProperties::serialize(std::ostream &os) const writeV3F32(os, collisionbox.MaxEdge); writeV3F32(os, selectionbox.MinEdge); writeV3F32(os, selectionbox.MaxEdge); - writeU8(os, pointable); + Pointabilities::serializePointabilityType(os, pointable); os << serializeString16(visual); writeV3F32(os, visual_size); writeU16(os, textures.size()); @@ -188,7 +188,7 @@ void ObjectProperties::deSerialize(std::istream &is) collisionbox.MaxEdge = readV3F32(is); selectionbox.MinEdge = readV3F32(is); selectionbox.MaxEdge = readV3F32(is); - pointable = readU8(is); + pointable = Pointabilities::deSerializePointabilityType(is); visual = deSerializeString16(is); visual_size = readV3F32(is); textures.clear(); diff --git a/src/object_properties.h b/src/object_properties.h index 44d2f29b29..557f128be3 100644 --- a/src/object_properties.h +++ b/src/object_properties.h @@ -25,6 +25,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include #include #include +#include "util/pointabilities.h" struct ObjectProperties { @@ -36,7 +37,7 @@ struct ObjectProperties aabb3f collisionbox = aabb3f(-0.5f, -0.5f, -0.5f, 0.5f, 0.5f, 0.5f); aabb3f selectionbox = aabb3f(-0.5f, -0.5f, -0.5f, 0.5f, 0.5f, 0.5f); bool rotate_selectionbox = false; - bool pointable = true; + PointabilityType pointable = PointabilityType::POINTABLE; std::string visual = "sprite"; std::string mesh = ""; v3f visual_size = v3f(1, 1, 1); diff --git a/src/raycast.cpp b/src/raycast.cpp index c41b9c3944..510a8f7f91 100644 --- a/src/raycast.cpp +++ b/src/raycast.cpp @@ -58,12 +58,14 @@ bool RaycastSort::operator() (const PointedThing &pt1, RaycastState::RaycastState(const core::line3d &shootline, - bool objects_pointable, bool liquids_pointable) : + bool objects_pointable, bool liquids_pointable, + const std::optional &pointabilities) : m_shootline(shootline), m_iterator(shootline.start / BS, shootline.getVector() / BS), m_previous_node(m_iterator.m_current_node_pos), m_objects_pointable(objects_pointable), - m_liquids_pointable(liquids_pointable) + m_liquids_pointable(liquids_pointable), + m_pointabilities(pointabilities) { } diff --git a/src/raycast.h b/src/raycast.h index 8da075738c..d33f13c987 100644 --- a/src/raycast.h +++ b/src/raycast.h @@ -38,7 +38,7 @@ public: * @param liquids pointable if false, liquid nodes won't be found */ RaycastState(const core::line3d &shootline, bool objects_pointable, - bool liquids_pointable); + bool liquids_pointable, const std::optional &pointabilities); //! Shootline of the raycast. core::line3d m_shootline; @@ -55,6 +55,7 @@ public: bool m_objects_pointable; bool m_liquids_pointable; + const std::optional &m_pointabilities; //! The code needs to search these nodes around the center node. core::aabbox3d m_search_range { 0, 0, 0, 0, 0, 0 }; diff --git a/src/script/common/c_content.cpp b/src/script/common/c_content.cpp index b50e02a80b..5259d3f384 100644 --- a/src/script/common/c_content.cpp +++ b/src/script/common/c_content.cpp @@ -83,6 +83,12 @@ void read_item_definition(lua_State* L, int index, getboolfield(L, index, "liquids_pointable", def.liquids_pointable); + lua_getfield(L, index, "pointabilities"); + if(lua_istable(L, -1)){ + def.pointabilities = std::make_optional( + read_pointabilities(L, -1)); + } + lua_getfield(L, index, "tool_capabilities"); if(lua_istable(L, -1)){ def.tool_capabilities = new ToolCapabilities( @@ -199,6 +205,10 @@ void push_item_definition_full(lua_State *L, const ItemDefinition &i) lua_setfield(L, -2, "usable"); lua_pushboolean(L, i.liquids_pointable); lua_setfield(L, -2, "liquids_pointable"); + if (i.pointabilities) { + push_pointabilities(L, *i.pointabilities); + lua_setfield(L, -2, "pointabilities"); + } if (i.tool_capabilities) { push_tool_capabilities(L, *i.tool_capabilities); lua_setfield(L, -2, "tool_capabilities"); @@ -311,7 +321,12 @@ void read_object_properties(lua_State *L, int index, } lua_pop(L, 1); - getboolfield(L, -1, "pointable", prop->pointable); + lua_getfield(L, -1, "pointable"); + if(!lua_isnil(L, -1)){ + prop->pointable = read_pointability_type(L, -1); + } + lua_pop(L, 1); + getstringfield(L, -1, "visual", prop->visual); getstringfield(L, -1, "mesh", prop->mesh); @@ -452,7 +467,7 @@ void push_object_properties(lua_State *L, ObjectProperties *prop) lua_pushboolean(L, prop->rotate_selectionbox); lua_setfield(L, -2, "rotate"); lua_setfield(L, -2, "selectionbox"); - lua_pushboolean(L, prop->pointable); + push_pointability_type(L, prop->pointable); lua_setfield(L, -2, "pointable"); lua_pushlstring(L, prop->visual.c_str(), prop->visual.size()); lua_setfield(L, -2, "visual"); @@ -781,8 +796,14 @@ void read_content_features(lua_State *L, ContentFeatures &f, int index) // This is used for collision detection. // Also for general solidness queries. getboolfield(L, index, "walkable", f.walkable); - // Player can point to these - getboolfield(L, index, "pointable", f.pointable); + + // Player can point to these, point through or it is blocking + lua_getfield(L, index, "pointable"); + if(!lua_isnil(L, -1)){ + f.pointable = read_pointability_type(L, -1); + } + lua_pop(L, 1); + // Player can dig these getboolfield(L, index, "diggable", f.diggable); // Player can climb these @@ -1005,7 +1026,7 @@ void push_content_features(lua_State *L, const ContentFeatures &c) lua_setfield(L, -2, "is_ground_content"); lua_pushboolean(L, c.walkable); lua_setfield(L, -2, "walkable"); - lua_pushboolean(L, c.pointable); + push_pointability_type(L, c.pointable); lua_setfield(L, -2, "pointable"); lua_pushboolean(L, c.diggable); lua_setfield(L, -2, "diggable"); @@ -1592,6 +1613,125 @@ ToolCapabilities read_tool_capabilities( return toolcap; } +/******************************************************************************/ +PointabilityType read_pointability_type(lua_State *L, int index) +{ + if (lua_isboolean(L, index)) { + if (lua_toboolean(L, index)) + return PointabilityType::POINTABLE; + else + return PointabilityType::POINTABLE_NOT; + } else { + const char* s = luaL_checkstring(L, index); + if (s && !strcmp(s, "blocking")) { + return PointabilityType::POINTABLE_BLOCKING; + } + } + throw LuaError("Invalid pointable type."); +} + +/******************************************************************************/ +Pointabilities read_pointabilities(lua_State *L, int index) +{ + Pointabilities pointabilities; + + lua_getfield(L, index, "nodes"); + if(lua_istable(L, -1)){ + int ti = lua_gettop(L); + lua_pushnil(L); + while(lua_next(L, ti) != 0) { + // key at index -2 and value at index -1 + std::string name = luaL_checkstring(L, -2); + + // handle groups + if(std::string_view(name).substr(0,6)=="group:") { + pointabilities.node_groups[name.substr(6)] = read_pointability_type(L, -1); + } else { + pointabilities.nodes[name] = read_pointability_type(L, -1); + } + + // removes value, keeps key for next iteration + lua_pop(L, 1); + } + } + lua_pop(L, 1); + + lua_getfield(L, index, "objects"); + if(lua_istable(L, -1)){ + int ti = lua_gettop(L); + lua_pushnil(L); + while(lua_next(L, ti) != 0) { + // key at index -2 and value at index -1 + std::string name = luaL_checkstring(L, -2); + + // handle groups + if(std::string_view(name).substr(0,6)=="group:") { + pointabilities.object_groups[name.substr(6)] = read_pointability_type(L, -1); + } else { + pointabilities.objects[name] = read_pointability_type(L, -1); + } + + // removes value, keeps key for next iteration + lua_pop(L, 1); + } + } + lua_pop(L, 1); + + return pointabilities; +} + +/******************************************************************************/ +void push_pointability_type(lua_State *L, PointabilityType pointable) +{ + switch(pointable) + { + case PointabilityType::POINTABLE: + lua_pushboolean(L, true); + break; + case PointabilityType::POINTABLE_NOT: + lua_pushboolean(L, false); + break; + case PointabilityType::POINTABLE_BLOCKING: + lua_pushliteral(L, "blocking"); + break; + } +} + +/******************************************************************************/ +void push_pointabilities(lua_State *L, const Pointabilities &pointabilities) +{ + // pointabilities table + lua_newtable(L); + + if (!pointabilities.nodes.empty() || !pointabilities.node_groups.empty()) { + // Create and fill table + lua_newtable(L); + for (const auto &entry : pointabilities.nodes) { + push_pointability_type(L, entry.second); + lua_setfield(L, -2, entry.first.c_str()); + } + for (const auto &entry : pointabilities.node_groups) { + push_pointability_type(L, entry.second); + lua_setfield(L, -2, ("group:" + entry.first).c_str()); + } + lua_setfield(L, -2, "nodes"); + } + + if (!pointabilities.objects.empty() || !pointabilities.object_groups.empty()) { + // Create and fill table + lua_newtable(L); + for (const auto &entry : pointabilities.objects) { + push_pointability_type(L, entry.second); + lua_setfield(L, -2, entry.first.c_str()); + } + for (const auto &entry : pointabilities.object_groups) { + push_pointability_type(L, entry.second); + lua_setfield(L, -2, ("group:" + entry.first).c_str()); + } + lua_setfield(L, -2, "objects"); + } +} + /******************************************************************************/ void push_dig_params(lua_State *L,const DigParams ¶ms) { diff --git a/src/script/common/c_content.h b/src/script/common/c_content.h index 4335479c4e..6f54b7b23a 100644 --- a/src/script/common/c_content.h +++ b/src/script/common/c_content.h @@ -39,6 +39,7 @@ extern "C" { #include "util/string.h" #include "itemgroup.h" #include "itemdef.h" +#include "util/pointabilities.h" #include "c_types.h" // We do an explicit path include because by default c_content.h include src/client/hud.h // prior to the src/hud.h, which is not good on server only build @@ -107,6 +108,11 @@ ItemStack read_item (lua_State *L, int index, IItemDefM struct TileAnimationParams read_animation_definition(lua_State *L, int index); +PointabilityType read_pointability_type (lua_State *L, int index); +Pointabilities read_pointabilities (lua_State *L, int index); +void push_pointability_type (lua_State *L, PointabilityType pointable); +void push_pointabilities (lua_State *L, const Pointabilities &pointabilities); + ToolCapabilities read_tool_capabilities (lua_State *L, int table); void push_tool_capabilities (lua_State *L, const ToolCapabilities &prop); diff --git a/src/script/lua_api/l_env.cpp b/src/script/lua_api/l_env.cpp index 33d7903167..b8daa86a0b 100644 --- a/src/script/lua_api/l_env.cpp +++ b/src/script/lua_api/l_env.cpp @@ -188,7 +188,7 @@ int LuaRaycast::create_object(lua_State *L) } LuaRaycast *o = new LuaRaycast(core::line3d(pos1, pos2), - objects, liquids); + objects, liquids, std::nullopt); *(void **) (lua_newuserdata(L, sizeof(void *))) = o; luaL_getmetatable(L, className); diff --git a/src/script/lua_api/l_env.h b/src/script/lua_api/l_env.h index 1378736d8a..f1fa680e93 100644 --- a/src/script/lua_api/l_env.h +++ b/src/script/lua_api/l_env.h @@ -336,8 +336,9 @@ public: LuaRaycast( const core::line3d &shootline, bool objects_pointable, - bool liquids_pointable) : - state(shootline, objects_pointable, liquids_pointable) + bool liquids_pointable, + const std::optional &pointabilities) : + state(shootline, objects_pointable, liquids_pointable, pointabilities) {} //! Creates a LuaRaycast and leaves it on top of the stack. diff --git a/src/server/luaentity_sao.cpp b/src/server/luaentity_sao.cpp index e4c392eb22..3995f656dd 100644 --- a/src/server/luaentity_sao.cpp +++ b/src/server/luaentity_sao.cpp @@ -245,7 +245,7 @@ std::string LuaEntitySAO::getClientInitializationData(u16 protocol_version) // PROTOCOL_VERSION >= 37 writeU8(os, 1); // version - os << serializeString16(""); // name + os << serializeString16(m_init_name); // name writeU8(os, 0); // is_player writeU16(os, getId()); //id writeV3F32(os, m_base_position); @@ -553,7 +553,7 @@ bool LuaEntitySAO::getCollisionBox(aabb3f *toset) const bool LuaEntitySAO::getSelectionBox(aabb3f *toset) const { - if (!m_prop.is_visible || !m_prop.pointable) { + if (!m_prop.is_visible) { return false; } diff --git a/src/server/player_sao.cpp b/src/server/player_sao.cpp index ede3ccb8e3..688dcde4e0 100644 --- a/src/server/player_sao.cpp +++ b/src/server/player_sao.cpp @@ -39,7 +39,7 @@ PlayerSAO::PlayerSAO(ServerEnvironment *env_, RemotePlayer *player_, session_t p m_prop.physical = false; m_prop.collisionbox = aabb3f(-0.3f, 0.0f, -0.3f, 0.3f, 1.77f, 0.3f); m_prop.selectionbox = aabb3f(-0.3f, 0.0f, -0.3f, 0.3f, 1.77f, 0.3f); - m_prop.pointable = true; + m_prop.pointable = PointabilityType::POINTABLE; // Start of default appearance, this should be overwritten by Lua m_prop.visual = "upright_sprite"; m_prop.visual_size = v3f(1, 2, 1); @@ -724,7 +724,7 @@ bool PlayerSAO::getCollisionBox(aabb3f *toset) const bool PlayerSAO::getSelectionBox(aabb3f *toset) const { - if (!m_prop.is_visible || !m_prop.pointable) { + if (!m_prop.is_visible) { return false; } diff --git a/src/serverenvironment.cpp b/src/serverenvironment.cpp index e92b00f579..e0cf993788 100644 --- a/src/serverenvironment.cpp +++ b/src/serverenvironment.cpp @@ -1830,7 +1830,8 @@ bool ServerEnvironment::getActiveObjectMessage(ActiveObjectMessage *dest) void ServerEnvironment::getSelectedActiveObjects( const core::line3d &shootline_on_map, - std::vector &objects) + std::vector &objects, + const std::optional &pointabilities) { std::vector objs; getObjectsInsideRadius(objs, shootline_on_map.start, @@ -1863,10 +1864,26 @@ void ServerEnvironment::getSelectedActiveObjects( current_raw_normal = current_normal; } if (collision) { - current_intersection += pos; - objects.emplace_back( - (s16) obj->getId(), current_intersection, current_normal, current_raw_normal, - (current_intersection - shootline_on_map.start).getLengthSQ()); + PointabilityType pointable; + if (pointabilities) { + if (LuaEntitySAO* lsao = dynamic_cast(obj)) { + pointable = pointabilities->matchObject(lsao->getName(), + usao->getArmorGroups()).value_or(props->pointable); + } else if (PlayerSAO* psao = dynamic_cast(obj)) { + pointable = pointabilities->matchPlayer(psao->getArmorGroups()).value_or( + props->pointable); + } else { + pointable = props->pointable; + } + } else { + pointable = props->pointable; + } + if (pointable != PointabilityType::POINTABLE_NOT) { + current_intersection += pos; + objects.emplace_back( + (s16) obj->getId(), current_intersection, current_normal, current_raw_normal, + (current_intersection - shootline_on_map.start).getLengthSQ(), pointable); + } } } } diff --git a/src/serverenvironment.h b/src/serverenvironment.h index bb689ea2c7..ad6a3acc5d 100644 --- a/src/serverenvironment.h +++ b/src/serverenvironment.h @@ -315,7 +315,8 @@ public: virtual void getSelectedActiveObjects( const core::line3d &shootline_on_map, - std::vector &objects + std::vector &objects, + const std::optional &pointabilities ); /* diff --git a/src/util/CMakeLists.txt b/src/util/CMakeLists.txt index 6bc97915f3..d8f106589d 100644 --- a/src/util/CMakeLists.txt +++ b/src/util/CMakeLists.txt @@ -8,6 +8,7 @@ set(UTIL_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/metricsbackend.cpp ${CMAKE_CURRENT_SOURCE_DIR}/numeric.cpp ${CMAKE_CURRENT_SOURCE_DIR}/pointedthing.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/pointabilities.cpp ${CMAKE_CURRENT_SOURCE_DIR}/quicktune.cpp ${CMAKE_CURRENT_SOURCE_DIR}/serialize.cpp ${CMAKE_CURRENT_SOURCE_DIR}/sha1.cpp diff --git a/src/util/pointabilities.cpp b/src/util/pointabilities.cpp new file mode 100644 index 0000000000..575ae269b0 --- /dev/null +++ b/src/util/pointabilities.cpp @@ -0,0 +1,147 @@ +/* +Minetest +Copyright (C) 2023 cx384 + +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 "pointabilities.h" + +#include "serialize.h" +#include "exceptions.h" +#include + +PointabilityType Pointabilities::deSerializePointabilityType(std::istream &is) +{ + PointabilityType pointable_type = static_cast(readU8(is)); + switch(pointable_type) { + case PointabilityType::POINTABLE: + case PointabilityType::POINTABLE_NOT: + case PointabilityType::POINTABLE_BLOCKING: + break; + default: + // Default to POINTABLE in case of unknown PointabilityType type. + pointable_type = PointabilityType::POINTABLE; + break; + } + return pointable_type; +} + +void Pointabilities::serializePointabilityType(std::ostream &os, PointabilityType pointable_type) +{ + writeU8(os, static_cast(pointable_type)); +} + +std::string Pointabilities::toStringPointabilityType(PointabilityType pointable_type) +{ + switch(pointable_type) { + case PointabilityType::POINTABLE: + return "true"; + case PointabilityType::POINTABLE_NOT: + return "false"; + case PointabilityType::POINTABLE_BLOCKING: + return "\"blocking\""; + } + return "unknown"; +} + +std::optional Pointabilities::matchNode(const std::string &name, + const ItemGroupList &groups) const +{ + auto i = nodes.find(name); + return i == nodes.end() ? matchGroups(groups, node_groups) : i->second; +} + +std::optional Pointabilities::matchObject(const std::string &name, + const ItemGroupList &groups) const +{ + auto i = objects.find(name); + return i == objects.end() ? matchGroups(groups, object_groups) : i->second; +} + +std::optional Pointabilities::matchPlayer(const ItemGroupList &groups) const +{ + return matchGroups(groups, object_groups); +} + +std::optional Pointabilities::matchGroups(const ItemGroupList &groups, + const std::unordered_map &pointable_groups) +{ + // prefers POINTABLE over POINTABLE_NOT over POINTABLE_BLOCKING + bool blocking = false; + bool not_pointable = false; + for (auto const &ability : pointable_groups) { + if (itemgroup_get(groups, ability.first) > 0) { + switch(ability.second) { + case PointabilityType::POINTABLE: + return PointabilityType::POINTABLE; + case PointabilityType::POINTABLE_NOT: + not_pointable = true; + break; + default: + blocking = true; + break; + } + } + } + if (not_pointable) + return PointabilityType::POINTABLE_NOT; + if (blocking) + return PointabilityType::POINTABLE_BLOCKING; + return std::nullopt; +} + +void Pointabilities::serializeTypeMap(std::ostream &os, + const std::unordered_map &map) +{ + writeU32(os, map.size()); + for (const auto &entry : map) { + os << serializeString16(entry.first); + writeU8(os, (u8)entry.second); + } +} + +void Pointabilities::deSerializeTypeMap(std::istream &is, + std::unordered_map &map) +{ + map.clear(); + u32 size = readU32(is); + for (u32 i = 0; i < size; i++) { + std::string name = deSerializeString16(is); + PointabilityType type = Pointabilities::deSerializePointabilityType(is); + map[name] = type; + } +} + +void Pointabilities::serialize(std::ostream &os) const +{ + writeU8(os, 0); // version + serializeTypeMap(os, nodes); + serializeTypeMap(os, node_groups); + serializeTypeMap(os, objects); + serializeTypeMap(os, object_groups); +} + +void Pointabilities::deSerialize(std::istream &is) +{ + int version = readU8(is); + if (version != 0) + throw SerializationError("unsupported Pointabilities version"); + + deSerializeTypeMap(is, nodes); + deSerializeTypeMap(is, node_groups); + deSerializeTypeMap(is, objects); + deSerializeTypeMap(is, object_groups); +} diff --git a/src/util/pointabilities.h b/src/util/pointabilities.h new file mode 100644 index 0000000000..4acc1ddb76 --- /dev/null +++ b/src/util/pointabilities.h @@ -0,0 +1,70 @@ +/* +Minetest +Copyright (C) 2023 cx384 + +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 +#include +#include "itemgroup.h" +#include +#include "irrlichttypes.h" + +enum class PointabilityType : u8 +{ + POINTABLE, + POINTABLE_NOT, // Can be pointed through. + POINTABLE_BLOCKING, +}; + +// An object to store overridden pointable properties +struct Pointabilities +{ + // Nodes + std::unordered_map nodes; + std::unordered_map node_groups; + + // Objects + std::unordered_map objects; + std::unordered_map object_groups; // armor_groups + + // Match functions return fitting pointability, + // otherwise the default pointability should be used. + + std::optional matchNode(const std::string &name, + const ItemGroupList &groups) const; + std::optional matchObject(const std::string &name, + const ItemGroupList &groups) const; + // For players only armor groups will work + std::optional matchPlayer(const ItemGroupList &groups) const; + + void serialize(std::ostream &os) const; + void deSerialize(std::istream &is); + + // For a save enum conversion. + static PointabilityType deSerializePointabilityType(std::istream &is); + static void serializePointabilityType(std::ostream &os, PointabilityType pointable_type); + static std::string toStringPointabilityType(PointabilityType pointable_type); + +private: + static std::optional matchGroups(const ItemGroupList &groups, + const std::unordered_map &pointable_groups); + static void serializeTypeMap(std::ostream &os, + const std::unordered_map &map); + static void deSerializeTypeMap(std::istream &is, + std::unordered_map &map); +}; diff --git a/src/util/pointedthing.cpp b/src/util/pointedthing.cpp index b4e48c8d51..f87021dc63 100644 --- a/src/util/pointedthing.cpp +++ b/src/util/pointedthing.cpp @@ -25,7 +25,7 @@ with this program; if not, write to the Free Software Foundation, Inc., PointedThing::PointedThing(const v3s16 &under, const v3s16 &above, const v3s16 &real_under, const v3f &point, const v3f &normal, - u16 box_id, f32 distSq): + u16 box_id, f32 distSq, PointabilityType pointab): type(POINTEDTHING_NODE), node_undersurface(under), node_abovesurface(above), @@ -33,17 +33,19 @@ PointedThing::PointedThing(const v3s16 &under, const v3s16 &above, intersection_point(point), intersection_normal(normal), box_id(box_id), - distanceSq(distSq) + distanceSq(distSq), + pointability(pointab) {} -PointedThing::PointedThing(u16 id, const v3f &point, - const v3f &normal, const v3f &raw_normal, f32 distSq) : +PointedThing::PointedThing(u16 id, const v3f &point, const v3f &normal, + const v3f &raw_normal, f32 distSq, PointabilityType pointab) : type(POINTEDTHING_OBJECT), object_id(id), intersection_point(point), intersection_normal(normal), raw_intersection_normal(raw_normal), - distanceSq(distSq) + distanceSq(distSq), + pointability(pointab) {} std::string PointedThing::dump() const @@ -118,12 +120,13 @@ bool PointedThing::operator==(const PointedThing &pt2) const { if ((node_undersurface != pt2.node_undersurface) || (node_abovesurface != pt2.node_abovesurface) - || (node_real_undersurface != pt2.node_real_undersurface)) + || (node_real_undersurface != pt2.node_real_undersurface) + || (pointability != pt2.pointability)) return false; } else if (type == POINTEDTHING_OBJECT) { - if (object_id != pt2.object_id) + if (object_id != pt2.object_id || pointability != pt2.pointability) return false; } return true; diff --git a/src/util/pointedthing.h b/src/util/pointedthing.h index 276b064b82..c44bcc2a89 100644 --- a/src/util/pointedthing.h +++ b/src/util/pointedthing.h @@ -23,8 +23,9 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "irr_v3d.h" #include #include +#include "pointabilities.h" -enum PointedThingType : u8 +enum PointedThingType :u8 { POINTEDTHING_NOTHING, POINTEDTHING_NODE, @@ -90,15 +91,20 @@ struct PointedThing * ray's start point and the intersection point in irrlicht coordinates. */ f32 distanceSq = 0; + /*! + * How the object or node has been pointed at. + */ + PointabilityType pointability = PointabilityType::POINTABLE_NOT; //! Constructor for POINTEDTHING_NOTHING PointedThing() = default; //! Constructor for POINTEDTHING_NODE PointedThing(const v3s16 &under, const v3s16 &above, const v3s16 &real_under, const v3f &point, const v3f &normal, - u16 box_id, f32 distSq); + u16 box_id, f32 distSq, PointabilityType pointability); //! Constructor for POINTEDTHING_OBJECT - PointedThing(u16 id, const v3f &point, const v3f &normal, const v3f &raw_normal, f32 distSq); + PointedThing(u16 id, const v3f &point, const v3f &normal, const v3f &raw_normal, f32 distSq, + PointabilityType pointability); std::string dump() const; void serialize(std::ostream &os) const; void deSerialize(std::istream &is); From afc48cf2242d576aed2371b4763a71fec8739857 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lars=20M=C3=BCller?= <34514239+appgurueu@users.noreply.github.com> Date: Mon, 22 Jan 2024 20:06:03 +0100 Subject: [PATCH 07/19] Address `set_player_privs` footgun (#14297) --------- Co-authored-by: grorp --- builtin/game/auth.lua | 26 ++++++++++++++++++++++++-- doc/lua_api.md | 20 ++++++++++++++++---- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/builtin/game/auth.lua b/builtin/game/auth.lua index fa1860d5d4..35046cec7a 100644 --- a/builtin/game/auth.lua +++ b/builtin/game/auth.lua @@ -92,8 +92,16 @@ core.builtin_auth_handler = { core_auth.save(auth_entry) - -- Run grant callbacks - for priv, _ in pairs(privileges) do + for priv, value in pairs(privileges) do + -- Warnings for improper API usage + if value == false then + core.log('deprecated', "`false` value given to `minetest.set_player_privs`, ".. + "this is almost certainly a bug, ".. + "granting a privilege rather than revoking it") + elseif value ~= true then + core.log('deprecated', "non-`true` value given to `minetest.set_player_privs`") + end + -- Run grant callbacks if not prev_privs[priv] then core.run_priv_callbacks(name, priv, nil, "grant") end @@ -180,6 +188,20 @@ core.set_player_privs = auth_pass("set_privileges") core.remove_player_auth = auth_pass("delete_auth") core.auth_reload = auth_pass("reload") +function core.change_player_privs(name, changes) + local privs = core.get_player_privs(name) + for priv, change in pairs(changes) do + if change == true then + privs[priv] = true + elseif change == false then + privs[priv] = nil + else + error("non-bool value given to `minetest.change_player_privs`") + end + end + core.set_player_privs(name, privs) +end + local record_login = auth_pass("record_login") core.register_on_joinplayer(function(player) record_login(player:get_player_name()) diff --git a/doc/lua_api.md b/doc/lua_api.md index 7de57aff74..3b69f98b33 100644 --- a/doc/lua_api.md +++ b/doc/lua_api.md @@ -5841,8 +5841,20 @@ Authentication * `name`: string; if omitted, all auth data should be considered modified * `minetest.set_player_password(name, password_hash)`: Set password hash of player `name`. -* `minetest.set_player_privs(name, {priv1=true,...})`: Set privileges of player - `name`. +* `minetest.set_player_privs(name, privs)`: Set privileges of player `name`. + * `privs` is a **set** of privileges: + A table where the keys are names of privileges and the values are `true`. + * Example: `minetest.set_player_privs("singleplayer", {interact = true, fly = true})`. + This **sets** the player privileges to `interact` and `fly`; + `singleplayer` will only have these two privileges afterwards. +* `minetest.change_player_privs(name, changes)`: Helper to grant or revoke privileges. + * `changes`: Table of changes to make. + A field `[privname] = true` grants a privilege, + whereas `[privname] = false` revokes a privilege. + * Example: `minetest.change_player_privs("singleplayer", {interact = true, fly = false})` + will grant singleplayer the `interact` privilege + and revoke singleplayer's `fly` privilege. + All other privileges will remain unchanged. * `minetest.auth_reload()` * See `reload()` in authentication handler definition @@ -10745,8 +10757,8 @@ Used by `minetest.register_authentication_handler`. set_privileges = function(name, privileges), -- Set privileges of player `name`. - -- `privileges` is in table form, auth data should be created if not - -- present. + -- `privileges` is in table form: keys are privilege names, values are `true`; + -- auth data should be created if not present. reload = function(), -- Reload authentication data from the storage location. From f0180ad488ec547758f56105b05e043db518086b Mon Sep 17 00:00:00 2001 From: Lars Mueller Date: Mon, 22 Jan 2024 21:57:30 +0100 Subject: [PATCH 08/19] Fix revoke callbacks being run for `false` values passed to `set_privileges` --- builtin/game/auth.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/builtin/game/auth.lua b/builtin/game/auth.lua index 35046cec7a..e45520e276 100644 --- a/builtin/game/auth.lua +++ b/builtin/game/auth.lua @@ -102,14 +102,14 @@ core.builtin_auth_handler = { core.log('deprecated', "non-`true` value given to `minetest.set_player_privs`") end -- Run grant callbacks - if not prev_privs[priv] then + if prev_privs[priv] == nil then core.run_priv_callbacks(name, priv, nil, "grant") end end -- Run revoke callbacks for priv, _ in pairs(prev_privs) do - if not privileges[priv] then + if privileges[priv] == nil then core.run_priv_callbacks(name, priv, nil, "revoke") end end From f6ecd931dc08a7a5a48b5d20787f6d8ba54462fc Mon Sep 17 00:00:00 2001 From: Lars Mueller Date: Mon, 22 Jan 2024 22:41:33 +0100 Subject: [PATCH 09/19] Minor documentation formatting fixes --- doc/lua_api.md | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/doc/lua_api.md b/doc/lua_api.md index 3b69f98b33..de67f7656a 100644 --- a/doc/lua_api.md +++ b/doc/lua_api.md @@ -5842,19 +5842,19 @@ Authentication * `minetest.set_player_password(name, password_hash)`: Set password hash of player `name`. * `minetest.set_player_privs(name, privs)`: Set privileges of player `name`. - * `privs` is a **set** of privileges: - A table where the keys are names of privileges and the values are `true`. - * Example: `minetest.set_player_privs("singleplayer", {interact = true, fly = true})`. - This **sets** the player privileges to `interact` and `fly`; - `singleplayer` will only have these two privileges afterwards. + * `privs` is a **set** of privileges: + A table where the keys are names of privileges and the values are `true`. + * Example: `minetest.set_player_privs("singleplayer", {interact = true, fly = true})`. + This **sets** the player privileges to `interact` and `fly`; + `singleplayer` will only have these two privileges afterwards. * `minetest.change_player_privs(name, changes)`: Helper to grant or revoke privileges. - * `changes`: Table of changes to make. - A field `[privname] = true` grants a privilege, - whereas `[privname] = false` revokes a privilege. - * Example: `minetest.change_player_privs("singleplayer", {interact = true, fly = false})` - will grant singleplayer the `interact` privilege - and revoke singleplayer's `fly` privilege. - All other privileges will remain unchanged. + * `changes`: Table of changes to make. + A field `[privname] = true` grants a privilege, + whereas `[privname] = false` revokes a privilege. + * Example: `minetest.change_player_privs("singleplayer", {interact = true, fly = false})` + will grant singleplayer the `interact` privilege + and revoke singleplayer's `fly` privilege. + All other privileges will remain unchanged. * `minetest.auth_reload()` * See `reload()` in authentication handler definition @@ -8196,10 +8196,10 @@ Can be obtained using `player:get_meta()`. A 16-bit pseudorandom number generator. Uses a well-known LCG algorithm introduced by K&R. -> [!NOTE] -> `PseudoRandom` is slower and has worse random distribution than `PcgRandom`. -> Use `PseudoRandom` only if you need output to match the well-known LCG algorithm introduced by K&R. -> Otherwise, use `PcgRandom`. +**Note**: +`PseudoRandom` is slower and has worse random distribution than `PcgRandom`. +Use `PseudoRandom` only if you need output to match the well-known LCG algorithm introduced by K&R. +Otherwise, use `PcgRandom`. * constructor `PseudoRandom(seed)` * `seed`: 32-bit signed number @@ -8778,12 +8778,12 @@ Used by `minetest.register_node`, `minetest.register_craftitem`, and -- The index can be a node/entity name or a group with the prefix `"group:"`. -- (For objects `armor_groups` are used and for players the entity name is irrelevant.) -- If multiple fields fit, the following priority order is applied: - -- value of matching node/entity name - -- `true` for any group - -- `false` for any group - -- `"blocking"` for any group - -- `liquids_pointable` if it is a liquid node - -- `pointable` property of the node or object + -- 1. value of matching node/entity name + -- 2. `true` for any group + -- 3. `false` for any group + -- 4. `"blocking"` for any group + -- 5. `liquids_pointable` if it is a liquid node + -- 6. `pointable` property of the node or object light_source = 0, -- When used for nodes: Defines amount of light emitted by node. From a29d3cf074578b70dbc70704616f2b2535f7d2bd Mon Sep 17 00:00:00 2001 From: grorp Date: Tue, 23 Jan 2024 21:33:27 +0100 Subject: [PATCH 10/19] Save the settings in more cases to avoid losing setting changes (especially on Android) (#14266) --- .../net/minetest/minetest/GameActivity.java | 11 ++++++++ builtin/mainmenu/settings/dlg_settings.lua | 16 ++++++++++++ src/client/clientlauncher.cpp | 25 ++++++++++++++++--- src/porting_android.cpp | 7 ++++++ 4 files changed, 55 insertions(+), 4 deletions(-) diff --git a/android/app/src/main/java/net/minetest/minetest/GameActivity.java b/android/app/src/main/java/net/minetest/minetest/GameActivity.java index f2ff09f3de..85336ac7ee 100644 --- a/android/app/src/main/java/net/minetest/minetest/GameActivity.java +++ b/android/app/src/main/java/net/minetest/minetest/GameActivity.java @@ -85,6 +85,17 @@ public class GameActivity extends NativeActivity { makeFullScreen(); } + private native void saveSettings(); + + @Override + protected void onStop() { + super.onStop(); + // Avoid losing setting changes in case the app is onDestroy()ed later. + // Saving stuff in onStop() is recommended in the Android activity + // lifecycle documentation. + saveSettings(); + } + @Override public void onBackPressed() { // Ignore the back press so Minetest can handle it diff --git a/builtin/mainmenu/settings/dlg_settings.lua b/builtin/mainmenu/settings/dlg_settings.lua index e953c76884..9e5d2a46c7 100644 --- a/builtin/mainmenu/settings/dlg_settings.lua +++ b/builtin/mainmenu/settings/dlg_settings.lua @@ -608,6 +608,16 @@ local function get_formspec(dialogdata) end +-- On Android, closing the app via the "Recents screen" won't result in a clean +-- exit, discarding any setting changes made by the user. +-- To avoid that, we write the settings file in more cases on Android. +function write_settings_early() + if PLATFORM == "Android" then + core.settings:write() + end +end + + local function buttonhandler(this, fields) local dialogdata = this.data dialogdata.leftscroll = core.explode_scrollbar_event(fields.leftscroll).value or dialogdata.leftscroll @@ -622,12 +632,15 @@ local function buttonhandler(this, fields) if fields.show_technical_names ~= nil then local value = core.is_yes(fields.show_technical_names) core.settings:set_bool("show_technical_names", value) + write_settings_early() + return true end if fields.show_advanced ~= nil then local value = core.is_yes(fields.show_advanced) core.settings:set_bool("show_advanced", value) + write_settings_early() local suggested_page_id = update_filtered_pages(dialogdata.query) @@ -672,12 +685,15 @@ local function buttonhandler(this, fields) for i, comp in ipairs(dialogdata.components) do if comp.on_submit and comp:on_submit(fields, this) then + write_settings_early() + -- Clear components so they regenerate dialogdata.components = nil return true end if comp.setting and fields["reset_" .. i] then core.settings:remove(comp.setting.name) + write_settings_early() -- Clear components so they regenerate dialogdata.components = nil diff --git a/src/client/clientlauncher.cpp b/src/client/clientlauncher.cpp index 93d1810826..ddfb4fea6e 100644 --- a/src/client/clientlauncher.cpp +++ b/src/client/clientlauncher.cpp @@ -250,11 +250,8 @@ bool ClientLauncher::run(GameStartData &start_data, const Settings &cmd_args) } // Break out of menu-game loop to shut down cleanly - if (!m_rendering_engine->run() || *kill) { - if (!g_settings_path.empty()) - g_settings->updateConfigFile(g_settings_path.c_str()); + if (!m_rendering_engine->run() || *kill) break; - } m_rendering_engine->get_video_driver()->setTextureCreationFlag( video::ETCF_CREATE_MIP_MAPS, g_settings->getBool("mip_map")); @@ -299,6 +296,16 @@ bool ClientLauncher::run(GameStartData &start_data, const Settings &cmd_args) receiver->m_touchscreengui = NULL; #endif + /* Save the settings when leaving the game. + * This makes sure that setting changes made in-game are persisted even + * in case of a later unclean exit from the mainmenu. + * This is especially useful on Android because closing the app from the + * "Recents screen" results in an unclean exit. + * Caveat: This means that the settings are saved twice when exiting Minetest. + */ + if (!g_settings_path.empty()) + g_settings->updateConfigFile(g_settings_path.c_str()); + // If no main menu, show error and exit if (skip_main_menu) { if (!error_message.empty()) { @@ -562,6 +569,16 @@ void ClientLauncher::main_menu(MainMenuData *menudata) /* leave scene manager in a clean state */ m_rendering_engine->get_scene_manager()->clear(); + + /* Save the settings when leaving the mainmenu. + * This makes sure that setting changes made in the mainmenu are persisted + * even in case of a later unclean exit from the game. + * This is especially useful on Android because closing the app from the + * "Recents screen" results in an unclean exit. + * Caveat: This means that the settings are saved twice when exiting Minetest. + */ + if (!g_settings_path.empty()) + g_settings->updateConfigFile(g_settings_path.c_str()); } void ClientLauncher::speed_tests() diff --git a/src/porting_android.cpp b/src/porting_android.cpp index 43c2cea357..a649751d32 100644 --- a/src/porting_android.cpp +++ b/src/porting_android.cpp @@ -28,6 +28,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "config.h" #include "filesys.h" #include "log.h" +#include "settings.h" #include #include @@ -39,6 +40,12 @@ with this program; if not, write to the Free Software Foundation, Inc., extern int main(int argc, char *argv[]); +extern "C" JNIEXPORT void JNICALL +Java_net_minetest_minetest_GameActivity_saveSettings(JNIEnv* env, jobject /* this */) { + if (!g_settings_path.empty()) + g_settings->updateConfigFile(g_settings_path.c_str()); +} + namespace porting { // used here: void cleanupAndroid(); From 9e3a11534f47bbbaa6fca53fa71550779028e965 Mon Sep 17 00:00:00 2001 From: sfan5 Date: Tue, 23 Jan 2024 22:33:33 +0100 Subject: [PATCH 11/19] Allow fog color to be overriden properly (#14296) --- .../shaders/nodes_shader/opengl_fragment.glsl | 4 +- .../object_shader/opengl_fragment.glsl | 4 +- doc/lua_api.md | 6 +- src/client/clouds.cpp | 6 +- src/client/game.cpp | 41 +++++------- src/client/renderingengine.h | 3 + src/client/sky.h | 17 +++-- src/network/clientpackethandler.cpp | 65 ++++++++++--------- src/script/lua_api/l_object.cpp | 16 +++-- src/server.cpp | 15 ++--- src/skyparams.h | 3 +- 11 files changed, 96 insertions(+), 84 deletions(-) diff --git a/client/shaders/nodes_shader/opengl_fragment.glsl b/client/shaders/nodes_shader/opengl_fragment.glsl index 8990e5bc05..0e75efedf3 100644 --- a/client/shaders/nodes_shader/opengl_fragment.glsl +++ b/client/shaders/nodes_shader/opengl_fragment.glsl @@ -1,7 +1,7 @@ uniform sampler2D baseTexture; uniform vec3 dayLight; -uniform vec4 skyBgColor; +uniform vec4 fogColor; uniform float fogDistance; uniform float fogShadingParameter; uniform vec3 eyePosition; @@ -448,7 +448,7 @@ void main(void) // Note: clarity = (1 - fogginess) float clarity = clamp(fogShadingParameter - fogShadingParameter * length(eyeVec) / fogDistance, 0.0, 1.0); - col = mix(skyBgColor, col, clarity); + col = mix(fogColor, col, clarity); col = vec4(col.rgb, base.a); gl_FragData[0] = col; diff --git a/client/shaders/object_shader/opengl_fragment.glsl b/client/shaders/object_shader/opengl_fragment.glsl index 80c18a1815..61e00485b6 100644 --- a/client/shaders/object_shader/opengl_fragment.glsl +++ b/client/shaders/object_shader/opengl_fragment.glsl @@ -1,7 +1,7 @@ uniform sampler2D baseTexture; uniform vec3 dayLight; -uniform vec4 skyBgColor; +uniform vec4 fogColor; uniform float fogDistance; uniform float fogShadingParameter; uniform vec3 eyePosition; @@ -449,7 +449,7 @@ void main(void) // Note: clarity = (1 - fogginess) float clarity = clamp(fogShadingParameter - fogShadingParameter * length(eyeVec) / fogDistance, 0.0, 1.0); - col = mix(skyBgColor, col, clarity); + col = mix(fogColor, col, clarity); col = vec4(col.rgb, base.a); gl_FragData[0] = col; diff --git a/doc/lua_api.md b/doc/lua_api.md index de67f7656a..d5a25ed9ff 100644 --- a/doc/lua_api.md +++ b/doc/lua_api.md @@ -7892,8 +7892,7 @@ child will follow movement and rotation of that bone. whether `set_sky` accepts this format. Check the legacy format otherwise. * Passing no arguments resets the sky to its default values. * `sky_parameters` is a table with the following optional fields: - * `base_color`: ColorSpec, changes fog in "skybox" and "plain". - (default: `#ffffff`) + * `base_color`: ColorSpec, meaning depends on `type` (default: `#ffffff`) * `body_orbit_tilt`: Float, rotation angle of sun/moon orbit in degrees. By default, orbit is controlled by a client-side setting, and this field is not set. After a value is assigned, it can only be changed to another float value. @@ -7950,6 +7949,9 @@ child will follow movement and rotation of that bone. Any value between [0.0, 0.99] set the fog_start as a fraction of the viewing_range. Any value < 0, resets the behavior to being client-controlled. (default: -1) + * `fog_color`: ColorSpec, override the color of the fog. + Unlike `base_color` above this will apply regardless of the skybox type. + (default: `"#00000000"`, which means no override) * `set_sky(base_color, type, {texture names}, clouds)` * Deprecated. Use `set_sky(sky_parameters)` * `base_color`: ColorSpec, defaults to white diff --git a/src/client/clouds.cpp b/src/client/clouds.cpp index fe243c7156..f07c2bcaff 100644 --- a/src/client/clouds.cpp +++ b/src/client/clouds.cpp @@ -165,9 +165,11 @@ void Clouds::render() 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, + // Set our own fog, unless it was already disabled + if (fog_start < FOG_RANGE_ALL) { + driver->setFog(fog_color, fog_type, cloud_full_radius * 0.5, cloud_full_radius*1.2, fog_density, fog_pixelfog, fog_rangefog); + } // Read noise diff --git a/src/client/game.cpp b/src/client/game.cpp index 54bff75573..d84c493a72 100644 --- a/src/client/game.cpp +++ b/src/client/game.cpp @@ -374,7 +374,7 @@ class GameGlobalShaderConstantSetter : public IShaderConstantSetter bool *m_force_fog_off; f32 *m_fog_range; bool m_fog_enabled; - CachedPixelShaderSetting m_sky_bg_color{"skyBgColor"}; + CachedPixelShaderSetting m_fog_color{"fogColor"}; CachedPixelShaderSetting m_fog_distance{"fogDistance"}; CachedPixelShaderSetting m_fog_shading_parameter{"fogShadingParameter"}; @@ -475,20 +475,13 @@ public: void onSetConstants(video::IMaterialRendererServices *services) override { - // Background color - video::SColor bgcolor = m_sky->getBgColor(); - video::SColorf bgcolorf(bgcolor); - float bgcolorfa[4] = { - bgcolorf.r, - bgcolorf.g, - bgcolorf.b, - bgcolorf.a, + video::SColorf fogcolorf(m_sky->getFogColor()); + float fogcolorfa[4] = { + fogcolorf.r, fogcolorf.g, fogcolorf.b, fogcolorf.a, }; - m_sky_bg_color.set(bgcolorfa, services); + m_fog_color.set(fogcolorfa, services); - // Fog distance float fog_distance = 10000 * BS; - if (m_fog_enabled && !*m_force_fog_off) fog_distance = *m_fog_range; @@ -983,7 +976,6 @@ private: bool *kill; std::string *error_message; bool *reconnect_requested; - scene::ISceneNode *skybox; PausedNodesList paused_animated_nodes; bool simple_singleplayer_mode; @@ -1522,7 +1514,6 @@ bool Game::createClient(const GameStartData &start_data) */ sky = new Sky(-1, m_rendering_engine, texture_src, shader_src); scsf->setSky(sky); - skybox = NULL; // This is used/set later on in the main run loop /* Pre-calculated values */ @@ -3045,10 +3036,6 @@ void Game::handleClientEvent_SetSky(ClientEvent *event, CameraOrientation *cam) // Whether clouds are visible in front of a custom skybox. sky->setCloudsEnabled(event->set_sky->clouds); - if (skybox) { - skybox->remove(); - skybox = NULL; - } // Clear the old textures out in case we switch rendering type. sky->clearSkyboxTextures(); // Handle according to type @@ -3108,6 +3095,8 @@ void Game::handleClientEvent_SetSky(ClientEvent *event, CameraOrientation *cam) else sky->setFogStart(rangelim(g_settings->getFloat("fog_start"), 0.0f, 0.99f)); + sky->setFogColor(event->set_sky->fog_color); + delete event->set_sky; } @@ -4055,7 +4044,7 @@ void Game::updateFrame(ProfilerGraph *graph, RunStats *stats, f32 dtime, draw_control->wanted_range = MYMIN(draw_control->wanted_range, sky->getFogDistance()); } if (draw_control->range_all && sky->getFogDistance() < 0) { - runData.fog_range = 100000 * BS; + runData.fog_range = FOG_RANGE_ALL; } else { runData.fog_range = draw_control->wanted_range * BS; } @@ -4297,7 +4286,7 @@ void Game::updateShadows() void Game::drawScene(ProfilerGraph *graph, RunStats *stats) { - const video::SColor bg_color = this->sky->getBgColor(); + const video::SColor fog_color = this->sky->getFogColor(); const video::SColor sky_color = this->sky->getSkyColor(); /* @@ -4305,21 +4294,21 @@ void Game::drawScene(ProfilerGraph *graph, RunStats *stats) */ if (this->m_cache_enable_fog) { this->driver->setFog( - bg_color, + fog_color, video::EFT_FOG_LINEAR, this->runData.fog_range * this->sky->getFogStart(), this->runData.fog_range * 1.0f, - 0.01f, + 0.f, // unused false, // pixel fog true // range fog ); } else { this->driver->setFog( - bg_color, + fog_color, video::EFT_FOG_LINEAR, - 100000 * BS, - 110000 * BS, - 0.01f, + FOG_RANGE_ALL, + FOG_RANGE_ALL + 100 * BS, + 0.f, // unused false, // pixel fog false // range fog ); diff --git a/src/client/renderingengine.h b/src/client/renderingengine.h index 801649ad43..06c3157d56 100644 --- a/src/client/renderingengine.h +++ b/src/client/renderingengine.h @@ -43,6 +43,9 @@ class Minimap; class RenderingCore; +// Instead of a mechanism to disable fog we just set it to be really far away +#define FOG_RANGE_ALL (100000 * BS) + class RenderingEngine { public: diff --git a/src/client/sky.h b/src/client/sky.h index b76a5311b2..c9064c0382 100644 --- a/src/client/sky.h +++ b/src/client/sky.h @@ -54,12 +54,12 @@ public: float getBrightness() { return m_brightness; } - const video::SColor &getBgColor() const + video::SColor getBgColor() const { return m_visible ? m_bgcolor : m_fallback_bg_color; } - const video::SColor &getSkyColor() const + video::SColor getSkyColor() const { return m_visible ? m_skycolor : m_fallback_bg_color; } @@ -90,6 +90,7 @@ public: 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(video::SColor fallback_bg_color) @@ -111,17 +112,23 @@ public: const std::string &use_sun_tint); void setInClouds(bool clouds) { m_in_clouds = clouds; } void clearSkyboxTextures() { m_sky_params.textures.clear(); } - void addTextureToSkybox(const std::string &texture, int material_id, + void addTextureToSkybox(const std::string &texture, int material_id, ITextureSource *tsrc); const video::SColorf &getCurrentStarColor() const { return m_star_color; } + + // Note: the Sky class doesn't use these values. It just stores them. void setFogDistance(s16 fog_distance) { m_sky_params.fog_distance = fog_distance; } s16 getFogDistance() const { return m_sky_params.fog_distance; } void setFogStart(float fog_start) { m_sky_params.fog_start = fog_start; } float getFogStart() const { return m_sky_params.fog_start; } - void setVolumetricLightStrength(float volumetric_light_strength) { m_sky_params.volumetric_light_strength = volumetric_light_strength; } - float getVolumetricLightStrength() const { return m_sky_params.volumetric_light_strength; } + void setFogColor(video::SColor v) { m_sky_params.fog_color = v; } + video::SColor getFogColor() const { + if (m_sky_params.fog_color.getAlpha() > 0) + return m_sky_params.fog_color; + return getBgColor(); + } private: aabb3f m_box; diff --git a/src/network/clientpackethandler.cpp b/src/network/clientpackethandler.cpp index 89ee9c8010..d28ff66ef6 100644 --- a/src/network/clientpackethandler.cpp +++ b/src/network/clientpackethandler.cpp @@ -1391,41 +1391,44 @@ void Client::handleCommand_HudSetSky(NetworkPacket* pkt) star_event->type = CE_SET_STARS; star_event->star_params = new StarParams(stars); m_client_event_queue.push(star_event); - } else { - SkyboxParams skybox; + return; + } + + SkyboxParams skybox; + + *pkt >> skybox.bgcolor >> skybox.type >> skybox.clouds >> + skybox.fog_sun_tint >> skybox.fog_moon_tint >> skybox.fog_tint_type; + + if (skybox.type == "skybox") { u16 texture_count; std::string texture; - - *pkt >> skybox.bgcolor >> skybox.type >> skybox.clouds >> - skybox.fog_sun_tint >> skybox.fog_moon_tint >> skybox.fog_tint_type; - - if (skybox.type == "skybox") { - *pkt >> texture_count; - for (int i = 0; i < texture_count; i++) { - *pkt >> texture; - skybox.textures.emplace_back(texture); - } + *pkt >> texture_count; + for (u16 i = 0; i < texture_count; i++) { + *pkt >> texture; + skybox.textures.emplace_back(texture); } - else if (skybox.type == "regular") { - *pkt >> skybox.sky_color.day_sky >> skybox.sky_color.day_horizon - >> skybox.sky_color.dawn_sky >> skybox.sky_color.dawn_horizon - >> skybox.sky_color.night_sky >> skybox.sky_color.night_horizon - >> skybox.sky_color.indoors; - } - - if (pkt->getRemainingBytes() >= 4) { - *pkt >> skybox.body_orbit_tilt; - } - - if (pkt->getRemainingBytes() >= 6) { - *pkt >> skybox.fog_distance >> skybox.fog_start; - } - - ClientEvent *event = new ClientEvent(); - event->type = CE_SET_SKY; - event->set_sky = new SkyboxParams(skybox); - m_client_event_queue.push(event); + } else if (skybox.type == "regular") { + auto &c = skybox.sky_color; + *pkt >> c.day_sky >> c.day_horizon >> c.dawn_sky >> c.dawn_horizon + >> c.night_sky >> c.night_horizon >> c.indoors; } + + if (pkt->getRemainingBytes() >= 4) { + *pkt >> skybox.body_orbit_tilt; + } + + if (pkt->getRemainingBytes() >= 6) { + *pkt >> skybox.fog_distance >> skybox.fog_start; + } + + if (pkt->getRemainingBytes() >= 4) { + *pkt >> skybox.fog_color; + } + + ClientEvent *event = new ClientEvent(); + event->type = CE_SET_SKY; + event->set_sky = new SkyboxParams(skybox); + m_client_event_queue.push(event); } void Client::handleCommand_HudSetSun(NetworkPacket *pkt) diff --git a/src/script/lua_api/l_object.cpp b/src/script/lua_api/l_object.cpp index 3d89bc2798..764cf87fe1 100644 --- a/src/script/lua_api/l_object.cpp +++ b/src/script/lua_api/l_object.cpp @@ -1995,15 +1995,21 @@ int ObjectRef::l_set_sky(lua_State *L) if (!lua_isnil(L, -1)) sky_params.fog_tint_type = luaL_checkstring(L, -1); lua_pop(L, 1); - - // pop "sky_color" table - lua_pop(L, 1); } + lua_pop(L, 1); + lua_getfield(L, 2, "fog"); if (lua_istable(L, -1)) { - sky_params.fog_distance = getintfield_default(L, -1, "fog_distance", sky_params.fog_distance); - sky_params.fog_start = getfloatfield_default(L, -1, "fog_start", sky_params.fog_start); + sky_params.fog_distance = getintfield_default(L, -1, + "fog_distance", sky_params.fog_distance); + sky_params.fog_start = getfloatfield_default(L, -1, + "fog_start", sky_params.fog_start); + + lua_getfield(L, -1, "fog_color"); + read_color(L, -1, &sky_params.fog_color); + lua_pop(L, 1); } + lua_pop(L, 1); } else { // Handle old set_sky calls, and log deprecated: log_deprecated(L, "Deprecated call to set_sky, please check lua_api.md"); diff --git a/src/server.cpp b/src/server.cpp index 4c0788e8d1..c307532465 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -1826,22 +1826,21 @@ void Server::SendSetSky(session_t peer_id, const SkyboxParams ¶ms) pkt << params.clouds; } else { // Handle current clients and future clients pkt << params.bgcolor << params.type - << params.clouds << params.fog_sun_tint - << params.fog_moon_tint << params.fog_tint_type; + << params.clouds << params.fog_sun_tint + << params.fog_moon_tint << params.fog_tint_type; if (params.type == "skybox") { pkt << (u16) params.textures.size(); for (const std::string &texture : params.textures) pkt << texture; } else if (params.type == "regular") { - pkt << params.sky_color.day_sky << params.sky_color.day_horizon - << params.sky_color.dawn_sky << params.sky_color.dawn_horizon - << params.sky_color.night_sky << params.sky_color.night_horizon - << params.sky_color.indoors; + auto &c = params.sky_color; + pkt << c.day_sky << c.day_horizon << c.dawn_sky << c.dawn_horizon + << c.night_sky << c.night_horizon << c.indoors; } - pkt << params.body_orbit_tilt; - pkt << params.fog_distance << params.fog_start; + pkt << params.body_orbit_tilt << params.fog_distance << params.fog_start + << params.fog_color; } Send(&pkt); diff --git a/src/skyparams.h b/src/skyparams.h index ff9a921ae9..a4d0fadac1 100644 --- a/src/skyparams.h +++ b/src/skyparams.h @@ -46,7 +46,7 @@ struct SkyboxParams float body_orbit_tilt { INVALID_SKYBOX_TILT }; s16 fog_distance { -1 }; float fog_start { -1.0f }; - float volumetric_light_strength { 0.0f }; + video::SColor fog_color; // override, only used if alpha > 0 }; struct SunParams @@ -102,6 +102,7 @@ public: sky.fog_sun_tint = video::SColor(255, 244, 125, 29); sky.fog_moon_tint = video::SColorf(0.5, 0.6, 0.8, 1).toSColor(); sky.fog_tint_type = "default"; + sky.fog_color = video::SColor(0); return sky; } From be7844192b26770d538be0103ec4e43d7ebb33d3 Mon Sep 17 00:00:00 2001 From: sfan5 Date: Fri, 19 Jan 2024 16:02:26 +0100 Subject: [PATCH 12/19] Don't save blocks asap for only lighting updates --- src/mapblock.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mapblock.h b/src/mapblock.h index 73aeaade7a..c3b508e116 100644 --- a/src/mapblock.h +++ b/src/mapblock.h @@ -155,7 +155,7 @@ public: { if (newflags != m_lighting_complete) { m_lighting_complete = newflags; - raiseModified(MOD_STATE_WRITE_NEEDED, MOD_REASON_SET_LIGHTING_COMPLETE); + raiseModified(MOD_STATE_WRITE_AT_UNLOAD, MOD_REASON_SET_LIGHTING_COMPLETE); } } From 6aa4f14a28b45de1f8f23c36ac4b427711e16df0 Mon Sep 17 00:00:00 2001 From: sfan5 Date: Fri, 19 Jan 2024 16:05:59 +0100 Subject: [PATCH 13/19] Drop some unneeded MOD_REASONs --- src/mapblock.cpp | 5 ++--- src/mapblock.h | 48 ++++++++++++++++++++++++------------------------ 2 files changed, 26 insertions(+), 27 deletions(-) diff --git a/src/mapblock.cpp b/src/mapblock.cpp index ef9b47b9e8..c48d3defe8 100644 --- a/src/mapblock.cpp +++ b/src/mapblock.cpp @@ -40,13 +40,11 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "util/basic_macros.h" static const char *modified_reason_strings[] = { - "initial", - "reallocate", + "reallocate or initial", "setIsUnderground", "setLightingExpired", "setGenerated", "setNode", - "setNodeNoCheck", "setTimestamp", "NodeMetaRef::reportMetadataChange", "clearAllObjects", @@ -73,6 +71,7 @@ MapBlock::MapBlock(v3s16 pos, IGameDef *gamedef): m_gamedef(gamedef) { reallocate(); + assert(m_modified > MOD_STATE_CLEAN); } MapBlock::~MapBlock() diff --git a/src/mapblock.h b/src/mapblock.h index c3b508e116..c888184984 100644 --- a/src/mapblock.h +++ b/src/mapblock.h @@ -43,27 +43,27 @@ class VoxelManipulator; //// MapBlock modified reason flags //// -#define MOD_REASON_INITIAL (1 << 0) -#define MOD_REASON_REALLOCATE (1 << 1) -#define MOD_REASON_SET_IS_UNDERGROUND (1 << 2) -#define MOD_REASON_SET_LIGHTING_COMPLETE (1 << 3) -#define MOD_REASON_SET_GENERATED (1 << 4) -#define MOD_REASON_SET_NODE (1 << 5) -#define MOD_REASON_SET_NODE_NO_CHECK (1 << 6) -#define MOD_REASON_SET_TIMESTAMP (1 << 7) -#define MOD_REASON_REPORT_META_CHANGE (1 << 8) -#define MOD_REASON_CLEAR_ALL_OBJECTS (1 << 9) -#define MOD_REASON_BLOCK_EXPIRED (1 << 10) -#define MOD_REASON_ADD_ACTIVE_OBJECT_RAW (1 << 11) -#define MOD_REASON_REMOVE_OBJECTS_REMOVE (1 << 12) -#define MOD_REASON_REMOVE_OBJECTS_DEACTIVATE (1 << 13) -#define MOD_REASON_TOO_MANY_OBJECTS (1 << 14) -#define MOD_REASON_STATIC_DATA_ADDED (1 << 15) -#define MOD_REASON_STATIC_DATA_REMOVED (1 << 16) -#define MOD_REASON_STATIC_DATA_CHANGED (1 << 17) -#define MOD_REASON_EXPIRE_DAYNIGHTDIFF (1 << 18) -#define MOD_REASON_VMANIP (1 << 19) -#define MOD_REASON_UNKNOWN (1 << 20) +enum ModReason : u32 { + MOD_REASON_REALLOCATE = 1 << 0, + MOD_REASON_SET_IS_UNDERGROUND = 1 << 1, + MOD_REASON_SET_LIGHTING_COMPLETE = 1 << 2, + MOD_REASON_SET_GENERATED = 1 << 3, + MOD_REASON_SET_NODE = 1 << 4, + MOD_REASON_SET_TIMESTAMP = 1 << 5, + MOD_REASON_REPORT_META_CHANGE = 1 << 6, + MOD_REASON_CLEAR_ALL_OBJECTS = 1 << 7, + MOD_REASON_BLOCK_EXPIRED = 1 << 8, + MOD_REASON_ADD_ACTIVE_OBJECT_RAW = 1 << 9, + MOD_REASON_REMOVE_OBJECTS_REMOVE = 1 << 10, + MOD_REASON_REMOVE_OBJECTS_DEACTIVATE = 1 << 11, + MOD_REASON_TOO_MANY_OBJECTS = 1 << 12, + MOD_REASON_STATIC_DATA_ADDED = 1 << 13, + MOD_REASON_STATIC_DATA_REMOVED = 1 << 14, + MOD_REASON_STATIC_DATA_CHANGED = 1 << 15, + MOD_REASON_EXPIRE_DAYNIGHTDIFF = 1 << 16, + MOD_REASON_VMANIP = 1 << 17, + MOD_REASON_UNKNOWN = 1 << 18, +}; //// //// MapBlock itself @@ -296,7 +296,7 @@ public: inline void setNodeNoCheck(s16 x, s16 y, s16 z, MapNode n) { data[z * zstride + y * ystride + x] = n; - raiseModified(MOD_STATE_WRITE_NEEDED, MOD_REASON_SET_NODE_NO_CHECK); + raiseModified(MOD_STATE_WRITE_NEEDED, MOD_REASON_SET_NODE); } inline void setNodeNoCheck(v3s16 p, MapNode n) @@ -525,8 +525,8 @@ private: block has been modified from the one on disk. - On the client, this is used for nothing. */ - u16 m_modified = MOD_STATE_WRITE_NEEDED; - u32 m_modified_reason = MOD_REASON_INITIAL; + u16 m_modified = MOD_STATE_CLEAN; + u32 m_modified_reason = 0; /* When block is removed from active blocks, this is set to gametime. From 89eabb5803e16b4fd4ac046a75abafb855949097 Mon Sep 17 00:00:00 2001 From: sfan5 Date: Fri, 19 Jan 2024 16:21:33 +0100 Subject: [PATCH 14/19] Drop speed tests and some other unused code from clientlauncher.cpp --- src/client/clientlauncher.cpp | 122 +--------------------------------- src/client/clientlauncher.h | 2 - src/main.cpp | 2 - 3 files changed, 2 insertions(+), 124 deletions(-) diff --git a/src/client/clientlauncher.cpp b/src/client/clientlauncher.cpp index ddfb4fea6e..eb4362348a 100644 --- a/src/client/clientlauncher.cpp +++ b/src/client/clientlauncher.cpp @@ -108,13 +108,6 @@ bool ClientLauncher::run(GameStartData &start_data, const Settings &cmd_args) return false; } - // Speed tests (done after irrlicht is loaded to get timer) - if (cmd_args.getFlag("speedtests")) { - dstream << "Running speed tests" << std::endl; - speed_tests(); - return true; - } - if (m_rendering_engine->get_video_driver() == NULL) { errorstream << "Could not initialize video driver." << std::endl; return false; @@ -394,7 +387,7 @@ bool ClientLauncher::launch_game(std::string &error_message, if (cmd_args.exists("password-file")) { std::ifstream passfile(cmd_args.get("password-file")); if (passfile.good()) { - getline(passfile, start_data.password); + std::getline(passfile, start_data.password); } else { error_message = gettext("Provided password file " "failed to open: ") @@ -451,8 +444,6 @@ bool ClientLauncher::launch_game(std::string &error_message, int world_index = menudata.selected_world; if (world_index >= 0 && world_index < (int)worldspecs.size()) { - g_settings->set("selected_world_path", - worldspecs[world_index].path); start_data.world_spec = worldspecs[world_index]; } @@ -524,15 +515,7 @@ bool ClientLauncher::launch_game(std::string &error_message, return false; } - if (porting::signal_handler_killstatus()) - return true; - - if (!start_data.game_spec.isValid()) { - error_message = gettext("Invalid gamespec."); - error_message += " (world.gameid=" + worldspec.gameid + ")"; - errorstream << error_message << std::endl; - return false; - } + return true; } start_data.world_path = start_data.world_spec.path; @@ -580,104 +563,3 @@ void ClientLauncher::main_menu(MainMenuData *menudata) if (!g_settings_path.empty()) g_settings->updateConfigFile(g_settings_path.c_str()); } - -void ClientLauncher::speed_tests() -{ - // volatile to avoid some potential compiler optimisations - volatile static s16 temp16; - volatile static f32 tempf; - // Silence compiler warning - (void)temp16; - static v3f tempv3f1; - static v3f tempv3f2; - static std::string tempstring; - static std::string tempstring2; - - tempv3f1 = v3f(); - tempv3f2 = v3f(); - tempstring.clear(); - tempstring2.clear(); - - { - infostream << "The following test should take around 20ms." << std::endl; - TimeTaker timer("Testing std::string speed"); - const u32 jj = 10000; - for (u32 j = 0; j < jj; j++) { - tempstring.clear(); - tempstring2.clear(); - const u32 ii = 10; - for (u32 i = 0; i < ii; i++) { - tempstring2 += "asd"; - } - for (u32 i = 0; i < ii+1; i++) { - tempstring += "asd"; - if (tempstring == tempstring2) - break; - } - } - } - - infostream << "All of the following tests should take around 100ms each." - << std::endl; - - { - TimeTaker timer("Testing floating-point conversion speed"); - tempf = 0.001; - for (u32 i = 0; i < 4000000; i++) { - temp16 += tempf; - tempf += 0.001; - } - } - - { - TimeTaker timer("Testing floating-point vector speed"); - - tempv3f1 = v3f(1, 2, 3); - tempv3f2 = v3f(4, 5, 6); - for (u32 i = 0; i < 10000000; i++) { - tempf += tempv3f1.dotProduct(tempv3f2); - tempv3f2 += v3f(7, 8, 9); - } - } - - { - TimeTaker timer("Testing std::map speed"); - - std::map map1; - tempf = -324; - const s16 ii = 300; - for (s16 y = 0; y < ii; y++) { - for (s16 x = 0; x < ii; x++) { - map1[v2s16(x, y)] = tempf; - tempf += 1; - } - } - for (s16 y = ii - 1; y >= 0; y--) { - for (s16 x = 0; x < ii; x++) { - tempf = map1[v2s16(x, y)]; - } - } - } - - { - infostream << "Around 5000/ms should do well here." << std::endl; - TimeTaker timer("Testing mutex speed"); - - std::mutex m; - u32 n = 0; - u32 i = 0; - do { - n += 10000; - for (; i < n; i++) { - m.lock(); - m.unlock(); - } - } - // Do at least 10ms - while(timer.getTimerTime() < 10); - - u32 dtime = timer.stop(); - u32 per_ms = n / dtime; - infostream << "Done. " << dtime << "ms, " << per_ms << "/ms" << std::endl; - } -} diff --git a/src/client/clientlauncher.h b/src/client/clientlauncher.h index d1fd9a2583..395f83b9d5 100644 --- a/src/client/clientlauncher.h +++ b/src/client/clientlauncher.h @@ -44,8 +44,6 @@ private: void main_menu(MainMenuData *menudata); - void speed_tests(); - bool skip_main_menu = false; bool random_input = false; RenderingEngine *m_rendering_engine = nullptr; diff --git a/src/main.cpp b/src/main.cpp index 6580613bae..d5d7269f1c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -379,8 +379,6 @@ static void set_allowed_options(OptionList *allowed_options) allowed_options->insert(std::make_pair("recompress", ValueSpec(VALUETYPE_FLAG, _("Recompress the blocks of the given map database.")))); #ifndef SERVER - allowed_options->insert(std::make_pair("speedtests", ValueSpec(VALUETYPE_FLAG, - _("Run speed tests")))); allowed_options->insert(std::make_pair("address", ValueSpec(VALUETYPE_STRING, _("Address to connect to. ('' = local game)")))); allowed_options->insert(std::make_pair("random-input", ValueSpec(VALUETYPE_FLAG, From 6df0de565fb08380827abc59a78d7feb03348e61 Mon Sep 17 00:00:00 2001 From: sfan5 Date: Fri, 19 Jan 2024 20:08:55 +0100 Subject: [PATCH 15/19] Check media requests on the server more carefully --- src/clientiface.h | 16 ++++++++---- src/network/serverpackethandler.cpp | 12 ++++----- src/server.cpp | 40 ++++++++++++++++++++--------- src/server.h | 2 +- 4 files changed, 46 insertions(+), 24 deletions(-) diff --git a/src/clientiface.h b/src/clientiface.h index 138e7fc796..1df67b5680 100644 --- a/src/clientiface.h +++ b/src/clientiface.h @@ -284,10 +284,10 @@ public: return m_blocks_sent.find(p) != m_blocks_sent.end(); } - // Increments timeouts and removes timed-out blocks from list - // NOTE: This doesn't fix the server-not-sending-block bug - // because it is related to emerging, not sending. - //void RunSendingTimeouts(float dtime, float timeout); + bool markMediaSent(const std::string &name) { + auto insert_result = m_media_sent.emplace(name); + return insert_result.second; // true = was inserted + } void PrintInfo(std::ostream &o) { @@ -310,7 +310,7 @@ public: ClientState getState() const { return m_state; } - std::string getName() const { return m_name; } + const std::string &getName() const { return m_name; } void setName(const std::string &name) { m_name = name; } @@ -394,6 +394,12 @@ private: const s16 m_max_gen_distance; const bool m_occ_cull; + /* + Set of media files the client has already requested + We won't send the same file twice to avoid bandwidth consumption attacks. + */ + std::unordered_set m_media_sent; + /* Blocks that are currently on the line. This is used for throttling the sending of blocks. diff --git a/src/network/serverpackethandler.cpp b/src/network/serverpackethandler.cpp index 934afb0559..253dcdba8d 100644 --- a/src/network/serverpackethandler.cpp +++ b/src/network/serverpackethandler.cpp @@ -356,24 +356,24 @@ void Server::handleCommand_Init2(NetworkPacket* pkt) void Server::handleCommand_RequestMedia(NetworkPacket* pkt) { - std::vector tosend; + std::unordered_set tosend; u16 numfiles; *pkt >> numfiles; session_t peer_id = pkt->getPeerId(); - infostream << "Sending " << numfiles << " files to " << - getPlayerName(peer_id) << std::endl; - verbosestream << "TOSERVER_REQUEST_MEDIA: requested file(s)" << std::endl; + verbosestream << "Client " << getPlayerName(peer_id) + << " requested media file(s):\n"; for (u16 i = 0; i < numfiles; i++) { std::string name; *pkt >> name; - tosend.emplace_back(name); - verbosestream << " " << name << std::endl; + tosend.emplace(name); + verbosestream << " " << name << "\n"; } + verbosestream << std::flush; sendRequestedMedia(peer_id, tosend); } diff --git a/src/server.cpp b/src/server.cpp index c307532465..f71d8d31de 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -137,6 +137,7 @@ void *ServerThread::run() } catch (con::PeerNotFoundException &e) { infostream<<"Server: PeerNotFoundException"<setAsyncFatalError(e.what()); } catch (LuaError &e) { @@ -2671,29 +2672,44 @@ struct SendableMedia }; void Server::sendRequestedMedia(session_t peer_id, - const std::vector &tosend) + const std::unordered_set &tosend) { - verbosestream<<"Server::sendRequestedMedia(): " - <<"Sending files to client"<getName() << std::endl; /* Read files */ // Put 5kB in one bunch (this is not accurate) - u32 bytes_per_bunch = 5000; + const u32 bytes_per_bunch = 5000; - std::vector< std::vector > file_bunches; + std::vector> file_bunches; file_bunches.emplace_back(); u32 file_size_bunch_total = 0; for (const std::string &name : tosend) { - if (m_media.find(name) == m_media.end()) { + auto it = m_media.find(name); + + if (it == m_media.end()) { errorstream<<"Server::sendRequestedMedia(): Client asked for " <<"unknown file \""<<(name)<<"\""<second; - const auto &m = m_media[name]; + // no_announce <=> usually ephemeral dynamic media, which may + // have duplicate filenames. So we can't check it. + if (!m.no_announce) { + if (!client->markMediaSent(name)) { + infostream << "Server::sendRequestedMedia(): Client asked has " + "requested \"" << name << "\" before, not sending it again." + << std::endl; + continue; + } + } // Read data std::string data; @@ -2717,11 +2733,11 @@ void Server::sendRequestedMedia(session_t peer_id, /* Create and send packets */ - u16 num_bunches = file_bunches.size(); + const u16 num_bunches = file_bunches.size(); for (u16 i = 0; i < num_bunches; i++) { + auto &bunch = file_bunches[i]; /* - u16 command - u16 total number of texture bunches + u16 total number of media bunches u16 index of this bunch u32 number of files in this bunch for each file { @@ -2735,14 +2751,14 @@ void Server::sendRequestedMedia(session_t peer_id, NetworkPacket pkt(TOCLIENT_MEDIA, 4 + 0, peer_id); pkt << num_bunches << i << (u32) file_bunches[i].size(); - for (const SendableMedia &j : file_bunches[i]) { + for (auto &j : bunch) { pkt << j.name; pkt.putLongString(j.data); } verbosestream << "Server::sendRequestedMedia(): bunch " << i << "/" << num_bunches - << " files=" << file_bunches[i].size() + << " files=" << bunch.size() << " size=" << pkt.getSize() << std::endl; Send(&pkt); } diff --git a/src/server.h b/src/server.h index bec2fe1cfd..2915c88cd3 100644 --- a/src/server.h +++ b/src/server.h @@ -515,7 +515,7 @@ private: void fillMediaCache(); void sendMediaAnnouncement(session_t peer_id, const std::string &lang_code); void sendRequestedMedia(session_t peer_id, - const std::vector &tosend); + const std::unordered_set &tosend); void stepPendingDynMediaCallbacks(float dtime); // Adds a ParticleSpawner on peer with peer_id (PEER_ID_INEXISTENT == all) From 13013d1b8b3faba518806400f8261d24d4020c26 Mon Sep 17 00:00:00 2001 From: sfan5 Date: Fri, 19 Jan 2024 20:14:54 +0100 Subject: [PATCH 16/19] Fix client loading not aborting correctly in certain cases --- src/client/game.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/client/game.cpp b/src/client/game.cpp index d84c493a72..1e62b4b7c4 100644 --- a/src/client/game.cpp +++ b/src/client/game.cpp @@ -1743,7 +1743,7 @@ bool Game::getServerContent(bool *aborted) // End condition if (client->mediaReceived() && client->itemdefReceived() && client->nodedefReceived()) { - break; + return true; } // Error conditions @@ -1802,7 +1802,9 @@ bool Game::getServerContent(bool *aborted) } } - return true; + *aborted = true; + infostream << "Connect aborted [device]" << std::endl; + return false; } From 362e4505e8798a6ed47b3a1d568873a3200463ec Mon Sep 17 00:00:00 2001 From: sfan5 Date: Sat, 20 Jan 2024 13:24:54 +0100 Subject: [PATCH 17/19] Minor improvements to media request / announce code I had to throw away the code switching sendRequestedMedia to a bin packing algorithm because it actually performed worse. :( --- src/server.cpp | 57 +++++++++++++++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 22 deletions(-) diff --git a/src/server.cpp b/src/server.cpp index f71d8d31de..3437d17f05 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -2628,28 +2628,30 @@ void Server::fillMediaCache() void Server::sendMediaAnnouncement(session_t peer_id, const std::string &lang_code) { + std::string lang_suffix = "."; + lang_suffix.append(lang_code).append(".tr"); + + auto include = [&] (const std::string &name, const MediaInfo &info) -> bool { + if (info.no_announce) + return false; + if (str_ends_with(name, ".tr") && !str_ends_with(name, lang_suffix)) + return false; + return true; + }; + // Make packet NetworkPacket pkt(TOCLIENT_ANNOUNCE_MEDIA, 0, peer_id); u16 media_sent = 0; - std::string lang_suffix; - lang_suffix.append(".").append(lang_code).append(".tr"); for (const auto &i : m_media) { - if (i.second.no_announce) - continue; - if (str_ends_with(i.first, ".tr") && !str_ends_with(i.first, lang_suffix)) - continue; - media_sent++; + if (include(i.first, i.second)) + media_sent++; } - pkt << media_sent; for (const auto &i : m_media) { - if (i.second.no_announce) - continue; - if (str_ends_with(i.first, ".tr") && !str_ends_with(i.first, lang_suffix)) - continue; - pkt << i.first << i.second.sha1_digest; + if (include(i.first, i.second)) + pkt << i.first << i.second.sha1_digest; } pkt << g_settings->get("remote_media"); @@ -2659,10 +2661,12 @@ void Server::sendMediaAnnouncement(session_t peer_id, const std::string &lang_co << "): count=" << media_sent << " size=" << pkt.getSize() << std::endl; } +namespace { + struct SendableMedia { - std::string name; - std::string path; + const std::string &name; + const std::string &path; std::string data; SendableMedia(const std::string &name, const std::string &path, @@ -2671,6 +2675,8 @@ struct SendableMedia {} }; +} + void Server::sendRequestedMedia(session_t peer_id, const std::unordered_set &tosend) { @@ -2680,16 +2686,22 @@ void Server::sendRequestedMedia(session_t peer_id, infostream << "Server::sendRequestedMedia(): Sending " << tosend.size() << " files to " << client->getName() << std::endl; - /* Read files */ + /* Read files and prepare bunches */ - // Put 5kB in one bunch (this is not accurate) + // Put 5KB in one bunch (this is not accurate) + // This is a tradeoff between burdening the network with too many packets + // and burdening it with too large split packets. const u32 bytes_per_bunch = 5000; std::vector> file_bunches; file_bunches.emplace_back(); - u32 file_size_bunch_total = 0; + // Note that applying a "real" bin packing algorithm here is not necessarily + // an improvement (might even perform worse) since games usually have lots + // of files larger than 5KB and the current algorithm already minimized + // the amount of bunches quite well (at the expense of overshooting). + u32 file_size_bunch_total = 0; for (const std::string &name : tosend) { auto it = m_media.find(name); @@ -2728,7 +2740,6 @@ void Server::sendRequestedMedia(session_t peer_id, file_bunches.emplace_back(); file_size_bunch_total = 0; } - } /* Create and send packets */ @@ -2747,18 +2758,20 @@ void Server::sendRequestedMedia(session_t peer_id, data } */ - NetworkPacket pkt(TOCLIENT_MEDIA, 4 + 0, peer_id); - pkt << num_bunches << i << (u32) file_bunches[i].size(); + + const u32 bunch_size = bunch.size(); + pkt << num_bunches << i << bunch_size; for (auto &j : bunch) { pkt << j.name; pkt.putLongString(j.data); } + bunch.clear(); // free memory early verbosestream << "Server::sendRequestedMedia(): bunch " << i << "/" << num_bunches - << " files=" << bunch.size() + << " files=" << bunch_size << " size=" << pkt.getSize() << std::endl; Send(&pkt); } From 731b84d72547ef2c00a7e3e5bd318b55bb619b0e Mon Sep 17 00:00:00 2001 From: sfan5 Date: Tue, 23 Jan 2024 21:32:36 +0100 Subject: [PATCH 18/19] Reduce some instances of useless data shuffling --- src/clientiface.cpp | 9 ++++----- src/clientiface.h | 6 +++--- src/server.cpp | 20 ++++---------------- 3 files changed, 11 insertions(+), 24 deletions(-) diff --git a/src/clientiface.cpp b/src/clientiface.cpp index c2d245f679..f76ef0a882 100644 --- a/src/clientiface.cpp +++ b/src/clientiface.cpp @@ -480,12 +480,11 @@ void RemoteClient::SetBlockNotSent(v3s16 p) m_blocks_modified.insert(p); } -void RemoteClient::SetBlocksNotSent(std::map &blocks) +void RemoteClient::SetBlocksNotSent(const std::vector &blocks) { m_nothing_to_send_pause_timer = 0; - for (auto &block : blocks) { - v3s16 p = block.first; + for (v3s16 p : blocks) { // remove the block from sending and sent sets, // and mark as modified if found if (m_blocks_sending.erase(p) + m_blocks_sent.erase(p) > 0) @@ -707,12 +706,12 @@ std::vector ClientInterface::getClientIDs(ClientState min_state) return reply; } -void ClientInterface::markBlockposAsNotSent(const v3s16 &pos) +void ClientInterface::markBlocksNotSent(const std::vector &positions) { RecursiveMutexAutoLock clientslock(m_clients_mutex); for (const auto &client : m_clients) { if (client.second->getState() >= CS_Active) - client.second->SetBlockNotSent(pos); + client.second->SetBlocksNotSent(positions); } } diff --git a/src/clientiface.h b/src/clientiface.h index 1df67b5680..c5a93576cc 100644 --- a/src/clientiface.h +++ b/src/clientiface.h @@ -267,7 +267,7 @@ public: void SentBlock(v3s16 p); void SetBlockNotSent(v3s16 p); - void SetBlocksNotSent(std::map &blocks); + void SetBlocksNotSent(const std::vector &blocks); /** * tell client about this block being modified right now. @@ -473,8 +473,8 @@ public: /* get list of active client id's */ std::vector getClientIDs(ClientState min_state=CS_Active); - /* mark block as not sent to active client sessions */ - void markBlockposAsNotSent(const v3s16 &pos); + /* mark blocks as not sent on all active clients */ + void markBlocksNotSent(const std::vector &positions); /* verify is server user limit was reached */ bool isUserLimitReached(); diff --git a/src/server.cpp b/src/server.cpp index 3437d17f05..26b58245b9 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -950,9 +950,7 @@ void Server::AsyncRunStep(float dtime, bool initial_step) } case MEET_OTHER: prof.add("MEET_OTHER", 1); - for (const v3s16 &modified_block : event->modified_blocks) { - m_clients.markBlockposAsNotSent(modified_block); - } + m_clients.markBlocksNotSent(event->modified_blocks); break; default: prof.add("unknown", 1); @@ -964,19 +962,9 @@ void Server::AsyncRunStep(float dtime, bool initial_step) /* Set blocks not sent to far players */ - if (!far_players.empty()) { - // Convert list format to that wanted by SetBlocksNotSent - std::map modified_blocks2; - for (const v3s16 &modified_block : event->modified_blocks) { - modified_blocks2[modified_block] = - m_env->getMap().getBlockNoCreateNoEx(modified_block); - } - - // Set blocks not sent - for (const u16 far_player : far_players) { - if (RemoteClient *client = getClient(far_player)) - client->SetBlocksNotSent(modified_blocks2); - } + for (const u16 far_player : far_players) { + if (RemoteClient *client = getClient(far_player)) + client->SetBlocksNotSent(event->modified_blocks); } delete event; From 6a2eb4da07aef285bdd1a0f609b98f74f1990835 Mon Sep 17 00:00:00 2001 From: Lars Mueller Date: Tue, 23 Jan 2024 23:19:04 +0100 Subject: [PATCH 19/19] Restore pointability backwards compatibility --- src/util/pointabilities.h | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/util/pointabilities.h b/src/util/pointabilities.h index 4acc1ddb76..1ad57143b1 100644 --- a/src/util/pointabilities.h +++ b/src/util/pointabilities.h @@ -26,8 +26,17 @@ with this program; if not, write to the Free Software Foundation, Inc., enum class PointabilityType : u8 { + // Can be pointed through. + // Note: This MUST be the 0-th item in the enum for backwards compat. + // Older Minetest versions send "pointable=false" as "0". + POINTABLE_NOT, + // Is pointable. + // Note: This MUST be the 1-th item in the enum for backwards compat: + // Older Minetest versions send "pointable=true" as "1". POINTABLE, - POINTABLE_NOT, // Can be pointed through. + // Note: Since (u8) 2 is truthy, + // older clients will understand this as "pointable=true", + // which is a reasonable fallback. POINTABLE_BLOCKING, };