Animations (tm.anim)
Normalize, validate, play and preview animation references, and the game side of the kit's animation picker (KAnimField / KAnimPicker). The picker UI lives in the kit. This import is what it talks to.
Server code can use the normalizer (normalize, props, toEmote). Playing, previewing and the picker callbacks are client only.
The animation reference
Every panel's picker produces the same JSON safe table:
| Kind | Fields |
|---|---|
| Emote | { emote } |
| Scenario | { scenario, enter? } (enter = false skips the walk-up or sit-down) |
| Raw clip | { dict, clip, flag, blendIn, blendOut, speed, props } |
All kinds also take duration in seconds (empty means until something else plays). Props are { model, bone, x, y, z, rx, ry, rz }[] (bone index, offsets in metres, rotations in degrees, at most 3).
normalize only keeps what differs from the defaults, so the stored value stays small: flag 1, blendIn 8, blendOut 8, speed 1. Limits (kept in step with the kit): flag 0 to 1023, blendIn 0.5 to 16, blendOut -16 to 16, speed 0.1 to 4, duration 0.1 to 3600, offsets -5 to 5, rotations -360 to 360, strings are trimmed and cut to 64 (emote, scenario, prop model) or 128 (dict, clip) bytes without splitting a character.
Shared (client and server)
tm.anim.normalize(value)
Returns the canonical reference, or nil when it is not complete. Also takes what older code passes: 'emoteName', 'WORLD_HUMAN_SMOKING' (ALL_CAPS with an underscore reads as a scenario) and { dict, name, flag }.
tm.anim.normalize({ dict = 'amb@world_human_smoking@male@male_a@base', clip = 'base', flag = 49, speed = 1.0 })
--> { dict = '...', clip = 'base', flag = 49 }
tm.anim.normalize('wave') --> { emote = 'wave' }
tm.anim.normalize({ dict = 'only_a_dict' }) --> niltm.anim.props(list)
Cleans a prop list. Entries without a valid model name are dropped. Returns nil when nothing is left.
tm.anim.toEmote(anim)
A raw clip in the shape an emotes provider plays (name for the clip, duration in milliseconds, props as { model (hash), bone, offset }). nil for anything but a dict / clip animation.
Client
tm.anim.play(ped, anim) / tm.anim.stop(ped)
play accepts anything normalize accepts, stops whatever played on the ped, and returns true when it started. A reference with a duration ends by itself unless something else was played on the ped meanwhile. stop removes props and clears the ped's tasks.
- Scenarios use the game's scenario tasks.
- Emotes cannot play (see Emotes below):
playshows an error notification and returnsfalse. - Raw clips play natively, props included.
tm.anim.length(anim), tm.anim.check(anim)
length is the duration, else the clip's own length in seconds (nil for loops, emotes and scenarios without one). check({ dict, clip, props }) is what the picker's "is this right?" hint reports: { dict, clip, length, models } where models maps each prop model name to whether the game knows it.
tm.anim.preview(anim, opts)
Shows the animation on a ped about two metres in front of the admin until Enter, Backspace or Escape. The ped is a clone of the admin, or opts.model (with opts.appearance when tm.worldtools.ped.spawnGhost is available, otherwise a plain ped of that model). opts.withHidden(run) runs the preview while the calling panel is hidden. Returns false when there was nothing to show or a preview is already open. Leaving the resource while previewing removes the ped.
Emotes
tm_lib has no emote system and talks to no emote resource. An emote reference is accepted by tm.anim.normalize (it is only data) but cannot play, and tm.anim.emotes() is always empty, so the picker's Emote tab shows nothing.
tm.anim.registerCallbacks(opts)
Registers the NUI callbacks the picker posts. Call it once from the client script that owns the panel.
tm.anim.registerCallbacks({
withHidden = Menu.withHidden, -- optional, hides the panel while a preview or the prop editor runs
guard = function() return panelOpen end, -- optional, true while the panel is open
})| Callback | Does |
|---|---|
getEmotes | The emote list above, for the Emote tab. |
checkAnim | tm.anim.check of the posted clip. |
previewAnim | { anim, model?, appearance? } runs the preview. |
placeAnimProp | "Place with gizmo": tm.animeditor.editProp(data) returns the prop's new bone and offset. A cancel answers { ok = false } with no error text. |
animLibrary, saveAnim, deleteAnim | "My animations", see below. |
guard and withHidden only affect the panel. The library is checked for staff on the server whatever the UI does.
My animations
Named animations staff save from any panel's picker. One library for every resource, kept in the tm_anim_library table (sql/anim_library.sql, created at start). They are templates: picking one copies the animation into the record, so nothing reads the library at runtime and deleting an entry never breaks anything that used it.
- Staff only: every request is checked on the server through
tm.staffwhen that import exists, otherwisetm.fw.isAdmin. - At most 300 entries, label up to 64 bytes. Saves are validated with
tm.anim.normalize, so a client cannot store anything else. - The library is read into memory once, so listing never touches the database. Saves and deletes write through.
- Server callbacks (owned by
tm_lib):tm_lib:anim:list,tm_lib:anim:save,tm_lib:anim:delete. They answer{ ok, item?, items?, error? }with a translatederror.
Locale keys
anim.* (errors and the preview hint) in tm_lib/locales/en.json.