local item_sync = dofile_once("mods/quant.ew/files/system/item_sync.lua")
local effect_sync = dofile_once("mods/quant.ew/files/system/game_effect_sync/game_effect_sync.lua")
local stain_sync = dofile_once("mods/quant.ew/files/system/effect_data_sync/effect_data_sync.lua")

local ffi = require("ffi")
local rpc = net.new_rpc_namespace()

local EnemyData = util.make_type({
    u32 = { "enemy_id" },
    f32 = { "x", "y", "vx", "vy" },
})

-- Variant of EnemyData for when we don't have any motion (or no VelocityComponent).
local EnemyDataNoMotion = util.make_type({
    u32 = { "enemy_id" },
    f32 = { "x", "y" },
})

local EnemyDataWorm = util.make_type({
    u32 = { "enemy_id" },
    f32 = { "x", "y", "vx", "vy", "tx", "ty" },
})

local EnemyDataKolmi = util.make_type({
    u32 = { "enemy_id" },
    f32 = { "x", "y", "vx", "vy" },
    bool = { "enabled" },
    vecfloat = { "legs" },
})

local EnemyDataMom = util.make_type({
    u32 = { "enemy_id" },
    f32 = { "x", "y", "vx", "vy" },
    vecbool = { "orbs" },
})

local EnemyDataFish = util.make_type({
    u32 = { "enemy_id" },
    f32 = { "x", "y", "vx", "vy" },
    u8 = { "r" },
})

local HpData = util.make_type({
    u32 = { "enemy_id" },
    f32 = { "hp", "max_hp" },
})

local should_wait = {}

local first = true

local FULL_TURN = math.pi * 2

local frame = 0

local enemy_sync = {}

local unsynced_enemys = {}

local dead_entities = {}
--this basically never happens, doesn't seem that useful anymore. Perhaps should be removed to conserve memory.
--local confirmed_kills = {}

local spawned_by_us = {}

-- HACK
local times_spawned_last_minute = {}

local DISTANCE_LIMIT = 128 * 6

for filename, _ in pairs(constants.phys_sync_allowed) do
    util.add_tag_to(filename, "prop_physics")
    -- Idk it just causes the minecart to not appear at all.
    -- util.replace_text_in(filename, 'kill_entity_after_initialized="1"', 'kill_entity_after_initialized="0"')
end

util.add_cross_call("ew_es_death_notify", function(enemy_id, responsible_id)
    local player_data = player_fns.get_player_data_by_local_entity_id(responsible_id)
    local responsible
    if player_data ~= nil then
        responsible = player_data.peer_id
    else
        responsible = responsible_id
    end
    local damage = EntityGetFirstComponentIncludingDisabled(enemy_id, "DamageModelComponent")
    table.insert(dead_entities, { enemy_id, responsible, ComponentGetValue2(damage, "wait_for_kill_flag_on_death") })
end)

local function world_exists_for(entity)
    local x, y = EntityGetFirstHitboxCenter(entity)
    local w, h = 5, 5 -- TODO
    w = w * 0.5
    h = h * 0.5
    return DoesWorldExistAt(x - w, y - h, x + w, y + h)
end

local function table_extend(to, from)
    for _, e in ipairs(from) do
        to[#to + 1] = e
    end
end

local function table_extend_filtered(to, from, filter)
    for _, e in ipairs(from) do
        if filter(e) then
            to[#to + 1] = e
        end
    end
end

local function get_sync_entities(return_all)
    local entities = EntityGetWithTag("enemy") or {}
    table_extend(entities, EntityGetWithTag("ew_enemy_sync_extra"))
    table_extend(entities, EntityGetWithTag("plague_rat"))
    table_extend(entities, EntityGetWithTag("seed_f"))
    table_extend(entities, EntityGetWithTag("seed_e"))
    table_extend(entities, EntityGetWithTag("seed_d"))
    table_extend(entities, EntityGetWithTag("seed_c"))
    table_extend(entities, EntityGetWithTag("perk_fungus_tiny"))
    table_extend(entities, EntityGetWithTag("helpless_animal"))
    table_extend_filtered(entities, EntityGetWithTag("hittable"), function(ent)
        local name = EntityGetName(ent)
        local file = EntityGetFilename(ent)
        return name == "$item_essence_stone"
            or name == "$animal_fish_giga"
            or file == "data/entities/buildings/spittrap_left.xml"
            or file == "data/entities/buildings/spittrap_right.xml"
            or file == "data/entities/buildings/thundertrap_left.xml"
            or file == "data/entities/buildings/thundertrap_right.xml"
            or file == "data/entities/buildings/arrowtrap_left.xml"
            or file == "data/entities/buildings/arrowtrap_right.xml"
            or file == "data/entities/buildings/firetrap_left.xml"
            or file == "data/entities/buildings/firetrap_right.xml"
        --data/entities/buildings/statue_trap_left.xml
        --data/entities/buildings/statue_trap_right.xml
    end)
    table_extend_filtered(entities, EntityGetWithTag("prop_physics"), function(ent)
        local f = EntityGetFilename(ent)
        if f ~= nil then
            return constants.phys_sync_allowed[f]
        end
        return true
    end)

    local entities2 = {}
    if return_all then
        table_extend_filtered(entities2, entities, function(ent)
            return not EntityHasTag(ent, "ew_no_enemy_sync")
        end)
    else
        table_extend_filtered(entities2, entities, function(ent)
            local x, y = EntityGetTransform(ent)
            local has_anyone = EntityHasTag(ent, "worm")
                or EntityGetFirstComponent(ent, "BossHealthBarComponent") ~= nil
                or #EntityGetInRadiusWithTag(x, y, DISTANCE_LIMIT, "ew_peer") ~= 0
                or #EntityGetInRadiusWithTag(x, y, DISTANCE_LIMIT, "polymorphed_player") ~= 0
            return has_anyone and not EntityHasTag(ent, "ew_no_enemy_sync")
        end)
    end

    return entities2
end

local was_held = {}

function enemy_sync.host_upload_entities()
    local entities = get_sync_entities()
    local enemy_data_list = {}
    for i, enemy_id in ipairs(entities) do
        if not world_exists_for(enemy_id) then
            goto continue
        end
        local filename = EntityGetFilename(enemy_id)
        filename = constants.interned_filename_to_index[filename] or filename

        local x, y, rot = EntityGetTransform(enemy_id)
        local character_data = EntityGetFirstComponentIncludingDisabled(enemy_id, "CharacterDataComponent")
        local vx, vy = 0, 0
        if character_data ~= nil then
            vx, vy = ComponentGetValue2(character_data, "mVelocity")
        else
            local velocity = EntityGetFirstComponentIncludingDisabled(enemy_id, "VelocityComponent")
            if velocity ~= nil then
                vx, vy = ComponentGetValue2(velocity, "mVelocity")
            end
        end
        local ai_component = EntityGetFirstComponentIncludingDisabled(enemy_id, "AnimalAIComponent")
        if ai_component ~= 0 and ai_component ~= nil then
            ComponentSetValue2(ai_component, "max_distance_to_cam_to_start_hunting", math.pow(2, 29))
        end

        local phys_info = util.get_phys_info(enemy_id, true)
        if phys_info == nil then
            goto continue
        end

        local hp, max_hp, has_hp = util.get_ent_health(enemy_id)
        if has_hp then
            util.ensure_component_present(enemy_id, "LuaComponent", "ew_death_notify", {
                script_death = "mods/quant.ew/files/resource/cbs/death_notify.lua",
            })
        end

        -- TODO: figure out how to sync those.
        -- local laser_sight_data = nil
        -- local laser_sight = EntityGetFirstComponentIncludingDisabled(enemy_id, "SpriteComponent", "laser_sight")
        -- if laser_sight ~= nil and laser_sight ~= 0 then
        --     -- local x, y, r =
        -- end

        local death_triggers = {}
        for _, com in ipairs(EntityGetComponent(enemy_id, "LuaComponent") or {}) do
            local script = ComponentGetValue2(com, "script_death")
            if script ~= nil and script ~= "" then
                table.insert(death_triggers, constants.interned_filename_to_index[script] or script)
            end
        end
        local en_data
        local worm = EntityGetFirstComponentIncludingDisabled(enemy_id, "WormAIComponent")
            or EntityGetFirstComponentIncludingDisabled(enemy_id, "BossDragonComponent")
        if EntityHasTag(enemy_id, "boss_centipede") then
            local legs = {}
            for _, leg in ipairs(EntityGetAllChildren(enemy_id, "foot")) do
                local limb = EntityGetFirstComponentIncludingDisabled(leg, "IKLimbComponent")
                local lx, ly = ComponentGetValue2(limb, "end_position")
                table.insert(legs, lx)
                table.insert(legs, ly)
            end
            en_data = EnemyDataKolmi({
                enemy_id = enemy_id,
                x = x,
                y = y,
                vx = vx,
                vy = vy,
                enabled = EntityGetFirstComponent(enemy_id, "BossHealthBarComponent", "disabled_at_start") ~= nil,
                legs = legs,
            })
        elseif EntityHasTag(enemy_id, "boss_wizard") then
            local orbs = { false, false, false, false, false, false, false, false }
            for _, child in ipairs(EntityGetAllChildren(enemy_id) or {}) do
                local var = EntityGetFirstComponentIncludingDisabled(child, "VariableStorageComponent")
                if EntityHasTag(child, "touchmagic_immunity") and var ~= nil then
                    local n = ComponentGetValue2(var, "value_int")
                    orbs[n] = true
                end
            end
            en_data = EnemyDataMom({
                enemy_id = enemy_id,
                x = x,
                y = y,
                vx = vx,
                vy = vy,
                orbs = orbs,
            })
        elseif worm ~= nil then
            local tx, ty = ComponentGetValue2(worm, "mTargetVec")
            en_data = EnemyDataWorm({
                enemy_id = enemy_id,
                x = x,
                y = y,
                vx = vx,
                vy = vy,
                tx = tx,
                ty = ty,
            })
        elseif math.abs(vx) < 0.01 and math.abs(vy) < 0.01 then
            en_data = EnemyDataNoMotion({
                enemy_id = enemy_id,
                x = x,
                y = y,
            })
        elseif EntityGetFirstComponentIncludingDisabled(enemy_id, "AdvancedFishAIComponent") ~= nil then
            en_data = EnemyDataFish({
                enemy_id = enemy_id,
                x = x,
                y = y,
                vx = vx,
                vy = vy,
                r = math.floor((rot % FULL_TURN) / FULL_TURN * 255),
            })
        else
            en_data = EnemyData({
                enemy_id = enemy_id,
                x = x,
                y = y,
                vx = vx,
                vy = vy,
            })
        end

        local wand
        local inv = EntityGetFirstComponentIncludingDisabled(enemy_id, "Inventory2Component")
        if inv ~= nil then
            local item = ComponentGetValue2(inv, "mActualActiveItem")
            if item ~= nil and EntityGetIsAlive(item) then
                if not EntityHasTag(item, "ew_global_item") then
                    item_sync.make_item_global(item)
                else
                    wand = item_sync.get_global_item_id(item)
                    if wand == nil then
                        EntityRemoveTag(item, "ew_global_item")
                        goto continue
                    end
                    if not item_sync.is_my_item(wand) then
                        item_sync.take_authority(wand)
                    end
                    was_held[wand] = true
                end
            end
        end

        local effect_data = effect_sync.get_sync_data(enemy_id, true)

        local has_laser
        local animations = {}

        for _, sprite in ipairs(EntityGetComponent(enemy_id, "SpriteComponent") or {}) do
            local animation
            if sprite ~= nil then
                animation = ComponentGetValue2(sprite, "rect_animation")
            end
            table.insert(animations, animation)
            if ComponentHasTag(sprite, "laser_sight") then
                has_laser = true
            end
        end
        local laser
        if has_laser and EntityGetName(enemy_id) ~= "$animal_turret" then
            local ai = EntityGetFirstComponentIncludingDisabled(enemy_id, "AnimalAIComponent")
            if ai ~= nil then
                local target = ComponentGetValue2(ai, "mGreatestPrey")
                local peer = player_fns.get_player_data_by_local_entity_id(target)
                if peer ~= nil then
                    laser = peer.peer_id
                end
            end
        end

        local dont_cull = EntityGetFirstComponent(enemy_id, "BossHealthBarComponent") ~= nil
            or worm ~= nil
            or EntityHasTag(enemy_id, "seed_f")
            or EntityHasTag(enemy_id, "seed_e")
            or EntityHasTag(enemy_id, "seed_d")
            or EntityHasTag(enemy_id, "seed_c")
            or EntityGetFilename(enemy_id) == "data/entities/buildings/essence_eater.xml"

        local stains = stain_sync.get_stains(enemy_id)

        table.insert(
            enemy_data_list,
            { filename, en_data, phys_info, wand, effect_data, animations, dont_cull, death_triggers, stains, laser }
        )
        ::continue::
    end

    rpc.handle_enemy_data(enemy_data_list, first)
    first = false
    if #dead_entities > 0 then
        rpc.handle_death_data(dead_entities)
    end
    dead_entities = {}
end

local function host_upload_health()
    local entities = get_sync_entities()

    local enemy_health_list = {}
    for i, enemy_id in ipairs(entities) do
        if not world_exists_for(enemy_id) then
            goto continue
        end

        local hp, max_hp, has_hp = util.get_ent_health(enemy_id)

        if has_hp then
            table.insert(
                enemy_health_list,
                HpData({
                    enemy_id = enemy_id,
                    hp = hp,
                    max_hp = max_hp,
                })
            )
        end

        ::continue::
    end

    if #enemy_health_list > 0 then
        rpc.handle_enemy_health(enemy_health_list)
    end
end

function enemy_sync.client_cleanup()
    local entities = get_sync_entities(true)
    for _, enemy_id in ipairs(entities) do
        if not EntityHasTag(enemy_id, "ew_replicated") then
            EntityKill(enemy_id)
        elseif not spawned_by_us[enemy_id] then
            EntityKill(enemy_id)
        end
    end
    for remote_id, enemy_data in pairs(ctx.entity_by_remote_id) do
        if frame > enemy_data.frame then
            EntityKill(enemy_data.id)
            ctx.entity_by_remote_id[remote_id] = nil
        end
    end
end

function enemy_sync.on_world_update_host()
    local rt = math.floor(tonumber(ModSettingGet("quant.ew.enemy_sync") or 2) or 2 + 0.5)
    local n = 0
    if rt == 3 then
        n = 2
    elseif rt == 2 then
        n = 1
    end
    if rt == 1 or GameGetFrameNum() % rt == n then
        enemy_sync.host_upload_entities()
    end
    if GameGetFrameNum() % 10 == 5 then
        host_upload_health()
    end
    for wand, _ in pairs(was_held) do
        if EntityGetRootEntity(wand) == wand then
            was_held[wand] = nil
            if item_sync.is_my_item(item_sync.get_global_item_id(wand)) then
                item_sync.make_item_global(wand)
            end
        end
    end
end

function enemy_sync.on_world_update_client()
    if GameGetFrameNum() % 12 == 1 then
        enemy_sync.client_cleanup()
    end
    if GameGetFrameNum() % (60 * 60) == 1 then
        times_spawned_last_minute = {}
    end
end

local kolmi_spawn

local function sync_enemy(enemy_info_raw, force_no_cull, host_fps)
    local filename = enemy_info_raw[1]
    filename = constants.interned_index_to_filename[filename] or filename

    local en_data = enemy_info_raw[2]
    local dont_cull = enemy_info_raw[7]
    local death_triggers = enemy_info_raw[8]
    local stains = enemy_info_raw[9]
    local has_laser = enemy_info_raw[10]
    local remote_enemy_id = en_data.enemy_id
    local x, y = en_data.x, en_data.y
    if not force_no_cull and not dont_cull then
        local my_x, my_y = EntityGetTransform(ctx.my_player.entity)
        if my_x == nil then
            goto continue
        end
        local c_x, c_y = GameGetCameraPos()
        local dx, dy = my_x - x, my_y - y
        local cdx, cdy = c_x - x, c_y - y
        if
            dx * dx + dy * dy > DISTANCE_LIMIT * DISTANCE_LIMIT
            and cdx * cdx + cdy * cdy > DISTANCE_LIMIT * DISTANCE_LIMIT
        then
            if ctx.entity_by_remote_id[remote_enemy_id] ~= nil then
                EntityKill(ctx.entity_by_remote_id[remote_enemy_id].id)
                ctx.entity_by_remote_id[remote_enemy_id] = nil
            end
            unsynced_enemys[remote_enemy_id] = enemy_info_raw
            goto continue
        else
            unsynced_enemys[remote_enemy_id] = nil
        end
    else
        unsynced_enemys[remote_enemy_id] = nil
    end
    local vx = 0
    local vy = 0
    if ffi.typeof(en_data) ~= EnemyDataNoMotion then
        vx, vy = en_data.vx, en_data.vy
    end
    local phys_infos = enemy_info_raw[3]
    local gid = enemy_info_raw[4]
    local effects = enemy_info_raw[5]
    local animation = enemy_info_raw[6]
    local has_died = filename == nil

    local frame_now = GameGetFrameNum()

    --[[if confirmed_kills[remote_enemy_id] then
        goto continue
    end]]

    if
        ctx.entity_by_remote_id[remote_enemy_id] ~= nil
        and not EntityGetIsAlive(ctx.entity_by_remote_id[remote_enemy_id].id)
    then
        ctx.entity_by_remote_id[remote_enemy_id] = nil
    end

    if ctx.entity_by_remote_id[remote_enemy_id] == nil then
        if filename == nil or filename == "" or not ModDoesFileExist(filename) then
            goto continue
        end
        times_spawned_last_minute[remote_enemy_id] = (times_spawned_last_minute[remote_enemy_id] or 0) + 1
        if times_spawned_last_minute[remote_enemy_id] > 5 then
            if times_spawned_last_minute[remote_enemy_id] == 6 then
                print("Entity has been spawned again more than 5 times in last minute, skipping " .. filename)
            end
            goto continue
        end
        local enemy_id
        enemy_id = EntityLoad(filename, x, y)
        if enemy_id == nil then
            print("entity is nil " .. tostring(filename))
            goto continue
        end
        spawned_by_us[enemy_id] = true
        EntityAddTag(enemy_id, "ew_replicated")
        EntityAddTag(enemy_id, "polymorphable_NOT")
        for _, com in ipairs(EntityGetComponent(enemy_id, "LuaComponent") or {}) do
            local script = ComponentGetValue2(com, "script_damage_received")
            if
                (
                    script ~= nil
                    and (
                        script == "data/scripts/animals/leader_damage.lua"
                        or script == "data/scripts/animals/giantshooter_death.lua"
                        or script == "data/scripts/animals/blob_damage.lua"
                    )
                )
                or ComponentGetValue2(com, "script_source_file")
                    == "data/scripts/props/suspended_container_physics_objects.lua"
            then
                EntityRemoveComponent(enemy_id, com)
            end
        end
        EntityAddComponent2(enemy_id, "LuaComponent", {
            _tags = "ew_immortal",
            script_damage_about_to_be_received = "mods/quant.ew/files/resource/cbs/immortal.lua",
        })
        local damage_component = EntityGetFirstComponentIncludingDisabled(enemy_id, "DamageModelComponent")
        if damage_component and damage_component ~= 0 then
            ComponentSetValue2(damage_component, "wait_for_kill_flag_on_death", true)
        end
        for _, name in ipairs({
            "AnimalAIComponent",
            "PhysicsAIComponent",
            "CameraBoundComponent",
            "AdvancedFishAIComponent",
            "AIAttackComponent",
        }) do
            local ai_component = EntityGetFirstComponentIncludingDisabled(enemy_id, name)
            if ai_component ~= 0 then
                EntityRemoveComponent(enemy_id, ai_component)
            end
        end
        ctx.entity_by_remote_id[remote_enemy_id] = { id = enemy_id, frame = frame_now }

        for _, phys_component in ipairs(EntityGetComponent(enemy_id, "PhysicsBody2Component") or {}) do
            if phys_component ~= nil and phys_component ~= 0 then
                ComponentSetValue2(phys_component, "destroy_body_if_entity_destroyed", true)
            end
        end

        -- Make sure stuff doesn't decide to explode on clients by itself.
        local expl_component = EntityGetFirstComponent(enemy_id, "ExplodeOnDamageComponent")
        if expl_component ~= nil and expl_component ~= 0 then
            ComponentSetValue2(expl_component, "explode_on_damage_percent", 0)
            ComponentSetValue2(expl_component, "physics_body_modified_death_probability", 0)
            ComponentSetValue2(expl_component, "explode_on_death_percent", 0)
        end
        local pick_up = EntityGetFirstComponentIncludingDisabled(enemy_id, "ItemPickUpperComponent")
        if pick_up ~= nil then
            EntityRemoveComponent(enemy_id, pick_up)
        end
        for _, sprite in pairs(EntityGetComponent(enemy_id, "SpriteComponent", "character") or {}) do
            ComponentRemoveTag(sprite, "character")
        end

        local ghost = EntityGetFirstComponentIncludingDisabled(enemy_id, "GhostComponent")
        if ghost ~= nil then
            ComponentSetValue2(ghost, "die_if_no_home", false)
        end
        if not EntityHasTag(enemy_id, "effectable_prop") then
            util.make_ephemerial(enemy_id)
        end
    end

    local enemy_data_new = ctx.entity_by_remote_id[remote_enemy_id]
    enemy_data_new.frame = frame_now
    local enemy_id = enemy_data_new.id

    if not has_died then
        local laser = EntityGetFirstComponentIncludingDisabled(enemy_id, "LaserEmitterComponent", "ew_laser")
        if has_laser then
            if laser == nil then
                laser = EntityAddComponent2(enemy_id, "LaserEmitterComponent", { _tags = "ew_laser" })
                ComponentObjectSetValue2(laser, "laser", "max_cell_durability_to_destroy", 0)
                ComponentObjectSetValue2(laser, "laser", "damage_to_cells", 0)
                ComponentObjectSetValue2(laser, "laser", "max_length", 1024)
                ComponentObjectSetValue2(laser, "laser", "beam_radius", 0)
                ComponentObjectSetValue2(laser, "laser", "beam_particle_chance", 80)
                ComponentObjectSetValue2(laser, "laser", "beam_particle_fade", 0)
                ComponentObjectSetValue2(laser, "laser", "hit_particle_chance", 0)
                ComponentObjectSetValue2(laser, "laser", "audio_enabled", false)
                ComponentObjectSetValue2(laser, "laser", "damage_to_entities", 0)
                ComponentObjectSetValue2(laser, "laser", "beam_particle_type", 225)
            end
            local target = ctx.players[has_laser].entity
            local lx, ly = EntityGetTransform(target)
            if lx ~= nil then
                local did_hit, _, _ = RaytracePlatforms(x, y, lx, ly)
                ComponentSetValue2(laser, "is_emitting", not did_hit)
                if not did_hit then
                    local dy = ly - y
                    local dx = lx - x
                    local theta = math.atan2(dy, dx)
                    ComponentSetValue2(laser, "laser_angle_add_rad", theta)
                    ComponentObjectSetValue2(laser, "laser", "max_length", math.sqrt(dx * dx + dy * dy))
                end
            end
        elseif laser ~= nil then
            ComponentSetValue2(laser, "is_emitting", false)
        end
        if not util.set_phys_info(enemy_id, phys_infos, host_fps) or enemy_id == kolmi_spawn then
            local m = host_fps / ctx.my_player.fps
            vx, vy = vx * m, vy * m
            local character_data = EntityGetFirstComponentIncludingDisabled(enemy_id, "CharacterDataComponent")
            if character_data ~= nil then
                ComponentSetValue2(character_data, "mVelocity", vx, vy)
            else
                local velocity_data = EntityGetFirstComponentIncludingDisabled(enemy_id, "VelocityComponent")
                if velocity_data ~= nil then
                    ComponentSetValue2(velocity_data, "mVelocity", vx, vy)
                end
            end
            if ffi.typeof(en_data) == EnemyDataFish then
                EntitySetTransform(enemy_id, x, y, en_data.r / 255 * FULL_TURN)
            else
                EntitySetTransform(enemy_id, x, y)
            end
        end
        local worm = EntityGetFirstComponentIncludingDisabled(enemy_id, "WormAIComponent")
            or EntityGetFirstComponentIncludingDisabled(enemy_id, "BossDragonComponent")
        if worm ~= nil and ffi.typeof(en_data) == EnemyDataWorm then
            local tx, ty = en_data.tx, en_data.ty
            ComponentSetValue2(worm, "mTargetVec", tx, ty)
        end
        if ffi.typeof(en_data) == EnemyDataKolmi and en_data.enabled then
            if kolmi_spawn ~= enemy_id then
                for _, c in ipairs(EntityGetComponentIncludingDisabled(enemy_id, "LuaComponent") or {}) do
                    EntityRemoveComponent(enemy_id, c)
                end
                kolmi_spawn = enemy_id
            end
            EntitySetComponentsWithTagEnabled(enemy_id, "enabled_at_start", false)
            EntitySetComponentsWithTagEnabled(enemy_id, "disabled_at_start", true)
            for i, leg in ipairs(EntityGetAllChildren(enemy_id, "foot")) do
                local limb = EntityGetFirstComponentIncludingDisabled(leg, "IKLimbComponent")
                ComponentSetValue2(limb, "end_position", en_data.legs[2 * i - 2], en_data.legs[2 * i - 1])
            end
        end

        local indexed = {}
        for _, com in ipairs(EntityGetComponent(enemy_id, "LuaComponent") or {}) do
            local script = ComponentGetValue2(com, "script_death")
            local has = false
            for _, inx in ipairs(death_triggers) do
                local script2 = constants.interned_index_to_filename[inx] or inx
                if script == script2 then
                    has = true
                    indexed[script] = true
                end
            end
            if not has then
                ComponentSetValue2(com, "script_death", "")
            end
        end
        for _, inx in ipairs(death_triggers) do
            local script = constants.interned_index_to_filename[inx] or inx
            if indexed[script] == nil then
                EntityAddComponent(enemy_id, "LuaComponent", {
                    script_death = script,
                    execute_every_n_frame = "-1",
                })
            end
        end
        if ffi.typeof(en_data) == EnemyDataMom then
            local orbs = en_data.orbs
            for _, child in ipairs(EntityGetAllChildren(enemy_id) or {}) do
                local var = EntityGetFirstComponentIncludingDisabled(child, "VariableStorageComponent")
                local damage_component = EntityGetFirstComponentIncludingDisabled(child, "DamageModelComponent")
                if EntityHasTag(child, "touchmagic_immunity") and var ~= nil then
                    local n = ComponentGetValue2(var, "value_int")
                    if orbs[n - 1] then
                        ComponentSetValue2(damage_component, "wait_for_kill_flag_on_death", true)
                    else
                        ComponentSetValue2(damage_component, "wait_for_kill_flag_on_death", false)
                        EntityKill(child)
                    end
                end
            end
        end
        effect_sync.apply_effects(effects, enemy_id, true)
        if stains ~= nil then
            stain_sync.sync_stains(stains, enemy_id)
        end
    end

    local inv = EntityGetFirstComponentIncludingDisabled(enemy_id, "Inventory2Component")
    local item
    if inv ~= nil then
        item = ComponentGetValue2(inv, "mActualActiveItem")
    end
    if gid ~= nil and (item == nil or item == 0 or not EntityGetIsAlive(item)) then
        local wand = item_sync.find_by_gid(gid)
        if wand ~= nil and EntityGetIsAlive(wand) then
            EntityAddTag(wand, "ew_client_item")
            local inventory
            for _, child in pairs(EntityGetAllChildren(enemy_id) or {}) do
                if EntityGetName(child) == "inventory_quick" then
                    inventory = child
                end
            end
            if inventory == nil then
                inventory = EntityCreateNew("inventory_quick")
                EntityAddChild(enemy_id, inventory)
            end
            if EntityGetParent(wand) ~= inventory then
                if EntityGetParent(wand) ~= 0 then
                    EntityRemoveFromParent(wand)
                end
                EntityAddChild(inventory, wand)
            end
            np.SetActiveHeldEntity(enemy_id, wand, false, false)
        elseif should_wait[gid] == nil or should_wait[gid] < GameGetFrameNum() then
            item_sync.rpc.request_send_again(gid)
            should_wait[gid] = GameGetFrameNum() + 15
        end
    end

    for i, sprite in pairs(EntityGetComponent(enemy_id, "SpriteComponent") or {}) do
        if animation[i] ~= nil then
            ComponentSetValue2(sprite, "rect_animation", animation[i])
            ComponentSetValue2(sprite, "next_rect_animation", animation[i])
        end
    end

    ::continue::
end

rpc.opts_reliable()
function rpc.handle_death_data(death_data)
    for _, remote_data in ipairs(death_data) do
        local remote_id = remote_data[1]
        --[[if confirmed_kills[remote_id] then
            GamePrint("Remote id has been killed already..?")
            goto continue
        end
        confirmed_kills[remote_id] = true]]
        local responsible_entity = 0
        local peer_data = player_fns.peer_get_player_data(remote_data[2], true)
        if peer_data ~= nil then
            responsible_entity = peer_data.entity
        elseif ctx.entity_by_remote_id[remote_data[2]] ~= nil then
            responsible_entity = ctx.entity_by_remote_id[remote_data[2]]
        end

        if unsynced_enemys[remote_id] ~= nil then
            sync_enemy(unsynced_enemys[remote_id], true, 60)
        end
        local enemy_data = ctx.entity_by_remote_id[remote_id]
        if enemy_data ~= nil and EntityGetIsAlive(enemy_data.id) then
            local enemy_id = enemy_data.id
            local immortal = EntityGetFirstComponentIncludingDisabled(enemy_id, "LuaComponent", "ew_immortal")
            if immortal ~= 0 then
                EntityRemoveComponent(enemy_id, immortal)
            end
            local protection_component_id = GameGetGameEffect(enemy_id, "PROTECTION_ALL")
            if protection_component_id ~= 0 then
                EntitySetComponentIsEnabled(enemy_id, protection_component_id, false)
            end

            local damage_component = EntityGetFirstComponentIncludingDisabled(enemy_id, "DamageModelComponent")
            if damage_component and damage_component ~= 0 then
                ComponentSetValue2(damage_component, "wait_for_kill_flag_on_death", false)
                ComponentSetValue2(damage_component, "ui_report_damage", false)
                ComponentSetValue2(damage_component, "hp", 2 ^ -38)
            end

            -- Enable explosion back
            local expl_component = EntityGetFirstComponent(enemy_id, "ExplodeOnDamageComponent")
            if expl_component ~= nil and expl_component ~= 0 then
                ComponentSetValue2(expl_component, "explode_on_death_percent", 1)
            end

            local current_hp = util.get_ent_health(enemy_id)
            local dmg = current_hp
            if dmg > 0 then
                EntityInflictDamage(enemy_id, dmg + 0.1, "DAMAGE_CURSE", "", "NONE", 0, 0, responsible_entity)
            end

            EntityInflictDamage(enemy_id, 1000000000, "DAMAGE_CURSE", "", "NONE", 0, 0, responsible_entity) -- Just to be sure
            if not remote_data[3] then
                EntityKill(enemy_id)
            else
                ComponentSetValue2(damage_component, "wait_for_kill_flag_on_death", true)
                ComponentSetValue2(damage_component, "kill_now", true)
            end
            ctx.entity_by_remote_id[remote_id] = nil
        end
        ::continue::
    end
end

function rpc.handle_enemy_data(enemy_data, is_first)
    if is_first then
        for _, n in pairs(ctx.entity_by_remote_id) do
            EntityKill(n.id)
        end
        ctx.entity_by_remote_id = {}
    end
    frame = GameGetFrameNum()
    for _, enemy_info_raw in ipairs(enemy_data) do
        sync_enemy(enemy_info_raw, false, ctx.rpc_player_data.fps)
    end
end

function rpc.handle_enemy_health(enemy_health_data)
    for _, en_data in ipairs(enemy_health_data) do
        local remote_enemy_id = en_data.enemy_id
        local hp = en_data.hp
        local max_hp = en_data.max_hp

        if
            ctx.entity_by_remote_id[remote_enemy_id] == nil
            or not EntityGetIsAlive(ctx.entity_by_remote_id[remote_enemy_id].id)
        then
            goto continue
        end
        local enemy_data = ctx.entity_by_remote_id[remote_enemy_id]
        local enemy_id = enemy_data.id

        local current_hp = util.get_ent_health(enemy_id)
        local dmg = current_hp - hp
        if dmg > 0 then
            -- Make sure the enemy doesn't die from the next EntityInflictDamage.
            if EntityGetName(enemy_id) ~= "$animal_boss_sky" then
                util.set_ent_health(enemy_id, { dmg * 2, dmg * 2 })
            else
                util.set_ent_health(enemy_id, { hp + dmg, max_hp })
            end
            -- Deal damage, so that game displays damage numbers.
            EntityInflictDamage(enemy_id, dmg, "DAMAGE_CURSE", "", "NONE", 0, 0, GameGetWorldStateEntity())
        end
        util.set_ent_health(enemy_id, { hp, max_hp })
        ::continue::
    end
end

function enemy_sync.on_projectile_fired(
    shooter_id,
    projectile_id,
    initial_rng,
    position_x,
    position_y,
    target_x,
    target_y,
    send_message,
    unknown1,
    multicast_index,
    unknown3
)
    local not_a_player = not EntityHasTag(shooter_id, "ew_no_enemy_sync")
        and not EntityHasTag(shooter_id, "player_unit")
        and not EntityHasTag(shooter_id, "ew_client")
    if not_a_player and ctx.is_host then
        local projectileComponent = EntityGetFirstComponentIncludingDisabled(projectile_id, "ProjectileComponent")
        if projectileComponent ~= nil then
            local entity_that_shot = ComponentGetValue2(projectileComponent, "mEntityThatShot")
            if entity_that_shot == 0 then
                rpc.replicate_projectile(
                    util.serialize_entity(projectile_id),
                    position_x,
                    position_y,
                    target_x,
                    target_y,
                    shooter_id,
                    initial_rng
                )
            end
        end
    end
end

rpc.opts_reliable()
function rpc.replicate_projectile(seri_ent, position_x, position_y, target_x, target_y, remote_source_ent, rng)
    if rng ~= nil then
        np.SetProjectileSpreadRNG(rng)
    end
    if ctx.entity_by_remote_id[remote_source_ent] == nil then
        return
    end
    local source_ent = ctx.entity_by_remote_id[remote_source_ent].id
    local ent = util.deserialize_entity(seri_ent)
    GameShootProjectile(source_ent, position_x, position_y, target_x, target_y, ent)
end

return enemy_sync
