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/game/auth.lua b/builtin/game/auth.lua index fa1860d5d4..e45520e276 100644 --- a/builtin/game/auth.lua +++ b/builtin/game/auth.lua @@ -92,16 +92,24 @@ core.builtin_auth_handler = { core_auth.save(auth_entry) - -- Run grant callbacks - for priv, _ in pairs(privileges) do - if not prev_privs[priv] then + 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 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 @@ -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/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/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 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/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); 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/android.md b/doc/android.md index 83772c5c5b..792289bc01 100644 --- a/doc/android.md +++ b/doc/android.md @@ -12,9 +12,8 @@ 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 wielded item (default) +* Long tap: Dig a node or use the wielded item * Press back: Pause menu * Touch buttons: Press button * Buttons: diff --git a/doc/lua_api.md b/doc/lua_api.md index ad29fea419..670ec02903 100644 --- a/doc/lua_api.md +++ b/doc/lua_api.md @@ -5204,6 +5204,10 @@ Minetest includes the following settings to control behavior of privileges: -- 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, } ``` @@ -5739,8 +5743,20 @@ Call these functions only at load time! * `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 @@ -7744,8 +7760,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. @@ -7798,6 +7813,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) > [!WARNING] > The darkening of the ColorSpec is subject to change. * `set_sky(base_color, type, {texture names}, clouds)` @@ -8039,10 +8057,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 @@ -8236,7 +8254,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. @@ -8593,6 +8613,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: + -- 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. -- Otherwise: Defines texture glow when viewed as a dropped item @@ -8634,6 +8675,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. @@ -8803,7 +8858,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 @@ -10535,8 +10594,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. 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 0000000000..fa021041a6 Binary files /dev/null and b/games/devtest/mods/testnodes/textures/testnodes_blocking_pointable.png differ diff --git a/games/devtest/mods/testnodes/textures/testnodes_not_pointable.png b/games/devtest/mods/testnodes/textures/testnodes_not_pointable.png new file mode 100644 index 0000000000..8509a68b7b Binary files /dev/null and b/games/devtest/mods/testnodes/textures/testnodes_not_pointable.png differ diff --git a/games/devtest/mods/testnodes/textures/testnodes_pointable.png b/games/devtest/mods/testnodes/textures/testnodes_pointable.png new file mode 100644 index 0000000000..70c8f36f5b Binary files /dev/null and b/games/devtest/mods/testnodes/textures/testnodes_pointable.png differ diff --git a/games/devtest/mods/testtools/init.lua b/games/devtest/mods/testtools/init.lua index 7aa55bdb30..09280f61f5 100644 --- a/games/devtest/mods/testtools/init.lua +++ b/games/devtest/mods/testtools/init.lua @@ -7,6 +7,20 @@ dofile(minetest.get_modpath("testtools") .. "/light.lua") dofile(minetest.get_modpath("testtools") .. "/privatizer.lua") dofile(minetest.get_modpath("testtools") .. "/particles.lua") +local pointabilities_nodes = { + nodes = { + ["group:blocking_pointable_test"] = true, + ["group:not_pointable_test"] = true, + }, +} + +local pointabilities_objects = { + objects = { + ["group:blocking_pointable_test"] = true, + ["group:not_pointable_test"] = true, + }, +} + minetest.register_tool("testtools:param2tool", { description = S("Param2 Tool") .."\n".. S("Modify param2 value of nodes") .."\n".. @@ -16,6 +30,7 @@ minetest.register_tool("testtools:param2tool", { S("Sneak+Place: -8"), inventory_image = "testtools_param2tool.png", groups = { testtool = 1, disable_repair = 1 }, + pointabilities = pointabilities_nodes, on_use = function(itemstack, user, pointed_thing) local pos = minetest.get_pointed_thing_position(pointed_thing) if pointed_thing.type ~= "node" or (not pos) then @@ -58,6 +73,7 @@ minetest.register_tool("testtools:node_setter", { S("Place in air: Manually select a node"), inventory_image = "testtools_node_setter.png", groups = { testtool = 1, disable_repair = 1 }, + pointabilities = pointabilities_nodes, on_use = function(itemstack, user, pointed_thing) local pos = minetest.get_pointed_thing_position(pointed_thing) if pointed_thing.type == "nothing" then @@ -118,6 +134,10 @@ minetest.register_tool("testtools:remover", { S("Punch: Remove pointed node or object"), inventory_image = "testtools_remover.png", groups = { testtool = 1, disable_repair = 1 }, + pointabilities = { + nodes = pointabilities_nodes.nodes, + objects = pointabilities_objects.objects, + }, on_use = function(itemstack, user, pointed_thing) local pos = minetest.get_pointed_thing_position(pointed_thing) if pointed_thing.type == "node" and pos ~= nil then @@ -139,6 +159,7 @@ minetest.register_tool("testtools:falling_node_tool", { S("Place: Move pointed node 2 units upwards, then make it fall"), inventory_image = "testtools_falling_node_tool.png", groups = { testtool = 1, disable_repair = 1 }, + pointabilities = pointabilities_nodes, on_place = function(itemstack, user, pointed_thing) -- Teleport node 1-2 units upwards (if possible) and make it fall local pos = minetest.get_pointed_thing_position(pointed_thing) @@ -192,6 +213,7 @@ minetest.register_tool("testtools:rotator", { S("Aux1+Punch: Roll"), inventory_image = "testtools_entity_rotator.png", groups = { testtool = 1, disable_repair = 1 }, + pointabilities = pointabilities_objects, on_use = function(itemstack, user, pointed_thing) if pointed_thing.type ~= "object" then return @@ -250,6 +272,7 @@ minetest.register_tool("testtools:object_mover", { S("Sneak+Place: Decrease distance"), inventory_image = "testtools_object_mover.png", groups = { testtool = 1, disable_repair = 1 }, + pointabilities = pointabilities_objects, on_place = mover_config, on_secondary_use = mover_config, on_use = function(itemstack, user, pointed_thing) @@ -296,6 +319,7 @@ minetest.register_tool("testtools:entity_scaler", { S("Sneak+Punch: Decrease scale"), inventory_image = "testtools_entity_scaler.png", groups = { testtool = 1, disable_repair = 1 }, + pointabilities = pointabilities_objects, on_use = function(itemstack, user, pointed_thing) if pointed_thing.type ~= "object" then return @@ -355,6 +379,7 @@ minetest.register_tool("testtools:branding_iron", { S("Devices that accept the returned name also accept \"player:\" 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 0000000000..aa1cdb97aa Binary files /dev/null and b/games/devtest/mods/testtools/textures/testtools_blocked_pointing_staff.png differ 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 0000000000..e4d80fb4d1 Binary files /dev/null and b/games/devtest/mods/testtools/textures/testtools_ultimate_pointing_staff.png differ diff --git a/src/client/clientenvironment.cpp b/src/client/clientenvironment.cpp index 54baf30a1f..b9bb82bdfa 100644 --- a/src/client/clientenvironment.cpp +++ b/src/client/clientenvironment.cpp @@ -489,7 +489,8 @@ ClientEnvEvent ClientEnvironment::getClientEnvEvent() void ClientEnvironment::getSelectedActiveObjects( const core::line3d &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/clientlauncher.cpp b/src/client/clientlauncher.cpp index 93d1810826..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; @@ -250,11 +243,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 +289,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()) { @@ -387,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: ") @@ -444,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]; } @@ -517,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; @@ -562,105 +552,14 @@ void ClientLauncher::main_menu(MainMenuData *menudata) /* leave scene manager in a clean state */ m_rendering_engine->get_scene_manager()->clear(); -} - -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; - } + + /* 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()); } 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/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/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 6916cefdcb..1e62b4b7c4 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; @@ -841,6 +834,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 +842,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, @@ -981,7 +976,6 @@ private: bool *kill; std::string *error_message; bool *reconnect_requested; - scene::ISceneNode *skybox; PausedNodesList paused_animated_nodes; bool simple_singleplayer_mode; @@ -1520,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 */ @@ -1750,7 +1743,7 @@ bool Game::getServerContent(bool *aborted) // End condition if (client->mediaReceived() && client->itemdefReceived() && client->nodedefReceived()) { - break; + return true; } // Error conditions @@ -1809,7 +1802,9 @@ bool Game::getServerContent(bool *aborted) } } - return true; + *aborted = true; + infostream << "Connect aborted [device]" << std::endl; + return false; } @@ -3043,10 +3038,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 @@ -3106,6 +3097,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; } @@ -3343,12 +3336,18 @@ 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); 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); @@ -3449,6 +3448,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) { @@ -3465,7 +3465,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) { @@ -4046,7 +4046,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; } @@ -4288,7 +4288,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(); /* @@ -4296,21 +4296,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 ); @@ -4484,8 +4484,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/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/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/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 138e7fc796..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. @@ -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. @@ -467,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/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/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..ee3e3829d9 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 */ @@ -71,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; @@ -84,6 +136,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; } @@ -115,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(); @@ -126,6 +180,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 +240,17 @@ 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); + + 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) @@ -260,6 +325,14 @@ 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); + + 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 c7a5795552..192e900952 100644 --- a/src/itemdef.h +++ b/src/itemdef.h @@ -28,9 +28,11 @@ 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; +struct PointedThing; #ifndef SERVER #include "client/tile.h" struct ItemMesh; @@ -50,6 +52,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 { /* @@ -77,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; @@ -92,6 +116,8 @@ struct ItemDefinition std::optional place_param2; bool wallmounted_rotate_vertical; + TouchInteraction touch_interaction; + /* Some helpful methods */ 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, 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 73aeaade7a..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 @@ -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); } } @@ -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. 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/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/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/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(); 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 430ae8e725..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( @@ -138,6 +144,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); } /******************************************************************************/ @@ -185,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"); @@ -199,6 +223,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"); } /******************************************************************************/ @@ -287,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); @@ -428,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"); @@ -757,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 @@ -981,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"); @@ -1568,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) { @@ -2001,11 +2165,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))); @@ -2057,8 +2226,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 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/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[]; 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/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..26b58245b9 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) { @@ -949,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); @@ -963,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; @@ -1826,22 +1815,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); @@ -2628,28 +2616,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 +2649,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,30 +2663,53 @@ 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; - // Put 5kB in one bunch (this is not accurate) - u32 bytes_per_bunch = 5000; + /* Read files and prepare bunches */ - std::vector< std::vector > file_bunches; + // 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) { - 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; @@ -2713,16 +2728,15 @@ void Server::sendRequestedMedia(session_t peer_id, file_bunches.emplace_back(); file_size_bunch_total = 0; } - } /* 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 { @@ -2732,18 +2746,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(); - for (const SendableMedia &j : file_bunches[i]) { + 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=" << 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) 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/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; } 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/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/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; }; 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..1ad57143b1 --- /dev/null +++ b/src/util/pointabilities.h @@ -0,0 +1,79 @@ +/* +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 +{ + // 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, + // Note: Since (u8) 2 is truthy, + // older clients will understand this as "pointable=true", + // which is a reasonable fallback. + 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);