Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[pls do not squash :3] Allow limiting object observers #13987

Merged
merged 4 commits into from
Aug 16, 2024

Conversation

appgurueu
Copy link
Contributor

@appgurueu appgurueu commented Nov 11, 2023

Closes #8045 straightforwardly: By managing an opt-in set of observers per-entity; in the process I also made players use a proper std::string for their name rather than a char*.

This feature should work with newer servers and older clients as well.

To do

  • API design: I am considering incremental methods add_observers / remove_observers in addition to the current set_observers / get_observers. Opinions?
    • Considered, not really necessary for a minimal feature yet. Unless someone asks for them, I won't add them; note that you can always easily implement them yourselves.
  • What about static_save? The easiest solution would probably be to disallow static_save for entities with managed observers.
  • What else did I fail to take into account?
    • Well well, let's find out! This PR is suspiciously simple, isn't it?

Feedback welcome!

How to test

Join a server with two clients. Spawn a testentities:observable. Punch the observable on one client: It should disappear for you and appear for the other client.

@appgurueu appgurueu marked this pull request as draft November 11, 2023 23:45
@MisterE123
Copy link
Contributor

RE: static_save: Why not clear the table of players allowed to view the entity when the entity is saved, and let the entity set its viewable players when it comes back from being saved?

@appgurueu
Copy link
Contributor Author

appgurueu commented Nov 12, 2023

RE: static_save: Why not clear the table of players allowed to view the entity when the entity is saved, and let the entity set its viewable players when it comes back from being saved?

I considered this, but I believe it's problematic as it could lead to some kind of "polling" / "spurious" wake ups: Suppose a player who's not supposed to be an observer comes near an entity. Since we don't know whether this player will be set as an observer, we have to wake the entity up. The entity then sets its observers, excluding the player. We should put the entity back to sleep, since there is no nearby observer. But if we do that, this cycle repeats ad infinitum while the player is in proximity - the entity gets woken up, put back to sleep, ...

An alternative is to always load entities if any player is in proximity - be that an observer or not - and only unload them if no player is in proximity - again regardless of the observer status of the players. I'm not sure whether this is cleaner, though. (Given that I have not touched this code, I believe this should be the current state of affairs.)

Yet another alternative would be letting the engine persist observers. That way it could check the observers before waking an object up.

Copy link
Collaborator

@sfan5 sfan5 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gave the code a quick look, not exhaustive

src/client/localplayer.h Outdated Show resolved Hide resolved
doc/lua_api.md Outdated Show resolved Hide resolved
src/script/lua_api/l_object.cpp Outdated Show resolved Hide resolved
src/script/lua_api/l_object.cpp Outdated Show resolved Hide resolved
src/script/lua_api/l_object.cpp Outdated Show resolved Hide resolved
src/server/serveractiveobject.cpp Outdated Show resolved Hide resolved
@MisterE123
Copy link
Contributor

MisterE123 commented Nov 12, 2023

RE: static_save: Why not clear the table of players allowed to view the entity when the entity is saved, and let the entity set its viewable players when it comes back from being saved?

I considered this, but I believe it's problematic as it could lead to some kind of "polling" / "spurious" wake ups: Suppose a player who's not supposed to be an observer comes near an entity. Since we don't know whether this player will be set as an observer, we have to wake the entity up. The entity then sets its observers, excluding the player. We should put the entity back to sleep, since there is no nearby observer. But if we do that, this cycle repeats ad infinitum while the player is in proximity - the entity gets woken up, put back to sleep, ...

An alternative is to always load entities if any player is in proximity - be that an observer or not - and only unload them if no player is in proximity - again regardless of the observer status of the players. I'm not sure whether this is cleaner, though. (Given that I have not touched this code, I believe this should be the current state of affairs.)

Yet another alternative would be letting the engine persist observers. That way it could check the observers before waking an object up.

I thought statically saved entites get woken up when the mapblock they are in loads, and put to sleep by the engine when the mapblock they are in unloads. Given that, the entity should be woken up regardless of who comes near. Are you telling me you will make custom wake/save logic that takes players who can view it into account? Does that mean that you cannot have an entity that is un-viewable by any player exist in an active state for a time?

I disagree that you should put the entity back to sleep if it is not viewable by players; that would be a hardcoded engine "feature" that would take control away from mods. Mods cnn easily use entity logic to delete or save entities if they really need to, but mods would not be able to easily work around an engine feature that automatically statically saved an unviewable entity if it was unviewable.

Usecase: an entity that is invisible to all players but which runs game logic at a particular location.

Usecase: an entity which is invisible to all or some players for a time, and at a specific time determined in on_step, shows itself to some players

Neither of these would be possible if you automatically statically saved entites when they did not have any viewers.

Your second alternative is the way entities currently work, and should be kept that way.

@appgurueu
Copy link
Contributor Author

I thought statically saved entites get woken up when the mapblock they are in loads, and put to sleep by the engine when the mapblock they are in unloads. Given that, the entity should be woken up regardless of who comes near.

Yes, that should be the current state of this PR. I'm not sure whether that's clean though, hence why I'm discussing it.

Are you telling me you will make custom wake/save logic that takes players who can view it into account? Does that mean that you cannot have an entity that is un-viewable by any player exist in an active state for a time?

This would be the third option, yes. I don't know if I will implement this though, it's still up for discussion.

I disagree that you should put the entity back to sleep if it is not viewable by players; that would be a hardcoded engine "feature" that would take control away from mods. Mods cnn easily use entity logic to delete or save entities if they really need to [...]

Good point on the deletion, though I'm not sure on the saving (unless you mean saving entities in something like a mod-managed file rather than the map database).

[...] but mods would not be able to easily work around an engine feature that automatically statically saved an unviewable entity if it was unviewable.

When is it useful to keep an entity that can't be observed around? I feel this is wasteful. Consider something like a statically saved ghost which is supposed to haunt a specific player: You'll wake this ghost up and keep it in memory if any player comes nearby, loading the mapblock (and thus the ghost).

That said, since entity static save is tied to mapblocks, I'm not sure how the ghost could efficiently be left asleep, and I don't want this PR to get out of scope, so leaving it "as is" is indeed probably the easier option here. I suppose this should be documented, though.

(There could be a workaround for this, actually: Have an invisible parent entity with unmanaged observers; make the child have static save = false, and let the parent (re)spawn the child if necessary. But I agree that it's dirty.)

Your second alternative is the way entities currently work, and should be kept that way.

Yes, it's also what I believe this PR currently implements (I haven't tested this yet). Keeping it this way and documenting it properly is probably be the cleanest & simplest solution. What do core devs think?

@appgurueu appgurueu changed the title Proof-of-concept of limiting object observers [pls do not squash :3] Proof-of-concept of limiting object observers Nov 12, 2023
@appgurueu appgurueu marked this pull request as ready for review November 14, 2023 12:57
@MisterE123
Copy link
Contributor

MisterE123 commented Nov 14, 2023

Oh, wow, this allows actual player invisibility

@MisterE123
Copy link
Contributor

I'm curious why you have managed players as:
{pl_name1=true, pl_name2=true}
Instead of:
{pl_name1,pl_name2}
Where only the players in the table can view the object, and an empty table makes no one able to view it, and setting observers to nil (not even an empty table) makes them unmanaged.

Given your system, I am a bit confused as to how you would set an entity to have no observers without constantly using the on_step to add player names to the table and set them to false.

@grorp
Copy link
Member

grorp commented Nov 14, 2023

I couldn't resist: I made a quick-and-dirty per-player hallucination mod with the set_observers API added by this PR.
https://github.com/grorp/hallucinations

Seems to work great!

Given your system, I am a bit confused as to how you would set an entity to have no observers without constantly using the on_step to add player names to the table and set them to false.

Wouldn't that be set_observers({})?

@appgurueu
Copy link
Contributor Author

Oh, wow, this allows actual player invisibility

Yeah, from the perspective of the client players are pretty much just CAOs, so I saw no reason to disallow player invisibility - with the exception that you can't hide a player from themselves this way, since that would break various assumptions the engine makes.

I'm curious why you have managed players as: {pl_name1=true, pl_name2=true}

Because this is a set of player names: Player names are either in the set, or they aren't; they may not appear twice. A table from player names to true captures this well. It also makes tests for inclusion convenient (on the Lua side of things): You can simply do if observers[name] then ... end. I think {[element] = true, ...} is simply the most natural way to model a set in Lua most of the time.

Other engine functions (e.g. set privs) represent sets this way too.

Instead of: {pl_name1,pl_name2}

Lists are another way to represent sets (which the engine also uses), but it doesn't make much of a difference, and I don't see any significant advantages of lists (except maybe for being slightly less typing work); duplicates would also need to be dealt with (error or silently ignore?).

Where only the players in the table can view the object and an empty table makes no one able to view it

Currently the case (using player names as keys, though)

and setting observers to nil (not even an empty table) makes them unmanaged.

Yeah, that's currently the case: Doing obj:set_observers() or obj:set_observers(nil) makes them unmanaged. I suppose I should make this clearer in the docs?

Given your system, I am a bit confused as to how you would set an entity to have no observers

Same as in your proposed list format: obj:set_observers({}), as grorp said.

without constantly using the on_step to add player names to the table and set them to false.

Oh no, don't do that! It's a set, not a map from player name to whether they are an observer 😄

This is interestingly the same confusion as for setting privs ({interact = false} will set the player privileges to exactly interact, rather than revoking interact or similar). I had hoped for my docs to be less ambiguous, but oh well... I should make the code error if false is used as a value, and expand the docs.

@appgurueu appgurueu changed the title [pls do not squash :3] Proof-of-concept of limiting object observers [pls do not squash :3] Allow limiting object observers Nov 14, 2023
@LoneWolfHT
Copy link
Contributor

I'm running this PR on the CaptureTheFlag server, as you can see if you look at teammate nametags. I'll let OP know if there are any issues

@jordan4ibanez
Copy link
Contributor

:3

@hlqkj
Copy link

hlqkj commented Nov 29, 2023

Is the TAB autocomplete in the game chat also benefiting from, or affected by this?

@sfan5 sfan5 self-requested a review November 29, 2023 10:12
@Zughy Zughy added the Concept approved Approved by a core dev: PRs welcomed! label Nov 29, 2023
@appgurueu
Copy link
Contributor Author

Is the TAB autocomplete in the game chat also benefiting from, or affected by this?

In its current form, this doesn't affect TAB autocompletion or the list of names sent to the serverlist at all. Depending on the use case, this could be desirable: Imagine something like an "invisibility" feature in a CTF server. You don't want to pretend that invisible players have left to the serverlist, or disable TAB autocomplete for them. On the other hand, a "cloaking" mod intended to conceal the presence of staff will want to disable both TAB autocomplete and sending the name to the server list; not sending names to the serverlist is also a demanded feature: Servers might have bot players (like NodeCore's spectator which streams to Twitch) which they wish to hide, or players may be asking for slightly more privacy than having their online status on a Minetest server readily available in some public JSON.

In conclusion, I believe it's probably best to leave these features up to a separate PR, to keep this PR simple and focused. Should it be considered necessary to "complete" this feature however, I already have a commit prepared which adapts the player list sent to clients based on who it is sent to, but I'm not very fond of it since it conflates "can see player in-game" and "knows that player is in-game" (e.g. available for a chat), which aren't exactly the same (refer to the example use cases above). The server list is yet another issue, which deserves its own API.

TL;DR: Controlling

  • Tab completion (player list sent to other clients) and
  • Serverlist visibility

deserve to be their own features in follow-up PRs. They are out of scope for this PR (which is only concerned with limiting who the active objects are sent to).

@hlqkj
Copy link

hlqkj commented Nov 30, 2023

Thanks for your answer! I think you are totally right. I was indeed thinking about cloak, but didn't consider the other use case...

doc/lua_api.md Outdated Show resolved Hide resolved
doc/lua_api.md Outdated Show resolved Hide resolved
doc/lua_api.md Outdated Show resolved Hide resolved
src/client/localplayer.cpp Outdated Show resolved Hide resolved
src/script/lua_api/l_object.cpp Outdated Show resolved Hide resolved
src/script/lua_api/l_object.cpp Show resolved Hide resolved
src/script/lua_api/l_object.cpp Outdated Show resolved Hide resolved
src/script/lua_api/l_object.cpp Outdated Show resolved Hide resolved
src/script/lua_api/l_object.cpp Outdated Show resolved Hide resolved
src/script/lua_api/l_object.cpp Outdated Show resolved Hide resolved
@sfan5
Copy link
Collaborator

sfan5 commented Dec 12, 2023

Thought: In the unmanaged state the engine should automatically "propagate" the observers to children objects.

Otherwise this feature is quite intrusive because if you want to attach something to the player (*) you suddenly have to change your code to care about observers (to not violate the conditions).

(*) or any other objects whose observer state you don't get to decide

@sfan5 sfan5 added the Action / change needed Code still needs changes (PR) / more information requested (Issues) label Dec 12, 2023
@Zughy Zughy modified the milestones: 5.10.0, 5.9.0 Jul 18, 2024
@appgurueu
Copy link
Contributor Author

Thanks for the review @SmallJoker. I think I have addressed it now, but some details might need further discussion.

Also relevant: #14342 would add some serverside sound tracking this somewhat depends on.

This PR currently can't play nice with attached sounds, since the engine doesn't keep (enough) track of them. It basically suffers from the same bug we've already been seeing when objects get out of range: The sound stays. Now we could fix this by making clients remove the sound, but then it would be gone if the object reappears.

For now I would add a note to the docs that this doesn't work. The main use case (visibility) won't be impacted by this. Thoughts?

@SmallJoker SmallJoker self-requested a review July 27, 2024 12:00
@BluebirdGreycoat
Copy link
Contributor

I don't know about others, but for me, a mention in the docs is just fine.

@SmallJoker
Copy link
Member

@appgurueu I'd also say that documenting it with a one or two-liner is good enough.

@grorp
Copy link
Member

grorp commented Jul 31, 2024

minetest.register_entity(":dodo", {})
minetest.register_on_joinplayer(function(player)
	local obj = assert(minetest.add_entity(player:get_pos() + vector.new(0, 5, 0), "dodo"))
	obj:set_observers({})
	player:set_attach(obj)
end)

in 9/10 cases, this results in:

ERROR[Main]: In thread 7fa029eb9080:
ERROR[Main]: /mt/src/client/localplayer.cpp:321: bool LocalPlayer::isDead() const: A fatal error occurred: LocalPlayer's CAO isn't initialized

probably because the player can't observe themself

@Zughy Zughy added the Action / change needed Code still needs changes (PR) / more information requested (Issues) label Jul 31, 2024
@appgurueu appgurueu removed the Action / change needed Code still needs changes (PR) / more information requested (Issues) label Aug 2, 2024
@appgurueu
Copy link
Contributor Author

probably because the player can't observe themself

Yes, thanks for pointing this out. This is effectively an error case, players must effectively observe themselves. I have documented this and added an error (which shuts down the server) in fedc99e (due to effective observers being computed lazily, this can unfortunately not be a proper Lua error with a stack trace).

There would be some alternatives, but they all seem messier to me:

  • Make players forcibly observe themselves, perhaps warn. Problem: Now they're attached to an object they don't know about. I don't think this inconsistency between client and server is a good idea.
  • Make players observe themselves and all their parents. This would work, but I don't think this follows the principle of least surprise (additionally you'd get questions / inconsistencies about get_effective_observers on the Lua side of things). It's also a bit uglier to implement.
  • Error on set_attach / set_observers if condition is violated: This would require reworking the code to eagerly compute effective observers, which has potential performance implications I'm not really a fan of.

Copy link
Member

@SmallJoker SmallJoker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no pointer hackery going on, and the attachments loops cannot be infinite due to sanity checks in UnitSAO::setAttachment. Overall I think the potential risk towards the 5.9.0 release is rather low (when this feature is not used).

Added a few comments for judgement.

doc/lua_api.md Outdated Show resolved Hide resolved
src/server/serveractiveobject.cpp Outdated Show resolved Hide resolved
src/server/serveractiveobject.cpp Outdated Show resolved Hide resolved
src/script/lua_api/l_object.cpp Outdated Show resolved Hide resolved
src/script/lua_api/l_object.cpp Show resolved Hide resolved
m_ao_manager.getAddedActiveObjectsAroundPos(playersao->getBasePosition(), radius_f,
player_radius_f, current_objects, added_objects);
if (!playersao->isEffectivelyObservedBy(playersao->getPlayer()->getName()))
throw ModError("Player does not observe itself");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Observation, no action needed. This can be triggered by attaching to an object that is not observed, or when removing the player from the parent's observer list.

minetest.register_chatcommand("p", {
	func = function(name, param)
		local player = minetest.get_player_by_name(name)
		local pos = player:get_pos()

		local obj = core.spawn_item(pos, "default:dirt")
		obj:set_observers({["foo"] = true, ["bar"] = true })

		player:set_attach(obj)
		return true, "OK!"
	end
})

It is similar to the situation in the previous review: #13987 (comment) but very difficult to make this less error-prone. The main issue is that this error cannot be captured by pcall (there is no active callback).

src/server.cpp Show resolved Hide resolved
@Zughy Zughy modified the milestones: 5.9.0, 5.10.0 Aug 5, 2024
@appgurueu appgurueu requested a review from SmallJoker August 13, 2024 00:56
builtin/game/misc.lua Outdated Show resolved Hide resolved
Copy link
Member

@SmallJoker SmallJoker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Works.

@sfan5
Copy link
Collaborator

sfan5 commented Aug 16, 2024

@appgurueu are you sure you don't want this squashed with commits like "more things" and "doc"?

@sfan5 sfan5 merged commit d3ca269 into luanti-org:master Aug 16, 2024
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Feature ✨ PRs that add or enhance a feature One approval ✅ ◻️ Roadmap: supported by core dev PR not adhering to the roadmap, yet some core dev decided to take care of it Roadmap The change matches an item on the current roadmap @ Script API @ Server / Client / Env.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Allow Changing Entity Visibility On A Per-Player Basis