Welcome to RUSaCis - эмулятор Interlude

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

Repeat Enchant - повторная заточка одной кнопкой - RusAcis v3.8

Atrein

Вассал
INTERLUDE
INTERFACE
Регистрация
16 Янв 2022
Сообщения
71
Реакции
38
Баллы
18
RaCoin
5
Config.java:
--- a/aCis_gameserver/java/net/sf/l2j/Config.java
+++ b/aCis_gameserver/java/net/sf/l2j/Config.java
@@ -955,6 +955,31 @@
     public static int ENCHANT_BOOST_ACTIVATION_SKILL_LEVEL;
     public static int ENCHANT_BOOST_ACTIVATION_HIT_TIME;
 
+    /**
+     * By Atrein:
+     * Enables the server-side repeat-enchant confirmation workflow.
+     */
+    public static boolean ENCHANT_REPEAT_ENABLED;
+    /**
+     * By Atrein:
+     * Confirmation lifetime in milliseconds.
+     */
+    public static int ENCHANT_REPEAT_TIMEOUT;
+    /**
+     * By Atrein:
+     * Allows repeat prompts for normal enchant scrolls.
+     */
+    public static boolean ENCHANT_REPEAT_NORMAL_SCROLLS;
+    /**
+     * By Atrein:
+     * Allows repeat prompts for Blessed enchant scrolls.
+     */
+    public static boolean ENCHANT_REPEAT_BLESSED_SCROLLS;
+    /**
+     * By Atrein:
+     * Allows repeat prompts for Crystal enchant scrolls.
+     */
+    public static boolean ENCHANT_REPEAT_CRYSTAL_SCROLLS;
    
     /**
      * By Atrein:
@@ -2591,6 +2616,11 @@
         ENCHANT_BOOST_ACTIVATION_SKILL_LEVEL = Math.max(1, rusacis.getProperty("EnchantBoostActivationSkillLevel", 1));
         ENCHANT_BOOST_ACTIVATION_HIT_TIME = Math.max(0, rusacis.getProperty("EnchantBoostActivationHitTime", 1000));
 
+        ENCHANT_REPEAT_ENABLED = rusacis.getProperty("EnchantRepeatEnabled", true);
+        ENCHANT_REPEAT_TIMEOUT = Math.max(1000, rusacis.getProperty("EnchantRepeatTimeout", 15000));
+        ENCHANT_REPEAT_NORMAL_SCROLLS = rusacis.getProperty("EnchantRepeatNormalScrolls", true);
+        ENCHANT_REPEAT_BLESSED_SCROLLS = rusacis.getProperty("EnchantRepeatBlessedScrolls", true);
+        ENCHANT_REPEAT_CRYSTAL_SCROLLS = rusacis.getProperty("EnchantRepeatCrystalScrolls", false);
         AUTO_REPAIR_CORRUPTED_RUNTIME_TABLES = rusacis.getProperty("AutoRepairCorruptedRuntimeTables", true);
        
         ANTIFEED_ENABLE = rusacis.getProperty("AntiFeedEnable", false);
rus_acis.properties — config RU:
--- a/aCis_gameserver/config/rus_acis.properties
+++ b/aCis_gameserver/config/rus_acis.properties
@@ -165,6 +165,24 @@
 EnchantBoostActivationSkillLevel = 1
 EnchantBoostActivationHitTime = 1000
 
+# -----------------------------------------------------------------
+# Repeat Enchant
+# -----------------------------------------------------------------
+
+# Показывать стандартное окно Да/Нет после подходящей попытки заточки.
+EnchantRepeatEnabled = True
+
+# Время действия предложения повторить заточку, миллисекунды.
+EnchantRepeatTimeout = 15000
+
+# Разрешить повтор для обычных свитков.
+EnchantRepeatNormalScrolls = True
+
+# Разрешить повтор для Blessed-свитков.
+EnchantRepeatBlessedScrolls = True
+
+# Разрешить повтор для Crystal-свитков.
+EnchantRepeatCrystalScrolls = False
 
 # Автоматически пересоздавать разрешённые временные таблицы, если MariaDB
 # возвращает ошибку 1033 (повреждённые или несовместимые метаданные таблицы).
rus_acis.properties — config EN:
--- a/aCis_gameserver/config/en/rus_acis.properties
+++ b/aCis_gameserver/config/en/rus_acis.properties
@@ -165,6 +165,24 @@
 EnchantBoostActivationSkillLevel = 1
 EnchantBoostActivationHitTime = 1000
 
+# -----------------------------------------------------------------
+# Repeat Enchant
+# -----------------------------------------------------------------
+
+# Show a standard Yes/No dialog after an eligible enchant attempt.
+EnchantRepeatEnabled = True
+
+# Repeat confirmation lifetime in milliseconds.
+EnchantRepeatTimeout = 15000
+
+# Allow repeat prompts for normal enchant scrolls.
+EnchantRepeatNormalScrolls = True
+
+# Allow repeat prompts for Blessed enchant scrolls.
+EnchantRepeatBlessedScrolls = True
+
+# Allow repeat prompts for Crystal enchant scrolls.
+EnchantRepeatCrystalScrolls = False
 
 # Automatically rebuild allow-listed disposable runtime tables when MariaDB
 # reports error 1033 (corrupted or incompatible table metadata).
rus_acis.properties — runtime RU:
--- a/aCis_gameserver/bin/rus_acis.properties
+++ b/aCis_gameserver/bin/rus_acis.properties
@@ -165,6 +165,24 @@
 EnchantBoostActivationSkillLevel = 1
 EnchantBoostActivationHitTime = 1000
 
+# -----------------------------------------------------------------
+# Repeat Enchant
+# -----------------------------------------------------------------
+
+# Показывать стандартное окно Да/Нет после подходящей попытки заточки.
+EnchantRepeatEnabled = True
+
+# Время действия предложения повторить заточку, миллисекунды.
+EnchantRepeatTimeout = 15000
+
+# Разрешить повтор для обычных свитков.
+EnchantRepeatNormalScrolls = True
+
+# Разрешить повтор для Blessed-свитков.
+EnchantRepeatBlessedScrolls = True
+
+# Разрешить повтор для Crystal-свитков.
+EnchantRepeatCrystalScrolls = False
 
 # Автоматически пересоздавать разрешённые временные таблицы, если MariaDB
 # возвращает ошибку 1033 (повреждённые или несовместимые метаданные таблицы).
rus_acis.properties — runtime EN:
--- a/aCis_gameserver/bin/en/rus_acis.properties
+++ b/aCis_gameserver/bin/en/rus_acis.properties
@@ -165,6 +165,24 @@
 EnchantBoostActivationSkillLevel = 1
 EnchantBoostActivationHitTime = 1000
 
+# -----------------------------------------------------------------
+# Repeat Enchant
+# -----------------------------------------------------------------
+
+# Show a standard Yes/No dialog after an eligible enchant attempt.
+EnchantRepeatEnabled = True
+
+# Repeat confirmation lifetime in milliseconds.
+EnchantRepeatTimeout = 15000
+
+# Allow repeat prompts for normal enchant scrolls.
+EnchantRepeatNormalScrolls = True
+
+# Allow repeat prompts for Blessed enchant scrolls.
+EnchantRepeatBlessedScrolls = True
+
+# Allow repeat prompts for Crystal enchant scrolls.
+EnchantRepeatCrystalScrolls = False
 
 # Automatically rebuild allow-listed disposable runtime tables when MariaDB
 # reports error 1033 (corrupted or incompatible table metadata).
EnchantScrolls.java:
--- a/aCis_gameserver/java/net/sf/l2j/gameserver/handler/itemhandlers/EnchantScrolls.java
+++ b/aCis_gameserver/java/net/sf/l2j/gameserver/handler/itemhandlers/EnchantScrolls.java
@@ -2,25 +2,49 @@
 
 import net.sf.l2j.gameserver.handler.IItemHandler;
 import net.sf.l2j.gameserver.model.actor.Playable;
 import net.sf.l2j.gameserver.model.actor.Player;
+import net.sf.l2j.gameserver.model.enchant.EnchantRepeatService;
+import net.sf.l2j.gameserver.model.enchant.EnchantSessionService;
 import net.sf.l2j.gameserver.model.item.instance.ItemInstance;
 import net.sf.l2j.gameserver.network.SystemMessageId;
-import net.sf.l2j.gameserver.network.serverpackets.ChooseInventoryItem;
+import net.sf.l2j.gameserver.network.serverpackets.PlaySound;
 
+/**
+ * Opens the stock item-enchant interface for enchant scrolls.
+ * <p>
+ * By Atrein:
+ * Manual scroll use invalidates only an old custom repeat confirmation and
+ * delegates active-scroll/window state to {@link EnchantSessionService}.
+ */
 public class EnchantScrolls implements IItemHandler
 {
+    private static final String ENCHANT_OPEN_SOUND = "InterfaceSound.charstat_open_01";
+
+    /**
+     * Creates the enchant-scroll item handler.
+     * <p>
+     * By Atrein: window state is delegated to the dedicated session service.
+     */
+    public EnchantScrolls()
+    {
+    }
+
     @Override
     public void useItem(Playable playable, ItemInstance item, boolean forceUse)
     {
-        if (!(playable instanceof Player))
+        if (!(playable instanceof Player player))
             return;
-       
-        final Player player = (Player) playable;
-       
-        if (player.getActiveEnchantItem() == null)
+
+        EnchantRepeatService.getInstance().clear(player);
+
+        final boolean openNewEnchantWindow = player.getActiveEnchantItem() == null;
+        if (openNewEnchantWindow)
             player.sendPacket(SystemMessageId.SELECT_ITEM_TO_ENCHANT);
-       
-        player.setActiveEnchantItem(item);
-        player.sendPacket(new ChooseInventoryItem(item.getItemId()));
+
+        if (!EnchantSessionService.getInstance().open(player, item))
+            return;
+
+        if (openNewEnchantWindow)
+            player.sendPacket(new PlaySound(ENCHANT_OPEN_SOUND));
     }
 }
RequestEnchantItem.java — diff:
# Architectural replacement.
# The previous packet contained the full enchant transaction inline:
# validation, scroll consumption, random roll, success/failure, skill refresh,
# crystal return, inventory packets and active-scroll cleanup.
# Those responsibilities were removed from the packet and moved to EnchantService.
--- a/aCis_gameserver/java/net/sf/l2j/gameserver/network/clientpackets/RequestEnchantItem.java
+++ b/aCis_gameserver/java/net/sf/l2j/gameserver/network/clientpackets/RequestEnchantItem.java
@@ imports and superclass @@
-import net.sf.l2j.commons.random.Rnd;
-import net.sf.l2j.Config;
-import net.sf.l2j.gameserver.data.SkillTable;
-import net.sf.l2j.gameserver.data.xml.ArmorSetData;
-import net.sf.l2j.gameserver.enums.Paperdoll;
-import net.sf.l2j.gameserver.enums.StatusType;
-import net.sf.l2j.gameserver.model.World;
 import net.sf.l2j.gameserver.model.actor.Player;
-import net.sf.l2j.gameserver.model.item.ArmorSet;
+import net.sf.l2j.gameserver.model.enchant.EnchantRepeatService;
+import net.sf.l2j.gameserver.model.enchant.EnchantService;
+import net.sf.l2j.gameserver.model.enchant.EnchantSessionService;
 import net.sf.l2j.gameserver.model.item.instance.ItemInstance;
-import net.sf.l2j.gameserver.model.item.kind.Armor;
-import net.sf.l2j.gameserver.model.item.kind.Item;
-import net.sf.l2j.gameserver.model.item.kind.Weapon;
 import net.sf.l2j.gameserver.network.SystemMessageId;
-import net.sf.l2j.gameserver.network.serverpackets.EnchantResult;
-import net.sf.l2j.gameserver.network.serverpackets.InventoryUpdate;
-import net.sf.l2j.gameserver.network.serverpackets.ItemList;
-import net.sf.l2j.gameserver.network.serverpackets.StatusUpdate;
-import net.sf.l2j.gameserver.network.serverpackets.SystemMessage;
-import net.sf.l2j.gameserver.skills.L2Skill;

-public final class RequestEnchantItem extends AbstractEnchantPacket
+public final class RequestEnchantItem extends L2GameClientPacket
@@ runImpl responsibility @@
-        // Legacy packet executed the entire enchant transaction here.
-        // It consumed the scroll, rolled the chance, changed/destroyed the item,
-        // updated skills and finally cleared player.setActiveEnchantItem(null).
+        final ItemInstance scroll = player.getActiveEnchantItem();
+        if (scroll == null)
+        {
+            EnchantRepeatService.getInstance().clear(player);
+            player.sendPacket(SystemMessageId.ENCHANT_SCROLL_CANCELLED);
+            EnchantSessionService.getInstance().close(player, true);
+            return;
+        }
+
+        EnchantService.getInstance().enchant(player, _objectId, scroll.getObjectId(), EnchantService.EnchantSource.MANUAL);
RequestEnchantItem.java — итоговый файл:
--- /dev/null
+++ b/aCis_gameserver/java/net/sf/l2j/gameserver/network/clientpackets/RequestEnchantItem.java
+package net.sf.l2j.gameserver.network.clientpackets;
+
+import net.sf.l2j.gameserver.model.actor.Player;
+import net.sf.l2j.gameserver.model.enchant.EnchantRepeatService;
+import net.sf.l2j.gameserver.model.enchant.EnchantService;
+import net.sf.l2j.gameserver.model.enchant.EnchantSessionService;
+import net.sf.l2j.gameserver.model.item.instance.ItemInstance;
+import net.sf.l2j.gameserver.network.SystemMessageId;
+
+/**
+ * Handles a target selected through the stock Item Enchant window.
+ * <p>
+ * By Atrein:
+ * Treats stock target selection, including the client's middle Repeat action,
+ * as an authoritative manual request. A pending custom confirmation is cleared,
+ * while the active scroll rebound by {@link EnchantSessionService} remains usable.
+ */
+public final class RequestEnchantItem extends L2GameClientPacket
+{
+    private int _objectId;
+
+    /**
+     * Creates an empty packet populated by {@link #readImpl()}.
+     * <p>
+     * By Atrein: the packet consumes the rebound active enchant scroll.
+     */
+    public RequestEnchantItem()
+    {
+    }
+
+    @Override
+    protected void readImpl()
+    {
+        _objectId = readD();
+    }
+
+    @Override
+    protected void runImpl()
+    {
+        final Player player = getClient().getPlayer();
+        if (player == null || _objectId <= 0)
+            return;
+
+        final ItemInstance scroll = player.getActiveEnchantItem();
+        if (scroll == null)
+        {
+            EnchantRepeatService.getInstance().clear(player);
+            player.sendPacket(SystemMessageId.ENCHANT_SCROLL_CANCELLED);
+            EnchantSessionService.getInstance().close(player, true);
+            return;
+        }
+
+        EnchantService.getInstance().enchant(player, _objectId, scroll.getObjectId(), EnchantService.EnchantSource.MANUAL);
+    }
+}
DlgAnswer.java:
--- a/aCis_gameserver/java/net/sf/l2j/gameserver/network/clientpackets/DlgAnswer.java
+++ b/aCis_gameserver/java/net/sf/l2j/gameserver/network/clientpackets/DlgAnswer.java
@@ -1,7 +1,8 @@
 package net.sf.l2j.gameserver.network.clientpackets;
 
 import net.sf.l2j.gameserver.model.actor.Player;
+import net.sf.l2j.gameserver.model.enchant.EnchantRepeatService;
 import net.sf.l2j.gameserver.model.entity.events.capturetheflag.CTFEvent;
 import net.sf.l2j.gameserver.model.entity.events.capturetheflag.CTFManager;
 import net.sf.l2j.gameserver.model.entity.events.deathmatch.DMEvent;
 import net.sf.l2j.gameserver.model.entity.events.deathmatch.DMManager;
@@ -31,8 +32,11 @@
         final Player player = getClient().getPlayer();
         if (player == null)
             return;
        
+        if (_messageId == SystemMessageId.S1.getId() && EnchantRepeatService.getInstance().handleAnswer(player, _answer, _requesterId))
+            return;
+       
         if (_requesterId == CTFManager.JOIN_CTF_REQ_ID && _answer == 1)
             CTFEvent.getInstance().onBypass("ctf_event_participation", player);
         else if (_requesterId == DMManager.JOIN_DM_REQ_ID && _answer == 1)
             DMEvent.getInstance().onBypass("dm_event_participation", player);
Player.java:
--- a/aCis_gameserver/java/net/sf/l2j/gameserver/model/actor/Player.java
+++ b/aCis_gameserver/java/net/sf/l2j/gameserver/model/actor/Player.java
@@ -142,10 +142,13 @@
 import net.sf.l2j.gameserver.model.actor.status.PlayerStatus;
 import net.sf.l2j.gameserver.model.actor.template.PetTemplate;
 import net.sf.l2j.gameserver.model.actor.template.PlayerTemplate;
 import net.sf.l2j.gameserver.model.craft.ManufactureList;
 import net.sf.l2j.gameserver.model.entity.autofarm.AutoFarmManager;
+import net.sf.l2j.gameserver.model.enchant.EnchantRepeatService;
+import net.sf.l2j.gameserver.model.enchant.EnchantService;
+import net.sf.l2j.gameserver.model.enchant.EnchantSessionService;
 import net.sf.l2j.gameserver.model.entity.events.capturetheflag.CTFEvent;
 import net.sf.l2j.gameserver.model.entity.events.deathmatch.DMEvent;
 import net.sf.l2j.gameserver.model.entity.events.lastman.LMEvent;
 import net.sf.l2j.gameserver.model.entity.events.teamvsteam.TvTEvent;
 import net.sf.l2j.gameserver.model.group.CommandChannel;
@@ -3543,14 +3546,17 @@
         onTradeCancel(this);
     }
    
     public void cancelActiveEnchant()
     {
+        // By Atrein: explicitly closing the stock enchant window always invalidates
+        // a pending custom repeat confirmation and its timeout task.
+        EnchantRepeatService.getInstance().clear(this);
         if (_activeEnchantItem == null)
             return;
 
-        setActiveEnchantItem(null);
+        EnchantSessionService.getInstance().close(this, false);
         sendPacket(EnchantResult.CANCELLED);
         sendPacket(SystemMessageId.ENCHANT_SCROLL_CANCELLED);
     }
    
     /**
@@ -7044,10 +7050,15 @@
             CTFEvent.getInstance().onLogout(this);
             DMEvent.getInstance().onLogout(this);
             LMEvent.getInstance().onLogout(this);
             TvTEvent.getInstance().onLogout(this);
            
+            // By Atrein: clear short-lived enchant state before inventory teardown.
+            EnchantRepeatService.getInstance().clear(this);
+            EnchantSessionService.getInstance().close(this, false);
+            EnchantService.getInstance().clear(this);
+           
             // Update inventory and remove them from the world
             getInventory().deleteMe();
            
             // Update warehouse and remove them from the world
             clearWarehouse();
EnchantRepeatService.java — новый файл:
--- /dev/null
+++ b/aCis_gameserver/java/net/sf/l2j/gameserver/model/enchant/EnchantRepeatService.java
@@ -0,0 +1,301 @@
+package net.sf.l2j.gameserver.model.enchant;
+
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import net.sf.l2j.commons.pool.ThreadPool;
+
+import net.sf.l2j.Config;
+import net.sf.l2j.gameserver.data.xml.EnchantData;
+import net.sf.l2j.gameserver.model.actor.Player;
+import net.sf.l2j.gameserver.model.item.instance.ItemInstance;
+import net.sf.l2j.gameserver.model.records.custom.EnchantScroll;
+import net.sf.l2j.gameserver.network.SystemMessageId;
+import net.sf.l2j.gameserver.network.serverpackets.ConfirmDlg;
+
+/**
+ * Manages short-lived custom repeat-enchant confirmation requests.
+ * <p>
+ * By Atrein:
+ * Keeps the stock Item Enchant session alive while the dialog is visible, uses
+ * one-time requester tokens and restores the stock window on cancel, close or
+ * server-side timeout without requiring a client modification.
+ */
+public final class EnchantRepeatService
+{
+    private static final int REQUEST_PREFIX = 0x6E000000;
+    private static final int REQUEST_PREFIX_MASK = 0xFF000000;
+    private static final int REQUEST_SEQUENCE_MASK = 0x00FFFFFF;
+
+    private final ConcurrentHashMap<Integer, RepeatContext> _contexts = new ConcurrentHashMap<>();
+    private final AtomicInteger _requestSequence = new AtomicInteger();
+
+    private EnchantRepeatService()
+    {
+    }
+
+    /**
+     * Offers a repeat action when the previous target survived and the stock
+     * enchant window is already rebound to another compatible scroll.
+     *
+     * @param player The player receiving the confirmation.
+     * @param item The previous enchant target.
+     * @param scrollItemId The scroll item ID used by the completed attempt.
+     * @param enchant The authoritative scroll definition.
+     */
+    public void offer(Player player, ItemInstance item, int scrollItemId, EnchantScroll enchant)
+    {
+        clear(player);
+        if (!Config.ENCHANT_REPEAT_ENABLED || player == null || item == null || enchant == null || !isScrollTypeAllowed(enchant))
+            return;
+        if (!EnchantSessionService.getInstance().isValidTarget(player, item, enchant))
+            return;
+
+        final ItemInstance activeScroll = restoreOrGetActiveScroll(player, scrollItemId);
+        if (activeScroll == null)
+            return;
+
+        final int requestId = nextRequestId();
+        final long expiresAt = System.currentTimeMillis() + Config.ENCHANT_REPEAT_TIMEOUT;
+        final RepeatContext context = new RepeatContext(requestId, item.getObjectId(), scrollItemId, expiresAt);
+        _contexts.put(player.getObjectId(), context);
+
+        final ScheduledFuture<?> timeoutTask = ThreadPool.schedule(() -> expire(player, context), Config.ENCHANT_REPEAT_TIMEOUT);
+        if (timeoutTask == null)
+        {
+            _contexts.remove(player.getObjectId(), context);
+            return;
+        }
+        context.setTimeoutTask(timeoutTask);
+        if (_contexts.get(player.getObjectId()) != context)
+        {
+            context.cancelTimeout();
+            return;
+        }
+
+        final String prompt = player.getSysString(10_374, item.getName(), activeScroll.getName());
+        player.sendPacket(new ConfirmDlg(SystemMessageId.S1).addString(prompt).addTime(Config.ENCHANT_REPEAT_TIMEOUT).addRequesterId(requestId));
+    }
+
+    /**
+     * Handles a standard dialog answer when the requester ID belongs to this service.
+     *
+     * @param player The answering player.
+     * @param answer The client answer; only {@code 1} means Yes.
+     * @param requesterId The requester ID echoed by the client.
+     * @return {@code true} when the requester ID belonged to repeat enchant.
+     */
+    public boolean handleAnswer(Player player, int answer, int requesterId)
+    {
+        if (!isRequesterId(requesterId))
+            return false;
+        if (player == null)
+            return true;
+
+        final RepeatContext context = _contexts.get(player.getObjectId());
+        if (context == null)
+            return true;
+
+        final boolean matchingContext = context.requestId() == requesterId;
+        if (!matchingContext)
+            return true;
+
+        final boolean expired = System.currentTimeMillis() > context.expiresAt();
+        final EnchantSessionPolicy.DialogAction action = EnchantSessionPolicy.resolveDialogAction(matchingContext, answer, expired);
+        if (action == EnchantSessionPolicy.DialogAction.IGNORE)
+            return true;
+        if (!_contexts.remove(player.getObjectId(), context))
+            return true;
+
+        context.cancelTimeout();
+        if (action == EnchantSessionPolicy.DialogAction.RESTORE_WINDOW)
+        {
+            EnchantSessionService.getInstance().restore(player, context.scrollItemId());
+            return true;
+        }
+
+        repeat(player, context);
+        return true;
+    }
+
+    /**
+     * By Atrein:
+     * Returns the active scroll bound to the stock enchant window, restoring the
+     * session first when necessary. A non-null result is always revalidated against
+     * the matching usable inventory scroll.
+     *
+     * @param player The player whose stock enchant session is checked.
+     * @param scrollItemId The scroll item ID that must remain active.
+     * @return The validated active scroll, or {@code null} when the session cannot be restored.
+     */
+    private static ItemInstance restoreOrGetActiveScroll(Player player, int scrollItemId)
+    {
+        ItemInstance scroll = EnchantSessionService.getInstance().findUsableScroll(player, scrollItemId);
+        ItemInstance activeScroll = player.getActiveEnchantItem();
+        if (scroll != null && activeScroll != null && activeScroll.getObjectId() == scroll.getObjectId())
+            return activeScroll;
+
+        if (!EnchantSessionService.getInstance().restore(player, scrollItemId))
+            return null;
+
+        scroll = EnchantSessionService.getInstance().findUsableScroll(player, scrollItemId);
+        activeScroll = player.getActiveEnchantItem();
+        if (scroll == null || activeScroll == null || activeScroll.getObjectId() != scroll.getObjectId())
+            return null;
+
+        return activeScroll;
+    }
+
+    private static void repeat(Player player, RepeatContext context)
+    {
+        final ItemInstance item = player.getInventory().getItemByObjectId(context.itemObjectId());
+        final ItemInstance scroll = EnchantSessionService.getInstance().findUsableScroll(player, context.scrollItemId());
+        if (item == null || scroll == null)
+        {
+            player.sendMessage(player.getSysString(10_376));
+            EnchantSessionService.getInstance().restore(player, context.scrollItemId());
+            return;
+        }
+
+        final EnchantScroll enchant = EnchantData.getInstance().getEnchantScroll(scroll);
+        if (enchant == null || !isScrollTypeAllowed(enchant) || !EnchantSessionService.getInstance().isValidTarget(player, item, enchant))
+        {
+            player.sendMessage(player.getSysString(10_377));
+            EnchantSessionService.getInstance().restore(player, context.scrollItemId());
+            return;
+        }
+        if (!EnchantSessionService.getInstance().prepareAttempt(player, scroll))
+        {
+            player.sendMessage(player.getSysString(10_376));
+            EnchantSessionService.getInstance().restore(player, context.scrollItemId());
+            return;
+        }
+
+        EnchantService.getInstance().enchant(player, item.getObjectId(), scroll.getObjectId(), EnchantService.EnchantSource.REPEAT);
+    }
+
+    private void expire(Player player, RepeatContext context)
+    {
+        if (player == null || !_contexts.remove(player.getObjectId(), context))
+            return;
+
+        context.cancelTimeout();
+        EnchantSessionService.getInstance().restore(player, context.scrollItemId());
+    }
+
+    /**
+     * Clears the pending custom repeat request without closing the stock window.
+     *
+     * @param player The player whose request is removed.
+     */
+    public void clear(Player player)
+    {
+        if (player == null)
+            return;
+
+        final RepeatContext context = _contexts.remove(player.getObjectId());
+        if (context != null)
+            context.cancelTimeout();
+    }
+
+    /**
+     * Tests whether a dialog requester ID belongs to repeat enchant.
+     *
+     * @param requesterId The requester ID received from {@code DlgAnswer}.
+     * @return {@code true} when the requester ID belongs to repeat enchant.
+     */
+    public static boolean isRequesterId(int requesterId)
+    {
+        return (requesterId & REQUEST_PREFIX_MASK) == REQUEST_PREFIX;
+    }
+
+    /**
+     * Tests whether the configured repeat policy allows the scroll category.
+     *
+     * @param enchant The scroll definition.
+     * @return {@code true} when custom repeat is enabled for the category.
+     */
+    static boolean isScrollTypeAllowed(EnchantScroll enchant)
+    {
+        if (enchant == null)
+            return false;
+        if (!enchant.cristalize())
+            return Config.ENCHANT_REPEAT_BLESSED_SCROLLS;
+        if (enchant.returnVal() == -1)
+            return Config.ENCHANT_REPEAT_CRYSTAL_SCROLLS;
+        return Config.ENCHANT_REPEAT_NORMAL_SCROLLS;
+    }
+
+    private int nextRequestId()
+    {
+        int sequence = _requestSequence.updateAndGet(value -> (value + 1) & REQUEST_SEQUENCE_MASK);
+        if (sequence == 0)
+            sequence = _requestSequence.incrementAndGet();
+        return REQUEST_PREFIX | sequence;
+    }
+
+    private static final class RepeatContext
+    {
+        private final int _requestId;
+        private final int _itemObjectId;
+        private final int _scrollItemId;
+        private final long _expiresAt;
+        private volatile ScheduledFuture<?> _timeoutTask;
+
+        private RepeatContext(int requestId, int itemObjectId, int scrollItemId, long expiresAt)
+        {
+            _requestId = requestId;
+            _itemObjectId = itemObjectId;
+            _scrollItemId = scrollItemId;
+            _expiresAt = expiresAt;
+        }
+
+        private int requestId()
+        {
+            return _requestId;
+        }
+
+        private int itemObjectId()
+        {
+            return _itemObjectId;
+        }
+
+        private int scrollItemId()
+        {
+            return _scrollItemId;
+        }
+
+        private long expiresAt()
+        {
+            return _expiresAt;
+        }
+
+        private void setTimeoutTask(ScheduledFuture<?> timeoutTask)
+        {
+            _timeoutTask = timeoutTask;
+        }
+
+        private void cancelTimeout()
+        {
+            final ScheduledFuture<?> timeoutTask = _timeoutTask;
+            if (timeoutTask != null)
+                timeoutTask.cancel(false);
+        }
+    }
+
+    /**
+     * Returns the shared repeat-enchant service instance.
+     *
+     * @return The singleton service instance.
+     */
+    public static EnchantRepeatService getInstance()
+    {
+        return SingletonHolder.INSTANCE;
+    }
+
+    private static class SingletonHolder
+    {
+        private static final EnchantRepeatService INSTANCE = new EnchantRepeatService();
+    }
+}
EnchantSessionService.java — новый файл:
--- /dev/null
+++ b/aCis_gameserver/java/net/sf/l2j/gameserver/model/enchant/EnchantSessionService.java
@@ -0,0 +1,227 @@
+package net.sf.l2j.gameserver.model.enchant;
+
+import net.sf.l2j.gameserver.data.xml.EnchantData;
+import net.sf.l2j.gameserver.enums.items.ItemLocation;
+import net.sf.l2j.gameserver.model.actor.Player;
+import net.sf.l2j.gameserver.model.item.instance.ItemInstance;
+import net.sf.l2j.gameserver.model.records.custom.EnchantScroll;
+import net.sf.l2j.gameserver.network.serverpackets.ChooseInventoryItem;
+import net.sf.l2j.gameserver.network.serverpackets.EnchantResult;
+import net.sf.l2j.gameserver.network.serverpackets.ItemList;
+
+/**
+ * Owns the server-side state of the stock Item Enchant window.
+ * <p>
+ * By Atrein:
+ * Rebinds a real remaining scroll after every completed attempt, rebuilds the
+ * client target list and keeps the normal Repeat button usable independently
+ * from the optional custom confirmation dialog.
+ */
+public final class EnchantSessionService
+{
+    private EnchantSessionService()
+    {
+    }
+
+    /**
+     * Opens or refreshes the stock Item Enchant window with a validated scroll.
+     *
+     * @param player The player opening the window.
+     * @param scroll The scroll selected by the player.
+     * @return {@code true} when the scroll was bound and the client list was sent.
+     */
+    public boolean open(Player player, ItemInstance scroll)
+    {
+        if (!isConnected(player) || !isUsableScroll(player, scroll))
+            return false;
+
+        bind(player, scroll);
+        return true;
+    }
+
+    /**
+     * Reconciles the stock enchant window after an authoritative attempt.
+     *
+     * @param player The enchanting player.
+     * @param scrollItemId The item ID of the consumed scroll type.
+     * @param previousTarget The item targeted by the completed attempt.
+     * @param enchant The server-side scroll definition.
+     * @param targetSurvived Whether the previous target still exists.
+     * @param repeatAllowed Whether the custom repeat dialog is enabled for this scroll type.
+     * @return The resulting session state.
+     */
+    public RefreshResult afterAttempt(Player player, int scrollItemId, ItemInstance previousTarget, EnchantScroll enchant, boolean targetSurvived, boolean repeatAllowed)
+    {
+        if (!isConnected(player) || enchant == null)
+        {
+            close(player, false);
+            return RefreshResult.CLOSED;
+        }
+
+        player.sendPacket(new ItemList(player, false));
+
+        final ItemInstance nextScroll = findUsableScroll(player, scrollItemId);
+        final boolean hasNextScroll = nextScroll != null;
+        final boolean hasCompatibleTarget = hasNextScroll && hasCompatibleTarget(player, enchant);
+        final boolean sameTargetValid = targetSurvived && isValidTarget(player, previousTarget, enchant);
+        final EnchantSessionPolicy.WindowDecision decision = EnchantSessionPolicy.afterAttempt(hasNextScroll, hasCompatibleTarget, targetSurvived, sameTargetValid, repeatAllowed);
+
+        if (!decision.keepWindowOpen())
+        {
+            close(player, false);
+            return RefreshResult.CLOSED;
+        }
+
+        bind(player, nextScroll);
+        return new RefreshResult(true, decision.offerRepeat(), nextScroll);
+    }
+
+    /**
+     * Restores the stock Item Enchant window after the custom dialog was cancelled,
+     * closed or expired.
+     *
+     * @param player The player whose standard window is restored.
+     * @param scrollItemId The scroll item ID used by the session.
+     * @return {@code true} when a usable scroll and target still exist.
+     */
+    public boolean restore(Player player, int scrollItemId)
+    {
+        if (!isConnected(player))
+        {
+            close(player, false);
+            return false;
+        }
+
+        final ItemInstance scroll = findUsableScroll(player, scrollItemId);
+        final EnchantScroll enchant = scroll == null ? null : EnchantData.getInstance().getEnchantScroll(scroll);
+        if (scroll == null || enchant == null || !hasCompatibleTarget(player, enchant))
+        {
+            close(player, false);
+            return false;
+        }
+
+        player.sendPacket(new ItemList(player, false));
+        bind(player, scroll);
+        return true;
+    }
+
+    /**
+     * Finds an owned inventory scroll of the requested item ID.
+     *
+     * @param player The scroll owner.
+     * @param scrollItemId The requested scroll item ID.
+     * @return A usable scroll instance, or {@code null}.
+     */
+    public ItemInstance findUsableScroll(Player player, int scrollItemId)
+    {
+        if (player == null || scrollItemId <= 0)
+            return null;
+
+        for (ItemInstance item : player.getInventory().getItemsByItemId(scrollItemId))
+        {
+            if (isUsableScroll(player, item))
+                return item;
+        }
+        return null;
+    }
+
+    /**
+     * Verifies that the previous or manually selected target can be enchanted with
+     * the supplied definition at its current state.
+     *
+     * @param player The item owner.
+     * @param item The candidate target.
+     * @param enchant The scroll definition.
+     * @return {@code true} when the target is currently valid.
+     */
+    public boolean isValidTarget(Player player, ItemInstance item, EnchantScroll enchant)
+    {
+        return player != null && item != null && enchant != null && item.getOwnerId() == player.getObjectId() && EnchantService.isEnchantable(item) && enchant.isValid(item) && enchant.getChance(item) > 0;
+    }
+
+    /**
+     * Binds a validated scroll before an immediate repeat transaction without
+     * reopening the target list.
+     *
+     * @param player The enchanting player.
+     * @param scroll The scroll to bind.
+     * @return {@code true} when the scroll was accepted.
+     */
+    boolean prepareAttempt(Player player, ItemInstance scroll)
+    {
+        if (!isConnected(player) || !isUsableScroll(player, scroll))
+            return false;
+
+        player.setActiveEnchantItem(scroll);
+        return true;
+    }
+
+    /**
+     * Clears only the server-side stock enchant session.
+     *
+     * @param player The player whose session is closed.
+     * @param notifyClient Whether to send the stock cancellation packet.
+     */
+    public void close(Player player, boolean notifyClient)
+    {
+        if (player == null)
+            return;
+
+        player.setActiveEnchantItem(null);
+        if (notifyClient && isConnected(player))
+            player.sendPacket(EnchantResult.CANCELLED);
+    }
+
+    private static boolean isConnected(Player player)
+    {
+        return player != null && player.isOnline() && player.getClient() != null && !player.getClient().isDetached();
+    }
+
+    private static boolean isUsableScroll(Player player, ItemInstance scroll)
+    {
+        return player != null && scroll != null && scroll.getOwnerId() == player.getObjectId() && scroll.getCount() > 0 && scroll.getLocation() == ItemLocation.INVENTORY && EnchantData.getInstance().getEnchantScroll(scroll) != null;
+    }
+
+    private boolean hasCompatibleTarget(Player player, EnchantScroll enchant)
+    {
+        for (ItemInstance item : player.getInventory().getItems())
+        {
+            if (isValidTarget(player, item, enchant))
+                return true;
+        }
+        return false;
+    }
+
+    private static void bind(Player player, ItemInstance scroll)
+    {
+        player.setActiveEnchantItem(scroll);
+        player.sendPacket(new ChooseInventoryItem(scroll.getItemId()));
+    }
+
+    /**
+     * Immutable result of rebuilding the stock Item Enchant session.
+     *
+     * @param windowReady Whether the stock window has a bound scroll and valid target.
+     * @param offerRepeat Whether the custom repeat dialog may be shown.
+     * @param activeScroll The scroll bound to the stock window.
+     */
+    public record RefreshResult(boolean windowReady, boolean offerRepeat, ItemInstance activeScroll)
+    {
+        private static final RefreshResult CLOSED = new RefreshResult(false, false, null);
+    }
+
+    /**
+     * Returns the shared enchant-session service.
+     *
+     * @return The singleton service instance.
+     */
+    public static EnchantSessionService getInstance()
+    {
+        return SingletonHolder.INSTANCE;
+    }
+
+    private static class SingletonHolder
+    {
+        private static final EnchantSessionService INSTANCE = new EnchantSessionService();
+    }
+}
EnchantSessionPolicy.java — новый файл:
--- /dev/null
+++ b/aCis_gameserver/java/net/sf/l2j/gameserver/model/enchant/EnchantSessionPolicy.java
@@ -0,0 +1,74 @@
+package net.sf.l2j.gameserver.model.enchant;
+
+/**
+ * Defines deterministic decisions for the Repeat Enchant state machine.
+ * <p>
+ * By Atrein:
+ * Keeps post-attempt and dialog-answer rules independent from packets, inventory
+ * access and timers so the critical state transitions can be regression-tested.
+ */
+public final class EnchantSessionPolicy
+{
+    private EnchantSessionPolicy()
+    {
+        throw new IllegalStateException("Utility class");
+    }
+
+    /**
+     * Resolves the normal enchant-window and custom repeat-dialog state after a
+     * completed enchant attempt.
+     *
+     * @param hasNextScroll Whether another scroll of the same item ID exists.
+     * @param hasCompatibleTarget Whether at least one target can still be enchanted.
+     * @param targetSurvived Whether the previous target still exists.
+     * @param sameTargetValid Whether the previous target can be enchanted again.
+     * @param repeatAllowed Whether the scroll category permits the custom repeat dialog.
+     * @return The immutable state decision.
+     */
+    public static WindowDecision afterAttempt(boolean hasNextScroll, boolean hasCompatibleTarget, boolean targetSurvived, boolean sameTargetValid, boolean repeatAllowed)
+    {
+        final boolean keepWindowOpen = hasNextScroll && hasCompatibleTarget;
+        final boolean offerRepeat = keepWindowOpen && targetSurvived && sameTargetValid && repeatAllowed;
+        return new WindowDecision(keepWindowOpen, offerRepeat);
+    }
+
+    /**
+     * Resolves a custom repeat-dialog answer.
+     *
+     * @param matchingContext Whether the requester ID matches the current context.
+     * @param answer The answer value received from the client.
+     * @param expired Whether the context already expired.
+     * @return The action that the service must perform.
+     */
+    public static DialogAction resolveDialogAction(boolean matchingContext, int answer, boolean expired)
+    {
+        if (!matchingContext)
+            return DialogAction.IGNORE;
+        if (expired || answer != 1)
+            return DialogAction.RESTORE_WINDOW;
+        return DialogAction.REPEAT;
+    }
+
+    /**
+     * Result of a completed-attempt state decision.
+     *
+     * @param keepWindowOpen Whether the stock Item Enchant window must remain ready.
+     * @param offerRepeat Whether the custom repeat confirmation may be shown.
+     */
+    public record WindowDecision(boolean keepWindowOpen, boolean offerRepeat)
+    {
+    }
+
+    /**
+     * Action produced by a custom repeat-dialog answer.
+     */
+    public enum DialogAction
+    {
+        /** Execute a new attempt against the previous target. */
+        REPEAT,
+        /** Keep and refresh the stock Item Enchant window. */
+        RESTORE_WINDOW,
+        /** Ignore a stale or unrelated requester ID. */
+        IGNORE
+    }
+}
EnchantService.java — новый файл:
--- /dev/null
+++ b/aCis_gameserver/java/net/sf/l2j/gameserver/model/enchant/EnchantService.java
@@ -0,0 +1,364 @@
+package net.sf.l2j.gameserver.model.enchant;
+
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.locks.ReentrantLock;
+
+import net.sf.l2j.commons.random.Rnd;
+
+import net.sf.l2j.Config;
+import net.sf.l2j.gameserver.data.SkillTable;
+import net.sf.l2j.gameserver.data.xml.ArmorSetData;
+import net.sf.l2j.gameserver.data.xml.EnchantData;
+import net.sf.l2j.gameserver.enums.Paperdoll;
+import net.sf.l2j.gameserver.enums.actors.MissionType;
+import net.sf.l2j.gameserver.enums.items.ItemLocation;
+import net.sf.l2j.gameserver.enums.items.WeaponType;
+import net.sf.l2j.gameserver.model.World;
+import net.sf.l2j.gameserver.model.actor.Player;
+import net.sf.l2j.gameserver.model.actor.container.player.MissionList;
+import net.sf.l2j.gameserver.model.item.ArmorSet;
+import net.sf.l2j.gameserver.model.item.instance.ItemInstance;
+import net.sf.l2j.gameserver.model.item.kind.Armor;
+import net.sf.l2j.gameserver.model.item.kind.Item;
+import net.sf.l2j.gameserver.model.item.kind.Weapon;
+import net.sf.l2j.gameserver.model.records.custom.EnchantScroll;
+import net.sf.l2j.gameserver.network.SystemMessageId;
+import net.sf.l2j.gameserver.network.serverpackets.EnchantResult;
+import net.sf.l2j.gameserver.network.serverpackets.SkillList;
+import net.sf.l2j.gameserver.network.serverpackets.SystemMessage;
+import net.sf.l2j.gameserver.skills.L2Skill;
+
+/**
+ * Executes the authoritative item enchant transaction.
+ * <p>
+ * By Atrein:
+ * Serializes attempts per player, revalidates all mutable resources, separates
+ * transaction outcome from client-window lifecycle and delegates the stock
+ * Item Enchant session to {@link EnchantSessionService}.
+ */
+public final class EnchantService
+{
+    private final ConcurrentHashMap<Integer, ReentrantLock> _playerLocks = new ConcurrentHashMap<>();
+
+    private EnchantService()
+    {
+    }
+
+    /**
+     * Performs one enchant attempt using explicit inventory object IDs.
+     *
+     * @param player The player performing the enchant.
+     * @param itemObjectId The target item object ID.
+     * @param scrollObjectId The enchant scroll object ID.
+     * @param source The source of the request.
+     */
+    public void enchant(Player player, int itemObjectId, int scrollObjectId, EnchantSource source)
+    {
+        if (player == null || itemObjectId <= 0 || scrollObjectId <= 0 || source == null)
+            return;
+
+        if (source == EnchantSource.MANUAL)
+            EnchantRepeatService.getInstance().clear(player);
+
+        final ReentrantLock lock = _playerLocks.computeIfAbsent(player.getObjectId(), id -> new ReentrantLock());
+        if (!lock.tryLock())
+        {
+            player.sendMessage(player.getSysString(10_378));
+            EnchantSessionService.getInstance().close(player, true);
+            return;
+        }
+
+        AttemptOutcome outcome = null;
+        try
+        {
+            outcome = execute(player, itemObjectId, scrollObjectId);
+        }
+        finally
+        {
+            lock.unlock();
+        }
+
+        if (outcome == null)
+            return;
+
+        final boolean repeatAllowed = Config.ENCHANT_REPEAT_ENABLED && EnchantRepeatService.isScrollTypeAllowed(outcome.enchant());
+        final EnchantSessionService.RefreshResult refresh = EnchantSessionService.getInstance().afterAttempt(player, outcome.scrollItemId(), outcome.item(), outcome.enchant(), outcome.itemSurvived(), repeatAllowed);
+        if (refresh.offerRepeat())
+            EnchantRepeatService.getInstance().offer(player, outcome.item(), outcome.scrollItemId(), outcome.enchant());
+    }
+
+    private static AttemptOutcome execute(Player player, int itemObjectId, int scrollObjectId)
+    {
+        if (!isConnected(player))
+        {
+            EnchantSessionService.getInstance().close(player, false);
+            return null;
+        }
+        if (player.isProcessingTransaction() || player.isOperating())
+        {
+            player.sendPacket(SystemMessageId.CANNOT_ENCHANT_WHILE_STORE);
+            EnchantSessionService.getInstance().close(player, true);
+            return null;
+        }
+        if (player.getActiveTradeList() != null)
+        {
+            player.cancelActiveTrade();
+            player.sendPacket(SystemMessageId.TRADE_ATTEMPT_FAILED);
+            EnchantSessionService.getInstance().close(player, true);
+            return null;
+        }
+
+        final ItemInstance item = player.getInventory().getItemByObjectId(itemObjectId);
+        final ItemInstance scroll = player.getInventory().getItemByObjectId(scrollObjectId);
+        if (item == null || scroll == null)
+        {
+            player.sendPacket(SystemMessageId.ENCHANT_SCROLL_CANCELLED);
+            EnchantSessionService.getInstance().close(player, true);
+            return null;
+        }
+
+        final EnchantScroll enchant = EnchantData.getInstance().getEnchantScroll(scroll);
+        if (!isValidAttempt(player, item, scroll, enchant))
+        {
+            player.sendPacket(SystemMessageId.INAPPROPRIATE_ENCHANT_CONDITION);
+            EnchantSessionService.getInstance().close(player, true);
+            return null;
+        }
+
+        final int scrollItemId = scroll.getItemId();
+        final MissionList mission = player.getMissions();
+        synchronized (item)
+        {
+            final double baseChance = enchant.getChance(item);
+            if (!isValidAttempt(player, item, scroll, enchant) || baseChance <= 0)
+            {
+                player.sendPacket(SystemMessageId.INAPPROPRIATE_ENCHANT_CONDITION);
+                EnchantSessionService.getInstance().close(player, true);
+                return null;
+            }
+
+            if (player.getInventory().destroyItem(scroll.getObjectId(), 1) == null)
+            {
+                player.sendPacket(SystemMessageId.NOT_ENOUGH_ITEMS);
+                EnchantSessionService.getInstance().close(player, true);
+                return null;
+            }
+
+            final EnchantBoostService.ApplicationResult boostResult = EnchantBoostService.getInstance().applyToAttempt(player, enchant, baseChance);
+            final double finalChance = boostResult.finalChance();
+            sendBoostResult(player, boostResult, finalChance);
+
+            if (Rnd.get(100) < finalChance)
+            {
+                applySuccess(player, item, enchant, mission);
+                player.broadcastUserInfo();
+                return new AttemptOutcome(item, scrollItemId, enchant, true);
+            }
+
+            final FailureResult failure = applyFailure(player, item, enchant, mission);
+            player.broadcastUserInfo();
+            return failure == FailureResult.ABORTED ? null : new AttemptOutcome(item, scrollItemId, enchant, failure == FailureResult.SURVIVED);
+        }
+    }
+
+    private static boolean isConnected(Player player)
+    {
+        return player != null && player.isOnline() && player.getClient() != null && !player.getClient().isDetached();
+    }
+
+    private static boolean isValidAttempt(Player player, ItemInstance item, ItemInstance scroll, EnchantScroll enchant)
+    {
+        return player != null && item != null && scroll != null && enchant != null && player.getActiveEnchantItem() != null && player.getActiveEnchantItem().getObjectId() == scroll.getObjectId() && item.getOwnerId() == player.getObjectId() && scroll.getOwnerId() == player.getObjectId() && scroll.getLocation() == ItemLocation.INVENTORY && scroll.getCount() > 0 && isEnchantable(item) && enchant.isValid(item);
+    }
+
+    private static void sendBoostResult(Player player, EnchantBoostService.ApplicationResult boostResult, double finalChance)
+    {
+        if (boostResult.status() == EnchantBoostService.ApplicationStatus.APPLIED)
+            player.sendMessage(player.getSysString(10_275, boostResult.configuredBonus(), boostResult.effectiveBonus(), (int) finalChance));
+        else if (boostResult.status() == EnchantBoostService.ApplicationStatus.SCROLL_NOT_ALLOWED)
+            player.sendMessage(player.getSysString(10_276));
+        else if (boostResult.status() == EnchantBoostService.ApplicationStatus.CHANCE_ALREADY_MAXIMUM)
+            player.sendMessage(player.getSysString(10_277, boostResult.configuredBonus()));
+    }
+
+    private static void applySuccess(Player player, ItemInstance item, EnchantScroll enchant, MissionList mission)
+    {
+        player.sendPacket(item.getEnchantLevel() == 0 ? SystemMessage.getSystemMessage(SystemMessageId.S1_SUCCESSFULLY_ENCHANTED).addItemName(item.getItemId()) : SystemMessage.getSystemMessage(SystemMessageId.S1_S2_SUCCESSFULLY_ENCHANTED).addNumber(item.getEnchantLevel()).addItemName(item.getItemId()));
+        item.setEnchantLevel(item.getEnchantLevel() + 1, player);
+        refreshEnchantedEquipmentSkills(player, item);
+        player.sendPacket(EnchantResult.SUCCESS);
+
+        final MissionType type = item.isWeapon() ? MissionType.ENCHANT_WEAPON : MissionType.ENCHANT_OTHER;
+        if (mission.getMission(type).getValue() < item.getEnchantLevel())
+            mission.set(type, item.getEnchantLevel(), false, false);
+        mission.update(MissionType.ENCHANT_SUCCESS);
+
+        if (enchant.announceTheEnchant(item) && enchant.message())
+            World.announceToOnlinePlayers(player.getSysString(10_060, player.getName(), item.getEnchantLevel(), item.getName()));
+    }
+
+    private static FailureResult applyFailure(Player player, ItemInstance item, EnchantScroll enchant, MissionList mission)
+    {
+        removeEquipmentSkillsBeforeFailure(player, item, enchant);
+        if (!enchant.cristalize())
+        {
+            player.sendMessage(player.getSysString(10_379, enchant.returnVal()));
+            if (enchant.returnVal() != -1)
+                item.setEnchantLevel(enchant.returnVal(), player);
+            player.sendPacket(EnchantResult.UNSUCCESS);
+            mission.update(MissionType.ENCHANT_FAILED);
+            return FailureResult.SURVIVED;
+        }
+
+        int crystalId = item.getItem().getCrystalItemId();
+        int count = item.getCrystalCount() - (item.getItem().getCrystalCount() + 1) / 2;
+        if (count < 1)
+            count = 1;
+
+        if (player.getInventory().destroyItem(item) == null)
+        {
+            EnchantSessionService.getInstance().close(player, true);
+            return FailureResult.ABORTED;
+        }
+        if (crystalId != 0)
+        {
+            player.getInventory().addItem(crystalId, count);
+            player.sendPacket(SystemMessage.getSystemMessage(SystemMessageId.EARNED_S2_S1_S).addItemName(crystalId).addItemNumber(count));
+        }
+        if (item.getEnchantLevel() > 0)
+            player.sendPacket(SystemMessage.getSystemMessage(SystemMessageId.ENCHANTMENT_FAILED_S1_S2_EVAPORATED).addNumber(item.getEnchantLevel()).addItemName(item.getItemId()));
+        else
+            player.sendPacket(SystemMessage.getSystemMessage(SystemMessageId.ENCHANTMENT_FAILED_S1_EVAPORATED).addItemName(item.getItemId()));
+        player.sendPacket(crystalId == 0 ? EnchantResult.UNK_RESULT_4 : EnchantResult.UNK_RESULT_1);
+        mission.update(MissionType.ENCHANT_FAILED);
+        return FailureResult.DESTROYED;
+    }
+
+    private static void refreshEnchantedEquipmentSkills(Player player, ItemInstance item)
+    {
+        if (!item.isEquipped())
+            return;
+        final Item template = item.getItem();
+        if (template instanceof Weapon weapon && item.getEnchantLevel() == 4)
+        {
+            if (!player.getWeaponGradePenalty())
+            {
+                final L2Skill skill = weapon.getEnchant4Skill();
+                if (skill != null)
+                {
+                    player.addSkill(skill, false);
+                    player.sendPacket(new SkillList(player));
+                }
+            }
+        }
+        else if (template instanceof Armor && item.getEnchantLevel() == 6)
+        {
+            final int chestId = player.getInventory().getItemIdFrom(Paperdoll.CHEST);
+            if (chestId == 0)
+                return;
+            final ArmorSet armorSet = ArmorSetData.getInstance().getSet(chestId);
+            if (armorSet == null || !armorSet.isEnchanted6(player))
+                return;
+            final int skillId = armorSet.getEnchant6skillId();
+            final L2Skill skill = skillId > 0 ? SkillTable.getInstance().getInfo(skillId, 1) : null;
+            if (skill != null)
+            {
+                player.addSkill(skill, false);
+                player.sendPacket(new SkillList(player));
+            }
+        }
+    }
+
+    private static void removeEquipmentSkillsBeforeFailure(Player player, ItemInstance item, EnchantScroll enchant)
+    {
+        if (!item.isEquipped() || (!enchant.cristalize() && enchant.returnVal() == -1))
+            return;
+        final Item template = item.getItem();
+        if (template instanceof Weapon weapon && item.getEnchantLevel() >= 4)
+        {
+            final L2Skill skill = weapon.getEnchant4Skill();
+            if (skill != null)
+            {
+                player.removeSkill(skill.getId(), false);
+                player.sendPacket(new SkillList(player));
+            }
+        }
+        else if (template instanceof Armor && item.getEnchantLevel() >= 6)
+        {
+            final int chestId = player.getInventory().getItemIdFrom(Paperdoll.CHEST);
+            final ArmorSet armorSet = chestId == 0 ? null : ArmorSetData.getInstance().getSet(chestId);
+            if (armorSet != null && armorSet.isEnchanted6(player))
+            {
+                final int skillId = armorSet.getEnchant6skillId();
+                if (skillId > 0)
+                {
+                    player.removeSkill(skillId, false);
+                    player.sendPacket(new SkillList(player));
+                }
+            }
+        }
+    }
+
+    /**
+     * Checks the static item restrictions shared by manual and repeat requests.
+     *
+     * @param item The item to inspect.
+     * @return {@code true} when the item satisfies the static enchant restrictions.
+     */
+    public static boolean isEnchantable(ItemInstance item)
+    {
+        if (item == null || item.isHeroItem() || item.isShadowItem() || item.isEtcItem() || item.getItem().getItemType() == WeaponType.FISHINGROD || !item.isEnchantable())
+            return false;
+        if (item.getLocation() != ItemLocation.INVENTORY && item.getLocation() != ItemLocation.PAPERDOLL)
+            return false;
+        return !item.isWeapon() || !item.getWeaponItem().isTravelerWeapon();
+    }
+
+    /**
+     * Clears the per-player serialization lock after logout.
+     *
+     * @param player The player being removed from the world.
+     */
+    public void clear(Player player)
+    {
+        if (player != null)
+            _playerLocks.remove(player.getObjectId());
+    }
+
+    /**
+     * Identifies how an enchant attempt entered the server workflow.
+     */
+    public enum EnchantSource
+    {
+        /** A target selected through the stock Item Enchant window. */
+        MANUAL,
+        /** The same target confirmed through the custom repeat dialog. */
+        REPEAT
+    }
+
+    private enum FailureResult
+    {
+        SURVIVED,
+        DESTROYED,
+        ABORTED
+    }
+
+    private record AttemptOutcome(ItemInstance item, int scrollItemId, EnchantScroll enchant, boolean itemSurvived)
+    {
+    }
+
+    /**
+     * Returns the shared enchant transaction service instance.
+     *
+     * @return The singleton service instance.
+     */
+    public static EnchantService getInstance()
+    {
+        return SingletonHolder.INSTANCE;
+    }
+
+    private static class SingletonHolder
+    {
+        private static final EnchantService INSTANCE = new EnchantService();
+    }
+}
sysstring.xml — en_US:
--- a/aCis_datapack/data/locale/en_US/sysstring.xml
+++ b/aCis_datapack/data/locale/en_US/sysstring.xml
@@ -472,6 +472,13 @@
     <string key="10371">Zone</string>
     <string key="10372">Route</string>
     <string key="10373"> (optional)</string>
+    <!-- gameserver.model.enchant.EnchantRepeatService && EnchantService -->
+    <string key="10374">Repeat enchantment of %s using %s?</string>
+    <string key="10375">The repeat enchant request has expired.</string>
+    <string key="10376">The item or enchant scroll is no longer available.</string>
+    <string key="10377">This item can no longer be enchanted with the selected scroll.</string>
+    <string key="10378">Another enchant operation is already in progress.</string>
+    <string key="10379">Blessed enchant failed. The item enchant level became %s.</string>
 
     <!-- gameserver.model.actor.Player: PK/Karma -->
     <string key="10380">PK character %s dropped: %s.</string>
sysstring.xml — ru_RU:
--- a/aCis_datapack/data/locale/ru_RU/sysstring.xml
+++ b/aCis_datapack/data/locale/ru_RU/sysstring.xml
@@ -487,6 +487,13 @@
     <string key="10371">Зона</string>
     <string key="10372">Маршрут</string>
     <string key="10373"> (необязательно)</string>
+    <!-- gameserver.model.enchant.EnchantRepeatService && EnchantService -->
+    <string key="10374">Повторить заточку предмета %s с помощью %s?</string>
+    <string key="10375">Время подтверждения повторной заточки истекло.</string>
+    <string key="10376">Предмет или свиток заточки больше недоступен.</string>
+    <string key="10377">Этот предмет больше нельзя заточить выбранным свитком.</string>
+    <string key="10378">Другая операция заточки уже выполняется.</string>
+    <string key="10379">Blessed-заточка не удалась. Уровень заточки предмета стал %s.</string>
 
     <!-- gameserver.model.actor.Player: PK/Karma -->
     <string key="10380">С PK-персонажа %s выпало: %s.</string>
 

Atrein

Вассал
INTERLUDE
INTERFACE
Регистрация
16 Янв 2022
Сообщения
71
Реакции
38
Баллы
18
RaCoin
5
Repeat Enchant - это удобная функция для повторной заточки предметов.
Обычно после каждой попытки заточки приходится заново использовать свиток, выбирать предмет и подтверждать действие. С этим модом всё проще: после заточки появляется предложение повторить попытку. Нажимаете «Да» - сервер автоматически выбирает тот же предмет и следующий подходящий свиток.
При этом система каждый раз проверяет, остался ли свиток, находится ли предмет в инвентаре и можно ли его ещё точить. Если свитки закончились, предмет был удалён или заточка больше невозможна, повторная попытка просто не запускается.
Функция работает как при успешной, так и при неудачной заточке. Её можно отдельно включить или отключить для обычных, Blessed и Crystal-свитков через конфигурацию сервера.
Проще говоря, Repeat Enchant убирает лишние действия и позволяет продолжать точить один предмет буквально одной кнопкой.
Пусть заточка проходит удачно, а нужный уровень берётся без лишней суеты. Приятной игры и успешных энчантов!
 
Сверху Снизу