-
Notifications
You must be signed in to change notification settings - Fork 173
lua: Add entityProxy, client, and bot APIs #3064
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
base: master
Are you sure you want to change the base?
Conversation
src/sgame/lua/SGameGlobal.cpp
Outdated
| // @usage sgame.SendServerCommand(-1, 'print "WAZZUP!!") -- Print wazzup to all connected clients. | ||
| static int SendServerCommand( lua_State* L ) | ||
| { | ||
| if ( !lua_isnumber( L, 1 ) || !lua_isstring( L, 2 ) ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason to warn about wrong argument types by hand here? Looks like most other functions just use checkinteger/checkstring immediately (which throws longjmp/exception if the value doesn't match).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just didn't understand this part that well a few years ago when i wrote this :D
| // @treturn EntityProxy|nil Returns EntityProxy if there one exists or nil. | ||
| // @usage local ent = sgame.entity[0] -- Get the first entity. | ||
| // @within entity | ||
| int index( lua_State* L ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should return a GentityRef or something with equivalent semantics of expiring based on the generation counter
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't work well for players. I don't want to have to refetch a player every time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Then use a client number or Client object in the cases where you want to keep the reference beyond death.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That is a worse developer experience. Client num means you have to fetch it every time. A client has no reference back to the parent entity.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The way I imagine it should work is that an API that accepts an entity argument should be able to take any of an entity object, a client object, or an entity number.
Your Juggernaut implementation is a good example of something that would benefit from GentityRef semantics. Currently it has a bug that if a player suicides, it continues being the juggernaut instead of choosing a different one. If you stored the current juggernaut as a GentityRef, then you could easily detect any weird condition causing the juggernaut to disappear by checking whether the ref is valid. You wouldn't have to worry about hooking all of the possibilities like disconnection, team change, or suicide.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My proposal is to have a GentityRef pointing to the current juggernaut and check every frame if it is empty, in which case a new one needs to be randomly selected. This would fix the suicide bug because the reference is invalidated a second or two after the player dies.
For scoreboard tracking, a stable client identifier would be used, same as now. Team change/disconnect hooks would still be needed, but only for clearing the scoreboard data of the player who left. I don't see when you would ever need to fetch the entity for a client number.
Why not just make the proxy return nil when inuse is false instead of every generation change? It sounds like that's the best of both worlds.
This is not an adequate solution for detecting entity expiration, even if you check every frame. For example if bot fill is active, and someone switches teams, it often happens that a bot client is removed and a new one immediately created with the same entity number.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this works well in practice. The entire point of Lua is to avoid being a per-frame thing. We should be working on events (die, team change, player spawn, etc). Having to refresh a counter every time is just extra annoyance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@illwieckz we need a decision here. We are at an impasse :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My proposal is to have a GentityRef pointing to the current juggernaut and check every frame if it is empty
The entire point of Lua is to avoid being a per-frame thing.
I agree that we should not aim for lua functions to be called at each server frame.
Nevertheless, the problem is real and can lead to subtle bugs. We can either accept that fact and fix it later, or fix it now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this general problem can only be solved by some method like GentityRef, or by not using proxy objects at all. Recently, I have explored a method of map scripting that does not rely on proxy objects, but on registering Lua callbacks (#3330).
My method's scope is much smaller, but it suffers the same problem. However, I noticed that a callback driven API can be easier to support, as it does not hold arbitrary state in Lua objects. A callback API could use simple dependency lists. For instance, a callback could be a function and a list of entities. Whenever an entity in the list is removed from the game, the callback would be notified by calling it in a specific way. That way, the burden of handling the problem can be delegated to the Lua script.
This will be the entrypoint for all sgame APIs. You can access them via sgame.foo. Right now, we have only implemented SendServerCommand() which can be used to send server commands to clients. Ex: /lua "sgame.SendServerCommand(-1, 'cp wzzup')"
This allows interacting with entities via Lua. You can find entities
using sgame.entity.FindById("whatever") or you can find entities by
their entity number using sgame.entity[458].
This allows accessing client specific fields for entities. Ex: /lua "print(sgame.entity[0].client.name)"
We can define our own iterator function like in This creates a closure, which is not neccessary, but you get the idea. |
Why are we even talking about fringe mods here? |
The proper way to do this would be |
|
What is now needed to merge this? |
|
a decision on the API |
Is it a decision you can make? At some point we have to merge something, or this will never be. |
That would be a nasty descision. There is a very real problem here that needs to be resolved somehow. I am not quite sure whether any of the proposed solutions are adequate. One proposed method is a poor person's generational garbage collector, and another is to accept that we will be running into subtle bugs randomly. I would like to propose a different API, reminiscent of Unix system calls. Unix became popular by simply not solving such complex problems in the kernel, but by delegating the burden to the user space. Its system calls usually work fine, but a caller has to be prepared for the occasional mysterious failure. We could do something similar with our Lua API, thereby keeping things simple. |
I give an example in the context of map entities in #3330 (comment). The method described there does not use proxy objects, so the API does not have to worry about whether they are still valid. Instead, it relies on callbacks. These callbacks can handle the problematic cases themselves. Thus, the API itself is very simple. |
|
Entity reference invalidation is not a complex problem. It is a very easy problem. All you have to do is store the generation number (no relation to generational garbage collection) on the proxy object, and then when then a reference to a freed entity whose slot was recycled is used, treat it the same as if a reference to a freed entity whose slot is empty were used. |
But we will still need to guard each occurance of |
EDIT: Now I think I understand the point of that comment. I was comparing against the current state of this PR #3064, while Sweet is comparing against a proposal in another thread. Yes, that's a valid point that you might need to check the validity of the entity each time you use it from Lua. It's true that event-driven programming without entity references can be more ergonomic for some use cases. But we still need entity references because people are going to want them (and resort to raw entity nums if they're not available) for other use cases. An example of this type of logic is the hive's and rocketpod's smart missiles. They need to keep track of the current target and each frame, check if the target is still valid and if so, steer toward the target. This would be much more awkward if we had to register event handlers for all the ways the target can become invalid (death, changing team, disconnecting, not sure if others?). |
Yes. I should have been more clear.
I guess you are right. Such event handlers would be very messy indeed. They would be even more messy if they avoided allocating lua functions at runtime, which is probably a requirement for such cases. I am in favour of the generation counter now, be it |
Hey, look, the commit is still around :) |
|
If there is consensus that gentifyref is better, then we can consolidate around that. I think we can just establish a rule to not maintain references to entities in callbacks, only track the entity numbers and that those entities may become invalid so you also must track those with different event handlers. I'm not sure that there is any way around this if you want a persistent way to track a player. |
Did you mean s/entity/client/ here?
Maybe assigning IDs to players could be useful in some cases. Though I'm not sure it would be any easier than client num + event handlers. We could also make a ClientRef for this. But then we might need two variants of ClientRef, one that invalidates on leaving the team and one that invalidates on disconnect? |
I didn't, but I realize I'm wrong. Consider my shitty dretch tutorial: https://github.com/DolceTriade/unvanquished-tutorial/blob/main/game/tutorials/dretch.lua#L14 I maintain a reference to the egg. I should not do this. I should give that entity an ID and every time I want to reference it, I need to re-fetch it using that ID (unless it is already passed in for me using a callback). I cannot use the entity number because there is no guarantee that the entity number won't belong to a different egg in the future if I forgot to track its death via its
Eh, I think for clients clientNum or manually assigning an ID is sufficient. Manually Assigning an ID is probably better actually because then I don't need to worry about disconnection (though I think I need to worry about team change?) because the ID shouldn't change? Also I'm not fully clear on the client entity life cycle, because the server maintains some state about disconnected clients and I haven't dug into this code to see how it works. In any case, I think gentityRef is fine with either clientNum or manual IDs. |
It is currently unclear if you should really use an ID to reference anything that is not a map entity. There is a (questionable) PR to assign IDs to the reactor and the overmind. But if this makes some sense, then only because these buildings are unique. The main idea about entity IDs was that a mapper may provide them. If the mappers does not, we can provide human readable IDs for them. As far as I know, we do not do any defragmentation in the entity array. An entity number will be valid until the entity is removed. So there is no real advantage in using an ID over the index. The best automatic IDs we could do for eggs would probably look like |
|
That being said, my proposal in #3345 (comment) could be used in the way you are describing. |
There is a proposal for the ID mechnism itself hidden in thise sentence. Currently, we require that there are not two entities with the same ID at the same time. You are proposing to strenghten this requirement such that two entities must not have the same the ID in all of time. One motivation for the ID field was to have something similar to the HTML DOM attribute |
I would be in favor of following this principle for automatic IDs.
Assuming we have GentityRef, then indeed probably most of the entity handles in your scripts should be GentityRef-based entity objects, not IDs. But I can think of a few reasons you might want to use user-assigned IDs:
I don't think anyone is proposing that. Ishq was saying how manually-assigned IDs could be used as a poor man's GentityRef in the context of a dretch tutorial that needs to keep a persistent reference to a particular egg. |
|
I want to maintain that behavior. Let me give a concrete example... I build an egg using Lua (or adopt an en existing egg in the map, that's not important). I want to make sure that I am always referencing this egg and not another egg. With the current implementation of this PR, I can do something like this: MYGLOBALEGG = nil
function makeEgg()
local egg = sgame.SpawnBuildable('eggpod', node.origin, node.angles, node.origin2, true)
egg.die = function()
MYGLOBALEGG = nil
end
MYGLOBALEGG = egg
end
function whereEgg()
if MYGLOBALEGG == nil then
return
end
print("My egg is alive!!!")
-- Keep checking egg every second
Timer.add(1000, whereEgg)
end
makeEgg()
-- Check egg every second
Timer.add(1000, whereEgg)I can store my reference to my egg outside of the scope and it will forever point to that entity number. Even if it dies or changes or anything else. So I must register a death handler to notify me when it changes (as death is the only way this particular entity can change). Technically speaking, even if I migrate to entityrefs, the above code will continue to be valid because eggs don't evolve, so as I am typing this, this is probably a bad example, but whatever, I'm continuing forward... function makeEgg()
local egg = sgame.SpawnBuildable('eggpod', node.origin, node.angles, node.origin2, true)
egg.id = "specialegg"
end
function whereEgg()
local egg = sgame.entities.find_entity_by_id("specialegg")
if egg == nil then
return
end
print("My egg is alive!!!")
-- Keep checking egg every second
Timer.add(1000, whereEgg)
end
makeEgg()
-- Check egg every second
Timer.add(1000, whereEgg)No references to entities outside the scope from which they were gotten. Since the ID is freed when an entity is destroyed, after the entity is gone, I won't be able to find it anymore. Technically, ANOTHER lua script could have run and given another egg the same ID, but I'm discounting that case because these IDs must be given explicitly and any other lua scripts must also be run explicitly and I don't think I worry about that case in practice. A potentially problematic implementation might be... egg_id = nil
function makeEgg()
local egg = sgame.SpawnBuildable('eggpod', node.origin, node.angles, node.origin2, true)
egg_id = egg.number
end
function whereEgg()
local egg = sgame.entities[egg.number]
if egg == nil then
return
end
print("My egg is alive!!!")
-- Keep checking egg every second
Timer.add(1000, whereEgg)
end
makeEgg()
-- Check egg every second
Timer.add(1000, whereEgg)In this case, even if the egg died, another entity could have taken over its number so in this case, I would need to register a death handler for this code to be correct. After typing all this out, I learned that entityRefs literally only affect things for clients and clients require special handling no matter what (team change, disconnect, death, evolution, weapon buys (no entity impact for this one), admin level changes, etc), so you probably need to constantly validate your client no matter what approach we take. I think we cannot get around this. |
|
Notice that #3330 (comment) solves the nasty problem that has stalled this PR for many months. |
|
I honestly do not get why you two are making this so complicated. |
You can access them like:
Notably, to be consistent with C++ (but inconsistent with Lua), we start the array at 0, which means we shouldn't use ipairs to iterate over entities. Just use a regular for loop or something to iterate in order.
You can also hook into the traditional gentity_t functions like act/touch/die etc by defining your own lua function. The parameters are documented in the code (and soon a generator to generate HTML docs will be committed.)