StatusUpdate

  • Автор темы Автор темы nesss
  • Дата начала Дата начала

nesss

Путник
Участник
Сообщения
129
Розыгрыши
0
Решения
3
Репутация
-2
Реакции
14
Баллы
85
Хроники
  1. Interlude
Исходники
Присутствуют
Сборка
Собственная
Всем привет ) по поводу обновления регенерации hp,mp,cp, слать пакет каждые 3 сек UserInfo жирно, я вот задумался о StatusUpdate, но столкнулся со следующим, если я шлю 1 стат на обнову, он попросту игнорируется клиентом, если добавляю второй стат скажем ХП и МП в момент получения пакета, идет микрофриз и статы клиент обновляет. Но тем же пакетом я обновляю вес инвентаря, и шлю 1 стат, и он обновляется без фризов. Попробовал вместо StatusUpdate слать каждые 3 сек UserInfo, и все четко обновляет без фризов, хотя пакет намного больше ) Может не правильно шлю статы? или нужно в пакете слать для обновления ХП определенные статы?
 
Зачем каждые три секунды его пересылать? Можно и почаще.. :)

Попробуй сразу пересылать все hp/maxHp, mp/maxMp и cp/maxCp. Должно работать немного лучше.

Насчет микрофриза, это как?
 
значения хп/мп/цп надо слать парами - текущее+максимальное
ну и как уже сказано выше - лучше сразу пачкой слать вот 6 значений всегда (в случае с нпс/мобами и т.п. слать 4 значения - цп то там не нужно).
Ну и фризов быть не должно даже если будет лететь по несколько десятков SU в секунду - так что я даже хз что такого надо было сделать чтобы пакет этот фризы вызывал.
 
Всем привет ) по поводу обновления регенерации hp,mp,cp, слать пакет каждые 3 сек UserInfo жирно, я вот задумался о StatusUpdate, но столкнулся со следующим, если я шлю 1 стат на обнову, он попросту игнорируется клиентом, если добавляю второй стат скажем ХП и МП в момент получения пакета, идет микрофриз и статы клиент обновляет. Но тем же пакетом я обновляю вес инвентаря, и шлю 1 стат, и он обновляется без фризов. Попробовал вместо StatusUpdate слать каждые 3 сек UserInfo, и все четко обновляет без фризов, хотя пакет намного больше ) Может не правильно шлю статы? или нужно в пакете слать для обновления ХП определенные статы?
Я у себя чутка переписал StatsChangeRecorder. Работает довольно эффективно и без фризов. Правда, у меня ХФ. Можно примерно понять логику и порядок отправки пакетов. Т.е вручную из основного кода дергается только метод sendChanges().

Код:
package ru.nts.gameserver.game.model;

import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import ru.nts.core.config.core.ServerConfig;
import ru.nts.core.config.gameplay.OtherConfig;
import ru.nts.core.network.outgoing.*;
import ru.nts.gameserver.game.instancemanager.CursedWeaponsManager;
import ru.nts.gameserver.game.instancemanager.PartyRoomManager;
import ru.nts.gameserver.game.model.entity.duel.Duel;
import ru.nts.gameserver.game.model.enums.EPrivateStoreType;
import ru.nts.gameserver.game.skills.Skill;
import ru.nts.gameserver.game.stats.base.Stats;
import ru.nts.gameserver.system.enums.ClassID;
import ru.nts.gameserver.system.enums.FutureType;
import ru.nts.gameserver.system.interfaces.IGConstant;
import ru.nts.gameserver.system.interfaces.IVars;
import ru.nts.gameserver.system.world.World;
import ru.nts.gameserver.system.world.WorldRegion;

import java.util.Arrays;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * @author Aristo
 * @since 11.07.2024
 */
@Slf4j
public class StatsChangeRecorder {

    private final int player_object_id;

    private String title = null;

    //Пепердолы. Их проверяем в первую очередь, т.к если что-то из этого поменялось, то остальное уже можно не проверять.
    private final int[] publicPaperDoll = new int[PcInventory.PLAYER_PUBLIC_INFO_PAPERDOLL_ORDER.length];
    private final int[] privatePaperDoll = new int[PcInventory.PLAYER_PRIVATE_INFO_PAPERDOLL_ORDER.length];

    private final SavedPublicInfo savedPublicInfo = new SavedPublicInfo();
    private final SavedPrivateInfo savedPrivateInfo = new SavedPrivateInfo();
    private final SavedStatusUpdate savedStatusUpdate = new SavedStatusUpdate();
    private final SavedEtcStatusUpdate savedEtcStatusUpdate = new SavedEtcStatusUpdate();
    private final SavedStorageMaxCount savedStorageMaxCount = new SavedStorageMaxCount();

    private final AtomicBoolean refreshing = new AtomicBoolean(false);

    public StatsChangeRecorder(Player player) {
        this.player_object_id = player.getObjectId();
    }

    /**
     * Основной метод автоматизации определения необходимости отсылать изменения информации в клиент.
     * ВАЖНО: Сохраняемые тут данные никак не влияют на то, что отправляется в клиент.
     * Они выполняют только роль снапшота и позволяют определить факт изменений в определенной категории информации и необходимость ее обновления в клиенте.
     * При отправке пакетов, они заполняются актуальными данными.
     */
    public void sendChanges() {
        if (getPlayer() instanceof Player player && (player.isReal() || !player.isInPowerSaveMode())) { //Ботам в неактивных регионах не обновляем никогда
            if (refreshing.compareAndSet(false,true)) {
                try {
                    boolean needTitleRefresh = false;
                    boolean needStatusRefresh = false;
                    boolean needEtcStatusRefresh = false;
                    boolean needStorageRefresh = false;
                    boolean needPrivateRefresh = false;
                    boolean needPublicRefresh = false;

                    //Проверяем наличие отложенных тасков на рассылку инфы. Если они есть, то все связанные проверки можно скипать.
                    final boolean publicInfoBroadcastAlreadyRequested = player.hasTask(FutureType.PLAYER_BROADCAST_PUBLIC_INFO_TASK);
                    final boolean privateInfoSendingAlreadyRequested = player.hasTask(FutureType.PLAYER_SEND_PRIVATE_INFO_TASK);

                    //Если отложенный таск на обновление статусов уже есть, то повторные проверки можно скипать, т.к они уже были отправлены менее 100мс назад.
                    final boolean publicInfoRefreshAlreadyRequested = player.hasTask(FutureType.PLAYER_REFRESH_PUBLIC_STATS_TASK);
                    final boolean privateInfoRefreshAlreadyRequested = player.isFake() || player.hasTask(FutureType.PLAYER_REFRESH_PRIVATE_STATS_TASK); //Ботам не обновляем никогда
                    final boolean statusRefreshAlreadyRequested = player.hasTask(FutureType.PLAYER_REFRESH_STATUS_STATS_TASK);
                    final boolean etcStatusRefreshAlreadyRequested = player.isFake() || player.hasTask(FutureType.PLAYER_REFRESH_ETC_STATUS_STATS_TASK); //Ботам не обновляем никогда
                    final boolean titleRefreshAlreadyRequested = player.hasTask(FutureType.PLAYER_REFRESH_TITLE_STATS_TASK);
                    final boolean storageRefreshAlreadyRequested = player.isFake() || player.hasTask(FutureType.PLAYER_REFRESH_STORAGE_STATS_TASK); //Ботам не обновляем никогда

                    //Фактически, рассылка тут не производится, а только запрашивается.
                    boolean publicInfoChanged = !publicInfoBroadcastAlreadyRequested && sendPublicInfoChanges();
                    //Если ничего не поменялось в публичной инфе и нет отложенного таска, то пробуем забродкастить изменения статуса.
                    if (!publicInfoChanged && !publicInfoBroadcastAlreadyRequested) {
                        if (!statusRefreshAlreadyRequested && broadcastPublicStatusUpdate()) {
                            needStatusRefresh = true;
                        }
                    } else {
                        needPublicRefresh = true;
                        needStatusRefresh = true;
                    }

                    //Фактически, рассылка тут не производится, а только запрашивается.
                    boolean privateInfoChanged = !privateInfoSendingAlreadyRequested && sendPrivateInfoChanges(publicInfoChanged);
                    //Если ничего не поменялось в приватной инфе и нет отложенного таска, то пробуем отослать изменения статуса.
                    if (!privateInfoChanged && !privateInfoSendingAlreadyRequested) {
                        if (!statusRefreshAlreadyRequested && sendPrivateStatusUpdate()) {
                            needStatusRefresh = true;
                        }
                    } else {
                        needPrivateRefresh = true;
                        needStatusRefresh = true;
                    }

                    //Проверяем наличие существующих запросов на обновление статов. Если они есть, то все связанные проверки можно скипать.
                    if (!statusRefreshAlreadyRequested && broadcastPartyStatusUpdate()) {
                        needStatusRefresh = true;
                    }

                    if (!statusRefreshAlreadyRequested && broadcastOlympiadStatusUpdate()) {
                        needStatusRefresh = true;
                    }

                    if (!statusRefreshAlreadyRequested && broadcastDuelStatusUpdate()) {
                        needStatusRefresh = true;
                    }

                    if (!etcStatusRefreshAlreadyRequested && sendEtcStatus()) {
                        needEtcStatusRefresh = true;
                    }

                    if (!storageRefreshAlreadyRequested && sendStorageMaxCountChanges()) {
                        needStorageRefresh = true;
                    }

                    if (!titleRefreshAlreadyRequested && sendTitleChanges()) {
                        needTitleRefresh = true;
                    }

                    //Запускаем таски на обновление статов по итогам обнаружения изменений в сохраненных данных.
                    if (needPublicRefresh) {
                        player.addTaskForce(FutureType.PLAYER_REFRESH_PUBLIC_STATS_TASK, this::refreshPublicSaves, ServerConfig.PLAYER_PACKET_CACHE_LIFE_TIME);
                    }

                    if (needPrivateRefresh) {
                        player.addTaskForce(FutureType.PLAYER_REFRESH_PRIVATE_STATS_TASK, this::refreshPrivateSaves, ServerConfig.PLAYER_PACKET_CACHE_LIFE_TIME);
                    }

                    if (needStatusRefresh) {
                        player.addTaskForce(FutureType.PLAYER_REFRESH_STATUS_STATS_TASK, this::refreshStatusSaves, ServerConfig.PLAYER_PACKET_CACHE_LIFE_TIME);
                    }

                    if (needEtcStatusRefresh) {
                        player.addTaskForce(FutureType.PLAYER_REFRESH_ETC_STATUS_STATS_TASK, this::refreshEtcStatusSaves, ServerConfig.PLAYER_PACKET_CACHE_LIFE_TIME);
                    }

                    if (needTitleRefresh) {
                        player.addTaskForce(FutureType.PLAYER_REFRESH_TITLE_STATS_TASK, this::refreshTitleSaves, ServerConfig.PLAYER_PACKET_CACHE_LIFE_TIME);
                    }

                    if (needStorageRefresh) {
                        player.addTaskForce(FutureType.PLAYER_REFRESH_STORAGE_STATS_TASK, this::refreshStorageSaves, ServerConfig.PLAYER_PACKET_CACHE_LIFE_TIME);
                    }
                } finally {
                    refreshing.compareAndSet(true,false);
                }
            }
        }
    }

    private Player getPlayer() {
        return World.getInstance().getPlayerByObjectID(player_object_id);
    }

    private void refreshTitleSaves() {
        if (getPlayer() instanceof Player p && p.getInventory() instanceof Inventory inv) {
            title = p.getTitle();
        }
    }

    private void refreshStatusSaves() {
        if (getPlayer() instanceof Player p && p.getInventory() instanceof Inventory inv) {
            savedStatusUpdate.fill(p);
        }
    }

    private void refreshEtcStatusSaves() {
        if (getPlayer() instanceof Player p) {
            savedEtcStatusUpdate.fill(p);
        }
    }

    private void refreshStorageSaves() {
        if (getPlayer() instanceof Player p && p.getInventory() instanceof Inventory inv) {
            savedStorageMaxCount.fill(p);
        }
    }

    private void refreshPublicSaves() {
        if (getPlayer() instanceof Player p && p.getInventory() instanceof Inventory inv) {
            for (int i = 0; i < PcInventory.PLAYER_PUBLIC_INFO_PAPERDOLL_ORDER.length; i++) {
                publicPaperDoll[i] = inv.getPaperdollObjectId(PcInventory.PLAYER_PUBLIC_INFO_PAPERDOLL_ORDER[i]);
            }

            savedPublicInfo.fill(p);
        }
    }

    private void refreshPrivateSaves() {
        if (getPlayer() instanceof Player p && p.getInventory() instanceof Inventory inv) {
            for (int i = 0; i < PcInventory.PLAYER_PRIVATE_INFO_PAPERDOLL_ORDER.length; i++) {
                privatePaperDoll[i] = inv.getPaperdollObjectId(PcInventory.PLAYER_PRIVATE_INFO_PAPERDOLL_ORDER[i]);
            }

            savedPrivateInfo.fill(p);
        }
    }

    private boolean sendPrivateStatusUpdate() {
        if (getPlayer() instanceof Player p && p.isReal()) {
            S_STATUS_UPDATE statusUpdate = savedStatusUpdate.makePrivateStatusUpdate(p);
            if (statusUpdate != null && statusUpdate.hasAttributes()) {
                p.sendPacket(statusUpdate);
                return true;
            }
        }
        return false;
    }

    private boolean broadcastPublicStatusUpdate() {
        if (getPlayer() instanceof Player p) {
            S_STATUS_UPDATE statusUpdate = savedStatusUpdate.makePublicStatusUpdate(p);
            if (statusUpdate != null && statusUpdate.hasAttributes()) {
                p.broadcastPacket(statusUpdate);
                return true;
            }
        }
        return false;
    }

    private boolean broadcastPartyStatusUpdate() {
        if (getPlayer() instanceof Player p && (p.isReal() || p instanceof FakePlayer f && f.isSlave())) {
            Party party = p.getParty();
            if (party != null && party.getPartyRealPlayersCount() > 1) {
                S_PARTY_SMALL_WINDOW_UPDATE partyUpdate = savedStatusUpdate.makePartyStatusUpdate(p);
                if (partyUpdate != null) {
                    party.broadcastToPartyMembersExclude(p, partyUpdate);
                    return true;
                }
            }
        }
        return false;
    }

    private boolean broadcastOlympiadStatusUpdate() {
        if (getPlayer() instanceof Player p && (p.isReal() || p instanceof FakePlayer f && f.isSlave())) {
            if (p.isInOlympiadMode() && p.isOlympiadStart()) {
                S_EX_OLYMPIAD_USER_INFO olympiadUpdate = savedStatusUpdate.makeOlympiadStatusUpdate(p, p.getOlympiadSide());
                if (olympiadUpdate != null) {
                    p.broadcastPacket(olympiadUpdate);
                    return true;
                }
            }
        }
        return false;
    }

    private boolean broadcastDuelStatusUpdate() {
        if (getPlayer() instanceof Player p && (p.isReal() || p instanceof FakePlayer f && f.isSlave())) {
            if (p.isInDuel() && (p.getDuelState() == IGConstant.DUELSTATE_DUELLING || p.getDuelState() == IGConstant.DUELSTATE_DEAD)) {
                S_EX_DUEL_UPDATE_USER_INFO duelUpdate = savedStatusUpdate.makeDuelStatusUpdate(p);
                if (duelUpdate != null && p.getDuel() instanceof Duel duel) {
                    if (p.getDuelSide() == 1) {
                        duel.broadcastToTeam2(duelUpdate);
                    } else if (p.getDuelSide() == 2) {
                        duel.broadcastToTeam1(duelUpdate);
                    }
                    return true;
                }
            }
        }
        return false;
    }

    private boolean sendTitleChanges() {
        if (getPlayer() instanceof Player p) {
            if (title == null && p.getTitle() != null || title != null && !title.equals(p.getTitle())) {
                p.broadcastPacketToOthers(new S_NICKNAME_CHANGED(p));
                return true;
            }
        }
        return false;
    }

    private boolean sendPrivateInfoChanges(boolean publicInfoChanged) {
        if (getPlayer() instanceof Player p && p.isReal()) {
            if (publicInfoChanged || privatePapperDollChanged() || privateInfoChanged()) {
                p.requestSendPlayerPrivateInfo();
                return true;
            }
        }
        return false;
    }

    private boolean sendPublicInfoChanges() {
        if (getPlayer() instanceof Player p) {
            if (publicPapperDollChanged() || publicInfoChanged()) {
                p.requestBroadcastPlayerInfo();
                return true;
            }
        }
        return false;
    }

    private boolean sendEtcStatus() {
        if (getPlayer() instanceof Player p) {
            S_ETC_STATUS_UPDATE etcStatusUpdate = savedEtcStatusUpdate.makeEtcStatusUpdate(p);
            if (etcStatusUpdate != null) {
                p.sendPacket(etcStatusUpdate);
                return true;
            }
        }
        return false;
    }

    private boolean sendStorageMaxCountChanges() {
        if (getPlayer() instanceof Player p) {
            S_EX_STORAGE_MAX_COUNT storageMaxCount = savedStorageMaxCount.makeStorageMaxCountUpdate(p);
            if (storageMaxCount != null) {
                p.sendPacket(storageMaxCount);
                return true;
            }
        }
        return false;
    }

    private boolean privatePapperDollChanged() {
        if (getPlayer() instanceof Player p && p.isReal()) {
            boolean changed = false;
            for (int i = 0; i < PcInventory.PLAYER_PRIVATE_INFO_PAPERDOLL_ORDER.length; i++) {
                if (p.getInventory().getPaperdollObjectId(PcInventory.PLAYER_PRIVATE_INFO_PAPERDOLL_ORDER[i]) != privatePaperDoll[i]) {
                    changed = true;
                    break;
                }
            }
            return changed;
        }
        return false;
    }

    private boolean publicPapperDollChanged() {
        if (getPlayer() instanceof Player p) {
            boolean changed = false;
            for (int i = 0; i < PcInventory.PLAYER_PUBLIC_INFO_PAPERDOLL_ORDER.length; i++) {
                if (p.getInventory().getPaperdollObjectId(PcInventory.PLAYER_PUBLIC_INFO_PAPERDOLL_ORDER[i]) != publicPaperDoll[i]) {
                    changed = true;
                    break;
                }
            }
            return changed;
        }
        return false;
    }

    private boolean publicInfoChanged() {
        if (getPlayer() instanceof Player p) {
            return !savedPublicInfo.equals(new SavedPublicInfo().fill(p));
        }
        return false;
    }

    private boolean privateInfoChanged() {
        if (getPlayer() instanceof Player p && p.isReal()) {
            return !savedPrivateInfo.equals(new SavedPrivateInfo().fill(p));
        }
        return false;
    }

    //Общие параметры. Могут быть как публичными, так и приватными.
    //Если тут что-то поменялось, то нужно обновлять и приватную инфу.
    @NoArgsConstructor
    private static class SavedPublicInfo {
        private WorldRegion region;

        private String name;
        private String title;
        private int race;
        private int sex;
        private int expand_deco_slot;
        private int cloak;
        private int recomendation_points_received;
        private int vehicle_game_object_id;
        private double movement_speed_mul;
        private double physical_attack_speed_mul;
        private double collision_radius;
        private double collision_height;
        private int hair_style;
        private int hair_color;
        private int face;
        private int abnormal_visual_effects;
        private int abnormal_visual_effects_special;
        private int clan_id;
        private int clan_crest_id;
        private int large_clan_crest_id;
        private int ally_id;
        private int ally_crest_id;
        private int class_id;
        private int sitting;
        private int running;
        private int combat;
        private int like_dead;
        private int invisible;
        private EPrivateStoreType private_store_type;
        private int weapon_enchant_effect;
        private int team;
        private int noble;
        private int hero;
        private int fishing;
        private int mount_type;
        private int pledge_class;
        private int pledge_type;
        private int clan_rep_score;
        private int cursed_weapon_level;
        private int mount_id;
        private int name_color;
        private int title_color;
        private int transformation;
        private int agathion;
        private int[] cubics_id;
        private int party_room_active;
        private int floating;
        private int battlefield_penalty;

        private int mounted;

        public synchronized SavedPublicInfo fill(Player player) {
            if (player instanceof Player p) {
                try {
                    region = p.getWorldRegion();

                    sitting = p.isSitting() ? 0 : 1;
                    combat = p.isInCombat() ? 1 : 0;
                    like_dead = p.isDead() ? 1 : 0;
                    invisible = p.isInvisible() ? 1 : 0;
                    recomendation_points_received = p.getRecSystem().getRecommendsHave();
                    if (clan_id > 0 && p.getClanId() != 0) {
                        clan_rep_score = p.getClan().getReputationScore();
                    } else {
                        clan_rep_score = 0;
                    }

                    if (p.isCursedWeaponEquipped()) {
                        name = p.getTransformationName();
                        clan_crest_id = 0;
                        ally_crest_id = 0;
                        large_clan_crest_id = 0;
                        cursed_weapon_level = CursedWeaponsManager.getInstance().getLevel(p.getCursedWeaponEquippedId());
                    } else {
                        name = p.getName();
                        clan_crest_id = p.getClanCrestId();
                        ally_crest_id = p.getAllyCrestId();
                        large_clan_crest_id = p.getClanCrestLargeId();
                        cursed_weapon_level = 0;
                    }

                    if (p.getMountEngine().isMounted()) {
                        weapon_enchant_effect = 0;
                        if (p.getTransformation() != 0) {
                            mount_id = 0;
                            mount_type = 0;
                        } else {
                            mount_id = p.getMountEngine().getMountNpcId() + 1000000;
                            mount_type = p.getMountEngine().getYongmaType();
                        }
                    } else {
                        weapon_enchant_effect = p.getEnchantEffect();
                        mount_id = 0;
                        mount_type = 0;
                    }
                    running = p.isRunning() ? 0x01 : 0x00; //changes the Speed display on Status Window
                    movement_speed_mul = p.getMovementSpeedMultiplier();

                    vehicle_game_object_id = p.isInBoat() ? p.getVehicle().getObjectId() : 0x00;

                    race = p.getRace().ordinal();
                    sex = p.getSex();
                    physical_attack_speed_mul = p.getStat().getAttackSpeedMultiplier();
                    collision_radius = p.getCurrentColRadius();
                    collision_height = p.getCurrentColHeight();
                    hair_style = p.getHairStyle();
                    hair_color = p.getHairColor();
                    face = p.getFace();
                    title = p.getTitle();
                    clan_id = p.getClanId();
                    ally_id = p.getAllyId();
                    private_store_type = p.getPrivateStore().getType();
                    cubics_id = p.getCubicsID();
                    abnormal_visual_effects = p.getAbnormalVisualEffects();
                    floating = p.isSwimming() ? 1 : (p.isFlying() ? 2 : 0);
                    abnormal_visual_effects_special = p.getAbnormalVisualEffectSpecial();
                    class_id = p.getClassId().getID();
                    team = p.getTeam(); //team circle around feet 1= Blue, 2 = red
                    noble = p.isNoble() || p.isGM() && OtherConfig.GM_HERO_AURA ? 1 : 0; //0x01: symbol on char menu ctrl+I
                    hero = p.isHero() || p.isGM() && OtherConfig.GM_HERO_AURA ? 1 : 0; //0x01: Hero Aura and symbol
                    fishing = p.isFishing() ? 1 : 0; // Fishing Mode
                    name_color = p.getNameColor();
                    pledge_class = p.getSocialClass();
                    pledge_type = p.getPledgeType();
                    title_color = p.getTitleColor();
                    transformation = p.getTransformation();
                    agathion = p.getAgathionId();
                    party_room_active = PartyRoomManager.getInstance().isLeader(p) ?  0x01 : 0x00;
                    expand_deco_slot = p.getInventory().getAllowedTalismans();
                    cloak = p.isStatActive(Stats.CLOAK) ? 1 : 0;
                    battlefield_penalty = p.getEffectBySkillId(Skill.SKILL_BATTLEFIELD_PENALTY) != null ? 0 : 1;

                    mounted = p.getMountEngine().isMounted() ? p.getMountEngine().getMountNpcId() : 0;
                } catch (Exception e) {
                    log.error("Cannot fill public info saves for {}", p.toIDString(), e);
                }
            }
            return this;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o instanceof SavedPublicInfo that) {
                return region == that.region && mounted == that.mounted && race == that.race && sex == that.sex && expand_deco_slot == that.expand_deco_slot && cloak == that.cloak && recomendation_points_received == that.recomendation_points_received && vehicle_game_object_id == that.vehicle_game_object_id && Double.compare(movement_speed_mul, that.movement_speed_mul) == 0 && Double.compare(physical_attack_speed_mul, that.physical_attack_speed_mul) == 0 && Double.compare(collision_radius, that.collision_radius) == 0 && Double.compare(collision_height, that.collision_height) == 0 && hair_style == that.hair_style && hair_color == that.hair_color && face == that.face && abnormal_visual_effects == that.abnormal_visual_effects && abnormal_visual_effects_special == that.abnormal_visual_effects_special && clan_id == that.clan_id && clan_crest_id == that.clan_crest_id && large_clan_crest_id == that.large_clan_crest_id && ally_id == that.ally_id && ally_crest_id == that.ally_crest_id && class_id == that.class_id && sitting == that.sitting && running == that.running && combat == that.combat && like_dead == that.like_dead && invisible == that.invisible && weapon_enchant_effect == that.weapon_enchant_effect && team == that.team && noble == that.noble && hero == that.hero && fishing == that.fishing && mount_type == that.mount_type && pledge_class == that.pledge_class && pledge_type == that.pledge_type && clan_rep_score == that.clan_rep_score && cursed_weapon_level == that.cursed_weapon_level && mount_id == that.mount_id && name_color == that.name_color && title_color == that.title_color && transformation == that.transformation && agathion == that.agathion && party_room_active == that.party_room_active && floating == that.floating && battlefield_penalty == that.battlefield_penalty && Objects.equals(name, that.name) && Objects.equals(title, that.title) && private_store_type == that.private_store_type && Objects.deepEquals(cubics_id, that.cubics_id);
            }
            return false;
        }

        @Override
        public int hashCode() {
            return Objects.hash(region, mounted, name, title, race, sex, expand_deco_slot, cloak, recomendation_points_received, vehicle_game_object_id, movement_speed_mul, physical_attack_speed_mul, collision_radius, collision_height, hair_style, hair_color, face, abnormal_visual_effects, abnormal_visual_effects_special, clan_id, clan_crest_id, large_clan_crest_id, ally_id, ally_crest_id, class_id, sitting, running, combat, like_dead, invisible, private_store_type, weapon_enchant_effect, team, noble, hero, fishing, mount_type, pledge_class, pledge_type, clan_rep_score, cursed_weapon_level, mount_id, name_color, title_color, transformation, agathion, Arrays.hashCode(cubics_id), party_room_active, floating, battlefield_penalty);
        }
    }

    //Только приватные параметры. Другие персонажи о них не знают.
    @NoArgsConstructor
    private static class SavedPrivateInfo {
        private int level;
        private long experience_points;
        private int str_value;
        private int con_value;
        private int dex_value;
        private int int_value;
        private int wit_value;
        private int men_value;
        private int current_load;
        private int maximum_load;
        private int physical_attack;
        private int physical_attack_speed;
        private int physical_defence;
        private int avoid;
        private int hit;
        private int critical_rate;
        private int magical_attack;
        private int magical_attack_speed;
        private int magical_defence;

        private int weapon_equipped_flag;

        private int recommendation_points_remaining;
        private int recommendation_points_received;
        private int skill_points;
        private int pledge_power;
        private int inventory_limit;
        private int can_crystalize;
        private int pk_points;
        private int pvp_points;
        private int defence_attribute_fire;
        private int defence_attribute_water;
        private int defence_attribute_wind;
        private int defence_attribute_earth;
        private int defence_attribute_holy;
        private int defence_attribute_unholy;
        private int fame;
        private int vitality;
        private int territory_id;
        private int disguised;
        private int attack_attribute_type;
        private int attack_attribute_value;

        public synchronized SavedPrivateInfo fill(Player player) {
            if (player instanceof Player p && p.isReal()) {
                try {
                    level = p.getLevel();
                    experience_points = p.getExp();
                    str_value = p.getStat().getSTR();
                    dex_value = p.getStat().getDEX();
                    con_value = p.getStat().getCON();
                    int_value = p.getStat().getINT();
                    wit_value = p.getStat().getWIT();
                    men_value = p.getStat().getMEN();
                    current_load = p.getCurrentLoad();
                    maximum_load = p.getMaxLoad();
                    physical_attack = (int) p.getStat().getPAtk();
                    physical_attack_speed = (int) p.getStat().getPAtkSpd();
                    physical_defence = (int) (p.getStat().getPDef());
                    avoid = (int) p.getStat().getEvasionRate();
                    hit = (int) p.getStat().getAccuracy();
                    critical_rate = (int) p.getStat().getCriticalHit();
                    magical_attack = (int) (p.getStat().getMAtk(null));
                    magical_attack_speed = (int) (p.getStat().getMAtkSpd());
                    magical_defence = (int) (p.getStat().getMDef(null));

                    weapon_equipped_flag = p.getActiveWeaponInstance() != null ? 0x28 : 0x14;
                    skill_points = (int)p.getSp();
                    can_crystalize = p.calcStat(Stats.CRYSTALLIZE, 0, null, null) > 0 ? 1 : 0;
                    pk_points = p.getPkKills();
                    pvp_points = p.getPvpKills();
                    pledge_power = p.getClanPrivileges();
                    recommendation_points_remaining = p.getRecSystem().getRecommendsLeft(); //c2 recommendations remaining
                    recommendation_points_received = p.getRecSystem().getRecommendsHave(); //c2 recommendations received
                    inventory_limit = p.getInventoryLimit();
                    attack_attribute_type = p.getStat().getAttackElement()[0];
                    attack_attribute_value = p.getStat().getAttackElement()[1];
                    defence_attribute_fire = (int) (p.getStat().getDefenceFire());
                    defence_attribute_water = (int) (p.getStat().getDefenceWater());
                    defence_attribute_wind = (int) (p.getStat().getDefenceWind());
                    defence_attribute_earth = (int) (p.getStat().getDefenceEarth());
                    defence_attribute_holy = (int) (p.getStat().getDefenceHoly());
                    defence_attribute_unholy = (int) (p.getStat().getDefenceDark());
                    fame = p.getFame();
                    vitality = p.getVitality().getPoints();
                    disguised = p.getVarInt(IVars.USER_DISGUISED);
                    territory_id = p.getDominionID();
                } catch (Exception e) {
                    log.error("Can't fill private info saves for {}", p.toIDString(), e);
                }
            }

            return this;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o instanceof SavedPrivateInfo that) {
                return level == that.level && experience_points == that.experience_points && str_value == that.str_value && con_value == that.con_value && dex_value == that.dex_value && int_value == that.int_value && wit_value == that.wit_value && men_value == that.men_value && current_load == that.current_load && maximum_load == that.maximum_load && physical_attack == that.physical_attack && physical_attack_speed == that.physical_attack_speed && physical_defence == that.physical_defence && avoid == that.avoid && hit == that.hit && critical_rate == that.critical_rate && magical_attack == that.magical_attack && magical_attack_speed == that.magical_attack_speed && magical_defence == that.magical_defence && weapon_equipped_flag == that.weapon_equipped_flag && recommendation_points_remaining == that.recommendation_points_remaining && recommendation_points_received == that.recommendation_points_received && skill_points == that.skill_points && pledge_power == that.pledge_power && inventory_limit == that.inventory_limit && can_crystalize == that.can_crystalize && pk_points == that.pk_points && pvp_points == that.pvp_points && defence_attribute_fire == that.defence_attribute_fire && defence_attribute_water == that.defence_attribute_water && defence_attribute_wind == that.defence_attribute_wind && defence_attribute_earth == that.defence_attribute_earth && defence_attribute_holy == that.defence_attribute_holy && defence_attribute_unholy == that.defence_attribute_unholy && fame == that.fame && vitality == that.vitality && territory_id == that.territory_id && disguised == that.disguised && attack_attribute_type == that.attack_attribute_type && attack_attribute_value == that.attack_attribute_value;
            }
            return false;
        }

        @Override
        public int hashCode() {
            return Objects.hash(level, experience_points, str_value, con_value, dex_value, int_value, wit_value, men_value, current_load, maximum_load, physical_attack, physical_attack_speed, physical_defence, avoid, hit, critical_rate, magical_attack, magical_attack_speed, magical_defence, weapon_equipped_flag, recommendation_points_remaining, recommendation_points_received, skill_points, pledge_power, inventory_limit, can_crystalize, pk_points, pvp_points, defence_attribute_fire, defence_attribute_water, defence_attribute_wind, defence_attribute_earth, defence_attribute_holy, defence_attribute_unholy, fame, vitality, territory_id, disguised, attack_attribute_type, attack_attribute_value);
        }
    }

    @NoArgsConstructor
    private static class SavedStatusUpdate {
        //Явно изменяют значения в клиенте.
        private int current_cp;
        private int maximum_cp;
        private int current_hp;
        private int maximum_hp;
        private int current_mp;
        private int maximum_mp;
        private int karma;
        private int pvp_flag;

        //Не изменяют значения или изменяют не явно.
        private int level;
        private long experience_points;
        private int str_value;
        private int con_value;
        private int dex_value;
        private int int_value;
        private int wit_value;
        private int men_value;
        private int current_load;
        private int maximum_load;
        private int physical_attack;
        private int physical_attack_speed;
        private int physical_defence;
        private int avoid;
        private int hit;
        private int critical_rate;
        private int magical_attack;
        private int magical_attack_speed;
        private int magical_defence;

        private ClassID class_id;
        private String name;

        public synchronized void fill(Player player) {
            if (player instanceof Player p) {
                try {
                    maximum_cp = (int) (p.getStat().getMaxCp());
                    current_cp = (int) (p.getCurrentCp());
                    current_hp = (int) p.getCurrentHp();
                    maximum_hp = (int) p.getStat().getMaxHp();
                    current_mp = (int) p.getCurrentMp();
                    maximum_mp = (int) p.getStat().getMaxMp();
                    karma = p.getKarma();
                    pvp_flag = p.getPvpFlag();

                    level = p.getLevel();
                    experience_points = p.getExp();
                    str_value = p.getStat().getSTR();
                    dex_value = p.getStat().getDEX();
                    con_value = p.getStat().getCON();
                    int_value = p.getStat().getINT();
                    wit_value = p.getStat().getWIT();
                    men_value = p.getStat().getMEN();
                    current_load = p.getCurrentLoad();
                    maximum_load = p.getMaxLoad();
                    physical_attack = (int) p.getStat().getPAtk();
                    physical_attack_speed = (int) p.getStat().getPAtkSpd();
                    physical_defence = (int) (p.getStat().getPDef());
                    avoid = (int) p.getStat().getEvasionRate();
                    hit = (int) p.getStat().getAccuracy();
                    critical_rate = (int) p.getStat().getCriticalHit();
                    magical_attack = (int) (p.getStat().getMAtk(null));
                    magical_attack_speed = (int) (p.getStat().getMAtkSpd());
                    magical_defence = (int) (p.getStat().getMDef(null));

                    class_id = p.getClassId();
                    name = p.getName();
                } catch (Exception e) {
                    log.error("Can't fill saved status update for {}", p.toIDString(), e);
                }
            }
        }

        public synchronized S_STATUS_UPDATE makePublicStatusUpdate(Player player) {
            if (player instanceof Player p) {
                try {
                    S_STATUS_UPDATE sa = new S_STATUS_UPDATE(p.getObjectId());

                    if (level != p.getLevel()) {
                        sa.addAttribute(S_STATUS_UPDATE.LEVEL, p.getLevel());
                    }

                    if (karma != p.getKarma()) {
                        sa.addAttribute(S_STATUS_UPDATE.KARMA, p.getKarma());
                    }

                    if (pvp_flag != p.getPvpFlag()) {
                        sa.addAttribute(S_STATUS_UPDATE.PVP_FLAG, p.getPvpFlag());
                    }

                    return sa;
                } catch (Exception e) {
                    log.error("Can't make public status update for {}", p.toIDString(), e);
                }
            }

            return null;
        }

        public synchronized S_STATUS_UPDATE makePrivateStatusUpdate(Player player) {
            if (player instanceof Player p) {
                try {
                    S_STATUS_UPDATE sa = new S_STATUS_UPDATE(p.getObjectId());

                    //В клиенте HF эти данные обрабатываются явно.
                    if (current_cp != p.getCurrentCp()) {
                        sa.addAttribute(S_STATUS_UPDATE.CUR_CP, p.getCurrentCp());
                    }

                    if (maximum_cp != p.getStat().getMaxCp()) {
                        sa.addAttribute(S_STATUS_UPDATE.MAX_CP, p.getStat().getMaxCp());
                    }

                    if (current_hp != (int) (p.getCurrentHp())) {
                        sa.addAttribute(S_STATUS_UPDATE.CUR_HP, p.getCurrentHp());
                    }

                    if (maximum_hp != p.getStat().getMaxHp()) {
                        sa.addAttribute(S_STATUS_UPDATE.MAX_HP, p.getStat().getMaxHp());
                    }

                    if (current_mp != (int) (p.getCurrentMp())) {
                        sa.addAttribute(S_STATUS_UPDATE.CUR_MP, p.getCurrentMp());
                    }

                    if (maximum_mp != p.getStat().getMaxMp()) {
                        sa.addAttribute(S_STATUS_UPDATE.MAX_MP, p.getStat().getMaxMp());
                    }

                    if (karma != p.getKarma()) {
                        sa.addAttribute(S_STATUS_UPDATE.KARMA, p.getKarma());
                    }

                    if (pvp_flag != p.getPvpFlag()) {
                        sa.addAttribute(S_STATUS_UPDATE.PVP_FLAG, p.getPvpFlag());
                    }

                    //В клиенте HF эти данные не обрабатываются или обрабатываются не явно.
                    if (level != p.getLevel()) {
                        sa.addAttribute(S_STATUS_UPDATE.LEVEL, p.getLevel());
                    }

                    if (experience_points != p.getExp()) {
                        sa.addAttribute(S_STATUS_UPDATE.EXP, p.getExp());
                    }

                    if (str_value != p.getStat().getSTR()) {
                        sa.addAttribute(S_STATUS_UPDATE.STR, p.getStat().getSTR());
                    }

                    if (dex_value != p.getStat().getDEX()) {
                        sa.addAttribute(S_STATUS_UPDATE.DEX, p.getStat().getDEX());
                    }

                    if (con_value != p.getStat().getCON()) {
                        sa.addAttribute(S_STATUS_UPDATE.CON, p.getStat().getCON());
                    }

                    if (int_value != p.getStat().getINT()) {
                        sa.addAttribute(S_STATUS_UPDATE.INT, p.getStat().getINT());
                    }

                    if (wit_value != p.getStat().getWIT()) {
                        sa.addAttribute(S_STATUS_UPDATE.WIT, p.getStat().getWIT());
                    }

                    if (men_value != p.getStat().getMEN()) {
                        sa.addAttribute(S_STATUS_UPDATE.MEN, p.getStat().getMEN());
                    }

                    if (current_load != p.getCurrentLoad()) {
                        sa.addAttribute(S_STATUS_UPDATE.CUR_LOAD, p.getCurrentLoad());
                    }

                    if (maximum_load != p.getMaxLoad()) {
                        sa.addAttribute(S_STATUS_UPDATE.MAX_LOAD, p.getMaxLoad());
                    }

                    if (physical_attack != p.getStat().getPAtk()) {
                        sa.addAttribute(S_STATUS_UPDATE.P_ATK, p.getStat().getPAtk());
                    }

                    if (physical_attack_speed != p.getStat().getPAtkSpd()) {
                        sa.addAttribute(S_STATUS_UPDATE.ATK_SPD, p.getStat().getPAtkSpd());
                    }

                    if (physical_defence != p.getStat().getPDef()) {
                        sa.addAttribute(S_STATUS_UPDATE.P_DEF, p.getStat().getPDef());
                    }

                    if (avoid != p.getStat().getEvasionRate()) {
                        sa.addAttribute(S_STATUS_UPDATE.EVASION, p.getStat().getAccuracy());
                    }

                    if (hit != p.getStat().getAccuracy()) {
                        sa.addAttribute(S_STATUS_UPDATE.ACCURACY, p.getStat().getAccuracy());
                    }

                    if (critical_rate != p.getStat().getCriticalHit()) {
                        sa.addAttribute(S_STATUS_UPDATE.CRITICAL, p.getStat().getCriticalHit());
                    }

                    if (magical_attack != p.getStat().getMAtk(null)) {
                        sa.addAttribute(S_STATUS_UPDATE.M_ATK, p.getStat().getMAtk(null));
                    }

                    if (magical_attack_speed != p.getStat().getMAtkSpd()) {
                        sa.addAttribute(S_STATUS_UPDATE.CAST_SPD, p.getStat().getMAtkSpd());
                    }

                    if (magical_defence != p.getStat().getMDef(null)) {
                        sa.addAttribute(S_STATUS_UPDATE.M_DEF, p.getStat().getMDef(null));
                    }

                    return sa;
                } catch (Exception e) {
                    log.error("Can't make private status update for {}", p.toIDString(), e);
                }
            }

            return null;
        }

        public synchronized S_PARTY_SMALL_WINDOW_UPDATE makePartyStatusUpdate(Player player) {
            if (player instanceof Player p) {
                try {
                    boolean update = false;

                    if (level != p.getLevel()) {
                        update = true;
                    }

                    if (!update && current_hp != (int) (p.getCurrentHp())) {
                        update = true;
                    }

                    if (!update && maximum_hp != p.getStat().getMaxHp()) {
                        update = true;
                    }

                    if (!update && current_mp != (int) (p.getCurrentMp())) {
                        update = true;
                    }

                    if (!update && maximum_mp != p.getStat().getMaxMp()) {
                        update = true;
                    }

                    if (!update && current_cp != p.getCurrentCp()) {
                        update = true;
                    }

                    if (!update && maximum_cp != p.getStat().getMaxCp()) {
                        update = true;
                    }

                    if (!update && class_id != p.getClassId()) {
                        update = true;
                    }

                    if (!update && name != p.getName()) {
                        update = true;
                    }

                    if (update) {
                        return new S_PARTY_SMALL_WINDOW_UPDATE(p);
                    }
                } catch (Exception e) {
                    log.error("Can't make party status update for {}", p.toIDString(), e);
                }
            }

            return null;
        }

        public synchronized S_EX_OLYMPIAD_USER_INFO makeOlympiadStatusUpdate(Player player, int side) {
            if (player instanceof Player p) {
                try {
                    boolean update = false;

                    if (level != p.getLevel()) {
                        update = true;
                    }

                    if (!update && current_hp != (int) (p.getCurrentHp())) {
                        update = true;
                    }

                    if (!update && maximum_hp != p.getStat().getMaxHp()) {
                        update = true;
                    }

                    if (!update && current_cp != p.getCurrentCp()) {
                        update = true;
                    }

                    if (!update && maximum_cp != p.getStat().getMaxCp()) {
                        update = true;
                    }

                    if (!update && class_id != p.getClassId()) {
                        update = true;
                    }

                    if (!update && name != p.getName()) {
                        update = true;
                    }

                    if (update) {
                        return new S_EX_OLYMPIAD_USER_INFO(p, side);
                    }
                } catch (Exception e) {
                    log.error("Can't make olympiad status update for {}", p.toIDString(), e);
                }
            }

            return null;
        }

        public synchronized S_EX_DUEL_UPDATE_USER_INFO makeDuelStatusUpdate(Player player) {
            if (player instanceof Player p) {
                try {
                    boolean update = false;

                    if (level != p.getLevel()) {
                        update = true;
                    }

                    if (!update && current_hp != (int) (p.getCurrentHp())) {
                        update = true;
                    }

                    if (!update && maximum_hp != p.getStat().getMaxHp()) {
                        update = true;
                    }

                    if (!update && current_mp != (int) (p.getCurrentMp())) {
                        update = true;
                    }

                    if (!update && maximum_mp != p.getStat().getMaxMp()) {
                        update = true;
                    }

                    if (!update && current_cp != p.getCurrentCp()) {
                        update = true;
                    }

                    if (!update && maximum_cp != p.getStat().getMaxCp()) {
                        update = true;
                    }

                    if (!update && class_id != p.getClassId()) {
                        update = true;
                    }

                    if (!update && name != p.getName()) {
                        update = true;
                    }

                    if (update) {
                        return new S_EX_DUEL_UPDATE_USER_INFO(p);
                    }
                } catch (Exception e) {
                    log.error("Can't make duel status update for {}", p.toIDString(), e);
                }
            }

            return null;
        }
    }

    @NoArgsConstructor
    private static class SavedEtcStatusUpdate {
        private int increasedForce;
        private int weightPenalty;
        private int messageRefusal;
        private int dangerArea;
        private int weaponPenalty;
        private int armorPenalty;
        private int charmOfCourage;
        private int deathPenaltyLevel;
        private int consumedSouls;

        public synchronized void fill(Player player) {
            if (player instanceof Player p) {
                try {
                    increasedForce = p.getEnergySaved();
                    weightPenalty = p.getWeightPenalty();
                    messageRefusal = p.getMessageRefusal() || p.getNoChannel() != 0 ? 1 : 0;
                    dangerArea = p.isInAreaDanger() ? 1 : 0;
                    weaponPenalty = p.getWeaponExpertisePenalty();
                    armorPenalty = p.getArmourExpertisePenalty();
                    charmOfCourage = p.isCharmOfCourage() ? 1 : 0;
                    deathPenaltyLevel = p.getDeathPenalty() != null ? p.getDeathPenalty().getLevel() : 0;
                    consumedSouls = p.getSoulSaved();
                } catch (Exception e) {
                    log.error("Can't fill etc status saves for {}", p.toIDString(), e);
                }
            }
        }

        public synchronized S_ETC_STATUS_UPDATE makeEtcStatusUpdate(Player player) {
            if (player instanceof Player p) {
                try {
                    boolean update = false;

                    if (consumedSouls != p.getSoulSaved()) {
                        update = true;
                    }

                    if (!update && increasedForce != p.getEnergySaved()) {
                        update = true;
                    }

                    if (!update && weightPenalty != p.getWeightPenalty()) {
                        update = true;
                    }

                    if (!update && messageRefusal != (p.getMessageRefusal() || p.getNoChannel() != 0 ? 1 : 0)) {
                        update = true;
                    }

                    if (!update && dangerArea != (p.isInAreaDanger() ? 1 : 0)) {
                        update = true;
                    }

                    if (!update && weaponPenalty != p.getWeaponExpertisePenalty()) {
                        update = true;
                    }

                    if (!update && armorPenalty != p.getArmourExpertisePenalty()) {
                        update = true;
                    }

                    if (!update && charmOfCourage != (p.isCharmOfCourage() ? 1 : 0)) {
                        update = true;
                    }

                    if (!update && deathPenaltyLevel != (p.getDeathPenalty() != null ? p.getDeathPenalty().getLevel() : 0)) {
                        update = true;
                    }

                    if (update) {
                        return new S_ETC_STATUS_UPDATE(p);
                    }
                } catch (Exception e) {
                    log.error("Can't make etc status update for {}", p.toIDString(), e);
                }
            }

            return null;
        }
    }

    @NoArgsConstructor
    private static class SavedStorageMaxCount {
        private int inventory_limit;
        private int warehouse_limit;
        private int trade_sell_limit;
        private int dwarven_recipe_limit;
        private int common_recipe_limit;
        private int freight_limit;
        private int trade_buy_limit;
        private int inventory_extra_slots;

        public synchronized void fill(Player player) {
            if (player instanceof Player p && p.isReal()) {
                try {
                    inventory_limit = p.getInventoryLimit();
                    warehouse_limit = p.getWarehouseLimit();
                    trade_sell_limit = p.getTradeSellLimit();
                    dwarven_recipe_limit = p.getDwarvenRecipeLimit();
                    common_recipe_limit = p.getCommonRecipeLimit();
                    freight_limit = p.getFreightLimit();
                    trade_buy_limit = p.getTradeBuyLimit();
                    inventory_extra_slots = (int) p.calcStat(Stats.INVENTORY_LIMIT, 0, null, null);
                } catch (Exception e) {
                    log.error("Can't fill storage max count for {}", p.toIDString(), e);
                }
            }

        }

        public synchronized S_EX_STORAGE_MAX_COUNT makeStorageMaxCountUpdate(Player player) {
            if (player instanceof Player p) {
                try {
                    boolean update = false;

                    if (inventory_limit != p.getInventoryLimit()) {
                        update = true;
                    }

                    if (!update && warehouse_limit != p.getWarehouseLimit()) {
                        update = true;
                    }

                    if (!update && trade_sell_limit != p.getTradeSellLimit()) {
                        update = true;
                    }

                    if (!update && dwarven_recipe_limit != p.getDwarvenRecipeLimit()) {
                        update = true;
                    }

                    if (!update && common_recipe_limit != p.getCommonRecipeLimit()) {
                        update = true;
                    }

                    if (!update && freight_limit != p.getFreightLimit()) {
                        update = true;
                    }

                    if (!update && trade_buy_limit != p.getTradeBuyLimit()) {
                        update = true;
                    }

                    if (!update && inventory_extra_slots != (int) p.calcStat(Stats.INVENTORY_LIMIT, 0, null, null)) {
                        update = true;
                    }

                    if (update) {
                        return new S_EX_STORAGE_MAX_COUNT(p);
                    }
                } catch (Exception e) {
                    log.error("Can't make storage max count update for {}", p.toIDString(), e);
                }
            }

            return null;
        }
    }
}
 
Добавил 3 значения максимального хп,мп,сп и микрофиз стал заметнее. Микрофриз это скажем я появляюсь чаром в начальной локации, горит анамация огня и она как бы в момент получения пакета приостанавливается на микросекунду, и потом дальше продолжает анимироваться ))) Посмотрел как реализовано в других сборках, там так, скажем тебе нанесли урон, хп уменьшилось, запускается поток из пула потоков с заданием в период каждые 3 сек обновлять текущее ХП и добавлять в него единицу регинирации, шлют в момент изменения ХП себе пакет UserInfo в такт 3 сек, а в список тех чаров, которые видимые (находятся неподалеку) отсылают как раз пакет StatusUpdate с теми значениями которые обновляются, скажем если обновляется только Cur_hp, то только шлют Cur_hp, если сразу обнова идет и Cur_hp, Cur_mp и Cur_cp, то шлет сразу 3 параметра. Я попробовал слать себе UserInfo, ну фриза нет, все обновляет.

Ну вот посмотрите, в момент обновления HP,MP,CP микрофриз идет, по анимации огня и головы, которая крутится, видно
 
Ну, надо спрашивать тех кто тоже с интерлюдом возится - есть ли у них такие фризы. Может это косяк самого клиента.
Но в хрониках выше точно нет подобного - хоть десятками/сотнями SU в секунду спамь.

Вот, одновременно обновляются цп/хп/мп при помощи SU и никаких фризов
 
Последнее редактирование:
раз в три секунды отрабатывает простой RegenTask, и плюс при каждом изменении HpCpMp (урон, использование скила и т.д.)
то что на видео - это вообще какая-то лютая жесть, я бы пакетхаком посмотрел что там еще прилетает

думаю, что там прилетает еще пакет с кривым опкодом, и клиента реагирует так (да, это для него норма)
 
Ну смотрите, тупо банально решил проверить как будет реагировать, если буду слать другой параметр в пакете StatusUpdate.
Java:
character.getCharacterStatus().addUpdate(StatusType.CURRENT_WEIGHT, character.getInventory().getCurrentWeight() + Random.nextInt(50000));
ну получается каждые 3 сек шлю пакет StatusUpdate c параметром текущего веса инвентаря, и для наглядности добавил рандомное значение, так вот, не одно фриза, все гладко работает. Вот именно проблема с обновлением HP,MP,CP
 
А в какой именно последовательности шлешь значения? Может в интерлюде это имеет значение.
У меня к примеру шлются парами в следущей очередности
Java:
CUR_HP, MAX_HP, CUR_MP, MAX_MP, CUR_CP, MAX_CP
ну или если к примеру только хп надо обновить
Java:
CUR_HP, MAX_HP
Ну и ид параметров точно правильные? может там еще косяк есть

Java:
package l2p.gameserver.enums;

import java.util.function.Function;

import l2p.gameserver.model.Creature;

public enum StatusUpdateType
{
    LEVEL(0x01, Creature::getLevel),

    EXP(0x02, creature -> (int) creature.getExp()),
    STR(0x03, Creature::getSTR),
    DEX(0x04, Creature::getDEX),
    CON(0x05, Creature::getCON),
    INT(0x06, Creature::getINT),
    WIT(0x07, Creature::getWIT),
    MEN(0x08, Creature::getMEN),

    /**
     * Даный параметр отсылается оффом в паре с MAX_HP, сначала CUR_HP, потом MAX_HP
     */
    CUR_HP(0x09, creature -> (int) creature.getCurrentHp()),
    MAX_HP(0x0A, Creature::getMaxHp),

    /**
     * Даный параметр отсылается оффом в паре с MAX_MP, сначала CUR_MP, потом MAX_MP
     */
    CUR_MP(0x0B, creature -> (int) creature.getCurrentMp()),
    MAX_MP(0x0C, Creature::getMaxMp),

    SP(0x0D, creature -> (int) creature.getSp()),

    /**
     * Меняется отображение только в инвентаре, для статуса требуется UserInfo
     */
    CUR_LOAD(0x0E, Creature::getCurrentWeight),
    MAX_LOAD(0x0F, Creature::getWeightLimit),

    P_ATK(0x11, creature -> creature.getPAtk(null, null)),
    ATK_SPEED(0x12, Creature::getPAtkSpd),
    P_DEF(0x13, creature -> creature.getPDef(null, null)),
    EVASION(0x14, creature -> creature.getPEvasion(null)),
    ACCURACY(0x15, creature -> creature.getPAccuracy()),
    CRITICAL(0x16, creature -> creature.getCriticalHit(null, null)),
    M_ATK(0x17, creature -> creature.getMAtk(null, null)),
    CAST_SPEED(0x18, Creature::getMAtkSpd),
    M_DEF(0x19, creature -> creature.getMDef(null, null)),
    PVP_FLAG(0x1A, creature -> (int) creature.getPvpFlag()),
    KARMA(0x1B, creature -> creature.isPlayer() ? creature.getPlayer().getKarma() : 0),

    /**
     * Даный параметр отсылается оффом в паре с MAX_CP, сначала CUR_CP, потом MAX_CP
     */
    CUR_CP(0x21, creature -> (int) creature.getCurrentCp()),
    MAX_CP(0x22, Creature::getMaxCp),

    LUC(0x26, Creature::getLUC),
    CHA(0x27, Creature::getCHA);

    private int _id;
    private Function<Creature, Integer> _value;

    StatusUpdateType(int id, Function<Creature, Integer> value)
    {
        _id = id;
        _value = value;
    }

    public int getId()
    {
        return _id;
    }

    public int getValue(Creature creature)
    {
        return _value.apply(creature).intValue();
    }
}
 
Последнее редактирование:
Ну последовательность у меня наоборот MAX_HP, CUR_HP. Только-что провел тесты, при отправке только MAX_HP, CUR_HP без МР и СР независимо, в одном пакете или в разных с МР и СР лагов нет, все четко, а вот при отправке МР по тому-же аналогу и СР идут фризы именно из-за них. Проверил как ты говоришь последовательность, поменял, не помогло.

Еще тесты провел, в общем: CUR_HP без MAX_HP клиент не видит, когда отправляю их парой, фризов нет, все четко работает. CUR_МP если отправлять только его, фризов нет, все работает хорошо, так же и по CUR_CP. Вот что странно CUR_HP без второго параметра клиент не видит, зато CUR_CP и CUR_МP без второго параметра отлично видит. Ужас какой, что от сюда можно вынести, то что скорее всего в интерлюде идет обработка пакета StatusUpdate по отдельности, ну типо проверяет if (CUR_HP != null && MAX_HP != null) cur_hp = readInt(); что-то в этом роде и такое ощущение, что в клиенте под каждую функцию из пакета запускается отдельный потом на обработку инфы, но пока предыдущая функция обрабатывается, следующая в ожидании обработки, и вот фриз происходит как раз из-за этого ожидания. А с пакетом UserInfo такого не происходит, так как там указанно, сразу читать весь пакет и обновлять все значения.

И скорее всего я прав, реализовал так и все заработало отлично, но только отправкой 3 пакетов StatusUpdate, в первом 2 параметра текущего и максимального хп, во втором только текущее мп и в третем только текущее сп, и если так высылать будет фриз все равно, но если между ними я поставил трейдслип на 500, по пол сек интервал между отправками, то все четко заработало, вот только ценой отправки 3х пакетов, ну скажем это 3 пакета отправляются если нужно обновлять все 3 параметра, а если нужно обновлять скажем только мп, то будет высылаться только 1 пакет. Перепробывал все варианты варировать в одном пакете данные параметры, все равно выдает фризы. Только разделив их между собой по отдельным пакетам работает все гладко.

Все, 100% решил все, одновременно без фризов высылаю CUR_HP и CUR_MP в одном пакете, а CUR_CP в отдельном по необходимости, так как в основном СР меняется при ПВП, а в простом каче задействуются НР и МР, плюс я подумал, что раньше то не было полоски СР, скорее всего ее крыво врезали в интерлюде, а вот в хрониках дальше там уже наверное переделали механику данного пакета в клиенте.

Вот код, работает как часы, что интересно, пакет с СР отправляется раньше на 1,5сек чем пакет с ХП и МП, но обновляется в клиенте синхронизировано СР с НР, но когда СР под фул, оно синхронизируется НР с МР ))))))
Java:
        final int idWorld = character.getIdWorld();

        if (currentHp >= maxHp && currentMp >= maxMp && currentCp >= maxCp) {

            stopRegeneration();
            return;

        }
        if (currentCp < maxCp) {

            final StatusUpdate statusUpdate = new StatusUpdate(idWorld);

            final int newCurrentCp = (int) (currentCp + Math.max(1, cpRegen));

            if (newCurrentCp >= maxCp) {

                currentCp = maxCp;

            } else {

                currentCp = newCurrentCp;

            }

            statusUpdate.addUpdate(StatusType.CURRENT_CP, (int) currentCp);
            character.sendPacket(statusUpdate);

            try {

                Thread.sleep(1500);

            } catch (InterruptedException e) {

                throw new RuntimeException(e);

            }

        }
        if (currentHp < maxHp || currentMp < maxMp) {

            final StatusUpdate statusUpdate = new StatusUpdate(idWorld);

            if (currentHp < maxHp ) {

                final int newCurrentHp = (int) (currentHp + Math.max(1, hpRegen));

                if (newCurrentHp >= maxHp) {

                    currentHp = maxHp;

                } else {

                    currentHp = newCurrentHp;

                }

                statusUpdate.addUpdate(StatusType.CURRENT_HP, (int) currentHp);

            }
            if (currentMp < maxMp ) {

                final int newCurrentMp = (int) (currentMp + Math.max(1, mpRegen));

                if (newCurrentMp >= maxMp) {

                    currentMp = maxMp;

                } else {

                    currentMp = newCurrentMp;

                }

                statusUpdate.addUpdate(StatusType.CURRENT_MP, (int) currentMp);

            }

            character.sendPacket(statusUpdate);

        }
 
А теперь подумай о том что ты sleep'ом паузишь полностью текущий поток и что в итоге будет если подобное будет вызываться у кучи игроков одновременно...

Если уж надо послать SU с мп/цп с задержкой, то делай выполнение таска с этим, а не ставь весь поток на паузу...
 
Последнее редактирование:
Ну дело в том, что я не могу вообще его убрать, так как если посылаю пакет за пакетом сразу, клиент как-бы не успевает обработать первый пакет с СР и дает фриз, попробую сейчас с слипом уменьшить до 20 мсек
 
Ты имеешь ввиду чисто запускать отдельный таск на реген СР?

Ну смотри, потестил, на какой минимум могу сократить паузу, это до 100 милсек. ставлю 90, ужа фриз получаю, 100 все четко. Это ж пауза происходить будет только при том, что у чара идет реген СР, если СР у него фул, а нужно реген ХП и МП, то эта пауза вообще не будет запускаться, в основном игроки используют ХП и МП, это кач и т.д. Вот я и думаю, стоит из-за такого из одной задачи делать 2? одну на СР и вторую на ХП и МП? Или все таки серверу будет легче запускать второй таск чем подождать одну десятую секунды?
 
Или все таки серверу будет легче запускать второй таск чем подождать одну десятую секунды?
Серверу по барабану, sleep не будет жрать такты процессора.
Другое дело, что вы потенциально блокируете IO как минимум текущего клиента, как максимум всех клиентов (например, если все потоки в тред пуле встали в этот sleep). В итоге это будет выглядеть как жуткие лаги.

Рекомендую обратить внимание на неблокирующие операции. C Java не знаком, но думаю, что т.н. таски с этим справятся.
 
Ну смотри, у меня идет так:
Java:
regTask = Threads.getInstance().scheduleAtFixedRate(this::doRegeneration, 1000, 3000);

Это таск на реген. Где doRegeneration метод в котором прописана логика, 1000 - это время, которое дается на выполнение этой логики, это 1 сек, и 3000 это период запуска таска. Подсчет там выполняется за 500-600 мсек. если 400 ставлю, бывает одну из ХП,МП,СП не успевает посчитать и добавить. И я в эту сек просто вставил 100 мсек подождать, все равно 400 мсек в холостую держут данный таск, не так ли?
 
Серверу по барабану, sleep не будет жрать такты процессора.
Другое дело, что вы потенциально блокируете IO как минимум текущего клиента, как максимум всех клиентов (например, если все потоки в тред пуле встали в этот sleep). В итоге это будет выглядеть как жуткие лаги.

Рекомендую обратить внимание на неблокирующие операции. C Java не знаком, но думаю, что т.н. таски с этим справятся.
А как насчет виртуальных потоков в Яве? Я не пробовал, но из того что читал sleep должен переключить task на тот который можно будет делать. А потом все вроде переключится обратно.
 
А как насчет виртуальных потоков в Яве? Я не пробовал, но из того что читал sleep должен переключить task на тот который можно будет делать. А потом все вроде переключится обратно.
С виртуальными потоками нужно быть оооочень аккуратным, т.к они имеют очень высокую склонность к дедлокам на синхронизированных участках кода. Причем это учесть прям очень сложно с разгону, особенно если пихать в них какие-то сложные куски кода. Виртуальные потоки очень хороши там, где нужно ждать какое-то IO. Например, можно выгружать в отдельный виртуальный поток запись пакета в сетевой буфер.
 
Какая там правильная механика регенерации? какой такт обновления ХП,МП,СП должен быть? я оптимизировал еще тот метод, внес флаги регенерации, засек выполнение метода при такте регенерации 0 мсек, меньше 1 миллисекунды ) вот только есть нюанс: или сделать равномерное время между тактами регенерации, что CP, что HP и МР но жертвуя двумя тактами вызова таска, или обойтись одним тактом, уменьшив нагрузку на железо в 2 раза и тогда получиться такая штука: если нужно обновлять скажем и СР и МР и НР, такт обновления будет равен для СР - 5 сек. и для НР и МР - 5 сек. так как такт задания 2,5 сек. я поставил, то есть 2,5 сек. прошло, обновило СР, потом 2,5 сек. прошло, обновило НР и МР, но если СР уже фул, а нужно обновлять НР и МР, тогда такт будет 2,5 сек. То есть если будет регенерация по отдельности, то регенерировать будет на 50% быстрее.

Вот так вот это все выглядит, если есть предложение, для улучшения, говорите:
Java:
 /**

     * <b>Описание:</b><br>

     * <small>Запускает регенерацию жизненных, магических и боевых сил.</small>

     */

    public synchronized void startRegeneration() {



        if (!character.isDeading() && regenTask == null) {



            if (currentCp < maxCp) {



                regenType = RegenType.CP;



            } else {



                regenType = RegenType.HP_MP;



            }



            regenTask = Threads.getInstance().scheduleAtFixedRate(this::doRegeneration, 2500, 2500);



        }



    }

  

    /**

     * <b>Описание:</b><br>

     * <small>Останавливает регенерацию жизненных, магических и боевых сил.</small>

     */

    public synchronized void stopRegeneration() {



        if (regenTask != null) {



            regenTask.cancel(false);

            regenTask = null;



        }



    }

  

    /**

     * <b>Описание:</b><br>

     * <small>Запускается в такт, пока идет регенерация жизненных, магических и боевых сил.</small>

     */

    private void doRegeneration() {



        if (currentCp >= maxCp && currentHp >= maxHp && currentMp >= maxMp) {



            stopRegeneration();

            return;



        }



        final int idWorld = character.getIdWorld();

        final double bonus = character.isSitting() ? 1.5 : 1;



        switch (regenType) {



            case CP -> {



                if (currentCp < maxCp) {



                    final int newCurrentCp = (int) (currentCp + Math.max(1, cpRegen * bonus));



                    if (newCurrentCp >= maxCp) {



                        currentCp = maxCp;



                    } else {



                        currentCp = newCurrentCp;



                    }



                    final StatusUpdate statusUpdate = new StatusUpdate(idWorld);

                    statusUpdate.addUpdate(StatusType.CURRENT_CP, (int) currentCp);

                    character.sendPacket(statusUpdate);



                }

                if (currentHp < maxHp || currentMp < maxMp) regenType = RegenType.HP_MP;



            }

            case HP_MP -> {



                if (currentHp < maxHp || currentMp < maxMp) {



                    if (currentHp < maxHp) {



                        final int newCurrentHp = (int) (currentHp + Math.max(1, hpRegen * bonus));



                        if (newCurrentHp >= maxHp) {



                            currentHp = maxHp;



                        } else {



                            currentHp = newCurrentHp;



                        }



                    }

                    if (currentMp < maxMp) {



                        final int newCurrentMp = (int) (currentMp + Math.max(1, mpRegen * bonus));



                        if (newCurrentMp >= maxMp) {



                            currentMp = maxMp;



                        } else {



                            currentMp = newCurrentMp;



                        }



                    }



                    final StatusUpdate statusUpdate = new StatusUpdate(idWorld);

                    statusUpdate.addUpdate(StatusType.CURRENT_HP, (int) currentHp);

                    statusUpdate.addUpdate(StatusType.CURRENT_MP, (int) currentMp);

                    character.sendPacket(statusUpdate);



                }

                if (currentCp < maxCp) regenType = RegenType.CP;



            }



        }



    }
 
Какая там правильная механика регенерации? какой такт обновления ХП,МП,СП должен быть? я оптимизировал еще тот метод, внес флаги регенерации, засек выполнение метода при такте регенерации 0 мсек, меньше 1 миллисекунды ) вот только есть нюанс: или сделать равномерное время между тактами регенерации, что CP, что HP и МР но жертвуя двумя тактами вызова таска, или обойтись одним тактом, уменьшив нагрузку на железо в 2 раза и тогда получиться такая штука: если нужно обновлять скажем и СР и МР и НР, такт обновления будет равен для СР - 5 сек. и для НР и МР - 5 сек. так как такт задания 2,5 сек. я поставил, то есть 2,5 сек. прошло, обновило СР, потом 2,5 сек. прошло, обновило НР и МР, но если СР уже фул, а нужно обновлять НР и МР, тогда такт будет 2,5 сек. То есть если будет регенерация по отдельности, то регенерировать будет на 50% быстрее.

Я никак не пойму, в чем смысл этой оптимизации? Избавиться от микро-фриза? Или просто вызывать код для StatusUpdate реже чем надо и оптимизировать длинну этого пакета? Если микро-фриз появляется, почему бы просто не поставит регенерацию CP в отдельный таск? Будет и пауза, ну и отдельный пакет. Ява не лопнет от этого.

обойтись одним тактом, уменьшив нагрузку на железо в 2 раза
Это какая такая нагрузка на железо? У вас максимум две формулы с умножением и сложением. Ява это все оптимизирует очень хорошо, а вот структуры данных, нет. Поэтому я нe вижу ни нагрузки, ни оптимизации, ну и никакого смысла разделять регенерацию по двум логическим линиям.

Кстати, переключения потоков между данными и синхронизация будут намного сильнее влиять на производительность вашего кода чем простой таск.
 
Назад
Сверху Снизу