• Listen to a special audio message from Bill Roper to the Hive Workshop community (Bill is a former Vice President of Blizzard Entertainment, Producer, Designer, Musician, Voice Actor) 🔗Click here to hear his message!
  • Read Evilhog's interview with Gregory Alper, the original composer of the music for WarCraft: Orcs & Humans 🔗Click here to read the full interview.
  • The Hive's 22nd Icon Contest: Creep Abilities is now concluded, time to vote for your favourite set of icons! Click here to vote!
  • ✅ The POLL for Hive's Texturing Contest #34 is OPEN! Vote for the TOP 3 SKINS! 🔗Click here to cast your vote!
  • ✅ The POLL for Hive's Techtree Contest #20 is OPEN! Vote for the TOP 3 FACTIONS! 🔗Click here to cast your vote!

[Trigger] [Lua] I don't know the cause of the desync.

Level 3
Joined
Nov 27, 2021
Messages
7
sorry for the rough English.

Desync occur when area-of-effect skills attack multiple NPCs (typically 30 or more zombies) simultaneously.

Also, even when using single-target skills or buff skills, desync occurs a considerable time later.

This is consistently reproducible within approximately 5 minutes.

This has been verified in both LAN (single PC, dual clients) and Battle.net environments.

The problematic path does not contain the GetLocalPlayer() function.

Does anyone know a solution for this situation?
 
Desyncs in Lua, especially when things get complex, can be tricky. It often boils down to subtle differences in how each player's computer handles information. Here are a few common culprits:


1. The pairs() Loop: This is a big one. Think of pairs() as a grab bag where the order of items isn't guaranteed. If you use pairs() to, say, decide which units to attack, the order might be different for each player, leading to different outcomes. Instead, use ipairs() (which goes in order) or a standard numbered for loop to keep everyone on the same page.

2. Floating-Point Fun: Imagine trying to measure something super tiny with a slightly off ruler. Those tiny errors can pile up, especially when you're doing a lot of math with damage or movement. Lua uses pretty precise numbers, but even those can vary slightly between computers. To avoid issues, round your results using math.floor() or math.ceil() before applying them to things like unit health or position.

3. Table Chaos: Imagine adding items to a list while other things are happening at the same time. If events happen just a bit differently on each machine (like a very fast timer), the lists can get out of sync. Make sure you set up and change your important game lists only when everyone is synced up and listening.

4. **Garbage Collection Gremlins: Lua has a "garbage collector" that cleans up unused memory. If this cleanup process does something that affects the game (like getting rid of a unit), and it happens at different times on different computers, you'll have a desync. *Be careful with __gc metamethods if they are doing crucial gameplay functions.

 
Desyncs in Lua, especially when things get complex, can be tricky. It often boils down to subtle differences in how each player's computer handles information. Here are a few common culprits:


1. The pairs() Loop: This is a big one. Think of pairs() as a grab bag where the order of items isn't guaranteed. If you use pairs() to, say, decide which units to attack, the order might be different for each player, leading to different outcomes. Instead, use ipairs() (which goes in order) or a standard numbered for loop to keep everyone on the same page.

2. Floating-Point Fun: Imagine trying to measure something super tiny with a slightly off ruler. Those tiny errors can pile up, especially when you're doing a lot of math with damage or movement. Lua uses pretty precise numbers, but even those can vary slightly between computers. To avoid issues, round your results using math.floor() or math.ceil() before applying them to things like unit health or position.

3. Table Chaos: Imagine adding items to a list while other things are happening at the same time. If events happen just a bit differently on each machine (like a very fast timer), the lists can get out of sync. Make sure you set up and change your important game lists only when everyone is synced up and listening.

4. **Garbage Collection Gremlins: Lua has a "garbage collector" that cleans up unused memory. If this cleanup process does something that affects the game (like getting rid of a unit), and it happens at different times on different computers, you'll have a desync. *Be careful with __gc metamethods if they are doing crucial gameplay functions.

Thank you for your answer. This kind of desync wouldn't occur solely due to the casting bar system or skill effects, right?
 
Thank you for your answer. This kind of desync wouldn't occur solely due to the casting bar system or skill effects, right?

Since you mentioned that you're not using GetLocalPlayer, the only way I can see desyncs happening is if:

Casting Bar Issues: Casting bars often use timers. If your Lua script uses a very fast timer (like every 0.01 seconds) to update a casting bar, and that timer accidentally changes a global variable or treats a unit differently on different computers, that could instantly cause a desync.

Group Loop Problems: If your skill effect involves going through all units in an area to apply visuals and damage, and you're using pairs() to do it, the order in which damage is applied might be different for each player. One player's computer might damage "Unit A" first, while another damages "Unit B" first. If "Unit A" dying triggers another effect, then the game states will be different.

If you have specific lines of code you're concerned about, do share them.
 
Since you mentioned that you're not using GetLocalPlayer, the only way I can see desyncs happening is if:
Casting Bar Issues: Casting bars often use timers. If your Lua script uses a very fast timer (like every 0.01 seconds) to update a casting bar, and that timer accidentally changes a global variable or treats a unit differently on different computers, that could instantly cause a desync.

Group Loop Problems: If your skill effect involves going through all units in an area to apply visuals and damage, and you're using pairs() to do it, the order in which damage is applied might be different for each player. One player's computer might damage "Unit A" first, while another damages "Unit B" first. If "Unit A" dying triggers another effect, then the game states will be different.

Ah, it seems the desync on the LAN has disappeared after removing the LUA-infused GUI file. However, I disabled some code during this process, so I will provide further updates if desync occurs again after re-enabling it.

A problem has occurred. A synchronization error occurred after restoring the trigger named 'buffsystem'. I have absolutely no idea what the cause is; I will attach the file, so could you possibly offer some advice? The symptoms are identical to before.

If you need the triggers that depend on this trigger, I can attach them for you.
 

Attachments

Last edited:
I cannot say for certain but imho everything else seems safe, but this:

local timerKey = tostring(GetHandleId(capturedUnit)) .. "_" .. capturedName

Since handle IDs aren't consistent across different players' computers, the generated timerKey could be different for each player.

Here's what could happen: One player's game might recognize an existing timer with a specific timerKey, while another player's game, with a different handle ID, might not find a timer with that key. This discrepancy causes the game to create different numbers of timers, leading to a divergence in game state and ultimately, a desync( probably not instant desync, but later).

I'll paste the code here, so someone can take a look:

Lua:
function BuffSystem.ApplyBuff(unit, buffData, source, level)
    -- [Restored 2026-05-03] CC immunity hook (priest cleanse).
    -- Reject Crowd Control buffs if target.runtimeFlags.ccImmune is true.
    -- ApplyCC internally calls ApplyBuff, so this single check covers all CC paths.
    -- NPCs are not subject to ccImmune (priest does not cleanse allied NPCs).
    if buffData and buffData.type == "Crowd Control" then
        local th = HeroSwap and HeroSwap.GetHeroByUnit
                   and HeroSwap.GetHeroByUnit(unit)
        if th and th.runtimeFlags and th.runtimeFlags.ccImmune then
            return false
        end
    end

    -- visualPreset merge — shallow-copy required to prevent skill_data pollution.
    -- Bug-fix design: prevents the first preset from becoming permanently locked
    -- when the same skill is re-cast.
    local bd = buffData
    if buffData and buffData.visualPreset then
        bd = {}
        for k, v in pairs(buffData) do bd[k] = v end
        local lib = (bd.category == "DEBUFF") and DEBUFF_VISUALS or BUFF_VISUALS
        local preset = lib[bd.visualPreset]
        if preset then
            -- Use `== nil` comparison instead of `or`.
            -- This structurally blocks a bug where `or` would pick the preset
            -- value when the skill explicitly disables a field (e.g. effect=false).
            -- Currently works by accident because stealth.effect=nil, but this
            -- handles future false-value cases.
            if bd.effect      == nil then bd.effect      = preset.effect      end
            if bd.icon        == nil then bd.icon        = preset.icon        end
            if bd.color       == nil then bd.color       = preset.color       end
            if bd.attachPoint == nil then bd.attachPoint = preset.attachPoint end
            -- displayName: skill name takes priority, only use preset if nil.
            if bd.displayName == nil then bd.displayName = preset.displayName end
        end
    end

    local BB = BuffSystem.BuffBot
    if BB then
        BB.Apply(unit, bd, source, level)
        -- [Workaround] BuffBot's CheckBuff periodic callback does not work
        -- in this environment, so buffs do not auto-expire. The World Editor
        -- uses an internal copy of the map, so BuffBot.lua modifications are
        -- not reflected. ALICE_CallDelayed works correctly via src/, so we
        -- bypass with an external timer that calls Dispel directly at the
        -- duration's expiry point.
        -- On re-apply (refresh), fallOffPoint is updated so remain > 0.1
        -- and the Dispel is automatically skipped.
        if bd and bd.duration and bd.duration > 0
            and BB.Dispel and BB.GetRemainingDuration and ALICE_CallDelayed then
            local capturedUnit = unit
            local capturedName = bd.name
            ALICE_CallDelayed(function()
                if capturedUnit and GetUnitTypeId(capturedUnit) ~= 0 then
                    local remain = BB.GetRemainingDuration(capturedUnit, capturedName) or 0
                    if remain <= 0.1 then
                        BB.Dispel(capturedUnit, capturedName)
                    end
                end
            end, bd.duration + 0.05)
        end

        -- Remaining-time tracking timer (text countdown + expiry-imminent flicker).
        -- BuffBot CheckBuff does not work in this environment, so we implement
        -- it externally. TimerStart fires immediately after BB.Apply, no
        -- ALICE_CallDelayed delay — runs every tick.
        -- Desync caution: BlzFrameSet* is only called inside the
        -- `GetLocalPlayer() == curOwner` branch. DestroyTimer must be called
        -- on every termination path (unit death / expiry).
        -- [Restored 2026-05-03 BISECT-C] LIGUI removal showed desync mitigation.
        -- Visual tick restored.
        if bd and bd.duration and bd.duration > 0
            and BB.GetData and BB.GetRemainingDuration and bd.name then

            local capturedUnit     = unit
            local capturedName     = bd.name
            -- owner is re-evaluated on every tick inside the timer callback.
            -- If unit ownership changes at runtime (-give, LinkHeroUnit, etc.)
            -- the captured owner becomes stale, causing a UX bug or potential
            -- future desync risk. GetOwningPlayer is a WC3 global state query
            -- that returns the same value on every machine — sync-safe.
            local FLICKER_DURATION = 5.0   -- flicker start threshold (seconds remaining)
            local FLICKER_FAST     = 2.0   -- fast-flicker threshold (seconds)
            local FLICKER_MIN      = 80    -- minimum alpha (0~255)
            local TICK             = 0.05  -- timer period (seconds)
            local TOOLTIP_WIDTH    = 0.24  -- BuffBot constant TOOLTIP_WIDTH (L123) hardcoded
            local counter          = 0
            local alphaRestored    = false

            -- [Re-apply leak fix] Prevent duplicate timers for the same
            -- unit×buffName by destroying any existing timer first.
            -- Re-apply (refresh) keeps only one timer.
            local timerKey = tostring(GetHandleId(capturedUnit)) .. "_" .. capturedName
            local existingTmr = _activeBuffTimers[timerKey]
            if existingTmr then
                PauseTimer(existingTmr)
                DestroyTimer(existingTmr)
                _activeBuffTimers[timerKey] = nil
            end
            local tmr = CreateTimer()
            _activeBuffTimers[timerKey] = tmr
            TimerStart(tmr, TICK, true, function()
                -- Unit destroyed → release timer immediately.
                if not capturedUnit or GetUnitTypeId(capturedUnit) == 0 then
                    DestroyTimer(tmr)
                    _activeBuffTimers[timerKey] = nil  -- lookup cleanup
                    return
                end

                -- Re-query current owner every tick to track ownership changes.
                local curOwner = GetOwningPlayer(capturedUnit)

                local remain = BB.GetRemainingDuration(capturedUnit, capturedName) or 0

                -- About to expire (Dispel workaround handles BB.Dispel) → release timer.
                if remain <= TICK then
                    DestroyTimer(tmr)
                    _activeBuffTimers[timerKey] = nil  -- lookup cleanup
                    return
                end

                local data = BB.GetData(capturedUnit, capturedName)
                if not data then return end

                -- Update text countdown (every tick, local machine only).
                -- Ported from BuffBot original L764-770, L783-790.
                if GetLocalPlayer() == curOwner then
                    -- "N s" text below the icon
                    if data.textFrame then
                        BlzFrameSetText(data.textFrame, GetDurationText(remain))
                    end
                    -- Tooltip "N seconds remaining" text + size adjustment
                    if data.tooltipText and data.tooltip then
                        BlzFrameSetText(data.tooltipText,
                            data.tooltip .. "|n" .. GetDurationTextExtended(remain))
                        if data.tooltipParent then
                            BlzFrameSetSize(data.tooltipText, TOOLTIP_WIDTH, 0.0)
                            BlzFrameSetSize(data.tooltipParent,
                                TOOLTIP_WIDTH + 0.01,
                                BlzFrameGetHeight(data.tooltipText) + 0.031)
                        end
                    end
                end

                -- Alpha flicker manipulation (only if parentFrame exists).
                if data.parentFrame then
                    if remain <= FLICKER_DURATION then
                        -- Entering flicker zone.
                        alphaRestored = false
                        -- Below 2 seconds: 2x speed flicker.
                        if remain <= FLICKER_FAST then
                            counter = counter + 2
                        else
                            counter = counter + 1
                        end
                        -- BuffBot original formula port (see L723-731).
                        local alpha = FLICKER_MIN + ((255 - FLICKER_MIN)
                            * (1 + math.cos(2 * bj_PI * counter * TICK / 1.0)) / 2) // 1
                        -- Local UI manipulation — desync prevention by running
                        -- only on the owning player's client.
                        if GetLocalPlayer() == curOwner then
                            BlzFrameSetAlpha(data.parentFrame, alpha)
                        end
                    elseif not alphaRestored then
                        -- Re-apply pushed remain back above 5 seconds → restore alpha 255 (one-shot).
                        alphaRestored = true
                        counter = 0
                        if GetLocalPlayer() == curOwner then
                            BlzFrameSetAlpha(data.parentFrame, 255)
                        end
                    end
                end
            end)
        end

        return true
    end

    -- Fallback path (BuffBot not loaded — never used in practice).
    local hero = getHeroFromUnit(unit)
    if not hero then return false end

    local buffId = bd.name or ""
    local existing = hero.activeBuffs[buffId]
    if existing then
        existing.remaining = bd.duration or existing.duration
        existing.sourceUnit = source
        return true
    end

    hero.activeBuffs[buffId] = CoreData.CreateBuffEntry({
        buffId    = buffId,
        sourceUnit = source,
        category  = bd.category or CoreData.BUFF_CATEGORY.BUFF,
        duration  = bd.duration or 0,
        remaining = bd.duration or 0,
        statMods  = bd.statMods,
        ratioMods = bd.ratioMods,
    })

    if BuffSystem.StatEngine then
        BuffSystem.StatEngine.RecalculateStats(hero)
    end
    return true
end

function BuffSystem.RemoveBuff(unit, buffName)
    local BB = BuffSystem.BuffBot
    if BB then
        BB.Dispel(unit, buffName)
        return true
    end

    local hero = getHeroFromUnit(unit)
    if not hero then return false end

    if hero.activeBuffs[buffName] then
        hero.activeBuffs[buffName] = nil
        if BuffSystem.StatEngine then
            BuffSystem.StatEngine.RecalculateStats(hero)
        end
        return true
    end
    return false
end

function BuffSystem.RemoveAllBuffs(unit, buffType)
    local BB = BuffSystem.BuffBot
    if BB then
        BB.DispelAll(unit, buffType)
        return
    end

    local hero = getHeroFromUnit(unit)
    if not hero then return end

    for buffId, buff in pairs(hero.activeBuffs) do
        if not buffType or buff.category == buffType then
            hero.activeBuffs[buffId] = nil
        end
    end
    if BuffSystem.StatEngine then
        BuffSystem.StatEngine.RecalculateStats(hero)
    end
end


If you need the triggers that depend on this trigger, I can attach them for you.
Sure, post them so we can narrow it down hopefuly.
 
I cannot say for certain but imho everything else seems safe, but this:

local timerKey = tostring(GetHandleId(capturedUnit)) .. "_" .. capturedName

Since handle IDs aren't consistent across different players' computers, the generated timerKey could be different for each player.

Here's what could happen: One player's game might recognize an existing timer with a specific timerKey, while another player's game, with a different handle ID, might not find a timer with that key. This discrepancy causes the game to create different numbers of timers, leading to a divergence in game state and ultimately, a desync( probably not instant desync, but later).

I'll paste the code here, so someone can take a look:

Lua:
function BuffSystem.ApplyBuff(unit, buffData, source, level)
    -- [Restored 2026-05-03] CC immunity hook (priest cleanse).
    -- Reject Crowd Control buffs if target.runtimeFlags.ccImmune is true.
    -- ApplyCC internally calls ApplyBuff, so this single check covers all CC paths.
    -- NPCs are not subject to ccImmune (priest does not cleanse allied NPCs).
    if buffData and buffData.type == "Crowd Control" then
        local th = HeroSwap and HeroSwap.GetHeroByUnit
                   and HeroSwap.GetHeroByUnit(unit)
        if th and th.runtimeFlags and th.runtimeFlags.ccImmune then
            return false
        end
    end

    -- visualPreset merge — shallow-copy required to prevent skill_data pollution.
    -- Bug-fix design: prevents the first preset from becoming permanently locked
    -- when the same skill is re-cast.
    local bd = buffData
    if buffData and buffData.visualPreset then
        bd = {}
        for k, v in pairs(buffData) do bd[k] = v end
        local lib = (bd.category == "DEBUFF") and DEBUFF_VISUALS or BUFF_VISUALS
        local preset = lib[bd.visualPreset]
        if preset then
            -- Use `== nil` comparison instead of `or`.
            -- This structurally blocks a bug where `or` would pick the preset
            -- value when the skill explicitly disables a field (e.g. effect=false).
            -- Currently works by accident because stealth.effect=nil, but this
            -- handles future false-value cases.
            if bd.effect      == nil then bd.effect      = preset.effect      end
            if bd.icon        == nil then bd.icon        = preset.icon        end
            if bd.color       == nil then bd.color       = preset.color       end
            if bd.attachPoint == nil then bd.attachPoint = preset.attachPoint end
            -- displayName: skill name takes priority, only use preset if nil.
            if bd.displayName == nil then bd.displayName = preset.displayName end
        end
    end

    local BB = BuffSystem.BuffBot
    if BB then
        BB.Apply(unit, bd, source, level)
        -- [Workaround] BuffBot's CheckBuff periodic callback does not work
        -- in this environment, so buffs do not auto-expire. The World Editor
        -- uses an internal copy of the map, so BuffBot.lua modifications are
        -- not reflected. ALICE_CallDelayed works correctly via src/, so we
        -- bypass with an external timer that calls Dispel directly at the
        -- duration's expiry point.
        -- On re-apply (refresh), fallOffPoint is updated so remain > 0.1
        -- and the Dispel is automatically skipped.
        if bd and bd.duration and bd.duration > 0
            and BB.Dispel and BB.GetRemainingDuration and ALICE_CallDelayed then
            local capturedUnit = unit
            local capturedName = bd.name
            ALICE_CallDelayed(function()
                if capturedUnit and GetUnitTypeId(capturedUnit) ~= 0 then
                    local remain = BB.GetRemainingDuration(capturedUnit, capturedName) or 0
                    if remain <= 0.1 then
                        BB.Dispel(capturedUnit, capturedName)
                    end
                end
            end, bd.duration + 0.05)
        end

        -- Remaining-time tracking timer (text countdown + expiry-imminent flicker).
        -- BuffBot CheckBuff does not work in this environment, so we implement
        -- it externally. TimerStart fires immediately after BB.Apply, no
        -- ALICE_CallDelayed delay — runs every tick.
        -- Desync caution: BlzFrameSet* is only called inside the
        -- `GetLocalPlayer() == curOwner` branch. DestroyTimer must be called
        -- on every termination path (unit death / expiry).
        -- [Restored 2026-05-03 BISECT-C] LIGUI removal showed desync mitigation.
        -- Visual tick restored.
        if bd and bd.duration and bd.duration > 0
            and BB.GetData and BB.GetRemainingDuration and bd.name then

            local capturedUnit     = unit
            local capturedName     = bd.name
            -- owner is re-evaluated on every tick inside the timer callback.
            -- If unit ownership changes at runtime (-give, LinkHeroUnit, etc.)
            -- the captured owner becomes stale, causing a UX bug or potential
            -- future desync risk. GetOwningPlayer is a WC3 global state query
            -- that returns the same value on every machine — sync-safe.
            local FLICKER_DURATION = 5.0   -- flicker start threshold (seconds remaining)
            local FLICKER_FAST     = 2.0   -- fast-flicker threshold (seconds)
            local FLICKER_MIN      = 80    -- minimum alpha (0~255)
            local TICK             = 0.05  -- timer period (seconds)
            local TOOLTIP_WIDTH    = 0.24  -- BuffBot constant TOOLTIP_WIDTH (L123) hardcoded
            local counter          = 0
            local alphaRestored    = false

            -- [Re-apply leak fix] Prevent duplicate timers for the same
            -- unit×buffName by destroying any existing timer first.
            -- Re-apply (refresh) keeps only one timer.
            local timerKey = tostring(GetHandleId(capturedUnit)) .. "_" .. capturedName
            local existingTmr = _activeBuffTimers[timerKey]
            if existingTmr then
                PauseTimer(existingTmr)
                DestroyTimer(existingTmr)
                _activeBuffTimers[timerKey] = nil
            end
            local tmr = CreateTimer()
            _activeBuffTimers[timerKey] = tmr
            TimerStart(tmr, TICK, true, function()
                -- Unit destroyed → release timer immediately.
                if not capturedUnit or GetUnitTypeId(capturedUnit) == 0 then
                    DestroyTimer(tmr)
                    _activeBuffTimers[timerKey] = nil  -- lookup cleanup
                    return
                end

                -- Re-query current owner every tick to track ownership changes.
                local curOwner = GetOwningPlayer(capturedUnit)

                local remain = BB.GetRemainingDuration(capturedUnit, capturedName) or 0

                -- About to expire (Dispel workaround handles BB.Dispel) → release timer.
                if remain <= TICK then
                    DestroyTimer(tmr)
                    _activeBuffTimers[timerKey] = nil  -- lookup cleanup
                    return
                end

                local data = BB.GetData(capturedUnit, capturedName)
                if not data then return end

                -- Update text countdown (every tick, local machine only).
                -- Ported from BuffBot original L764-770, L783-790.
                if GetLocalPlayer() == curOwner then
                    -- "N s" text below the icon
                    if data.textFrame then
                        BlzFrameSetText(data.textFrame, GetDurationText(remain))
                    end
                    -- Tooltip "N seconds remaining" text + size adjustment
                    if data.tooltipText and data.tooltip then
                        BlzFrameSetText(data.tooltipText,
                            data.tooltip .. "|n" .. GetDurationTextExtended(remain))
                        if data.tooltipParent then
                            BlzFrameSetSize(data.tooltipText, TOOLTIP_WIDTH, 0.0)
                            BlzFrameSetSize(data.tooltipParent,
                                TOOLTIP_WIDTH + 0.01,
                                BlzFrameGetHeight(data.tooltipText) + 0.031)
                        end
                    end
                end

                -- Alpha flicker manipulation (only if parentFrame exists).
                if data.parentFrame then
                    if remain <= FLICKER_DURATION then
                        -- Entering flicker zone.
                        alphaRestored = false
                        -- Below 2 seconds: 2x speed flicker.
                        if remain <= FLICKER_FAST then
                            counter = counter + 2
                        else
                            counter = counter + 1
                        end
                        -- BuffBot original formula port (see L723-731).
                        local alpha = FLICKER_MIN + ((255 - FLICKER_MIN)
                            * (1 + math.cos(2 * bj_PI * counter * TICK / 1.0)) / 2) // 1
                        -- Local UI manipulation — desync prevention by running
                        -- only on the owning player's client.
                        if GetLocalPlayer() == curOwner then
                            BlzFrameSetAlpha(data.parentFrame, alpha)
                        end
                    elseif not alphaRestored then
                        -- Re-apply pushed remain back above 5 seconds → restore alpha 255 (one-shot).
                        alphaRestored = true
                        counter = 0
                        if GetLocalPlayer() == curOwner then
                            BlzFrameSetAlpha(data.parentFrame, 255)
                        end
                    end
                end
            end)
        end

        return true
    end

    -- Fallback path (BuffBot not loaded — never used in practice).
    local hero = getHeroFromUnit(unit)
    if not hero then return false end

    local buffId = bd.name or ""
    local existing = hero.activeBuffs[buffId]
    if existing then
        existing.remaining = bd.duration or existing.duration
        existing.sourceUnit = source
        return true
    end

    hero.activeBuffs[buffId] = CoreData.CreateBuffEntry({
        buffId    = buffId,
        sourceUnit = source,
        category  = bd.category or CoreData.BUFF_CATEGORY.BUFF,
        duration  = bd.duration or 0,
        remaining = bd.duration or 0,
        statMods  = bd.statMods,
        ratioMods = bd.ratioMods,
    })

    if BuffSystem.StatEngine then
        BuffSystem.StatEngine.RecalculateStats(hero)
    end
    return true
end

function BuffSystem.RemoveBuff(unit, buffName)
    local BB = BuffSystem.BuffBot
    if BB then
        BB.Dispel(unit, buffName)
        return true
    end

    local hero = getHeroFromUnit(unit)
    if not hero then return false end

    if hero.activeBuffs[buffName] then
        hero.activeBuffs[buffName] = nil
        if BuffSystem.StatEngine then
            BuffSystem.StatEngine.RecalculateStats(hero)
        end
        return true
    end
    return false
end

function BuffSystem.RemoveAllBuffs(unit, buffType)
    local BB = BuffSystem.BuffBot
    if BB then
        BB.DispelAll(unit, buffType)
        return
    end

    local hero = getHeroFromUnit(unit)
    if not hero then return end

    for buffId, buff in pairs(hero.activeBuffs) do
        if not buffType or buff.category == buffType then
            hero.activeBuffs[buffId] = nil
        end
    end
    if BuffSystem.StatEngine then
        BuffSystem.StatEngine.RecalculateStats(hero)
    end
end



Sure, post them so we can narrow it down hopefuly.
Is that the reason the inconsistent and irregular desync triggers caused by skill usage?

for now, i will upload the triggers connected to the buff system. the Lua code is a bit long, so i would appreciate it if you could take a look when you have some spare time!
 

Attachments

A problem has occurred. A synchronization error occurred after restoring the trigger named 'buffsystem'. I have absolutely no idea what the cause is; I will attach the file, so could you possibly offer some advice? The symptoms are identical to before.
Your buff system has multiple standard pairs(), something Axolotl warned you about above:
and you're using pairs() to do it, the order in which damage is applied might be different for each player. One player's computer might damage "Unit A" first, while another damages "Unit B" first. If "Unit A" dying triggers another effect, then the game states will be different.
In later files the LLM slaps lipstick on the pig by doing pairs and inserting elements into an intermediate table to sort them. It doesn't know https://www.hiveworkshop.com/threads/syncedtable.332894/ exists.
Finally, you cannot magically teach it to avoid desync by instructing it to be deterministic. Much less so in an environment that's never been documented to be understood by people. The current mapmaking consists of roughly following your gut instinct and supposed best practices with little objective confirmation about the actual pitfalls.

-- Desync fix (2026-04-30): previously used GroupEnumUnitsInRange + ForGroup
-- directly. WC3 Reforged Lua ForGroup enumerates units in non-deterministic
-- order
bullshit. ForGroup works the same between Jass and Lua, unless Lua-Infused GUI hooked into it too.

In the end, LIGUI may be not the culprit but cause the desync happen at a different time. Observer (quantum physics) - Wikipedia
 
Your buff system has multiple standard pairs(), something Axolotl warned you about above:

In later files the LLM slaps lipstick on the pig by doing pairs and inserting elements into an intermediate table to sort them. It doesn't know https://www.hiveworkshop.com/threads/syncedtable.332894/ exists.
Finally, you cannot magically teach it to avoid desync by instructing it to be deterministic. Much less so in an environment that's never been documented to be understood by people. The current mapmaking consists of roughly following your gut instinct and supposed best practices with little objective confirmation about the actual pitfalls.


bullshit. ForGroup works the same between Jass and Lua, unless Lua-Infused GUI hooked into it too.

In the end, LIGUI may be not the culprit but cause the desync happen at a different time. Observer (quantum physics) - Wikipedia
IMG_2483.gif
 
Back
Top