Исходник Semi proper animation fix (sup paste ready)

edit added "silent updates" fix after comment war lol.

credits: https://yougame.biz/threads/325142/

Пожалуйста, авторизуйтесь для просмотра ссылки.

formated bonzo code, added velocity recalculation and land/fall fix:
Expand Collapse Copy
#include "includes.h"
// credits: https://yougame.biz/threads/325142/
// https://github.com/LAITHCOOL/laith-legendware-rework/blob/05d18bde61644291e29a5d81f9772c16770fe223/cheats/lagcompensation/animation_system.cpp

void handle_silent_updates(Player* player, c_animstate* state) {
    if (!player || !state) return;

    // Check if the last frame was updated or if there is a significant time difference
    if (state->last_update_frame == g_csgo.m_globals->m_framecount) {
        // No need to update, as the frame was recently updated
        return;
    }

    // If a silent update is detected, reapply or interpolate the last known correct state
    float time_difference = g_csgo.m_globals->m_curtime - state->last_update_time;

    // If the time difference exceeds a threshold, consider it a silent update
    if (time_difference > g_csgo.m_globals->m_interval * 2) {
        // Reapply the last known state or interpolate to the current state
        state->eye_yaw = interpolate(state->eye_yaw, player->m_flLowerBodyYawTarget(), time_difference);
    }

    // Update the state variables to the current frame and time
    state->last_update_frame = g_csgo.m_globals->m_framecount;
    state->last_update_time = g_csgo.m_globals->m_curtime;

// Mini update function for handling animations
void anims::mini_update(Player* player) {
    if (!player) return;  // check for null pointer.

    C_AnimationLayer backup_layers[13];
    player->GetAnimLayers(backup_layers);

    // backup globalvars.
    float curtime = g_csgo.m_globals->m_curtime;
    float frame = g_csgo.m_globals->m_frame;
    float frametime = g_csgo.m_globals->m_frametime;

    // set frametime to IPT just like on the server during simulation.
    g_csgo.m_globals->m_curtime = player->m_flSimulationTime();
    g_csgo.m_globals->m_frame = g_csgo.m_cl->m_server_tick;
    g_csgo.m_globals->m_frametime = g_csgo.m_globals->m_interval;

    // remove abs velocity.
    player->m_iEFlags() &= ~0x1000;
    player->SetAbsVelocity(player->m_vecVelocity());

  // Handle silent updates before updating animations
   handle_silent_updates(player, player->m_AnimState());

    // update animations.
    g_hooks.m_UpdateClientSideAnimation(player);

    // restore layers to networked.
    player->SetAnimLayers(backup_layers);

    // restore once we're done.
    g_csgo.m_globals->m_frame = frame;
    g_csgo.m_globals->m_curtime = curtime;
    g_csgo.m_globals->m_frametime = frametime;
}

// function to reset state variables.
void reset_state_vars(Player* player, c_animstate* state, LagRecord* record) {
    if (!player || !state || !record) return;

    state->on_ground = !!(player->m_fFlags() & FL_ONGROUND);
    state->landing = false;
    state->abs_yaw_last = state->abs_yaw = player->m_flLowerBodyYawTarget();
    state->eye_yaw = player->m_flLowerBodyYawTarget();
    state->eye_pitch = record->m_eye_angles.x;
    state->primary_cycle = record->m_layers[6].m_cycle;
    state->move_weight = record->m_layers[6].m_weight;
    state->strafe_sequence = record->m_layers[7].m_sequence;
    state->strafe_change_weight = record->m_layers[7].m_weight;
    state->strafe_change_cycle = record->m_layers[7].m_cycle;
    state->acceleration_weight = record->m_layers[12].m_weight;

    player->m_flOldSimulationTime() = state->last_update_time = record->m_sim_time - g_csgo.m_globals->m_interval;
    state->last_update_frame = g_csgo.m_cl->m_server_tick - 1;
    state->duration_in_air = 0.f;

    record->m_anim_velocity = player->m_vecVelocity();
    if (state->on_ground) {
        record->m_anim_velocity.z = 0.f;
        if (state->primary_cycle == 0.f || state->move_weight == 0.f) {
            record->m_anim_velocity = vec3_t();
        }
    }
}

// function to apply previous animation state.
void apply_previous_state(Player* player, c_animstate* state, LagRecord* record,  LagRecord* previous) {
    if (!player || !state || !record || !previous) return;

    player->SetAnimLayers(previous->m_layers);
    player->SetPoseParameters(previous->m_poses);
    state->primary_cycle = previous->m_layers[6].m_cycle;
    state->move_weight = previous->m_layers[6].m_weight;
    state->strafe_sequence = previous->m_layers[7].m_sequence;
    state->strafe_change_weight = previous->m_layers[7].m_weight;
    state->strafe_change_cycle = previous->m_layers[7].m_cycle;
    state->acceleration_weight = previous->m_layers[12].m_weight;

    // https://gitlab.com/KittenPopo/csgo-2018-source/-/blob/main/game/client/c_baseplayer.cpp#L1139
    // void C_BasePlayer::PostDataUpdate( DataUpdateType_t updateType )
    record->m_anim_velocity = (record->m_origin - previous->m_origin) * (1.f / game::TICKS_TO_TIME(record->m_lag));

    // task for reader: add proper velocity recalculation
    // hint CCSGOPlayerAnimState::SetUpAliveLoop
    // Ensure the player is on the ground and adjust the z-component of the velocity
    if ((previous->m_flags & FL_ONGROUND) && (record->m_flags & FL_ONGROUND)) {
        record->m_anim_velocity.z = 0.f;

        // Check if the player is in a fake walk state where the animation might have stopped
        if (record->m_layers[6].m_playback_rate == 0.f) {
            record->m_anim_velocity = vec3_t(); // Set velocity to zero if not moving
        }
    }
    else {
        // Handle airborne or jumping states where gravity and falling velocity should be calculated
        if (!(previous->m_flags & FL_ONGROUND)) {
            record->m_anim_velocity.z -= g_csgo.sv_gravity->GetFloat() * g_csgo.m_globals->m_interval;
        }
    }

    // Apply the recalculated velocity to the player's current state
    player->m_vecVelocity() = record->m_anim_velocity;
    player->SetAbsVelocity(record->m_anim_velocity);
}

void SimulatePlayerActivity(Player* pPlayer, LagRecord* m_LagRecord, LagRecord* m_PrevRecord) {
    if (!pPlayer || !m_LagRecord || !m_PrevRecord) return;

    // get animation layers.
    const C_AnimationLayer* m_JumpingLayer = &pPlayer->m_AnimOverlay()[ANIMATION_LAYER_MOVEMENT_JUMP_OR_FALL];
    const C_AnimationLayer* m_LandingLayer = &pPlayer->m_AnimOverlay()[ANIMATION_LAYER_MOVEMENT_LAND_OR_CLIMB];
    const C_AnimationLayer* m_PrevJumpingLayer = &pPlayer->m_AnimOverlay()[ANIMATION_LAYER_MOVEMENT_JUMP_OR_FALL];
    const C_AnimationLayer* m_PrevLandingLayer = &pPlayer->m_AnimOverlay()[ANIMATION_LAYER_MOVEMENT_LAND_OR_CLIMB];

    // detect jump/land, collect its data, rebuild time in air.
    const int nJumpingActivity = pPlayer->GetSequenceActivity(m_JumpingLayer->m_sequence);
    const int nLandingActivity = pPlayer->GetSequenceActivity(m_LandingLayer->m_sequence);

    // collect jump data.
    if (nJumpingActivity == ACT_CSGO_JUMP) {
        // check duration bounds.
        if (m_JumpingLayer->m_weight > 0.0f && m_JumpingLayer->m_playback_rate > 0.0f) {
            // check cycle changed.
            if (m_JumpingLayer->m_cycle < m_PrevJumpingLayer->m_cycle) {
                m_LagRecord->m_flDurationInAir = m_JumpingLayer->m_cycle / m_JumpingLayer->m_playback_rate;
                if (m_LagRecord->m_flDurationInAir > 0.0f) {
                    m_LagRecord->m_nActivityTick = game::TIME_TO_TICKS(m_LagRecord->m_sim_time - m_LagRecord->m_flDurationInAir) + 1;
                    m_LagRecord->m_nActivityType = EPlayerActivityC::CJump;
                }
            }
        }
    }

    // collect land data.
    if (nLandingActivity == ACT_CSGO_LAND_LIGHT || nLandingActivity == ACT_CSGO_LAND_HEAVY) {
        // weight changing everytime on activity switch in this layer.
        if (m_LandingLayer->m_weight > 0.0f && m_PrevLandingLayer->m_weight <= 0.0f) {
            // check cycle changed.
            if (m_LandingLayer->m_cycle > m_PrevLandingLayer->m_cycle) {
                float flLandDuration = m_LandingLayer->m_cycle / m_LandingLayer->m_playback_rate;

                if (flLandDuration > 0.0f) {
                    m_LagRecord->m_nActivityTick = game::TIME_TO_TICKS(m_LagRecord->m_sim_time - flLandDuration) + 1;
                    m_LagRecord->m_nActivityType = EPlayerActivityC::CLand;

                    // determine duration in air.
                    float flDurationInAir = (m_JumpingLayer->m_cycle - m_LandingLayer->m_cycle);
                    if (flDurationInAir < 0.0f)
                        flDurationInAir += 1.0f;

                    // set time in air.
                    m_LagRecord->m_flDurationInAir = flDurationInAir / m_JumpingLayer->m_playback_rate;
                }
            }
        }
    }
}

float ComputeActivityPlayback(Player* pPlayer, LagRecord* m_Record) {
    if (!pPlayer || !m_Record)  return 0.0f;

    // get animation layers.
    C_AnimationLayer* m_JumpingLayer = &pPlayer->m_AnimOverlay()[ANIMATION_LAYER_MOVEMENT_JUMP_OR_FALL];
    C_AnimationLayer* m_LandingLayer = &pPlayer->m_AnimOverlay()[ANIMATION_LAYER_MOVEMENT_LAND_OR_CLIMB];
    C_AnimationLayer* moving = &pPlayer->m_AnimOverlay()[ANIMATION_LAYER_MOVEMENT_MOVE];

    // determine playback.
    float flActivityPlayback = 0.0f;

    switch (m_Record->m_nActivityType) {
    case EPlayerActivityC::CJump:
    {
        flActivityPlayback = pPlayer->GetLayerSequenceCycleRate(m_JumpingLayer, m_JumpingLayer->m_sequence);
    }
    break;

    case EPlayerActivityC::CLand:
    {
        flActivityPlayback = pPlayer->GetLayerSequenceCycleRate(m_LandingLayer, m_LandingLayer->m_sequence);
    }
    break;
    }

    return flActivityPlayback;

}

// Function to recalculate player velocity based on different conditions.
void RecalculateVelocity(Player* player, LagRecord* record, LagRecord* previous) {
    // Check if the player is valid
    if (!player || !record) return;

    // Check if previous record exists
    if (!previous) {
        // Default velocity calculation, using current velocity and potentially other factors.
        record->m_anim_velocity = player->m_vecVelocity();
        return;
    }

    // Calculate the velocity difference between the current and previous records
    vec3_t velocity_diff = record->m_origin - previous->m_origin;
    float time_diff = game::TICKS_TO_TIME(record->m_lag);

    // Handle case where there is no time difference to prevent division by zero
    if (time_diff > 0.f) {
        record->m_anim_velocity = velocity_diff * (1.f / time_diff);
    }
    else {
        // Set velocity to zero if time difference is invalid
        record->m_anim_velocity = vec3_t();
    }

    // Adjust z component for on-ground state
    if (record->m_flags & FL_ONGROUND) {
        record->m_anim_velocity.z = 0.f;
    }
    else {
        // Apply gravity for airborne state
        record->m_anim_velocity.z -= g_csgo.sv_gravity->GetFloat() * g_csgo.m_globals->m_interval;
    }
}

// function to handle the main update logic.
void perform_update_logic(Player* player, LagRecord* record, LagRecord* previous, c_animstate* state) {
    if (!player || !record || !state) return;

    // simulate player activity (jump/land) before processing.
    SimulatePlayerActivity(player, record, previous);

    // Recalculate velocity before updating animations.
    RecalculateVelocity(player, record, previous);

    // compute activity playback ( jump and land ).
    record->m_flActivityPlayback = ComputeActivityPlayback(player, record);

    for (int i = 1; i <= record->m_lag; i++) {
        const float interp = std::clamp(static_cast<float>(i) / static_cast<float>(record->m_lag), 0.f, 1.f);

        if (previous && record->m_lag > 1) {
            g_csgo.m_globals->m_curtime = math::lerp(interp, previous->m_sim_time, record->m_sim_time);
            player->m_vecVelocity() = math::lerp(interp, previous->m_anim_velocity, record->m_anim_velocity);
            player->m_flDuckAmount() = math::lerp(interp, previous->m_duck, record->m_duck);

            // handle jump and land activity.
            if (record->m_nActivityType != EPlayerActivityC::CNoActivity) {
                if (record->m_lag == record->m_nActivityTick) {
                    // compute the correct animation layer.
                    int nLayer = ANIMATION_LAYER_MOVEMENT_JUMP_OR_FALL;
                    if (record->m_nActivityType == EPlayerActivityC::CLand)
                        nLayer = ANIMATION_LAYER_MOVEMENT_LAND_OR_CLIMB;

                    // set the player's animation state based on activity.
                    player->m_AnimOverlay()[nLayer].m_cycle = 0.0f;
                    player->m_AnimOverlay()[nLayer].m_weight = 0.0f;
                    player->m_AnimOverlay()[nLayer].m_playback_rate = record->m_flActivityPlayback;

                    // update player's ground state based on activity.
                    if (record->m_nActivityType == EPlayerActivityC::CJump)
                        player->m_fFlags() &= ~FL_ONGROUND;
                    else if (record->m_nActivityType == EPlayerActivityC::CLand)
                        player->m_fFlags() |= FL_ONGROUND;
                }
                else if (record->m_lag < record->m_nActivityTick) {
                    // force the player's ground state before the activity tick.
                    if (record->m_nActivityType == EPlayerActivityC::CJump)
                        player->m_fFlags() |= FL_ONGROUND;
                    else if (record->m_nActivityType == EPlayerActivityC::CLand)
                        player->m_fFlags() &= ~FL_ONGROUND;
                }
            }
        }
        else {
            player->m_vecVelocity() = record->m_anim_velocity;
        }

        player->m_iEFlags() &= ~0x1000;
        player->SetAbsVelocity(player->m_vecVelocity());
        player->m_angEyeAngles() = record->m_eye_angles;

        g_hooks.m_UpdateClientSideAnimation(player);
        record->m_abs_ang = ang_t(0.f, state->abs_yaw, 0.f);
        player->GetPoseParameters(record->m_poses);
    }

    player->UpdateCollisionBounds();
    record->m_maxs = player->m_vecMaxs();
    record->m_mins = player->m_vecMins();
    player->SetAnimLayers(record->m_layers);
    record->m_setup = player->SetupBones(record->m_bones, 128, BONE_USED_BY_ANYTHING, state->last_update_time);
}

// restore global variables after animation update.
void restore_globals(float frame, float curtime, float frametime) {
    g_csgo.m_globals->m_frame = frame;
    g_csgo.m_globals->m_curtime = curtime;
    g_csgo.m_globals->m_frametime = frametime;
}

// update function for handling detailed player animations.
void anims::update(Player* player, LagRecord* record, LagRecord* previous) {
    c_animstate* state = player->m_PlayerAnimState();

    // backup globalvars.
    float curtime = g_csgo.m_globals->m_curtime;
    float frame = g_csgo.m_globals->m_frame;
    float frametime = g_csgo.m_globals->m_frametime;

    // backup current animation state.
    AnimationBackup_t backup;
    backup.store(player);

    // set timing and simulate player animation state.
    g_csgo.m_globals->m_curtime = record->m_anim_time;
    g_csgo.m_globals->m_frame = g_csgo.m_cl->m_server_tick;
    g_csgo.m_globals->m_frametime = g_csgo.m_globals->m_interval;

    // handle player origins and reset state vars if needed.
    player->SetAbsOrigin(player->m_vecOrigin());

    // fix our animstate vars when animstate resets
    if (state->last_update_time == 0.f || !previous) {
        reset_state_vars(player, state, record);
    }
    // or fix animstate netvars using networked values
    else if (previous) {
        apply_previous_state(player, state, record, previous);
    }

    // resolve angles and update animations.
    if (!previous || (record->m_lag != 1 || previous->m_lag != 1)) {
        g_resolver.ResolveAngles(player, record);
    }

    perform_update_logic(player, record, previous, state);

    // restore backup and globalvars.
    backup.apply(player);
    restore_globals(frame, curtime, frametime);
}

// task: hook C_BasePlayer::PostDataUpdate() and call this in it
// or use FRAME_NET_UPDATE_POSTDATAUPDATE_START
void anims::player_instance(Player* player) {
    if (!player) return;

    c_animstate* state = player->m_PlayerAnimState();
    if (!state)
        return;

    AimPlayer* data = &g_aimbot.m_players[player->index() - 1];

    if (!player->alive() || !g_cl.m_processing) {
        if (player->m_flSimulationTime() != player->m_flOldSimulationTime())
            mini_update(player);

        data->m_records.clear();
        data->m_clear_next = true;
        return;
    }


    if (player->dormant()) {
        if (data->m_records.empty() || !data->m_records[0]->dormant()) {
            data->m_records.emplace_front(std::make_shared< LagRecord >(player));
            LagRecord* current = data->m_records.front().get();
            current->m_dormant = true;
            current->m_setup = false;
        }


        data->m_clear_next = true;
        return;
    }

    const float new_spawn = player->m_flSpawnTime();

    // fix
    if (player != data->m_player || data->m_clear_next || data->m_spawn != new_spawn) {
        // reset animation state and records (they're all invalid).
        game::ResetAnimationState(state);
        data->m_records.clear();
        data->m_walk_record = LagRecordMove{};

        // alternative fix.
        player->m_flOldSimulationTime() = player->m_flSimulationTime() - g_csgo.m_globals->m_interval;
    }

    // update our vars
    data->m_player = player;
    data->m_clear_next = false;
    data->m_spawn = new_spawn;

    // player updated.
    if (data->m_records.empty() || data->m_records[0]->m_sim_time != player->m_flSimulationTime()) {
        // emplace new record.
        data->m_records.emplace_front(std::make_shared< LagRecord >(player));

        // get our new record.
        LagRecord* current = data->m_records.front().get();
        current->m_dormant = false;

        // update animations.
        update(player, current, data->m_records.size() > 1 ? data->m_records[1].get() : nullptr);
    }

    // don't store records that are too old.
    while (data->m_records.size() > 1) {
        if (std::abs(g_csgo.m_cl->m_server_tick - data->m_records.back()->m_tick) < 256)
            break;

        data->m_records.pop_back();
    }
}
 
Последнее редактирование:
SetupAliveLoop is way more accurate than layer 6 because layer 6 has a lot of modifiers based on the direction and land weight + most of your favourite p2c use aliveloop(
Пожалуйста, авторизуйтесь для просмотра ссылки.
)
Also disabling the interpolation the proper way (not using varmap) is better if you want your visual models to not get fucked by those too
SetAbsVelocity doesn't even matter as long as you strip 0x1000 from the eflags the client will calc CalcAbsVelocity which will set it to m_vecVelocity and also set VELOCITY_CHANGED

+ the proper way to update anims is using Player::PostDataUpdate, FRAME_NET_UPDATE_POSTDATAUPDATE_START is just the easy way if you just wanna c+p it into your supremacy paste since it's called before var interpolation

+ when setting up bones not actual 256 or 128 bones are setup, only the ones that are used in models are, aka like 98 bones
this is verifiable by printing bonecache size, you could use -1(will force the game to calc how much bones the player has), 128 or 256 as long as it's more than the bonecache needs it will work(note: the reason for that is because of the ( mask & BONE_USED_BY_HITBOX ) & !( mask & BONE_USED_BY_ATTACHMENTS ), it only setup the matrices for those bones)

also animstate::reset doesnt get called on pvs and respawn (e.g in warmup), my fix is just a simple yet effective fix for that + it fixes old simulation time
i also chose to use 256 instead of tickrate for the only reason that supremacy breaks if you .pop_back a record currently in use in their shot system, using 256 is a safe way to clear records without them being used anywhere in the cheat
1. It's Not a Virtual Function in All Builds

  • Unlike UpdateClientSideAnimation, PostDataUpdate isn't always part of the VMT.
  • In some versions, it's a networked interface call (handled internally by the engine using a function pointer or dispatcher), making it non-trivial to hook via VMT.
  • Even if it is in the VMT, the index may differ between builds or be hard to locate without proper symbol or RTTI access.

🧱 2. Compiler Differences + Lack of Symbols

  • Many crash logs say PDB not loaded, because you're working without debug symbols.
  • That makes it hard to verify if:
    • You're hooking the correct function
    • The call convention and signature match
    • Your offset/index is even valid
  • On older builds, internal layout may vary slightly (due to updates or OS/compiler changes).

💣 3. It’s Called Internally by the Engine

  • PostDataUpdate is called by the engine's networking layer after receiving and applying netvars (RecvTable).
  • It’s invoked per-entity, not globally — so your hook must be 100% correct or you’ll crash.
  • A mistake means you’re corrupting an engine-controlled callback, not just your own code.

🧠 4. Most Hooking Attempts Misuse the Signature

  • People try to hook it with:
    cpp
    CopyEdit
    void __fastcall PostDataUpdate(void* ecx, void* edx, DataUpdateType_t type)
    But the original function might not even use __fastcall or may have an unexpected calling convention.
  • If you’re replacing it via VMT:
    • You need exact matching signature
    • And correct cast via util::force_cast<>() or similar — any mismatch = crash

🪤 5. Hook Scope Confusion (Global vs Per-Player)

  • PostDataUpdate is per-player.
  • If you assign your hook globally:
    cpp
    CopyEdit
    g_hooks.m_PostDataUpdate = vmt->add<...>();
    Then overwrite it per player, meaning only the last hooked player keeps the pointer.
  • This leads to:
    • Undefined behavior
    • Accessing a garbage function pointer
    • Calls to null (causing "unresolved external symbol")

🩹 Why People Abandon It

  • It’s brittle.
  • Hard to debug without symbols.
  • You often gain nothing over calling the same logic in FRAME_NET_UPDATE_POSTDATAUPDATE_END.

✅ Recommendation for Legacy HVH

  • Don't hook PostDataUpdate unless you're doing something that must be done before FrameStageNotify.
  • Prefer FRAME_NET_UPDATE_POSTDATAUPDATE_END in FrameStageNotify — it’s safer, cleaner, and works in all versions.
 
1. It's Not a Virtual Function in All Builds

  • Unlike UpdateClientSideAnimation, PostDataUpdate isn't always part of the VMT.
  • In some versions, it's a networked interface call (handled internally by the engine using a function pointer or dispatcher), making it non-trivial to hook via VMT.
  • Even if it is in the VMT, the index may differ between builds or be hard to locate without proper symbol or RTTI access.

🧱 2. Compiler Differences + Lack of Symbols

  • Many crash logs say PDB not loaded, because you're working without debug symbols.
  • That makes it hard to verifyif:
    • You're hooking the correct function
    • The call convention and signature match
    • Your offset/index is even valid
  • On older builds, internal layout may vary slightly (due to updates or OS/compiler changes).

💣 3. It’s Called Internally by the Engine

  • PostDataUpdate is called by the engine's networking layer after receiving and applying netvars (RecvTable).
  • It’s invoked per-entity, not globally — so your hook must be 100% correct or you’ll crash.
  • A mistake means you’re corrupting an engine-controlled callback, not just your own code.

🧠 4. Most Hooking Attempts Misuse the Signature

  • People try to hook it with:
    cpp
    CopyEdit
    void __fastcall PostDataUpdate(void* ecx, void* edx, DataUpdateType_t type)
    But the original function might not even use __fastcall or may have an unexpected calling convention.
  • If you’re replacing it via VMT:
    • You need exact matching signature
    • And correct cast via util::force_cast<>() or similar — any mismatch = crash

🪤 5. Hook Scope Confusion (Global vs Per-Player)

  • PostDataUpdate is per-player.
  • If you assign your hook globally:
    cpp
    CopyEdit
    g_hooks.m_PostDataUpdate = vmt->add<...>();
    Then overwrite it per player, meaning only the last hooked player keeps the pointer.
  • This leads to:
    • Undefined behavior
    • Accessing a garbage function pointer
    • Calls to null (causing "unresolved external symbol")

🩹 Why People Abandon It

  • It’s brittle.
  • Hard to debug without symbols.
  • You often gain nothing over calling the same logic in FRAME_NET_UPDATE_POSTDATAUPDATE_END.

✅ Recommendation for Legacy HVH

  • Don't hook PostDataUpdate unless you're doing something that must be done before FrameStageNotify.
  • Prefer FRAME_NET_UPDATE_POSTDATAUPDATE_END in FrameStageNotify — it’s safer, cleaner, and works in all versions.
full AI right?
 
Назад
Сверху Снизу