Modding:Entity instance attributes

From Vintage Story Wiki
Revision as of 00:16, 3 November 2024 by Bluelightning32 (talk | contribs) (Create page)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

Entity instance attributes are key values that can be set on any entity in the world. They can only be modified with server commands and code mods. These are different from entity type attributes, which apply to entity types (a pattern used to spawn an entity into the world). Unlike entity type attributes, entity instance attributes cannot be set in the json files, because they are unique to every instance of the entity.

For each entity, there are 3 kinds of entity instance attributes:

WatchedAttributes
the most commonly used entity instance attributes. Modifications on the server side are stored in the save game and synchronized to the clients. Modifications on the client side are not synchronized to the server, and they are lost the next time the server synchronizes the entity attributes.
DebugAttributes
only used if the EntityDebugMode is enabled. The base game uses this to show the active AI tasks and animations for entities. When enabled, bulk changes to these attributes on the server side are synchronized to the client, but small changes are dropped due to a bug in the synchronization code (in ServerPackets.GetBulkEntityDebugAttributesPacket in VintagestoryLib.dll).
Attributes
stored to the save game on the server side, but not synchronized to the client. Modifications on the client side are kept until the entity is unloaded.

Debug commands

Server admins can read WatchedAttributes of the currently selected entity with this server command: /entity cmd l[] attr [attribute name]

WatchedAttributes of the currently selected entity can be modified with this command: /entity cmd l[] setattr string [attribute name] [value]

Modifying attributes in code

The entity instance attributes are read and modified like any other TreeAttributes. An attribute can hold many different types. int, float, and string are common types. Here's an example from Entity.cs where the onHurtCounter attribute is read and set.

                    WatchedAttributes.SetInt("onHurtCounter", WatchedAttributes.GetInt("onHurtCounter") + 1);

Synchronization packets

EntityBulkAttributes showing a partial update of the WatchedAttributes for one entity and a full update for another entity

When the entity is first spawned, the WatchedAttributes are sent to the client in the Entity packet, along with everything else about the entity. After that, changes to individual attributes are sent in the partialUpdates field of the BulkEntityAttributes packet. However, if 10 or more attributes are modified before the next BulkEntityAttributes packet is sent, then instead all of the entity's WatchedAttributes are sent in the fullUpdates field of the BulkEntityAttributes packet.

Because the server supports partial updates, modders can generally add extra WatchedAttributes without worrying about the network cost.

Listening to changes

Listeners can be registered for all 3 kinds of instance attributes to get notifications when the attribute changes. Although, the client will only get notifications for server modifications if the attributes are synchronized from the server to client (basically just WatchedAttributes).

Use RegisterModifiedListener to register a listener. The listener is added to the attribute for one entity (not entity type), so one needs to somehow find the entity first in order to add the listener. Listeners can be removed with the UnregisterListener. Note that listeners are registered for specific attributes, but unregistering a listener removes it from all attributes.

As an example, CharacterExtraDialogs adds listeners on the client side to the hunger, stats, and bodyTemp attributes when the dialog is opened. It does this so that it can redraw the health bars in the dialog. Since the WatchedAttributes are synchronized, the dialog is redrawn on the client side when the character is hurt on the server side. When the dialog is closed, the listeners are unregistered.

        private void Dlg_OnClosed()
        {
            capi.World.Player.Entity.WatchedAttributes.UnregisterListener(UpdateStatBars);
            capi.World.Player.Entity.WatchedAttributes.UnregisterListener(UpdateStats);
        }

        private void Dlg_OnOpened()
        {
            capi.World.Player.Entity.WatchedAttributes.RegisterModifiedListener("hunger", UpdateStatBars);
            capi.World.Player.Entity.WatchedAttributes.RegisterModifiedListener("stats", UpdateStats);
            capi.World.Player.Entity.WatchedAttributes.RegisterModifiedListener("bodyTemp", UpdateStats);
        }

Even if an attribute is set to it's current value (essentially the value is unchanged), the modification listeners are still invoked. The onHurt listeners rely on this; the listeners are triggered whenever the entity is hurt, even if the damage is the same as last time (onHurt is set to its current value).

If the modifications are sent to the client through the fullUpdates field (because 10 or more attributes were changed), then the client cannot tell which fields were really modified. So the client invokes all registered listeners, even if the field did not change. So the listeners have to be aware that they may get spurious notifications. The onHurt listener in Entity.cs uses the onHurtCounter to filter out spurious notifications (it ignores the onHurt notification if onHurtCounter was not modified). This strategy can be used for modders adding new attributes, but none of the existing attributes (except for onHurt) have a corresponding counter attribute.

            WatchedAttributes.RegisterModifiedListener("onHurt", () => {
                float damage = WatchedAttributes.GetFloat("onHurt", 0);
                if (damage == 0) return;
                int newOnHurtCounter = WatchedAttributes.GetInt("onHurtCounter");
                if (newOnHurtCounter == onHurtCounter) return;

                onHurtCounter = newOnHurtCounter;
...
            });

Base game attributes

Code mods can create new attributes with any name. These are the attributes from the base game. In the below list, a dot is used to signify a subattribute of a TreeAttribute. To read these attributes, one needs to read the parent attribute with GetTreeAttribute, then get the child from it; the game does not parse the dot notation shown below. Attribute names are case sensitive.

WatchedAttributes

entityDead: (int)
0 if the entity is alive, or 1 if the entity is dead.
idleSoundChanceModifier: (float)
animations: (TreeAttribute)
extraInfoText: (TreeAttribute)
onHurt: (float)
the amount of damage applied last time the entity was hurt. Listen to this to get notifications when the entity is hurt.
onHurtCounter: (int)
the number of times the entity has been hurt. This is used to detect spurious notifications.
onHurtDir: (float)
angle that the damage came from, measured from the top down view.
onFire: ()
grow: (TreeAttribute)
textureIndex: (int)
kbdirX: (double)
X component of the direction that the entity is currently getting knocked back.
kbdirY: (double)
Y component of the direction that the entity is currently getting knocked back.
kbdirZ: (double)
Z component of the direction that the entity is currently getting knocked back.
hunger.currentsaturation: (float)
how full the player is.
hunger.maxsaturation: (float)
maximum fullness that the player can have.
bodyTemp.bodytemp: (float)
player temperature.
wetness: (float)
wetness in the range of 0 to 1.
health.currenthealth: (float)
current hit points.
health.maxhealth: (float)
maximum hit points.
stats.walkspeed: (TreeAttribute with all entries of float type)
stats.healingeffectivness: (TreeAttribute with all entries of float type)
stats.hungerrate: (TreeAttribute with all entries of float type)
stats.rangedWeaponsAcc: (TreeAttribute with all entries of float type)
stats.rangedWeaponsSpeed: (TreeAttribute with all entries of float type)
stats.rangedWeaponsSpeed: (TreeAttribute with all entries of float type)
guardedEntityId: (long)
the target entity that the guard entity is guarding.
guardedPlayerUid: (string)
the target player that the guard entity is guarding.

Attributes

dmgkb: (int)
set to 1 if a knockback should be started on the entity's next physics tick. It is set back to 0 when the knockback is started.