diff --git a/gamedata/scripts/callbacks_gameobject.script b/gamedata/scripts/callbacks_gameobject.script index 512727e88..52347fc88 100644 --- a/gamedata/scripts/callbacks_gameobject.script +++ b/gamedata/scripts/callbacks_gameobject.script @@ -193,6 +193,22 @@ _G.CActorHudOnNearWall = function(arg) SendScriptCallback("actor_hud_on_nearwall", arg) end +-- Script hud motion end callback +-- part is 0 for the right hand, 1 for the left hand, 2 for both +-- section is the hud section the motion belongs to, anm_name the motion that ended +-- reason is 0 when the motion reached its end, 1 when something stopped it early +AddScriptCallback("actor_hud_on_script_anim_end") +_G.CActorHudOnScriptAnimEnd = function(part, section, anm_name, reason) + SendScriptCallback("actor_hud_on_script_anim_end", part, section, anm_name, reason) +end + +-- Hands section change callback +-- fires when the player hud switches its hands section, sends the new and the previous section name +AddScriptCallback("actor_hud_on_hands_changed") +_G.CActorHudOnHandsChanged = function(new_section, old_section) + SendScriptCallback("actor_hud_on_hands_changed", new_section, old_section) +end + -- Mouse callbacks -- Scroll wheel callback, sends mouse wheel direction as a number -- 1 is up, 0 is down diff --git a/gamedata/scripts/lua_help_ex.script b/gamedata/scripts/lua_help_ex.script index eba40586e..447bfda1c 100644 --- a/gamedata/scripts/lua_help_ex.script +++ b/gamedata/scripts/lua_help_ex.script @@ -210,6 +210,8 @@ get_player_hud() { void set_hands(string section) void reset_hands() + string section_name() // the hud section currently loaded, empty string when none is + bool set_hands_visuals(string right_visual, string left_visual) // swaps the hand models inside the loaded section, left falls back to the right visual when empty, false when a visual is missing, is not animated, or lacks the bones the hud binds } namespace ActorMenu { @@ -545,6 +547,40 @@ // } // level.add_bullet(t) + // HUD hands, script motions and blend anms + u32 play_hud_motion(u8 hand, string itm_name, string anm_name, bool bMixIn, float speed, bool keep_freelook) // hand 0 right, 1 left, 2 both, returns the motion length in ms, 0 when the section is missing or the hand is out of range + void set_hud_motion_freelook(u8 hand, bool keep) // hand 0 right, 1 left, 2 both, keep lets freelook stay active while that script motion plays + bool hud_motion_exists(string section, string anm_name) // true when the section defines the motion and the loaded hands model has that cycle + int hud_motion_blocked_reason() // 0 nothing blocks, 1 right item pending, 2 right item boring, 3 left item pending, 4 left item boring, 5 a script motion already plays + int hud_motion_part() // 0 right, 1 left, 2 both for the script motion playing now, nil when none plays + bool hud_needs_blend(u8 part) // part 0 right, 1 left, true when that hand wants a blend anm this frame + void play_hud_anm(string name, u8 part, float speed, float power, bool bLooped) + void play_hud_anm(string name, u8 part, float speed, float power, bool bLooped, bool no_restart) + void play_hud_anm(string name, u8 part, float speed, float power, bool bLooped, bool no_restart, string pivot_bone, int priority) // priority orders the blend layers, higher is applied later, default 0 + void set_hud_anm_priority(string name, int priority) // reorders a blend layer that is already playing + void stop_hud_anm(string name, bool bForce, float fade_ms) // fades the layer out over fade_ms milliseconds + function hud_anm_state(string name) -> bool found, bool active, float blend // blend runs 0 to 1, found is false when no layer carries that name + bool hud_anm_exists(string name) // true when the anm file is found under $level$ or $game_anims$ + void set_hud_cycle_speed(u8 part, float speed) // part 0 both, 1 left hand, 2 right hand, multiplies the speed of the cycles playing there + void set_hud_cycle_time(u8 part, float time) // part 0 both, 1 left hand, 2 right hand, time is the position in the cycle, clamped 0 to 1 + void resync_hud_anim(u8 part) // part 1 left hand or 2 right hand, copies the cycles playing on that part and their time onto the other hand partitions + void set_blend_move_anims_override(bool mode) // true forces the movement blend anms on, false forces them off, nil returns control to the console flag + bool get_blend_move_anims() // the movement blend anm state in effect, override included + bool set_bare_hands(string section) // hud section idled when both hands are empty, nil clears it, false when the section is missing or has no anm_idle on the hands model + string get_bare_hands() // the bare hands section, nil when none is set + bool bare_hands_live() // true while the bare hands idle is the thing being drawn + u32 bare_hands_skipped() // hud draws and ui queries skipped since the hud was created while a bare hands section was set + void set_hud_offset(u8 part, float x, float y, float z, float pitch, float yaw, float roll, float blend_ms) // part 0 right, 1 left, 2 both, position in metres, angles in degrees with the aim_hud_offset_rot signs, eased in over blend_ms milliseconds + void clear_hud_offset(u8 part, float blend_ms) // part 0 right, 1 left, 2 both, eases the offset back out over blend_ms milliseconds + float get_hud_fov() // the hud fov the renderer is using + bool hud_visible() // true while any of the hud weapon draw flags is set + game_object hud_attached_item(u16 slot) // slot 0 right hand, 1 left hand, 2 scope, nil when the slot is empty + + // Actor freelook + int actor_freelook_state() // 0 disabled, 1 enabling, 2 enabled, 3 disabling + float actor_freelook_factor() // how far the freelook camera has taken control, 0 to 1 + float actor_freelook_yaw_split() // signed radians between the body yaw and the camera yaw + // Script Attachments function add_attachment(string, string) function get_attachment(string) @@ -887,6 +923,10 @@ bool set_hud_inertion_enabled() void hud_inertion_enabled(bool value) + // HUD item animation state + bool need_blend_anm() // true when this hud item wants a blend anm playing, false when the object is not a hud item + float hud_fov() // the hud fov this item renders with, 0 when the object is not a hud item + // Torch function update_torch() diff --git a/src/xrGame/Weapon.cpp b/src/xrGame/Weapon.cpp index 7c271b4d8..7f0b29a95 100644 --- a/src/xrGame/Weapon.cpp +++ b/src/xrGame/Weapon.cpp @@ -1307,9 +1307,9 @@ bool CWeapon::NeedBlendAnm() if (IsZoomed() && psDeviceFlags2.test(rsAimSway)) return true; - if (psDeviceFlags2.test(rsBlendMoveAnims)) + if (blend_move_anims_enabled()) return true; - + return inherited::NeedBlendAnm(); } @@ -3223,7 +3223,7 @@ void CWeapon::render_hud_mode() bool CWeapon::MovingAnimAllowedNow() { - return !IsZoomed() && !psDeviceFlags2.test(rsBlendMoveAnims); + return !IsZoomed() && !blend_move_anims_enabled(); } bool CWeapon::IsHudModeNow() diff --git a/src/xrGame/WeaponMagazinedWGrenade.cpp b/src/xrGame/WeaponMagazinedWGrenade.cpp index a60906c24..773b8f440 100644 --- a/src/xrGame/WeaponMagazinedWGrenade.cpp +++ b/src/xrGame/WeaponMagazinedWGrenade.cpp @@ -855,7 +855,7 @@ void CWeaponMagazinedWGrenade::PlayAnimIdle() if (m_bGrenadeMode) { - if (act_state == 0 || psDeviceFlags2.test(rsBlendMoveAnims)) + if (act_state == 0 || blend_move_anims_enabled()) iAmmoElapsed == 0 && HudAnimationExist("anm_idle_empty_g") ? PlayHUDMotion("anm_idle_empty_g", TRUE, NULL, GetState()) : PlayHUDMotion("anm_idle_g", TRUE, NULL, GetState()); @@ -880,7 +880,7 @@ void CWeaponMagazinedWGrenade::PlayAnimIdle() } else { - if (act_state == 0 || psDeviceFlags2.test(rsBlendMoveAnims)) + if (act_state == 0 || blend_move_anims_enabled()) iAmmoElapsed == 0 && HudAnimationExist("anm_idle_empty_w_gl") ? PlayHUDMotion("anm_idle_empty_w_gl", TRUE, NULL, GetState()) : PlayHUDMotion("anm_idle_w_gl", TRUE, NULL, GetState()); diff --git a/src/xrGame/console_commands.cpp b/src/xrGame/console_commands.cpp index 0eac01e43..4532e8031 100644 --- a/src/xrGame/console_commands.cpp +++ b/src/xrGame/console_commands.cpp @@ -140,6 +140,7 @@ extern BOOL pseudogiantDodgeWhileFalling; // Verdatim extern BOOL AllowAccelDuringLookOut; // Verdatim extern BOOL scale_hud_motion_marks_by_speed; // Verdatim + //demonized: new console vars extern BOOL firstPersonDeath; extern BOOL pseudogiantCanDamageObjects; @@ -2530,6 +2531,7 @@ void CCC_RegisterCommands() CMD3(CCC_Mask, "hud_crosshair", &psHUD_Flags, HUD_CROSSHAIR); CMD3(CCC_Mask, "hud_crosshair_dist", &psHUD_Flags, HUD_CROSSHAIR_DIST); + //#ifdef DEBUG CMD4(CCC_Float, "hud_fov", &psHUD_FOV_def, 0.1f, 1.0f); CMD4(CCC_Float, "fov", &g_fov, 5.0f, 180.0f); diff --git a/src/xrGame/level_script.cpp b/src/xrGame/level_script.cpp index 7b22c58b6..5bad6910c 100644 --- a/src/xrGame/level_script.cpp +++ b/src/xrGame/level_script.cpp @@ -1568,6 +1568,7 @@ void reload_language() } #include "player_hud.h" +#include "../xrEngine/CameraBase.h" void hud_adj_offs(int off, int idx, float x, float y, float z) { @@ -1639,21 +1640,126 @@ u32 PlayHudMotion(u8 hand, LPCSTR itm_name, LPCSTR anm_name, bool bMixIn = true, return g_player_hud->script_anim_play(hand, itm_name, anm_name, bMixIn, speed); } +// hand 0 is the right, 1 the left, 2 both +void SetHudMotionFreelook(u8 hand, bool keep) +{ + if (!g_player_hud) + return; + + if (hand > 2) + { + Msg("!set_hud_motion_freelook called with part %d, must be 0, 1 or 2", hand); + return; + } + + g_player_hud->SetScriptAnimFreelook(hand, keep); +} + +u32 PlayHudMotionFreelook(u8 hand, LPCSTR itm_name, LPCSTR anm_name, bool bMixIn, float speed, bool keep_freelook) +{ + if (!g_player_hud) + return 0; + + if (hand > 2) + { + Msg("!play_hud_motion called with part %d, must be 0, 1 or 2", hand); + return 0; + } + + if (!itm_name || !xr_strlen(itm_name) || !pSettings->section_exist(itm_name)) + { + Msg("!play_hud_motion section [%s] does not exist", itm_name ? itm_name : ""); + return 0; + } + + SetHudMotionFreelook(hand, keep_freelook); + + return g_player_hud->script_anim_play(hand, itm_name, anm_name, bMixIn, speed); +} + void StopHudMotion() { - g_player_hud->StopScriptAnim(); + if (!g_player_hud) + return; + + g_player_hud->StopScriptAnim(true); } float MotionLength(LPCSTR section, LPCSTR name, float speed) { + if (!g_player_hud) + return 0.f; + return g_player_hud->motion_length_script(section, name, speed); } bool AllowHudMotion() { + if (!g_player_hud) + return false; + return g_player_hud->allow_script_anim(); } +bool HudMotionExists(LPCSTR section, LPCSTR anm_name) +{ + if (!g_player_hud || !g_player_hud->m_model) + return false; + + if (!section || !section[0] || !anm_name || !anm_name[0]) + { + Msg("!hud_motion_exists called with an empty section or motion name"); + return false; + } + + if (!pSettings->section_exist(section)) + { + Msg("!hud_motion_exists section [%s] does not exist", section); + return false; + } + + player_hud_motion_container* pm = g_player_hud->get_hand_motions(section); + player_hud_motion* phm = pm ? pm->find_motion(anm_name) : nullptr; + + return phm && !phm->m_animations.empty() && g_player_hud->m_model->ID_Cycle_Safe(phm->m_base_name).valid(); +} + +int HudMotionBlockedReason() +{ + if (!g_player_hud) + return 0; + + return g_player_hud->script_anim_blocked_reason(); +} + +::luabind::object HudMotionPart() +{ + lua_State* L = ai().script_engine().lua(); + + if (g_player_hud && g_player_hud->script_anim_part < 3) + return ::luabind::object(L, int(g_player_hud->script_anim_part)); + + ::luabind::object none(L); + lua_pushnil(L); + none.set(); + + return none; +} + +bool HudNeedsBlend(u8 part) +{ + if (!g_player_hud) + return false; + + if (part > 1) + { + Msg("!hud_needs_blend called with part %d, must be 0 or 1", part); + return false; + } + + return g_player_hud->need_blend_anm(part); +} + bool MotionExists(LPCSTR model_path, LPCSTR motion_name) { ::Render->hud_loading = true; @@ -1671,6 +1777,17 @@ void PlayBlendAnm(LPCSTR name, u8 part, float speed, float power, bool bLooped, g_player_hud->PlayBlendAnm(name, part, speed, power, bLooped, no_restart, pivot_bone); } +// short call forms kept as exact arities so overload matching still finds a body +void PlayBlendAnm6(LPCSTR name, u8 part, float speed, float power, bool bLooped, bool no_restart) +{ + PlayBlendAnm(name, part, speed, power, bLooped, no_restart, ""); +} + +void PlayBlendAnm5(LPCSTR name, u8 part, float speed, float power, bool bLooped) +{ + PlayBlendAnm(name, part, speed, power, bLooped, false, ""); +} + void StopBlendAnm(LPCSTR name, bool bForce) { g_player_hud->StopBlendAnm(name, bForce); @@ -1686,6 +1803,254 @@ float SetBlendAnmTime(LPCSTR name, float time) return g_player_hud->SetBlendAnmTime(name, time); } +void PlayBlendAnmPrio(LPCSTR name, u8 part, float speed, float power, bool bLooped, bool no_restart, LPCSTR pivot_bone, int priority) +{ + if (!g_player_hud) + return; + + g_player_hud->PlayBlendAnm(name, part, speed, power, bLooped, no_restart, pivot_bone); + g_player_hud->SetBlendAnmPriority(name, priority); +} + +void SetBlendAnmPriority(LPCSTR name, int priority) +{ + if (!g_player_hud) + return; + + g_player_hud->SetBlendAnmPriority(name, priority); +} + +void StopBlendAnmFade(LPCSTR name, bool bForce, float fade_ms) +{ + if (!g_player_hud) + return; + + g_player_hud->StopBlendAnmFade(name, bForce, fade_ms / 1000.f); +} + +bool BlendAnmState(LPCSTR name, bool& active, float& blend) +{ + if (!g_player_hud) + { + active = false; + blend = 0.f; + return false; + } + + return g_player_hud->BlendAnmState(name, active, blend); +} + +bool BlendAnmExists(LPCSTR name) +{ + if (!name || !name[0]) + { + Msg("!hud_anm_exists called with an empty name"); + return false; + } + + string_path full_path; + + return !!FS.exist(full_path, "$level$", name) || !!FS.exist(full_path, "$game_anims$", name); +} + +// part 0 is the root, 1 the left hand, 2 the right hand +void SetHudCycleSpeed(u8 part, float speed) +{ + if (!g_player_hud || !g_player_hud->m_model || !g_player_hud->m_model_2) + return; + + if (part > 2) + { + Msg("!set_hud_cycle_speed called with part %d, must be 0, 1 or 2", part); + return; + } + + g_player_hud->set_part_cycle_speed(part, speed); +} + +void SetHudCycleTime(u8 part, float time) +{ + if (!g_player_hud || !g_player_hud->m_model || !g_player_hud->m_model_2) + return; + + if (part > 2) + { + Msg("!set_hud_cycle_time called with part %d, must be 0, 1 or 2", part); + return; + } + + clamp(time, 0.f, 1.f); + g_player_hud->set_part_cycle_time(part, time); +} + +void ResyncHudAnim(u8 part) +{ + if (!g_player_hud || !g_player_hud->m_model || !g_player_hud->m_model_2) + return; + + if (part != 1 && part != 2) + { + Msg("!resync_hud_anim called with part %d, must be 1 or 2", part); + return; + } + + g_player_hud->re_sync_anim(part); +} + +void SetBlendMoveAnimsOverride(const ::luabind::object& mode) +{ + if (!mode || mode.type() != LUA_TBOOLEAN) + g_blend_move_anims_override = -1; + else + g_blend_move_anims_override = ::luabind::object_cast(mode) ? 1 : 0; + + if (g_player_hud) + g_player_hud->updateMovementLayerState(); +} + +bool GetBlendMoveAnims() +{ + return blend_move_anims_enabled(); +} + +bool SetBareHands(const ::luabind::object& section) +{ + if (!g_player_hud) + return false; + + if (!section || section.type() != LUA_TSTRING) + return g_player_hud->SetBareHands(nullptr); + + return g_player_hud->SetBareHands(::luabind::object_cast(section)); +} + +::luabind::object GetBareHands() +{ + lua_State* L = ai().script_engine().lua(); + + if (g_player_hud && g_player_hud->bare_hands_section().size()) + return ::luabind::object(L, g_player_hud->bare_hands_section().c_str()); + + ::luabind::object none(L); + lua_pushnil(L); + none.set(); + + return none; +} + +bool BareHandsLive() +{ + return g_player_hud && g_player_hud->bare_hands_active(); +} + +u32 BareHandsSkipped() +{ + return g_player_hud ? g_player_hud->bare_hands_skipped() : 0; +} + +// part 0 is the right hand, 1 the left hand, 2 both, angles in degrees and position in metres +void SetHudOffset(u8 part, float x, float y, float z, float pitch, float yaw, float roll, float blend_ms) +{ + if (!g_player_hud) + return; + + if (part > 2) + { + Msg("!set_hud_offset called with part %d, must be 0, 1 or 2", part); + return; + } + + Fvector pos = { x, y, z }; + Fvector rot = { yaw, pitch, roll }; + + g_player_hud->SetHudOffset(part, pos, rot, blend_ms / 1000.f); +} + +void ClearHudOffset(u8 part, float blend_ms) +{ + if (!g_player_hud) + return; + + if (part > 2) + { + Msg("!clear_hud_offset called with part %d, must be 0, 1 or 2", part); + return; + } + + g_player_hud->ClearHudOffset(part, blend_ms / 1000.f); +} + +ENGINE_API extern float psHUD_FOV; + +float GetHudFov() +{ + return psHUD_FOV; +} + +// 0 = disabled, 1 = enabling, 2 = enabled, 3 = disabling +int ActorFreelookState() +{ + CActor* actor = g_actor; + if (!actor) + return 0; + + switch (actor->cam_freelook) + { + case eflEnabling: + return 1; + case eflEnabled: + return 2; + case eflDisabling: + return 3; + default: + return 0; + } +} + +float ActorFreelookFactor() +{ + CActor* actor = g_actor; + + return actor ? actor->freelook_cam_control : 0.f; +} + +// signed radians between the body yaw and the camera yaw +float ActorFreelookYawSplit() +{ + CActor* actor = g_actor; + if (!actor || !actor->cam_FirstEye()) + return 0.f; + + float body_yaw = -angle_normalize_signed(actor->old_torso_yaw); + float cam_yaw = -angle_normalize_signed(actor->cam_FirstEye()->yaw); + + return angle_difference_signed(body_yaw, cam_yaw); +} + +bool HudVisible() +{ + return !!psHUD_Flags.is(HUD_WEAPON | HUD_WEAPON_RT | HUD_WEAPON_RT2 | HUD_DRAW_RT2); +} + +// slot 0 is the right hand, 1 the left hand, 2 the scope +CScriptGameObject* HudAttachedItem(u16 slot) +{ + if (!g_player_hud) + return nullptr; + + if (slot > SCOPE_ATTACH_IDX) + { + Msg("!hud_attached_item called with slot %d, must be 0, 1 or 2", slot); + return nullptr; + } + + attachable_hud_item* item = g_player_hud->attached_item(slot); + if (!item || !item->m_parent_hud_item || !item->m_parent_hud_item->has_object()) + return nullptr; + + return item->m_parent_hud_item->object().lua_game_object(); +} + void block_all_except_movement(bool b) { g_block_all_except_movement = b; @@ -1732,6 +2097,9 @@ void remove_hud_model(LPCSTR section) const u32 ActorMovingState() { + if (!g_actor) + return 0; + return g_actor->MovingState(); } @@ -2663,6 +3031,9 @@ void CLevel::script_register(lua_State* L) def("hold_action", &LevelHoldAction), def("actor_moving_state", &ActorMovingState), + def("actor_freelook_state", &ActorFreelookState), + def("actor_freelook_factor", &ActorFreelookFactor), + def("actor_freelook_yaw_split", &ActorFreelookYawSplit), def("get_env_rads", &get_env_rads), def("iterate_nearest", &iterate_nearest), def("pick_material", &PickMaterial), @@ -2827,14 +3198,41 @@ void CLevel::script_register(lua_State* L) def("reload_language", &reload_language), def("get_resolutions", &vid_modes_string), def("play_hud_motion", PlayHudMotion), + def("play_hud_motion", PlayHudMotionFreelook), + def("set_hud_motion_freelook", SetHudMotionFreelook), def("stop_hud_motion", StopHudMotion), def("get_motion_length", MotionLength), def("hud_motion_allowed", AllowHudMotion), def("motion_exists", MotionExists), + def("hud_motion_exists", HudMotionExists), + def("hud_motion_blocked_reason", HudMotionBlockedReason), + def("hud_motion_part", HudMotionPart), + def("hud_needs_blend", HudNeedsBlend), def("play_hud_anm", PlayBlendAnm), + def("play_hud_anm", PlayBlendAnmPrio), + def("play_hud_anm", PlayBlendAnm6), + def("play_hud_anm", PlayBlendAnm5), + def("set_hud_anm_priority", SetBlendAnmPriority), def("stop_hud_anm", StopBlendAnm), + def("stop_hud_anm", StopBlendAnmFade), def("stop_all_hud_anms", StopAllBlendAnms), def("set_hud_anm_time", SetBlendAnmTime), + def("hud_anm_state", BlendAnmState, pure_out_value<2>() + pure_out_value<3>()), + def("hud_anm_exists", BlendAnmExists), + def("set_hud_cycle_speed", SetHudCycleSpeed), + def("set_hud_cycle_time", SetHudCycleTime), + def("resync_hud_anim", ResyncHudAnim), + def("set_blend_move_anims_override", SetBlendMoveAnimsOverride), + def("get_blend_move_anims", GetBlendMoveAnims), + def("set_bare_hands", SetBareHands), + def("get_bare_hands", GetBareHands), + def("bare_hands_live", BareHandsLive), + def("bare_hands_skipped", BareHandsSkipped), + def("set_hud_offset", SetHudOffset), + def("clear_hud_offset", ClearHudOffset), + def("get_hud_fov", GetHudFov), + def("hud_visible", HudVisible), + def("hud_attached_item", HudAttachedItem), def("only_allow_movekeys", block_all_except_movement), def("only_movekeys_allowed", only_movement_allowed), def("set_actor_allow_ladder", set_actor_allow_ladder), diff --git a/src/xrGame/player_hud.cpp b/src/xrGame/player_hud.cpp index 58aa5a49f..6ea1e63e2 100644 --- a/src/xrGame/player_hud.cpp +++ b/src/xrGame/player_hud.cpp @@ -15,6 +15,16 @@ extern int g_nearwall; player_hud* g_player_hud = NULL; +int g_blend_move_anims_override = -1; + +bool blend_move_anims_enabled() +{ + if (g_blend_move_anims_override >= 0) + return g_blend_move_anims_override > 0; + + return !!psDeviceFlags2.test(rsBlendMoveAnims); +} + Fvector _ancor_pos; Fvector _wpn_root_pos; @@ -631,6 +641,14 @@ u32 attachable_hud_item::anim_play(const shared_str& anm_name_b, BOOL bMixIn, co float speed, bool bMixIn2) { player_hud_motion* anm = find_motion(anm_name_b); + if (!anm || anm->m_animations.empty()) + { + Msg("!hud motion [%s] in section [%s] has no animations", anm_name_b.c_str(), m_sect_name.c_str()); + md = nullptr; + rnd_idx = 0; + return 0; + } + rnd_idx = (u8)Random.randI(anm->m_animations.size()); const motion_descr& M = anm->m_animations[rnd_idx]; if (speed == 1.f) @@ -724,9 +742,19 @@ player_hud::player_hud() m_transform_2.identity(); m_adjust_mode = false; script_anim_part = u8(-1); + script_anim_last_part = u8(-1); + script_anim_keep_freelook[0] = false; + script_anim_keep_freelook[1] = false; script_anim_offset_factor = 0.f; + script_anim_offset[0].set(0.f, 0.f, 0.f); + script_anim_offset[1].set(0.f, 0.f, 0.f); m_item_pos.identity(); script_override_arms = false; + m_bare_hands_motions = nullptr; + m_bare_hands_idle = u8(-1); + m_bare_hands_live = false; + m_bare_hands_replay = false; + m_bare_skipped = 0; //Bone Callback Params m_bone_callback_params.insert(mk_pair(r_finger0, xr_new())); @@ -778,6 +806,8 @@ player_hud::~player_hud() delete_data(m_script_layers); delete_data(m_movement_layers); delete_data(m_bone_callback_params); + + g_blend_move_anims_override = -1; } void player_hud::FingerCallback(CBoneInstance* B) @@ -818,9 +848,14 @@ void player_hud::load(const shared_str& player_hud_sect, bool force) { if (!force && player_hud_sect == m_sect_name) return; + shared_str prev_sect = m_sect_name; m_sect_name = player_hud_sect; - if (script_override_arms) return; + if (script_override_arms) + { + notify_hands_changed(prev_sect); + return; + } if (m_model) { @@ -840,6 +875,12 @@ void player_hud::load(const shared_str& player_hud_sect, bool force) bool b_reload = (m_attached_items[0] != nullptr || m_attached_items[1] != nullptr); ::Render->hud_loading = false; + setup_hands(player_hud_sect, b_reload); + notify_hands_changed(prev_sect); +} + +void player_hud::setup_hands(const shared_str& player_hud_sect, bool b_reload) +{ u16 l_arm = m_model->dcast_PKinematics()->LL_BoneID("l_clavicle"); u16 r_arm = m_model_2->dcast_PKinematics()->LL_BoneID("r_clavicle"); @@ -851,9 +892,13 @@ void player_hud::load(const shared_str& player_hud_sect, bool force) //u16 bone_r_triggerfinger01 = m_model->dcast_PKinematics()->LL_BoneID("bip01_r_finger11"); //u16 bone_r_triggerfinger02 = m_model->dcast_PKinematics()->LL_BoneID("bip01_r_finger12"); - m_model->dcast_PKinematics()->LL_GetBoneInstance(bone_r_finger0).set_callback(bctCustom, FingerCallback, m_bone_callback_params[r_finger0]); - m_model->dcast_PKinematics()->LL_GetBoneInstance(bone_r_finger01).set_callback(bctCustom, FingerCallback, m_bone_callback_params[r_finger01]); - m_model->dcast_PKinematics()->LL_GetBoneInstance(bone_r_finger02).set_callback(bctCustom, FingerCallback, m_bone_callback_params[r_finger02]); + // LL_GetBoneInstance has no range check so a missing finger bone skips its callback + if (bone_r_finger0 != BI_NONE) + m_model->dcast_PKinematics()->LL_GetBoneInstance(bone_r_finger0).set_callback(bctCustom, FingerCallback, m_bone_callback_params[r_finger0]); + if (bone_r_finger01 != BI_NONE) + m_model->dcast_PKinematics()->LL_GetBoneInstance(bone_r_finger01).set_callback(bctCustom, FingerCallback, m_bone_callback_params[r_finger01]); + if (bone_r_finger02 != BI_NONE) + m_model->dcast_PKinematics()->LL_GetBoneInstance(bone_r_finger02).set_callback(bctCustom, FingerCallback, m_bone_callback_params[r_finger02]); //m_model->dcast_PKinematics()->LL_GetBoneInstance(bone_r_triggerfinger0).set_callback(bctCustom, FingerCallback, m_bone_callback_params[bip01_r_finger1]); //m_model->dcast_PKinematics()->LL_GetBoneInstance(bone_r_triggerfinger01).set_callback(bctCustom, FingerCallback, m_bone_callback_params[bip01_r_finger11]); @@ -863,18 +908,39 @@ void player_hud::load(const shared_str& player_hud_sect, bool force) m_model->dcast_PKinematics()->LL_SetBoneVisible(l_arm, FALSE, TRUE); m_model_2->dcast_PKinematics()->LL_SetBoneVisible(r_arm, FALSE, TRUE); - CInifile::Sect& _sect = pSettings->r_section(player_hud_sect); - CInifile::SectCIt _b = _sect.Data.begin(); - CInifile::SectCIt _e = _sect.Data.end(); - for (; _b != _e; ++_b) + // ancor ids come from the model just bound, keep the old list if the new one is empty + xr_vector fresh_ancors; + if (pSettings->section_exist(player_hud_sect)) { - if (strstr(_b->first.c_str(), "ancor_") == _b->first.c_str()) + CInifile::Sect& _sect = pSettings->r_section(player_hud_sect); + CInifile::SectCIt _b = _sect.Data.begin(); + CInifile::SectCIt _e = _sect.Data.end(); + for (; _b != _e; ++_b) { - const shared_str& _bone = _b->second; - m_ancors.push_back(m_model->dcast_PKinematics()->LL_BoneID(_bone)); + if (strstr(_b->first.c_str(), "ancor_") == _b->first.c_str()) + { + const shared_str& _bone = _b->second; + fresh_ancors.push_back(m_model->dcast_PKinematics()->LL_BoneID(_bone)); + } } } + if (!fresh_ancors.empty()) + m_ancors.swap(fresh_ancors); + + for (hand_motions* phm : m_hand_motions) + { + phm->pm.m_anims.clear(); + if (pSettings->section_exist(phm->section.c_str())) + phm->pm.load(m_model, phm->section); + } + + m_hud_offsets[0].clear(); + m_hud_offsets[1].clear(); + script_anim_keep_freelook[0] = false; + script_anim_keep_freelook[1] = false; + m_bare_hands_idle = u8(-1); + if (!b_reload) { m_model->PlayCycle("hand_idle_doun"); @@ -900,6 +966,127 @@ void player_hud::load(const shared_str& player_hud_sect, bool force) //--DSR-- HeatVision_end } +void player_hud::notify_hands_changed(const shared_str& prev_sect) +{ + if (prev_sect == m_sect_name) + return; + + LPCSTR now = m_sect_name.size() ? m_sect_name.c_str() : ""; + LPCSTR prev = prev_sect.size() ? prev_sect.c_str() : ""; + + ::luabind::functor on_hands_changed; + if (ai().script_engine().functor("_G.CActorHudOnHandsChanged", on_hands_changed)) + on_hands_changed(now, prev); +} + +static bool hud_visual_exists(LPCSTR visual) +{ + string_path fn, name; + + if (0 == strext(visual)) + strconcat(sizeof(name), name, visual, ".ogf"); + else + xr_strcpy(name, sizeof(name), visual); + + return !!FS.exist(visual) || !!FS.exist(fn, "$level$", name) || !!FS.exist(fn, "$game_meshes$", name); +} + +static LPCSTR hands_visual_missing_part(IKinematicsAnimated* model, const LPCSTR* bones, u32 bone_count) +{ + IKinematics* kin = model->dcast_PKinematics(); + + for (u32 i = 0; i < bone_count; ++i) + { + if (kin->LL_BoneID(bones[i]) == BI_NONE) + return bones[i]; + } + + if (!model->ID_Cycle_Safe("hand_idle_doun").valid()) + return "hand_idle_doun"; + + return nullptr; +} + +bool player_hud::set_hands_visuals(LPCSTR right_visual, LPCSTR left_visual) +{ + if (!right_visual || !right_visual[0]) + { + Msg("!player_hud::set_hands_visuals called without a right hand visual"); + return false; + } + + if (!left_visual || !left_visual[0]) + left_visual = right_visual; + + if (!hud_visual_exists(right_visual) || !hud_visual_exists(left_visual)) + { + Msg("!player_hud::set_hands_visuals cannot find [%s] or [%s]", right_visual, left_visual); + return false; + } + + ::Render->hud_loading = true; + IRenderVisual* visual = ::Render->model_Create(right_visual); + IRenderVisual* visual_2 = ::Render->model_Create(left_visual); + ::Render->hud_loading = false; + + IKinematicsAnimated* model = smart_cast(visual); + IKinematicsAnimated* model_2 = smart_cast(visual_2); + + if (!model || !model_2) + { + Msg("!player_hud::set_hands_visuals [%s] or [%s] is not an animated visual", right_visual, left_visual); + + if (visual) + ::Render->model_Delete(visual); + + if (visual_2) + ::Render->model_Delete(visual_2); + + return false; + } + + // setup_hands binds these bones and plays that idle so a visual without them never replaces the hands + static const LPCSTR right_parts[] = {"l_clavicle", "r_finger0", "r_finger01", "r_finger02"}; + static const LPCSTR left_parts[] = {"r_clavicle"}; + + LPCSTR bad_visual = right_visual; + LPCSTR missing = hands_visual_missing_part(model, right_parts, 4); + + if (!missing) + { + bad_visual = left_visual; + missing = hands_visual_missing_part(model_2, left_parts, 1); + } + + if (missing) + { + Msg("!player_hud::set_hands_visuals [%s] has no [%s], hud section [%s] keeps its hands", bad_visual, missing, m_sect_name.size() ? m_sect_name.c_str() : ""); + + ::Render->model_Delete(visual); + ::Render->model_Delete(visual_2); + + return false; + } + + if (m_model) + { + IRenderVisual* v = m_model->dcast_RenderVisual(); + ::Render->model_Delete(v); + } + if (m_model_2) + { + IRenderVisual* v = m_model_2->dcast_RenderVisual(); + ::Render->model_Delete(v); + } + + m_model = model; + m_model_2 = model_2; + + setup_hands(m_sect_name, (m_attached_items[0] != nullptr || m_attached_items[1] != nullptr)); + + return true; +} + void player_hud::load_script(LPCSTR section) { script_override_arms = false; @@ -907,9 +1094,14 @@ void player_hud::load_script(LPCSTR section) script_override_arms = true; } +bool player_hud::bare_hands_scripted() const +{ + return m_bare_hands_sect.size() && script_anim_part != u8(-1); +} + bool player_hud::render_item_ui_query() { - bool res = false; + bool res = m_bare_hands_live || bare_hands_active() || bare_hands_scripted(); if (m_attached_items[0]) res |= m_attached_items[0]->render_item_ui_query(); @@ -919,6 +1111,9 @@ bool player_hud::render_item_ui_query() if (m_attached_items[SCOPE_ATTACH_IDX]) res |= m_attached_items[SCOPE_ATTACH_IDX]->render_item_ui_query(); + if (!res && m_bare_hands_sect.size() && !m_attached_items[0] && !m_attached_items[1]) + ++m_bare_skipped; + return res; } @@ -943,8 +1138,18 @@ void player_hud::render_item_ui() if (g_actor->GetAttachments()->size()) { for (auto& pair : *g_actor->GetAttachments()) - if (pair.second->GetType() == eSA_HUD) - pair.second->RenderUI(); + { + if (pair.second->GetType() != eSA_HUD) continue; + + if (!m_bare_hands_live && !bare_hands_active() && !bare_hands_scripted() && !m_attached_items[0] && !m_attached_items[1]) + { + if (m_bare_hands_sect.size()) + ++m_bare_skipped; + continue; + } + + pair.second->RenderUI(); + } } } @@ -952,6 +1157,12 @@ void player_hud::render_hud() { bool b_r0 = ((m_attached_items[0] && m_attached_items[0]->need_renderable()) || script_anim_part == 0 || script_anim_part == 2); bool b_r1 = ((m_attached_items[1] && m_attached_items[1]->need_renderable()) || script_anim_part == 1 || script_anim_part == 2); + // bare hands are drawn with nothing attached, the cached flag lags one tick behind a motion that just ended + if (m_bare_hands_live || bare_hands_active()) + b_r0 = b_r1 = true; + + if (!b_r0 && !b_r1 && m_bare_hands_sect.size() && !m_attached_items[0] && !m_attached_items[1]) + ++m_bare_skipped; if (!b_r0 && !b_r1) return; @@ -1016,6 +1227,12 @@ u32 player_hud::motion_length_script(LPCSTR section, LPCSTR anm_name, float spee return 0; } + if (phm->m_animations.empty()) + { + Msg("!script motion [%s] in section [%s] has no animations", anm_name, section); + return 0; + } + const CMotionDef* temp; return motion_length(phm->m_animations[0].mid, temp, speed); } @@ -1038,7 +1255,7 @@ u32 player_hud::motion_length(const MotionID& M, const CMotionDef*& md, float sp { md = m_model->LL_GetMotionDef(M); VERIFY(md); - if (md->flags & esmStopAtEnd) + if (md != nullptr && md->flags & esmStopAtEnd) { CMotion* motion = m_model->LL_GetRootMotion(M); return iFloor(0.5f + 1000.f * motion->GetLength() / (md->Dequantize(md->speed) * speed)); @@ -1072,6 +1289,8 @@ extern float psHUD_FOV; void player_hud::update(const Fmatrix& cam_trans) { + update_bare_hands(); + Fmatrix trans = cam_trans; Fmatrix trans_b = cam_trans; CWeapon* wep = smart_cast(Actor()->inventory().ActiveItem()); @@ -1160,32 +1379,50 @@ void player_hud::update(const Fmatrix& cam_trans) trans = trans_2; // override hand offset for single hand animation - if (script_anim_part == 2 || (script_anim_part && !m_attached_items[0] && !m_attached_items[1])) + if (script_anim_part == 2 || (script_anim_part && script_anim_part != u8(-1) && !m_attached_items[0] && !m_attached_items[1])) { m1pos = script_anim_offset[0]; m2pos = script_anim_offset[0]; m1rot = script_anim_offset[1]; m2rot = script_anim_offset[1]; - trans = trans_b; - trans_2 = trans_b; + + if (!script_anim_keep_freelook[0]) + trans = trans_b; + + if (!script_anim_keep_freelook[1]) + trans_2 = trans_b; } else if (script_anim_offset_factor != 0.f) { - Fvector& hand_pos = script_anim_part == 0 ? m1pos : m2pos; - Fvector& hand_rot = script_anim_part == 0 ? m1rot : m2rot; - - hand_pos.lerp(script_anim_part == 0 ? m1pos : m2pos, script_anim_offset[0], script_anim_offset_factor); - hand_rot.lerp(script_anim_part == 0 ? m1rot : m2rot, script_anim_offset[1], script_anim_offset_factor); + // the live part is already cleared when a motion ends so the blend follows the part that played + const u8 blend_part = script_anim_last_part; + const bool blend_hand_0 = (blend_part == 0 || blend_part == 2); + const bool blend_hand_1 = (blend_part != 0); - if (script_anim_part == 0) + if (blend_hand_0) { - trans_b.inertion(trans, script_anim_offset_factor); - trans = trans_b; + m1pos.lerp(m1pos, script_anim_offset[0], script_anim_offset_factor); + m1rot.lerp(m1rot, script_anim_offset[1], script_anim_offset_factor); + + if (!script_anim_keep_freelook[0]) + { + Fmatrix base = trans_b; + base.inertion(trans, script_anim_offset_factor); + trans = base; + } } - else + + if (blend_hand_1) { - trans_b.inertion(trans_2, script_anim_offset_factor); - trans_2 = trans_b; + m2pos.lerp(m2pos, script_anim_offset[0], script_anim_offset_factor); + m2rot.lerp(m2rot, script_anim_offset[1], script_anim_offset_factor); + + if (!script_anim_keep_freelook[1]) + { + Fmatrix base = trans_b; + base.inertion(trans_2, script_anim_offset_factor); + trans_2 = base; + } } } @@ -1214,9 +1451,9 @@ void player_hud::update(const Fmatrix& cam_trans) continue; if (anm->active) - anm->blend_amount += Device.fTimeDelta / .4f; + anm->blend_amount += Device.fTimeDelta / anm->m_fade_time; else - anm->blend_amount -= Device.fTimeDelta / .4f; + anm->blend_amount -= Device.fTimeDelta / anm->m_fade_time; clamp(anm->blend_amount, 0.f, 1.f); @@ -1268,9 +1505,29 @@ void player_hud::update(const Fmatrix& cam_trans) } } + for (u8 part = 0; part < 2; ++part) + { + hud_offset& off = m_hud_offsets[part]; + + if (off.active) + off.blend_amount += Device.fTimeDelta / off.m_fade_time; + else + off.blend_amount -= Device.fTimeDelta / off.m_fade_time; + + clamp(off.blend_amount, 0.f, 1.f); + + if (off.blend_amount == 0.f) + continue; + + if (part == 0) + m_transform.mulB_43(off.XFORM()); + else + m_transform_2.mulB_43(off.XFORM()); + } + bool need_blend[2]; - need_blend[0] = ((script_anim_part == 0 || script_anim_part == 2) || (m_attached_items[0] && m_attached_items[0]->m_parent_hud_item->NeedBlendAnm())); - need_blend[1] = ((script_anim_part == 1 || script_anim_part == 2) || (m_attached_items[1] && m_attached_items[1]->m_parent_hud_item->NeedBlendAnm())); + need_blend[0] = need_blend_anm(0); + need_blend[1] = need_blend_anm(1); for (movement_layer* anm : m_movement_layers) { @@ -1360,7 +1617,7 @@ void player_hud::update(const Fmatrix& cam_trans) void player_hud::updateMovementLayerState() { - CActor* pActor = Actor(); + CActor* pActor = g_actor; if (!pActor) return; @@ -1370,7 +1627,7 @@ void player_hud::updateMovementLayerState() anm->Stop(false); } - bool need_blend = (script_anim_part != u8(-1) || (m_attached_items[0] && m_attached_items[0]->m_parent_hud_item->NeedBlendAnm()) || (m_attached_items[1] && m_attached_items[1]->m_parent_hud_item->NeedBlendAnm())); + bool need_blend = (script_anim_part != u8(-1) || bare_hands_active() || (m_attached_items[0] && m_attached_items[0]->m_parent_hud_item->NeedBlendAnm()) || (m_attached_items[1] && m_attached_items[1]->m_parent_hud_item->NeedBlendAnm())); if (need_blend) { @@ -1438,20 +1695,119 @@ void player_hud::PlayBlendAnm(LPCSTR name, u8 part, float speed, float power, bo script_layer* anm = xr_new(name, part, speed, power, bLooped, pivot_bone); m_script_layers.push_back(anm); + sort_script_layers(); } void player_hud::StopBlendAnm(LPCSTR name, bool bForce) +{ + StopBlendAnmFade(name, bForce, 0.f); +} + +void player_hud::StopBlendAnmFade(LPCSTR name, bool bForce, float fade_time) { for (script_layer* anm : m_script_layers) { if (!xr_strcmp(*anm->m_name, name)) { + if (fade_time > 0.f) + anm->m_fade_time = _max(fade_time, .001f); + anm->Stop(bForce); return; } } } +void player_hud::sort_script_layers() +{ + std::stable_sort(m_script_layers.begin(), m_script_layers.end(), + [](const script_layer* a, const script_layer* b) { return a->m_priority < b->m_priority; }); +} + +void player_hud::SetBlendAnmPriority(LPCSTR name, int priority) +{ + for (script_layer* anm : m_script_layers) + { + if (!xr_strcmp(*anm->m_name, name)) + { + if (anm->m_priority != priority) + { + anm->m_priority = priority; + sort_script_layers(); + } + return; + } + } +} + +bool player_hud::BlendAnmState(LPCSTR name, bool& active, float& blend) +{ + for (script_layer* anm : m_script_layers) + { + if (!xr_strcmp(*anm->m_name, name)) + { + active = anm->active; + blend = anm->blend_amount; + return true; + } + } + + active = false; + blend = 0.f; + return false; +} + +void player_hud::SetHudOffset(u8 part, const Fvector& pos, const Fvector& rot, float fade_time) +{ + if (part == 2) + { + SetHudOffset(0, pos, rot, fade_time); + SetHudOffset(1, pos, rot, fade_time); + return; + } + + if (part > 1) + return; + + hud_offset& off = m_hud_offsets[part]; + off.m_pos = pos; + off.m_rot = rot; + off.m_fade_time = _max(fade_time, .001f); + off.active = true; +} + +void player_hud::ClearHudOffset(u8 part, float fade_time) +{ + if (part == 2) + { + ClearHudOffset(0, fade_time); + ClearHudOffset(1, fade_time); + return; + } + + if (part > 1) + return; + + hud_offset& off = m_hud_offsets[part]; + off.m_fade_time = _max(fade_time, .001f); + off.active = false; +} + +void player_hud::SetScriptAnimFreelook(u8 part, bool keep) +{ + if (part == 2) + { + script_anim_keep_freelook[0] = keep; + script_anim_keep_freelook[1] = keep; + return; + } + + if (part > 1) + return; + + script_anim_keep_freelook[part] = keep; +} + void player_hud::StopAllBlendAnms(bool bForce) { for (script_layer* anm : m_script_layers) @@ -1506,13 +1862,130 @@ void play_blend(player_hud* hud, u8 pid, const MotionID& M, BOOL bMixIn, float s } } +bool player_hud::bare_hands_active() const +{ + return m_bare_hands_sect.size() && !m_attached_items[0] && !m_attached_items[1] && script_anim_part == u8(-1); +} + +// 0 = idle, 1 = moving, 2 = sprint +u8 player_hud::bare_hands_idle_kind() +{ + CActor* actor = g_actor; + if (!actor || !actor->AnyMove()) + return 0; + + // the movement layers play the walk when they are on, the same check a hud item uses + if (blend_move_anims_enabled()) + return 0; + + CEntity::SEntityState state; + actor->g_State(state); + + if (state.bSprint) + return 2; + + if (!state.bCrouch) + return 1; + + return 0; +} + +void player_hud::update_bare_hands() +{ + bool live = bare_hands_active(); + + // anything that took the hands has to replay the idle once they are free again + if (!live) + m_bare_hands_replay = true; + + if (live != m_bare_hands_live) + { + m_bare_hands_live = live; + updateMovementLayerState(); + } + else if (live && m_bare_hands_replay) + { + // one update past the flip so a script's next clip goes first + m_bare_hands_replay = false; + m_bare_hands_idle = u8(-1); + } + + if (!live || !m_bare_hands_motions) + return; + + u8 kind = bare_hands_idle_kind(); + if (kind == m_bare_hands_idle) + return; + + m_bare_hands_idle = kind; + + LPCSTR name = kind == 2 ? "anm_idle_sprint" : kind == 1 ? "anm_idle_moving" : "anm_idle"; + player_hud_motion* pm = m_bare_hands_motions->find_motion(name); + + if (!pm || pm->m_animations.empty()) + pm = m_bare_hands_motions->find_motion("anm_idle"); + + if (!pm || pm->m_animations.empty()) + return; + + const motion_descr& M = pm->m_animations[Random.randI(pm->m_animations.size())]; + play_blend(this, 0, M.mid, TRUE, 1.f); +} + +bool player_hud::SetBareHands(LPCSTR section) +{ + if (!section || !section[0]) + { + m_bare_hands_sect = nullptr; + m_bare_hands_motions = nullptr; + update_bare_hands(); + + return true; + } + + if (!m_model) + { + Msg("!player_hud::SetBareHands called with no hands model loaded"); + return false; + } + + if (!pSettings->section_exist(section)) + { + Msg("!player_hud::SetBareHands section [%s] does not exist", section); + return false; + } + + player_hud_motion_container* pm = get_hand_motions(section); + player_hud_motion* idle = pm ? pm->find_motion("anm_idle") : nullptr; + + if (!idle || idle->m_animations.empty() || !m_model->ID_Cycle_Safe(idle->m_base_name).valid()) + { + Msg("!player_hud::SetBareHands section [%s] has no anm_idle on the hands model", section); + return false; + } + + m_bare_hands_sect = section; + m_bare_hands_motions = pm; + m_bare_hands_idle = u8(-1); + update_bare_hands(); + + return true; +} + extern BOOL print_bone_warnings; -void player_hud::StopScriptAnim() +void player_hud::StopScriptAnim(bool forced) { u8 part = script_anim_part; + shared_str section = script_anim_section; + shared_str anm_name = script_anim_name; + script_anim_part = u8(-1); script_anim_item_model = nullptr; script_anim_lead_gun = false; + script_anim_section = nullptr; + script_anim_name = nullptr; + script_anim_keep_freelook[0] = false; + script_anim_keep_freelook[1] = false; updateMovementLayerState(); @@ -1525,10 +1998,21 @@ void player_hud::StopScriptAnim() } } - if (part < 2 && !m_attached_items[part]) + // bare hands keep their last frame, the bare hands update restarts the idle itself + if (part < 2 && !bare_hands_active()) re_sync_anim(part + 1); - else + + if (part > 1 || m_attached_items[part]) OnMovementChanged((ACTOR_DEFS::EMoveCommand)0); + + // fires last so a handler that starts the next motion is not overwritten by the resync + // reason 0 = the motion reached its end, 1 = something stopped it early + if (part < 3 && section.size()) + { + ::luabind::functor on_anim_end; + if (ai().script_engine().functor("_G.CActorHudOnScriptAnimEnd", on_anim_end)) + on_anim_end(int(part), section.c_str(), anm_name.c_str(), forced ? 1 : 0); + } } //part: 0 = right arm; 1 = left arm @@ -1631,6 +2115,7 @@ u32 player_hud::script_anim_play(u8 hand, LPCSTR section, LPCSTR anm_name, bool script_anim_offset[0] = offs; script_anim_offset[1] = rrot; script_anim_part = hand; + script_anim_last_part = hand; player_hud_motion_container* pm = get_hand_motions(section); player_hud_motion* phm = pm->find_motion(anm_name); @@ -1644,8 +2129,24 @@ u32 player_hud::script_anim_play(u8 hand, LPCSTR section, LPCSTR anm_name, bool return 0; } + if (phm->m_animations.empty()) + { + Msg("!script motion [%s] in section [%s] has no animations", anm_name, section); + m_bStopAtEndAnimIsRunning = true; + script_anim_end = Device.dwTimeGlobal; + + return 0; + } + + // set once the motion is known to exist so a failed play reports nothing + script_anim_section = section; + script_anim_name = anm_name; + const motion_descr& M = phm->m_animations[Random.randI(phm->m_animations.size())]; + // the loader replaces a missing cycle with the idle down pose under the requested name, so check the picked entry by its name + const bool cycle_resolved = m_model->ID_Cycle_Safe(M.name).valid(); + if (script_anim_item_model) { shared_str item_anm_name; @@ -1674,7 +2175,7 @@ u32 player_hud::script_anim_play(u8 hand, LPCSTR section, LPCSTR anm_name, bool play_blend(this, (hand == 2 ? 0 : hand == 0 ? 2 : 1), M.mid, bMixIn, speed, true); - const CMotionDef* md; + const CMotionDef* md = nullptr; u32 length = motion_length(M.mid, md, speed); if (length > 0) @@ -1682,8 +2183,15 @@ u32 player_hud::script_anim_play(u8 hand, LPCSTR section, LPCSTR anm_name, bool m_bStopAtEndAnimIsRunning = true; script_anim_end = Device.dwTimeGlobal + length; } + else if (!cycle_resolved) + { + m_bStopAtEndAnimIsRunning = true; + script_anim_end = Device.dwTimeGlobal; + } else + { m_bStopAtEndAnimIsRunning = false; + } updateMovementLayerState(); @@ -1722,13 +2230,28 @@ void player_hud::attach_item(CHudItem* item) } } +static void sync_part_cycle(IKinematicsAnimated* model, u16 pid, const MotionID& M, const CBlend* src) +{ + CBlend* B = model->PlayCycle(pid, M, TRUE); + if (!B) + return; + + B->timeCurrent = src->timeCurrent; + B->speed = src->speed; +} + //sync anim of other part to selected part (1 = sync to left hand anim; 2 = sync to right hand anim) void player_hud::re_sync_anim(u8 part) { - u32 bc = part == 1 ? m_model_2->LL_PartBlendsCount(part) : m_model->LL_PartBlendsCount(part); + if (part != 1 && part != 2) + return; + + IKinematicsAnimated* src = part == 1 ? m_model_2 : m_model; + + u32 bc = src->LL_PartBlendsCount(part); for (u32 bidx = 0; bidx < bc; ++bidx) { - CBlend* BR = part == 1 ? m_model_2->LL_PartBlend(part, bidx) : m_model->LL_PartBlend(part, bidx); + CBlend* BR = src->LL_PartBlend(part, bidx); if (!BR) continue; @@ -1737,21 +2260,12 @@ void player_hud::re_sync_anim(u8 part) u16 pc = m_model->partitions().count(); //same on both armatures for (u16 pid = 0; pid < pc; ++pid) { - if (pid == 0) - { - CBlend* B = m_model->PlayCycle(0, M, TRUE); - B->timeCurrent = BR->timeCurrent; - B->speed = BR->speed; - B = m_model_2->PlayCycle(0, M, TRUE); - B->timeCurrent = BR->timeCurrent; - B->speed = BR->speed; - } - else if (pid != part) - { - CBlend* B = part == 1 ? m_model->PlayCycle(pid, M, TRUE) : m_model_2->PlayCycle(pid, M, TRUE); - B->timeCurrent = BR->timeCurrent; - B->speed = BR->speed; - } + // the right arm armature never drives its left hand partition, the source already plays + if (pid != 1 && !(src == m_model && pid == part)) + sync_part_cycle(m_model, pid, M, BR); + + if (!(src == m_model_2 && pid == part)) + sync_part_cycle(m_model_2, pid, M, BR); } } } @@ -1784,7 +2298,9 @@ void player_hud::set_part_cycle_speed(u8 part, float speed) { set_part_cycle_speed(1, speed); set_part_cycle_speed(2, speed); + return; } + u32 bc = part == 1 ? m_model_2->LL_PartBlendsCount(part) : m_model->LL_PartBlendsCount(part); for (u32 bidx = 0; bidx < bc; ++bidx) { @@ -1848,14 +2364,44 @@ void player_hud::detach_item(CHudItem* item) bool player_hud::allow_script_anim() { - if (m_attached_items[0] && (m_attached_items[0]->m_parent_hud_item->IsPending() || m_attached_items[0]->m_parent_hud_item->GetState() == CHudItem::EHudStates::eBore)) - return false; - else if (m_attached_items[1] && (m_attached_items[1]->m_parent_hud_item->IsPending() || m_attached_items[1]->m_parent_hud_item->GetState() == CHudItem::EHudStates::eBore)) - return false; - else if (script_anim_part != u8(-1)) + return script_anim_blocked_reason() == 0; +} + +// 0 = allowed, 1/2 = right hand item pending/bore, 3/4 = left hand item, 5 = a script anim holds the hands +int player_hud::script_anim_blocked_reason() +{ + if (m_attached_items[0]) + { + if (m_attached_items[0]->m_parent_hud_item->IsPending()) + return 1; + if (m_attached_items[0]->m_parent_hud_item->GetState() == CHudItem::EHudStates::eBore) + return 2; + } + + if (m_attached_items[1]) + { + if (m_attached_items[1]->m_parent_hud_item->IsPending()) + return 3; + if (m_attached_items[1]->m_parent_hud_item->GetState() == CHudItem::EHudStates::eBore) + return 4; + } + + if (script_anim_part != u8(-1)) + return 5; + + return 0; +} + +bool player_hud::need_blend_anm(u8 part) +{ + if (part > 1) return false; - return true; + if (bare_hands_active()) + return true; + + return ((script_anim_part == part || script_anim_part == 2) || + (m_attached_items[part] && m_attached_items[part]->m_parent_hud_item->NeedBlendAnm())); } void player_hud::calc_transform(u16 attach_slot_idx, const Fmatrix& offset, Fmatrix& result, bool leadGun) diff --git a/src/xrGame/player_hud.h b/src/xrGame/player_hud.h index 35a37c077..dccaaacd0 100644 --- a/src/xrGame/player_hud.h +++ b/src/xrGame/player_hud.h @@ -152,6 +152,8 @@ struct script_layer Fmatrix blend; u8 m_part; shared_str m_pivot_bone; + int m_priority; + float m_fade_time; script_layer(LPCSTR name, u8 part, float speed = 1.f, float power = 1.f, bool looped = true, LPCSTR pivot_bone = nullptr) { @@ -159,6 +161,8 @@ struct script_layer m_part = part; m_power = power; m_pivot_bone = pivot_bone; + m_priority = 0; + m_fade_time = .4f; blend.identity(); anm = xr_new(); anm->Load(name); @@ -216,6 +220,57 @@ struct script_layer } }; +struct hud_offset +{ + Fvector m_pos; + Fvector m_rot; + float blend_amount; + float m_fade_time; + bool active; + Fmatrix blend; + + hud_offset() + { + clear(); + } + + void clear() + { + m_pos.set(0.f, 0.f, 0.f); + m_rot.set(0.f, 0.f, 0.f); + blend_amount = 0.f; + m_fade_time = .4f; + active = false; + blend.identity(); + } + + const Fmatrix& XFORM() + { + auto min_jerk_interp = [](float t) { + return 10*t*t*t - 15*t*t*t*t + 6*t*t*t*t*t; + }; + + float eased = min_jerk_interp(blend_amount); + + Fvector ypr = m_rot; + ypr.mul(PI / 180.f); + + Fmatrix full; + // rotation signs match aim_hud_offset_rot + full.setHPB(-ypr.x, -ypr.y, -ypr.z); + + Fquaternion qA; qA.identity(); + Fquaternion qB; qB.set(full); + Fquaternion q; q.slerp(qA, qB, eased); + + Fvector t = m_pos; + t.mul(eased); + + blend.mk_xform(q, t); + return blend; + } +}; + struct BoneCallbackParams { Fvector m_current; @@ -368,7 +423,7 @@ class player_hud void load_default() { load("actor_hud_05", true); }; void update(const Fmatrix& trans); void updateMovementLayerState(); - void StopScriptAnim(); + void StopScriptAnim(bool forced = false); void PlayBlendAnm(LPCSTR name, u8 part = 0, float speed = 1.f, float power = 1.f, bool bLooped = true, bool no_restart = false, LPCSTR pivot_bone = nullptr); void StopBlendAnm(LPCSTR name, bool bForce = false); void StopAllBlendAnms(bool bForce); @@ -379,11 +434,23 @@ class player_hud u32 anim_play(u16 part, const MotionID& M, BOOL bMixIn, const CMotionDef*& md, float speed, u16 override_part = u16(-1)); u32 script_anim_play(u8 hand, LPCSTR itm_name, LPCSTR anm_name, bool bMixIn = true, float speed = 1.f); const shared_str& section_name() const { return m_sect_name; } + bool set_hands_visuals(LPCSTR right_visual, LPCSTR left_visual); + bool SetBareHands(LPCSTR section); + const shared_str& bare_hands_section() const { return m_bare_hands_sect; } + bool bare_hands_active() const; + bool bare_hands_scripted() const; + u32 bare_hands_skipped() const { return m_bare_skipped; } void OnFrame(); void net_Relcase(CObject* obj); u8 script_anim_part; + // the part of the last script motion, kept while the offset blends back out + u8 script_anim_last_part; + bool script_anim_keep_freelook[2]; + hud_offset m_hud_offsets[2]; Fvector script_anim_offset[2]; + shared_str script_anim_section; + shared_str script_anim_name; u32 script_anim_end; float script_anim_offset_factor; bool m_bStopAtEndAnimIsRunning; @@ -414,6 +481,16 @@ class player_hud void detach_item(CHudItem* item); bool allow_script_anim(); + int script_anim_blocked_reason(); + bool need_blend_anm(u8 part); + + void StopBlendAnmFade(LPCSTR name, bool bForce, float fade_time); + void SetBlendAnmPriority(LPCSTR name, int priority); + bool BlendAnmState(LPCSTR name, bool& active, float& blend); + + void SetHudOffset(u8 part, const Fvector& pos, const Fvector& rot, float fade_time); + void ClearHudOffset(u8 part, float fade_time); + void SetScriptAnimFreelook(u8 part, bool keep); void detach_all_items() { @@ -437,9 +514,20 @@ class player_hud bool inertion_allowed(); private: + void sort_script_layers(); + void setup_hands(const shared_str& player_hud_sect, bool b_reload); + void notify_hands_changed(const shared_str& prev_sect); + void update_bare_hands(); + u8 bare_hands_idle_kind(); const Fvector attach_rot(u8 part) const; const Fvector attach_pos(u8 part) const; shared_str m_sect_name; + shared_str m_bare_hands_sect; + player_hud_motion_container* m_bare_hands_motions; + u8 m_bare_hands_idle; + bool m_bare_hands_live; + bool m_bare_hands_replay; + u32 m_bare_skipped; xr_vector m_ancors; attachable_hud_item* m_attached_items[3]; static void _BCL FingerCallback(CBoneInstance* B); @@ -491,3 +579,8 @@ class player_hud }; extern player_hud* g_player_hud; + + +// -1 follows the console flag, 0 forces the movement blend off, 1 forces it on +extern int g_blend_move_anims_override; +bool blend_move_anims_enabled(); diff --git a/src/xrGame/player_hud_script.cpp b/src/xrGame/player_hud_script.cpp index 2233b4aae..8a0a974a0 100644 --- a/src/xrGame/player_hud_script.cpp +++ b/src/xrGame/player_hud_script.cpp @@ -8,6 +8,11 @@ player_hud* get_player_hud() return g_player_hud; } +LPCSTR hud_section_name(player_hud* hud) +{ + return (hud && hud->section_name().size()) ? hud->section_name().c_str() : ""; +} + #pragma optimize("s",on) void player_hud::script_register(lua_State* L) { @@ -16,7 +21,9 @@ void player_hud::script_register(lua_State* L) class_("player_hud") .def(constructor<>()) .def("set_hands", &player_hud::load_script) - .def("reset_hands", &player_hud::reset_model_script), + .def("reset_hands", &player_hud::reset_model_script) + .def("section_name", &hud_section_name) + .def("set_hands_visuals", &player_hud::set_hands_visuals), def("get_player_hud", &get_player_hud) ]; diff --git a/src/xrGame/script_game_object.h b/src/xrGame/script_game_object.h index 71cc24df4..bad87350d 100644 --- a/src/xrGame/script_game_object.h +++ b/src/xrGame/script_game_object.h @@ -1058,6 +1058,8 @@ class CScriptGameObject //Any class that is derived from CHudItem u32 PlayHudMotion(LPCSTR M, bool bMixIn, u32 state, float speed = 0.f, float end = 0.f); + bool NeedBlendAnm(); + float GetHudFov(); void SwitchState(u32 state); u32 GetState(); Fvector hud_fire_point(); diff --git a/src/xrGame/script_game_object3.cpp b/src/xrGame/script_game_object3.cpp index 1a6011629..0ccea22b1 100644 --- a/src/xrGame/script_game_object3.cpp +++ b/src/xrGame/script_game_object3.cpp @@ -1842,6 +1842,26 @@ u32 CScriptGameObject::PlayHudMotion(LPCSTR M, bool bMixIn, u32 state, float spe return itm->PlayHUDMotion(M, bMixIn, itm, state, speed, end); } +bool CScriptGameObject::NeedBlendAnm() +{ + CInventoryItem* inv_itm = object().cast_inventory_item(); + CHudItem* itm = inv_itm ? inv_itm->cast_hud_item() : nullptr; + if (!itm) + return false; + + return itm->NeedBlendAnm(); +} + +float CScriptGameObject::GetHudFov() +{ + CInventoryItem* inv_itm = object().cast_inventory_item(); + CHudItem* itm = inv_itm ? inv_itm->cast_hud_item() : nullptr; + if (!itm) + return 0.f; + + return itm->GetHudFov(); +} + void CScriptGameObject::SwitchState(u32 state) { CWeapon* Weapon = object().cast_weapon(); diff --git a/src/xrGame/script_game_object_script3.cpp b/src/xrGame/script_game_object_script3.cpp index 3e462a03b..d7d6e4c28 100644 --- a/src/xrGame/script_game_object_script3.cpp +++ b/src/xrGame/script_game_object_script3.cpp @@ -518,6 +518,8 @@ class_ script_register_game_object2(class_ .def("weapon_in_grenade_mode", &CScriptGameObject::WeaponInGrenadeMode) // For CHudItem .def("play_hud_motion", SAFE_WRAP(&CScriptGameObject::PlayHudMotion)) + .def("need_blend_anm", SAFE_WRAP(&CScriptGameObject::NeedBlendAnm)) + .def("hud_fov", SAFE_WRAP(&CScriptGameObject::GetHudFov)) .def("switch_state", SAFE_WRAP(&CScriptGameObject::SwitchState)) .def("get_state", SAFE_WRAP(&CScriptGameObject::GetState)) .def("hud_fire_point", SAFE_WRAP(&CScriptGameObject::hud_fire_point))