// ============================================================================
// CIVCRAFT 2 - MILLENAIRE CARPET MARKERS -> MCA + EASY NPC
// Minecraft 1.21.1 / NeoForge
//
// PURPOSE
// -------
// Reads carpet blocks ONLY at exact positions defined by operational
// Millenaire building templates. A player-placed carpet elsewhere is ignored.
//
// Each carpet color maps to a ROLE. Data rules then decide whether that role
// should spawn:
//   - an MCA villager, or
//   - an Easy NPC DATA preset.
//
// IMPORTANT DESIGN RULE:
// The carpet is removed ONLY AFTER a backend reports a successful spawn.
// A missing mod, missing Easy NPC preset, or Java/API exception leaves the
// carpet intact for a later retry.
//
// This is intentionally ONE-SHOT in v1.
// It spawns the NPC once and remembers that template slot as processed.
// Respawning/guard replacement can be added later after both backends have
// proven stable in your actual pack.
//
// Put this file in:
//   kubejs/server_scripts/
//
// Do not run older experimental CC2 NPC carpet bridge scripts at the same time.
// ============================================================================

console.info('[CC2 Dual NPC Markers] SCRIPT LOADED')


// ============================================================================
// CORE SETTINGS
// ============================================================================

var CC2DUAL_STATE_VERSION = 'dual1'
var CC2DUAL_SCAN_INTERVAL_TICKS = 100
var CC2DUAL_DEBUG = true

// If true, a successful NPC spawn consumes the carpet marker.
var CC2DUAL_REMOVE_MARKER_AFTER_SUCCESS = true


// ============================================================================
// CARPET COLOR -> ROLE
// ============================================================================
//
// If you want an actual colored floor in a Millenaire building, use WOOL.
// Carpet is reserved as invisible build-time metadata.
//
var CC2DUAL_MARKER_ROLES = {
    'minecraft:white_carpet':      'ambient',
    'minecraft:orange_carpet':     'tavern_patron',
    'minecraft:magenta_carpet':    'traveler',
    'minecraft:light_blue_carpet': 'resident',
    'minecraft:yellow_carpet':     'merchant',
    'minecraft:lime_carpet':       'worker',
    'minecraft:pink_carpet':       'social',
    'minecraft:gray_carpet':       'guard',
    'minecraft:light_gray_carpet': 'sentry',
    'minecraft:cyan_carpet':       'specialist',
    'minecraft:purple_carpet':     'entertainer',
    'minecraft:blue_carpet':       'official',
    'minecraft:brown_carpet':      'laborer',
    'minecraft:green_carpet':      'farmer',
    'minecraft:red_carpet':        'soldier',
    'minecraft:black_carpet':      'special'
}


// ============================================================================
// ROLE DEFAULTS
// ============================================================================
//
// SOCIAL / ORDINARY LIFE roles default to MCA because MCA can generate a
// proper MCA citizen with its own normal name/gender/age behavior.
//
// Scripted/specialized roles intentionally have NO backend by default.
// Configure those as Easy NPC presets in culture/village/building rules.
//
// Any rule may use:
//
//   backend: 'mca'
//   gender: 'random' | 'male' | 'female'
//   name: 'Exact Name'
//   names: ['Name A', 'Name B']
//   age: 0                       // optional; omitted = MCA's own normal choice
//
// or:
//
//   backend: 'easy_npc'
//   preset: 'namespace:preset_id'
//   presets: ['namespace:a', 'namespace:b']
//
// common fields:
//
//   enabled: true
//   removeMarker: true
//   offsetX: 0
//   offsetY: 0
//   offsetZ: 0
//
var CC2DUAL_ROLE_DEFAULTS = {
    ambient:       { backend: 'mca', gender: 'random' },
    tavern_patron: { backend: 'mca', gender: 'random' },
    traveler:      { backend: 'mca', gender: 'random' },
    resident:      { backend: 'mca', gender: 'random' },

    merchant:      { backend: null },

    worker:        { backend: 'mca', gender: 'random' },
    social:        { backend: 'mca', gender: 'random' },

    guard:         { backend: null },
    sentry:        { backend: null },
    specialist:    { backend: null },
    entertainer:   { backend: null },
    official:      { backend: null },

    laborer:       { backend: 'mca', gender: 'random' },
    farmer:        { backend: 'mca', gender: 'random' },

    soldier:       { backend: null },
    special:       { backend: null }
}


// ============================================================================
// DATA-DRIVEN OVERRIDES
// ============================================================================
//
// Resolution order:
//
//   role default
//       -> culture
//       -> village type
//       -> building
//       -> exact culture + village type + building
//       -> exact generated village UUID
//
// Later layers override earlier layers.
//
// ---------------------------------------------------------------------------
// CULTURE RULES
// ---------------------------------------------------------------------------

var CC2DUAL_CULTURE_RULES = {

    // Example: make Norman guards Easy NPC presets.
    //
    // IMPORTANT: uncomment only AFTER that Easy NPC DATA preset exists.
    //
    // 'millenaire:civnormans': {
    //
    //     guard: {
    //         backend: 'easy_npc',
    //         preset: 'billenaire:norman_guard'
    //     },
    //
    //     sentry: {
    //         backend: 'easy_npc',
    //         preset: 'billenaire:norman_sentry'
    //     },
    //
    //     merchant: {
    //         backend: 'easy_npc',
    //         preset: 'billenaire:norman_merchant'
    //     }
    // }
}


// ---------------------------------------------------------------------------
// VILLAGE TYPE RULES
// ---------------------------------------------------------------------------

var CC2DUAL_VILLAGE_RULES = {

    // Example:
    //
    // 'millenaire:civnormans/survivalvillage': {
    //     traveler: {
    //         backend: 'mca',
    //         gender: 'random'
    //     }
    // }
}


// ---------------------------------------------------------------------------
// BUILDING RULES
// ---------------------------------------------------------------------------

var CC2DUAL_BUILDING_RULES = {

    // Example: every orange carpet in the Norman tavern picks a random
    // Easy NPC patron preset.
    //
    // 'millenaire:civnormans/tavern': {
    //     tavern_patron: {
    //         backend: 'easy_npc',
    //         presets: [
    //             'billenaire:norman_patron_1',
    //             'billenaire:norman_patron_2',
    //             'billenaire:norman_patron_3'
    //         ]
    //     }
    // }
}


// ---------------------------------------------------------------------------
// EXACT CONTEXT RULES
// ---------------------------------------------------------------------------
//
// Key format:
//   culture||villageType||building
//
var CC2DUAL_EXACT_RULES = {

    // Example:
    //
    // 'millenaire:civnormans||millenaire:civnormans/survivalvillage||millenaire:civnormans/well': {
    //     guard: {
    //         backend: 'easy_npc',
    //         preset: 'billenaire:norman_guard'
    //     }
    // }
}


// ---------------------------------------------------------------------------
// EXACT GENERATED VILLAGE UUID RULES
// ---------------------------------------------------------------------------
//
// Useful for one special settlement with unique named characters.
// The UUID is printed in debug scan messages.
//
var CC2DUAL_VILLAGE_INSTANCE_RULES = {

    // 'paste-generated-village-uuid-here': {
    //     official: {
    //         backend: 'easy_npc',
    //         preset: 'billenaire:founder'
    //     }
    // }
}


// ============================================================================
// MILLENAIRE / MINECRAFT JAVA CLASSES
// ============================================================================
//
// Deliberately obnoxious names avoid Rhino top-level variable collisions with
// your other KubeJS scripts.
//

var $CC2DUAL_VillageSavedData =
    Java.loadClass('org.millenaire.village.VillageSavedData')

var $CC2DUAL_ModCultures =
    Java.loadClass('org.millenaire.culture.ModCultures')

var $CC2DUAL_BuildingPlacer =
    Java.loadClass('org.millenaire.world.BuildingPlacer')

var $CC2DUAL_BuiltInRegistries =
    Java.loadClass('net.minecraft.core.registries.BuiltInRegistries')

var $CC2DUAL_Blocks =
    Java.loadClass('net.minecraft.world.level.block.Blocks')

var $CC2DUAL_Vec3 =
    Java.loadClass('net.minecraft.world.phys.Vec3')

var $CC2DUAL_ResourceLocation =
    Java.loadClass('net.minecraft.resources.ResourceLocation')

var $CC2DUAL_MobSpawnType =
    Java.loadClass('net.minecraft.world.entity.MobSpawnType')


// ============================================================================
// LAZY BACKEND CLASS LOADING
// ============================================================================
//
// Lazy loading is intentional:
// - MCA can be absent without making the whole marker scanner fail to load.
// - Easy NPC can be added later.
// - A missing backend only affects markers configured for that backend.
//

var CC2DUAL_mcaLoadAttempted = false
var CC2DUAL_mcaFactoryClass = null
var CC2DUAL_mcaGenderClass = null
var CC2DUAL_mcaLoadError = null

var CC2DUAL_easyLoadAttempted = false
var CC2DUAL_easyHandlerClass = null
var CC2DUAL_easyLoadError = null


function CC2DUAL_loadMCA() {

    if (CC2DUAL_mcaLoadAttempted) {
        return CC2DUAL_mcaFactoryClass != null
    }

    CC2DUAL_mcaLoadAttempted = true

    try {

        CC2DUAL_mcaFactoryClass =
            Java.loadClass('net.conczin.mca.entity.VillagerFactory')

        CC2DUAL_mcaGenderClass =
            Java.loadClass('net.conczin.mca.entity.ai.relationship.Gender')

        console.info('[CC2 Dual NPC Markers] MCA backend available')

        return true
    }

    catch (error) {

        CC2DUAL_mcaLoadError = String(error)

        console.warn(
            '[CC2 Dual NPC Markers] MCA backend unavailable: ' +
            CC2DUAL_mcaLoadError
        )

        return false
    }
}


function CC2DUAL_loadEasyNPC() {

    if (CC2DUAL_easyLoadAttempted) {
        return CC2DUAL_easyHandlerClass != null
    }

    CC2DUAL_easyLoadAttempted = true

    try {

        CC2DUAL_easyHandlerClass =
            Java.loadClass(
                'de.markusbordihn.easynpc.api.handler.EasyNPCEntityHandler'
            )

        console.info('[CC2 Dual NPC Markers] Easy NPC backend available')

        return true
    }

    catch (error) {

        CC2DUAL_easyLoadError = String(error)

        console.warn(
            '[CC2 Dual NPC Markers] Easy NPC backend unavailable: ' +
            CC2DUAL_easyLoadError
        )

        return false
    }
}


// ============================================================================
// GENERAL HELPERS
// ============================================================================

var CC2DUAL_tickCounter = 0
var CC2DUAL_warned = {}


function CC2DUAL_safeKey(value) {

    return String(value)
        .replace(/[^A-Za-z0-9_]/g, '_')
}


function CC2DUAL_copyObject(source) {

    var result = {}

    if (source == null) {
        return result
    }

    for (var key in source) {
        result[key] = source[key]
    }

    return result
}


function CC2DUAL_mergeObject(target, source) {

    if (source == null) {
        return
    }

    for (var key in source) {

        if (
            (key == 'names' || key == 'presets') &&
            source[key] != null
        ) {

            target[key] = []

            for (var i = 0; i < source[key].length; i++) {
                target[key].push(String(source[key][i]))
            }
        }

        else {
            target[key] = source[key]
        }
    }
}


function CC2DUAL_getRoleRule(map, id, role) {

    if (map == null || id == null) {
        return null
    }

    var group = map[String(id)]

    if (group == null) {
        return null
    }

    return group[role]
}


function CC2DUAL_getVillageUUID(village) {

    try {
        return String(village.getId().uuid())
    }

    catch (error1) {

        try {
            return String(village.getId().uuid)
        }

        catch (error2) {
            return String(village.getId())
        }
    }
}


function CC2DUAL_chooseString(singleValue, arrayValue) {

    if (
        arrayValue != null &&
        arrayValue.length > 0
    ) {

        return String(
            arrayValue[
                Math.floor(
                    Math.random() *
                    arrayValue.length
                )
            ]
        )
    }

    if (
        singleValue != null &&
        String(singleValue).length > 0
    ) {
        return String(singleValue)
    }

    return null
}


// ============================================================================
// RULE RESOLUTION
// ============================================================================

function CC2DUAL_resolveSpec(village, building, role) {

    var cultureId =
        String(village.getCultureId())

    var villageTypeId =
        String(village.getVillageTypeId())

    var buildingId =
        String(building.getPlanSetId())

    var villageUUID =
        CC2DUAL_getVillageUUID(village)

    var exactKey =
        cultureId +
        '||' +
        villageTypeId +
        '||' +
        buildingId

    var spec =
        CC2DUAL_copyObject(
            CC2DUAL_ROLE_DEFAULTS[role]
        )

    CC2DUAL_mergeObject(
        spec,
        CC2DUAL_getRoleRule(
            CC2DUAL_CULTURE_RULES,
            cultureId,
            role
        )
    )

    CC2DUAL_mergeObject(
        spec,
        CC2DUAL_getRoleRule(
            CC2DUAL_VILLAGE_RULES,
            villageTypeId,
            role
        )
    )

    CC2DUAL_mergeObject(
        spec,
        CC2DUAL_getRoleRule(
            CC2DUAL_BUILDING_RULES,
            buildingId,
            role
        )
    )

    CC2DUAL_mergeObject(
        spec,
        CC2DUAL_getRoleRule(
            CC2DUAL_EXACT_RULES,
            exactKey,
            role
        )
    )

    CC2DUAL_mergeObject(
        spec,
        CC2DUAL_getRoleRule(
            CC2DUAL_VILLAGE_INSTANCE_RULES,
            villageUUID,
            role
        )
    )

    if (spec.enabled == null) {
        spec.enabled = true
    }

    if (spec.removeMarker == null) {
        spec.removeMarker =
            CC2DUAL_REMOVE_MARKER_AFTER_SUCCESS
    }

    if (spec.offsetX == null) spec.offsetX = 0
    if (spec.offsetY == null) spec.offsetY = 0
    if (spec.offsetZ == null) spec.offsetZ = 0

    spec.role = role
    spec.cultureId = cultureId
    spec.villageTypeId = villageTypeId
    spec.buildingId = buildingId
    spec.villageUUID = villageUUID

    return spec
}


// ============================================================================
// PERSISTENT SLOT KEYS
// ============================================================================

function CC2DUAL_slotBase(
    level,
    village,
    building,
    planLevel,
    relativePos,
    role
) {

    var origin =
        building.getOrigin()

    return (
        'cc2dual_' +
        CC2DUAL_STATE_VERSION + '_' +
        CC2DUAL_safeKey(String(level.dimension)) + '_' +
        CC2DUAL_safeKey(CC2DUAL_getVillageUUID(village)) + '_' +
        CC2DUAL_safeKey(String(building.getPlanSetId())) + '_' +
        origin.getX() + '_' +
        origin.getY() + '_' +
        origin.getZ() + '_' +
        CC2DUAL_safeKey(String(building.getVariant())) + '_' +
        planLevel + '_' +
        relativePos.getX() + '_' +
        relativePos.getY() + '_' +
        relativePos.getZ() + '_' +
        CC2DUAL_safeKey(role)
    )
}


function CC2DUAL_buildingDoneKey(
    level,
    village,
    building
) {

    var origin =
        building.getOrigin()

    return (
        'cc2dual_' +
        CC2DUAL_STATE_VERSION +
        '_building_' +
        CC2DUAL_safeKey(String(level.dimension)) + '_' +
        CC2DUAL_safeKey(CC2DUAL_getVillageUUID(village)) + '_' +
        CC2DUAL_safeKey(String(building.getPlanSetId())) + '_' +
        origin.getX() + '_' +
        origin.getY() + '_' +
        origin.getZ() + '_' +
        CC2DUAL_safeKey(String(building.getVariant())) + '_' +
        building.getLevel()
    )
}


// ============================================================================
// MCA BACKEND
// ============================================================================

function CC2DUAL_spawnMCA(
    level,
    markerPos,
    spec
) {

    if (!CC2DUAL_loadMCA()) {

        return {
            success: false,
            backend: 'mca',
            reason: (
                'MCA classes unavailable: ' +
                CC2DUAL_mcaLoadError
            )
        }
    }

    try {

        var spawnX =
            markerPos.getX() +
            0.5 +
            Number(spec.offsetX)

        var spawnY =
            markerPos.getY() +
            0.05 +
            Number(spec.offsetY)

        var spawnZ =
            markerPos.getZ() +
            0.5 +
            Number(spec.offsetZ)

        var factory =
            CC2DUAL_mcaFactoryClass
                .newVillager(level)

        // Three arguments deliberately select MCA's unambiguous
        // withPosition(double, double, double) overload.
        factory.withPosition(
            spawnX,
            spawnY,
            spawnZ
        )

        var gender =
            spec.gender == null
            ? 'random'
            : String(spec.gender).toLowerCase()

        if (gender == 'male') {
            factory.withGender(
                CC2DUAL_mcaGenderClass.MALE
            )
        }

        else if (gender == 'female') {
            factory.withGender(
                CC2DUAL_mcaGenderClass.FEMALE
            )
        }

        // random = deliberately do not call withGender().
        // MCA's own VillagerFactory chooses its normal random gender.

        var chosenName =
            CC2DUAL_chooseString(
                spec.name,
                spec.names
            )

        if (chosenName != null) {
            factory.withName(chosenName)
        }

        // Omit age to let MCA's own factory choose its normal age.
        if (spec.age != null) {
            factory.withAge(
                Number(spec.age)
            )
        }

        var entity =
            factory.spawn(
                $CC2DUAL_MobSpawnType.COMMAND
            )

        if (entity == null) {

            return {
                success: false,
                backend: 'mca',
                reason: 'MCA VillagerFactory returned null'
            }
        }

        entity.addTag('cc2_dual_npc')
        entity.addTag('cc2_dual_mca')
        entity.addTag(
            'cc2_role_' +
            CC2DUAL_safeKey(spec.role)
        )

        return {
            success: true,
            backend: 'mca',
            uuid: String(entity.getUUID()),
            label: String(entity.getName().getString())
        }
    }

    catch (error) {

        return {
            success: false,
            backend: 'mca',
            reason: String(error)
        }
    }
}


// ============================================================================
// EASY NPC BACKEND
// ============================================================================

function CC2DUAL_spawnEasyNPC(
    level,
    markerPos,
    spec
) {

    if (!CC2DUAL_loadEasyNPC()) {

        return {
            success: false,
            backend: 'easy_npc',
            reason: (
                'Easy NPC classes unavailable: ' +
                CC2DUAL_easyLoadError
            )
        }
    }

    var presetId =
        CC2DUAL_chooseString(
            spec.preset,
            spec.presets
        )

    if (presetId == null) {

        return {
            success: false,
            backend: 'easy_npc',
            reason: 'No Easy NPC DATA preset configured'
        }
    }

    try {

        var spawnX =
            markerPos.getX() +
            0.5 +
            Number(spec.offsetX)

        var spawnY =
            markerPos.getY() +
            0.05 +
            Number(spec.offsetY)

        var spawnZ =
            markerPos.getZ() +
            0.5 +
            Number(spec.offsetZ)

        var presetResource =
            $CC2DUAL_ResourceLocation
                .parse(presetId)

        var position =
            new $CC2DUAL_Vec3(
                spawnX,
                spawnY,
                spawnZ
            )

        // Exact five-argument Easy NPC 1.21.1 API:
        //
        // spawnFromPreset(
        //     ResourceLocation preset,
        //     ServerLevel serverLevel,
        //     Vec3 position,
        //     UUID uuid,
        //     ServerPlayer owner
        // )
        //
        // null UUID lets Easy NPC create/use the preset's UUID behavior.
        // null owner is supported by the API.
        //
        // This overload defaults to PresetType.DATA.
        var optionalEasyNPC =
            CC2DUAL_easyHandlerClass
                .spawnFromPreset(
                    presetResource,
                    level,
                    position,
                    null,
                    null
                )

        if (
            optionalEasyNPC == null ||
            !optionalEasyNPC.isPresent()
        ) {

            return {
                success: false,
                backend: 'easy_npc',
                preset: presetId,
                reason: (
                    'EasyNPCEntityHandler returned Optional.empty()'
                )
            }
        }

        var easyNPC =
            optionalEasyNPC.get()

        var entity =
            easyNPC.getEntity()

        if (entity == null) {

            return {
                success: false,
                backend: 'easy_npc',
                preset: presetId,
                reason: 'Easy NPC wrapper had no entity'
            }
        }

        entity.addTag('cc2_dual_npc')
        entity.addTag('cc2_dual_easy_npc')
        entity.addTag(
            'cc2_role_' +
            CC2DUAL_safeKey(spec.role)
        )

        return {
            success: true,
            backend: 'easy_npc',
            preset: presetId,
            uuid: String(entity.getUUID()),
            label: presetId
        }
    }

    catch (error) {

        return {
            success: false,
            backend: 'easy_npc',
            preset: presetId,
            reason: String(error)
        }
    }
}


// ============================================================================
// BACKEND DISPATCH
// ============================================================================

function CC2DUAL_spawnForSpec(
    level,
    markerPos,
    spec
) {

    if (spec.backend == null) {

        return {
            success: false,
            backend: 'none',
            reason: 'No backend configured for role'
        }
    }

    var backend =
        String(spec.backend)
            .toLowerCase()

    if (backend == 'mca') {

        return CC2DUAL_spawnMCA(
            level,
            markerPos,
            spec
        )
    }

    if (
        backend == 'easy_npc' ||
        backend == 'easynpc' ||
        backend == 'easy'
    ) {

        return CC2DUAL_spawnEasyNPC(
            level,
            markerPos,
            spec
        )
    }

    return {
        success: false,
        backend: backend,
        reason: (
            'Unknown backend "' +
            backend +
            '"'
        )
    }
}


// ============================================================================
// ONE EXACT MILLENAIRE CARPET MARKER
// ============================================================================

function CC2DUAL_processMarker(
    server,
    level,
    village,
    building,
    planLevel,
    placementStep,
    expectedCarpetId,
    role
) {

    var relativePos =
        placementStep.relativePos()

    var worldPos =
        building
            .getOrigin()
            .offset(relativePos)

    var spec =
        CC2DUAL_resolveSpec(
            village,
            building,
            role
        )

    var base =
        CC2DUAL_slotBase(
            level,
            village,
            building,
            planLevel,
            relativePos,
            role
        )

    var processedKey =
        base + '_processed'

    var uuidKey =
        base + '_uuid'

    var backendKey =
        base + '_backend'

    var profileKey =
        base + '_profile'

    var data =
        server.persistentData

    // Already successfully handled in an earlier scan.
    if (data.getBoolean(processedKey)) {

        return {
            done: true
        }
    }


    // Disabled rules deliberately do nothing and leave the marker in place.
    if (!spec.enabled) {

        return {
            done: false
        }
    }


    // If the role has not been assigned a backend yet, leave the carpet there.
    if (spec.backend == null) {

        var noBackendWarning =
            spec.cultureId +
            '|' +
            spec.villageTypeId +
            '|' +
            spec.buildingId +
            '|' +
            role +
            '|no_backend'

        if (!CC2DUAL_warned[noBackendWarning]) {

            console.warn(
                '[CC2 Dual NPC Markers] NO BACKEND: ' +
                'role=' + role +
                ', culture=' + spec.cultureId +
                ', village=' + spec.villageTypeId +
                ', building=' + spec.buildingId +
                '. Carpet left in place.'
            )

            CC2DUAL_warned[noBackendWarning] = true
        }

        return {
            done: false
        }
    }


    // Check the real world block. This is why an arbitrary player-placed
    // carpet elsewhere is ignored: we only reach this position through
    // Millenaire's compiled placement steps.
    var actualBlockId =
        $CC2DUAL_BuiltInRegistries.BLOCK
            .getKey(
                level
                    .getBlockState(worldPos)
                    .getBlock()
            )
            .toString()


    if (
        actualBlockId !=
        expectedCarpetId
    ) {

        // If an old upgrade level's marker no longer exists physically,
        // retire that obsolete slot rather than scanning it forever.
        if (
            planLevel <
            building.getLevel()
        ) {

            data.putBoolean(
                processedKey,
                true
            )

            return {
                done: true
            }
        }

        return {
            done: false
        }
    }


    // The backend is called BEFORE the carpet is touched.
    var result =
        CC2DUAL_spawnForSpec(
            level,
            worldPos,
            spec
        )


    if (!result.success) {

        var failureWarning =
            base +
            '|' +
            result.backend +
            '|' +
            result.reason

        if (!CC2DUAL_warned[failureWarning]) {

            console.warn(
                '[CC2 Dual NPC Markers] SPAWN FAILED: ' +
                'backend=' + result.backend +
                ', role=' + role +
                ', culture=' + spec.cultureId +
                ', village=' + spec.villageTypeId +
                ', building=' + spec.buildingId +
                ', marker=' + expectedCarpetId +
                ', at=' + worldPos +
                ', reason=' + result.reason +
                '. Carpet left in place.'
            )

            CC2DUAL_warned[failureWarning] = true
        }

        return {
            done: false
        }
    }


    // SUCCESS. Only now is the marker consumed.
    if (spec.removeMarker) {

        level.setBlockAndUpdate(
            worldPos,
            $CC2DUAL_Blocks.AIR
                .defaultBlockState()
        )
    }


    data.putBoolean(
        processedKey,
        true
    )

    data.putString(
        uuidKey,
        result.uuid == null
        ? ''
        : String(result.uuid)
    )

    data.putString(
        backendKey,
        String(result.backend)
    )

    data.putString(
        profileKey,
        result.label == null
        ? ''
        : String(result.label)
    )


    console.info(
        '[CC2 Dual NPC Markers] SPAWN OK: ' +
        'backend=' + result.backend +
        ', role=' + role +
        ', profile="' +
        (
            result.label == null
            ? ''
            : result.label
        ) +
        '"' +
        ', culture=' + spec.cultureId +
        ', village=' + spec.villageTypeId +
        ', villageUUID=' + spec.villageUUID +
        ', building=' + spec.buildingId +
        ', marker=' + expectedCarpetId +
        ', at=' + worldPos +
        ', uuid=' + result.uuid
    )


    return {
        done: true
    }
}


// ============================================================================
// ONE OPERATIONAL MILLENAIRE BUILDING
// ============================================================================

function CC2DUAL_scanBuilding(
    server,
    level,
    village,
    building
) {

    if (!building.isOperational()) {
        return
    }


    var doneKey =
        CC2DUAL_buildingDoneKey(
            level,
            village,
            building
        )


    if (
        server.persistentData
            .getBoolean(doneKey)
    ) {
        return
    }


    var planSet =
        $CC2DUAL_ModCultures
            .getBuildingPlanSet(
                building.getPlanSetId()
            )


    if (planSet == null) {
        return
    }


    var variant =
        building.getVariant()

    var currentLevel =
        building.getLevel()

    var markerCount = 0
    var doneCount = 0
    var pendingCount = 0


    // Scan every built level so a marker introduced by an earlier level remains
    // recognized after the building is upgraded.
    for (
        var planLevel = 0;
        planLevel <= currentLevel;
        planLevel++
    ) {

        var planId

        try {

            planId =
                planSet.getPlanId(
                    variant,
                    planLevel
                )
        }

        catch (planIdError) {
            continue
        }


        if (planId == null) {
            continue
        }


        var plan =
            $CC2DUAL_ModCultures
                .getBuildingPlan(planId)


        if (plan == null) {
            continue
        }


        var placementSteps =
            $CC2DUAL_BuildingPlacer
                .compilePlacementSteps(
                    level,
                    plan,
                    building.getOrigin(),
                    building.getRotation()
                )


        for (
            var i = 0;
            i < placementSteps.size();
            i++
        ) {

            var placementStep =
                placementSteps.get(i)


            var templateBlockId =
                $CC2DUAL_BuiltInRegistries.BLOCK
                    .getKey(
                        placementStep
                            .blockState()
                            .getBlock()
                    )
                    .toString()


            var role =
                CC2DUAL_MARKER_ROLES[
                    templateBlockId
                ]


            if (role == null) {
                continue
            }


            markerCount++


            var result =
                CC2DUAL_processMarker(
                    server,
                    level,
                    village,
                    building,
                    planLevel,
                    placementStep,
                    templateBlockId,
                    role
                )


            if (result.done) {
                doneCount++
            }

            else {
                pendingCount++
            }
        }
    }


    if (markerCount == 0) {

        server.persistentData
            .putBoolean(
                doneKey,
                true
            )

        return
    }


    if (CC2DUAL_DEBUG) {

        console.info(
            '[CC2 Dual NPC Markers] SCAN: ' +
            'culture=' +
            village.getCultureId() +
            ', village=' +
            village.getVillageTypeId() +
            ', villageUUID=' +
            CC2DUAL_getVillageUUID(village) +
            ', building=' +
            building.getPlanSetId() +
            ', level=' +
            currentLevel +
            ', markers=' +
            markerCount +
            ', done=' +
            doneCount +
            ', pending=' +
            pendingCount
        )
    }


    // This v1 system is intentionally one-shot.
    // Once every marker in this building has succeeded, stop scanning it.
    if (
        pendingCount == 0
    ) {

        server.persistentData
            .putBoolean(
                doneKey,
                true
            )
    }
}


// ============================================================================
// SERVER TICK
// ============================================================================

ServerEvents.tick(function (event) {

    CC2DUAL_tickCounter++


    if (
        CC2DUAL_tickCounter <
        CC2DUAL_SCAN_INTERVAL_TICKS
    ) {
        return
    }


    CC2DUAL_tickCounter = 0


    try {

        var levels =
            event.server
                .getAllLevels()
                .iterator()


        while (levels.hasNext()) {

            var cc2DualLevel =
                levels.next()


            var savedData =
                $CC2DUAL_VillageSavedData
                    .get(cc2DualLevel)


            var villages =
                savedData
                    .getVillageManager()
                    .getAllVillages()
                    .iterator()


            while (villages.hasNext()) {

                var cc2DualVillage =
                    villages.next()


                var buildings =
                    cc2DualVillage
                        .getBuildings()
                        .iterator()


                while (buildings.hasNext()) {

                    CC2DUAL_scanBuilding(
                        event.server,
                        cc2DualLevel,
                        cc2DualVillage,
                        buildings.next()
                    )
                }
            }
        }
    }

    catch (error) {

        console.error(
            '[CC2 Dual NPC Markers] MAIN ERROR: ' +
            error
        )
    }
})
