// Started 05/08/2024 by Sparks for Wonder
import { world, system, BlockPermutation, ItemComponentTypes, EquipmentSlot, EntityComponentTypes, ItemStack } from "@minecraft/server";
import { ActionFormData } from "@minecraft/server-ui";
import { rollStructure, guassianRand, ORES_END, ORES_NETHER, ORES_OVERWORLD } from "./spawn_rules";
//import { ActionFormData } from "@minecraft/server-ui";
//=================== SETTINGS =====================
var runTerrainSystem = world.getDynamicProperty("runTerrainSystem") == undefined ? false : world.getDynamicProperty("runTerrainSystem"); //set this to false if you want to disable chunk scanning.
const DEBUG = false; //set this to true to get information about chunk processing
var showRegionData = true //adds an additional display to the screen (only visible if DEBUG is also true). Can be toggled with scriptevents wonder_ores:showRegionData and wonder_ores:hideRegionData
;
const PLAYERSAFERADIUS = 7; //The radius of chunks around a player that are marked to never load new structures (stops strucures popping up in their faces or structures appearing in explored chunks)
const MAXSTRUCTUREATTEMPTS = 0; //the maximum amount of times a region can attempt to place a structure before it is considered "complete"
const MAXSTRUCTURES = 0; //the maximum amount of successfully placed structures a region is allowed to have
const MINSTRUCTUREDISTANCE = 12; //MAX 15 the RADIUS of chunks around a point that needs to be structure free to allow it to have a structure
const STEEPESTVALIDSLOPE = 7; //how sharp a slope of terrain is allowed for a structure to place itself. Higher numbers mean more structures, but worse looking terrain blending.
const ALLOWEDOREBLOCKS = [
    "minecraft:stone",
    "minecraft:deepslate",
    "minecraft:end_stone",
    "minecraft:netherrack",
    "minecraft:diorite",
    "minecraft:andesite",
    "minecraft:granite"
]; //the blocks that ores can generate in
//==================================================
if (MINSTRUCTUREDISTANCE > 15) console.warn("MINSTRUCTUREDISTANCE cannot be higher than 15 in the Wonderland settings!");
//USING HOES, AXES AND SHOVELS
world.beforeEvents.itemUseOn.subscribe((event)=>{
    const { block, itemStack, source } = event;
    //if it's one of our hoes, axes or shovels
    if (itemStack.typeId.indexOf("wonder_ores:") > -1 && (itemStack.typeId.indexOf("_hoe") > -1 || itemStack.typeId.indexOf("_axe") > -1 || itemStack.typeId.indexOf("_shovel") > -1)) {
        var takesDamage = false;
        if (itemStack.typeId.indexOf("_hoe") > -1) {
            if (block != undefined && event.blockFace != "Down" && block.above().isAir) {
                var tillable = [
                    "minecraft:dirt",
                    "minecraft:coarse_dirt",
                    "minecraft:grass_block"
                ];
                if (tillable.includes(block.typeId)) {
                    takesDamage = true;
                }
            }
        }
        if (itemStack.typeId.indexOf("_shovel") > -1) {
            if (block != undefined && block.above().isAir) {
                var pathable = [
                    "minecraft:grass_block",
                    "minecraft:dirt",
                    "minecraft:coarse_dirt",
                    "minecraft:dirt_with_roots",
                    "minecraft:podzol",
                    "minecraft:mycelium"
                ];
                if (pathable.includes(block.typeId)) {
                    takesDamage = true;
                }
            }
        }
        if (itemStack.typeId.indexOf("_axe") > -1) {
            if (block != undefined) {
                var strippable = [
                    "minecraft:oak_log",
                    "minecraft:spruce_log",
                    "minecraft:birch_log",
                    "minecraft:dark_oak_log",
                    "minecraft:acacia_log",
                    "minecraft:cherry_log",
                    "minecraft:mangrove_log",
                    "minecraft:jungle_log",
                    "minecraft:oak_wood",
                    "minecraft:spruce_wood",
                    "minecraft:birch_wood",
                    "minecraft:dark_oak_wood",
                    "minecraft:acacia_wood",
                    "minecraft:cherry_wood",
                    "minecraft:mangrove_wood",
                    "minecraft:jungle_wood",
                    "minecraft:bamboo_block"
                ];
                var strippableCopper = [
                    "exposed_",
                    "oxidized_",
                    "weathered_",
                    "waxed_"
                ];
                var validAxeBlock = false;
                if (strippable.includes(block.typeId)) validAxeBlock = true;
                for (var copperType of strippableCopper){
                    if (block.typeId.indexOf(copperType) > -1) {
                        validAxeBlock = true;
                        break;
                    }
                }
                if (validAxeBlock) {
                    takesDamage = true;
                }
            }
        }
        if (takesDamage) {
            system.run(()=>{
                if (itemStack.typeId.indexOf("_shovel") > -1) {
                    world.playSound("step.grass", block.location);
                    if (block.typeId != "minecraft:grass_block") {
                        //for some reason custom shovels ONLY path grass blocks, not other blocks
                        block.setType("minecraft:grass_path");
                    }
                }
                if (itemStack.typeId.indexOf("_hoe") > -1) world.playSound("step.gravel", block.location); //because ofc it is
                if (itemStack.typeId.indexOf("_axe") > -1) {
                    if (block.typeId.indexOf("copper") > -1) {
                        if (block.typeId.indexOf("waxed") > -1) {
                            world.playSound("copper.wax.off", block.location);
                        } else {
                            world.playSound("scrape", block.location);
                        }
                    } else {
                        world.playSound("dig.wood", block.location);
                    }
                }
                if (itemStack.typeId.indexOf("bedrock") == -1) {
                    var durability = itemStack.getComponent(ItemComponentTypes.Durability);
                    if (durability.damage == durability.maxDurability) {
                        source.getComponent(EntityComponentTypes.Equippable).setEquipment(EquipmentSlot.Mainhand, undefined);
                        source.playSound("random.break", {
                            location: source.location
                        });
                    } else {
                        itemStack.getComponent(ItemComponentTypes.Durability).damage += 1;
                        source.getComponent(EntityComponentTypes.Equippable).setEquipment(EquipmentSlot.Mainhand, itemStack);
                    }
                }
            });
        }
    }
});
// BREAKING BLOCKS WITH TOOLS
world.afterEvents.playerBreakBlock.subscribe((event)=>{
    //fire pickaxe
    if (event.itemStackAfterBreak != undefined && event.itemStackBeforeBreak.typeId != undefined && event.itemStackBeforeBreak.typeId == "wonder_ores:flame_pickaxe") {
        var entities = world.getDimension(event.dimension.id).getEntitiesAtBlockLocation(event.block.location);
        for (var entity of entities){
            if (entity.typeId == "minecraft:item") {
                var itemStack = entity.getComponent(EntityComponentTypes.Item)["itemStack"];
                var itemName = itemStack.typeId.split(":")[1].replace("deepslate_", "").replace("_deepslate", "").replace("_nether", "").replace("nether_", "").replace("_end", "".replace("end_", ""));
                var itemNameSpace = itemStack.typeId.split(":")[0];
                if (itemName.indexOf("_ore") > -1 || itemName.indexOf("raw_") > -1) {
                    //it's an ore, see if there's an ingot to match it
                    if (itemName.indexOf("raw_") > -1) itemName = itemName.replace("raw_", "") + "_ingot";
                    try {
                        var ingot = new ItemStack(itemNameSpace + ":" + itemName.replace("ore", "ingot"), 1);
                        world.getDimension(event.dimension.id).spawnItem(ingot, {
                            x: event.block.location.x + 0.5,
                            y: event.block.location.y + 0.5,
                            z: event.block.location.z + 0.5
                        });
                        entity.remove();
                    } catch (e) {
                    //there isn't an ingot for this ore
                    }
                }
            }
        }
    }
    //get held item
    var itemStack = event.itemStackAfterBreak;
    if (itemStack != undefined && itemStack.typeId.indexOf("wonder_ores:") > -1 && (itemStack.typeId.indexOf("_hoe") > -1 || itemStack.typeId.indexOf("_axe") > -1 || itemStack.typeId.indexOf("_shovel") > -1 || itemStack.typeId.indexOf("_sword") > -1 || itemStack.typeId.indexOf("_pickaxe") > -1)) {
        var durability = itemStack.getComponent(ItemComponentTypes.Durability);
        if (durability != undefined) {
            if (durability.damage == durability.maxDurability) {
                event.player.getComponent(EntityComponentTypes.Equippable).setEquipment(EquipmentSlot.Mainhand, undefined);
                event.player.playSound("random.break", {
                    location: event.player.location
                });
            } else {
                itemStack.getComponent(ItemComponentTypes.Durability).damage += 1;
                event.player.getComponent(EntityComponentTypes.Equippable).setEquipment(EquipmentSlot.Mainhand, itemStack);
            }
        }
    }
});
world.beforeEvents.playerBreakBlock.subscribe((event)=>{
    //ruby and sapphire drops
    var item = "";
    if (event.block.typeId.indexOf("wonder_ores:sapphire_ore") == 0) item = "wonder_ores:sapphire_nugget";
    if (event.block.typeId.indexOf("wonder_ores:ruby_ore") == 0) item = "wonder_ores:ruby_nugget";
    if (item != "") {
        var xp = 1 + Math.round(Math.random() * 4);
        var count = 1 + Math.round(Math.random() * 3);
        if (event.itemStack != undefined && event.itemStack.getComponent("minecraft:enchantable") != undefined) {
            //add to count if fortune used
            var enchants = event.itemStack.getComponent("minecraft:enchantable").getEnchantments();
            for (var enchant of enchants){
                if (enchant.type.id == "fortune") {
                    count += enchant.level;
                }
                if (enchant.type.id == "silk_touch") {
                    count = 0;
                    xp = 0;
                    item = event.block.typeId;
                }
            }
        }
        system.run(()=>{
            if (count > 0) world.getDimension(event.dimension.id).spawnItem(new ItemStack(item, count), {
                x: event.block.location.x + 0.5,
                y: event.block.location.y + 0.5,
                z: event.block.location.z + 0.5
            });
            for(var i = 0; i < xp; i++){
                world.getDimension(event.dimension.id).spawnEntity("minecraft:xp_orb", {
                    x: event.block.location.x + 0.5,
                    y: event.block.location.y + 0.5,
                    z: event.block.location.z + 0.5
                });
            }
        });
    }
});
world.beforeEvents.worldInitialize.subscribe((initEvent)=>{
    initEvent.blockComponentRegistry.registerCustomComponent('wonder_ores:rainbow_glow', {
        onStepOn: (e)=>{
            var states = e.block.permutation.getAllStates();
            if (states["wonder_ores:glowing"] != undefined) states["wonder_ores:glowing"] = true;
            e.block.setPermutation(BlockPermutation.resolve(e.block.typeId, states));
        },
        onPlace: (e)=>{
            var states = e.block.permutation.getAllStates();
            if (states["wonder_ores:glowing"] != undefined) states["wonder_ores:glowing"] = true;
            e.block.setPermutation(BlockPermutation.resolve(e.block.typeId, states));
        }
    });
    initEvent.blockComponentRegistry.registerCustomComponent('wonder_ores:stop_rainbow_glow_random_tick', {
        onRandomTick: (e)=>{
            var states = e.block.permutation.getAllStates();
            if (states["wonder_ores:glowing"] != undefined) states["wonder_ores:glowing"] = false;
            e.block.setPermutation(BlockPermutation.resolve(e.block.typeId, states));
        },
        onTick: (e)=>{
            world.getDimension(e.dimension.id).spawnParticle("wonder_ores:rainbow_ore_sparkle", e.block.location);
        }
    });
    initEvent.blockComponentRegistry.registerCustomComponent('wonder_ores:rainbow_step', {
        onStepOn: (e)=>{
            var color = Math.abs(e.block.location.x + e.block.location.z) % 7;
            world.getDimension(e.dimension.id).spawnParticle("wonder_ores:rainbow_step_" + color, e.block.location);
        },
        onPlace: (e)=>{
            var color = Math.abs(e.block.location.x + e.block.location.z) % 7;
            world.getDimension(e.dimension.id).spawnParticle("wonder_ores:rainbow_step_" + color, e.block.location);
        }
    });
    initEvent.blockComponentRegistry.registerCustomComponent('wonder_ores:flame_ore_gyser', {
        onRandomTick: (e)=>{
            if (e.block.above().typeId == "minecraft:lava") world.getDimension(e.dimension.id).spawnParticle("wonder_ores:flame_ore_gyser", e.block.location);
        }
    });
});
/**============================================================================================
 * ================================== PLACE SPAWN STRUCTURE ===================================
 * this code block places a spawn structure at the very first player to join the world.
 * Dependancies: a spawn structure located at structures/wonder_ores/spawn.mcstructure
 */ //-------------------- SETTINGS -------------------
const STRUCTUREOFFSET = {
    x: -8,
    y: -1,
    z: -8
}; //how to offset the strucure from the player so that the player spawns in the right spot
const PLAYEROFFSETX = 0.5; //used to slightly teleport the player to center them on an even-width build if needed.
const PLAYEROFFSETZ = 0.5;
const PLAYERROTATE = {
    x: 0,
    y: 0
}; //turn the player if they're facing the wrong way
const LOWESTSPAWNHEIGHT = 64; //the lowest point that the player is allowed to spawn at. Stops spawns appearing below the water line
const STARTINGITEMS = [
    {
        quantity: 1,
        name: "wonder_ores:guide_book"
    }
];
//-------------------------------------------------
world.afterEvents.playerSpawn.subscribe((event)=>{
    var joins = world.getDynamicProperty("player_joins") == undefined ? 1 : Number(world.getDynamicProperty("player_joins")) + 1;
    world.setDynamicProperty("player_joins", joins);
    //only trigger if the player has joined for the first time and the structure isn't placed already
    if (event.initialSpawn && !event.player.hasTag("joined")) {
        if (joins == 1) event.player.setDynamicProperty("owner", true);
        /*if (world.getDynamicProperty("spawn_placed") == undefined) {
            //wait for the chunks to be loaded (structures can cross into unloaded chunks so we only check at the player and at lower coordinates)
            var waitToPlaceSpawn = system.runInterval(() => {
                var playerLocation = event.player.location;
                var dimension = world.getDimension(event.player.dimension.id);
                var block1 = dimension.getBlock(playerLocation);
                var block2 = dimension.getBlock({ x: playerLocation.x - 32, y: playerLocation.y, z: playerLocation.z - 32 });
                if (block1 != undefined && block2 != undefined) {
                    //the world is loaded enough to start placing the structure
                    var spawnHeight = Math.max(LOWESTSPAWNHEIGHT, dimension.getTopmostBlock(playerLocation).location.y);
                    dimension.runCommand(`structure load wonder_ores:spawn ${playerLocation.x + STRUCTUREOFFSET.x} ${spawnHeight + STRUCTUREOFFSET.y} ${playerLocation.z + STRUCTUREOFFSET.z}`);
                    event.player.setSpawnPoint({ x: playerLocation.x + PLAYEROFFSETX, y: spawnHeight + 3, z: playerLocation.z + PLAYEROFFSETZ, dimension: event.player.dimension });
                    world.setDefaultSpawnLocation({ x: playerLocation.x + PLAYEROFFSETX, y: spawnHeight + 3, z: playerLocation.z + PLAYEROFFSETZ });
                    event.player.teleport({ x: playerLocation.x + PLAYEROFFSETX, y: spawnHeight + 3, z: playerLocation.z + PLAYEROFFSETZ }, { rotation: PLAYERROTATE });
                    world.setDynamicProperty("spawn_placed", true);
                    //stop the clock
                    system.clearRun(waitToPlaceSpawn);
                }
            }, 10);
        }*/ try {
            for (var item of STARTINGITEMS){
                event.player.runCommand(`give @s ${item.name} ${item.quantity}`);
            }
        } catch (e) {
            console.error("tried to give the player an item that doesn't exist or has its quantity set too high");
        }
        event.player.addTag("joined");
    }
});
/**============================================================================================
 * ================================ END PLACE SPAWN STRUCTURE =================================
 */ function revealChunkBorder() {
    var players = world.getAllPlayers();
    for(var i = 0; i < players.length; i++){
        var x = Math.floor(players[i].location.x / 16) * 16;
        var y = players[i].location.y - 5;
        var z = Math.floor(players[i].location.z / 16) * 16;
        world.getDimension(players[i].dimension.id).spawnParticle("minecraft:blue_flame_particle", {
            x: x,
            y: 73,
            z: z
        });
        for(var dx = 0; dx < 16.1; dx += 0.5){
            for(var dz = 0; dz < 16.1; dz += 0.5){
                if (dx == 0 || dx > 15.9 || dz == 0 || dz > 15.9) {
                    try {
                        world.getDimension(players[i].dimension.id).spawnParticle("minecraft:basic_flame_particle", {
                            x: x + dx,
                            y: y,
                            z: z + dz
                        });
                    } catch (e) {}
                }
            }
        }
    }
}
function revealRegionBorder() {
    var players = world.getAllPlayers();
    for(var i = 0; i < players.length; i++){
        var x = Math.floor(players[i].location.x / 256) * 256;
        var y = players[i].location.y - 4;
        var z = Math.floor(players[i].location.z / 256) * 256;
        for(var dx = 0; dx < 257; dx += 2){
            for(var dz = 0; dz < 257; dz += 2){
                try {
                    if (dx == 0 || dx > 255.9 || dz == 0 || dz > 255.9) world.getDimension(players[i].dimension.id).spawnParticle("minecraft:campfire_smoke_particle", {
                        x: x + dx,
                        y: y,
                        z: z + dz
                    });
                } catch (e) {}
            }
        }
    }
}
world.afterEvents.worldInitialize.subscribe((event)=>{
    //manager.start();
    system.runInterval(()=>{
        //display ore warning (orening) to new players
        if (world.getDynamicProperty("orening_selection_made") == undefined || !world.getDynamicProperty("orening_selection_made")) {
            world.getAllPlayers().forEach((player)=>{
                if (!player.hasTag("wonder_ores_seen_warning") && player.getDynamicProperty("owner") != undefined) {
                    var worening = new ActionFormData();
                    worening.title({
                        "translate": "actionform.ore_warning_title"
                    }).body({
                        "translate": "actionform.ore_warning_body",
                        "with": [
                            "\n"
                        ]
                    }).button({
                        "translate": "actionform.ore_warning.button.accept",
                        "with": [
                            "\n"
                        ]
                    }, "textures/wonder/ores/ui/tick").button({
                        "translate": "actionform.ore_warning.button.decline",
                        "with": [
                            "\n"
                        ]
                    }, "textures/wonder/ores/ui/cross");
                    worening.show(player).then((response)=>{
                        if (response.selection != undefined) {
                            if (response.selection == 0) {
                                runTerrainSystem = true;
                                world.setDynamicProperty("runTerrainSystem", true);
                                player.addTag("wonder_ores_seen_warning");
                            }
                            if (response.selection == 1) {
                                runTerrainSystem = false;
                                world.setDynamicProperty("runTerrainSystem", false);
                                player.addTag("wonder_ores_seen_warning");
                            }
                            world.setDynamicProperty("orening_selection_made", true);
                        }
                    });
                }
            });
        }
        if (runTerrainSystem) {
            checkOnPlayers();
            var kickstartPlayer = world.getAllPlayers();
            if (Object.keys(regionData).length == 0) {
                if (kickstartPlayer.length > 0) scanSurroundings(kickstartPlayer[0], true);
            }
            processWatchers();
            if (DEBUG) {
                if (showRegionData) {
                    var players = world.getAllPlayers();
                    for (var player of players){
                        var playerChunkX = Math.floor(player.location.x / 16);
                        var playerChunkZ = Math.floor(player.location.z / 16);
                        var playerRegionX = Math.floor(playerChunkX / 16);
                        var playerRegionZ = Math.floor(playerChunkZ / 16);
                        var rx = playerRegionX - 1;
                        var rz = playerRegionZ + 1;
                        var displayData = "";
                        for(rz; rz < playerRegionZ - 2; rz--){
                            var row = "";
                            for(rx; rx < playerRegionX + 2; rx++){
                                if (regionData["r" + rx + "o" + rz] == undefined) {
                                    row += "";
                                }
                            }
                        }
                    }
                }
            }
            if (DEBUG) {
                revealChunkBorder();
                revealRegionBorder();
            }
        }
    }, 40);
    system.runInterval(()=>{
        //check for tickingareas that have stalled and lost their watcher
        var watchers = [];
        for (var area of manager.tickingAreaStates){
            if (area.busy && !area.pending) {
                if (watchers.length == 0) {
                    //get watchers
                    watchers = world.getDimension(area.dimension).getEntities({
                        type: "wonder_ores:structure_watcher"
                    });
                }
                var foundWatcher = false;
                for (var watcher of watchers){
                    if (watcher.hasTag("watcher_for_area_" + area.id)) {
                        foundWatcher = true;
                        break;
                    }
                }
                if (!foundWatcher) {
                    print("§6Found and removed stalled tickingarea " + area.id);
                    world.getDimension(area.dimension).runCommand("/tickingarea remove area" + area.id);
                    area.busy = false;
                }
            }
        }
    }, 600);
});
var surroundings = [];
var playerList = {};
//this variable is populated by the saved data of the 9 regions around each player
//it is slowly processed over time looking for chunks that could have a structure added
//chunks are eligible if:
//- the region hasn't exhausted its spawn tries
//- the structure is further than 6 chunks from a player
//- the structure is not too close to another chunk containing a structure
//- the biome and available space are valid
var regionData = {}; //{"rxoz":{chunks:{"x,z":0,..},structureAttempts:0, placedStructures:0},...}
class TickingAreaManager {
    loadArea(location, length, width, dimension) {
        //add the area to the queue
        this.tickingAreaQueue.unshift({
            location: location,
            length: length,
            width: width,
            dimension: dimension
        });
        //don't let the queue get too long or it'll throw an error
        if (this.tickingAreaQueue.length > 450) {
            this.tickingAreaQueue.pop();
        }
    }
    freeArea(location, succeeded) {
        location.x = Math.floor(location.x / 16) * 16;
        location.z = Math.floor(location.z / 16) * 16;
        var chunkX = Math.floor(location.x / 16);
        var chunkZ = Math.floor(location.z / 16);
        var regionX = Math.floor(chunkX / 16);
        var regionZ = Math.floor(chunkZ / 16);
        //mark the chunk as unscanned if it faild to place something
        if (!succeeded) {
            setChunkData({
                x: chunkX,
                y: 0,
                z: chunkZ
            }, 0); //unmark the save data and treat it as an unprocessed chunk
            setChunkData({
                x: chunkX,
                y: 0,
                z: chunkZ + 1
            }, 0);
            setChunkData({
                x: chunkX,
                y: 0,
                z: chunkZ + 2
            }, 0);
            setChunkData({
                x: chunkX + 1,
                y: 0,
                z: chunkZ
            }, 0);
            setChunkData({
                x: chunkX + 1,
                y: 0,
                z: chunkZ + 1
            }, 0);
            setChunkData({
                x: chunkX + 1,
                y: 0,
                z: chunkZ + 2
            }, 0);
            setChunkData({
                x: chunkX + 2,
                y: 0,
                z: chunkZ
            }, 0);
            setChunkData({
                x: chunkX + 2,
                y: 0,
                z: chunkZ + 1
            }, 0);
            setChunkData({
                x: chunkX + 2,
                y: 0,
                z: chunkZ + 2
            }, 0);
        } else {
            if (regionData["r" + regionX + "o" + regionZ] != undefined) {
                regionData["r" + regionX + "o" + regionZ].placedStructures++;
                regionData["r" + regionX + "o" + regionZ].structureLocations.push(location);
            } else {
                //the region was unloaded before the tickingarea finished processing things. Load up the saved region and update it retroactively
                var region = world.getDynamicProperty("r" + regionX + "o" + regionZ);
                if (region != undefined) {
                    region = JSON.parse(String(region));
                    region.placedStructures++;
                    region.structureLocations.push(location);
                    world.setDynamicProperty("r" + regionX + "o" + regionZ, JSON.stringify(region));
                }
            }
        }
        for (var area of this.tickingAreaStates){
            if (area.location != undefined && area.location.x == location.x && area.location.z == location.z) {
                area.busy = false;
                area.pending = false;
                area.location = undefined;
                world.getDimension(area.dimension).runCommand(`/tickingarea remove area${area.id}`);
                break;
            }
        }
        world.setDynamicProperty("r" + regionX + "o" + regionZ, JSON.stringify(regionData["r" + regionX + "o" + regionZ]));
    }
    start() {
        //set up to ensure "this" context is nnt lost by the runInterval
        this.networkClock = system.runInterval(()=>{
            this.processTickingAreas();
        }, 20);
    }
    stop() {
        system.clearRun(this.networkClock);
    }
    processTickingAreas() {
        if (DEBUG) {
            var busyAreas = 0;
            var pendingAreas = 0;
            for (var area of this.tickingAreaStates){
                if (area.pending) {
                    pendingAreas++;
                }
                if (area.busy) {
                    busyAreas++;
                }
            }
            var placed = 0;
            var tried = 0;
            for(var region in regionData){
                placed += regionData[region].placedStructures;
                tried += regionData[region].structureAttempts;
            }
            world.getDimension("overworld").runCommand("/title @a actionbar Tickingareas| Busy: " + busyAreas + ", Pending: " + pendingAreas + " Q: " + this.tickingAreaQueue.length + " \n§rRegions: " + Object.keys(regionData).length + " §aPlaced: " + placed + " §6 Tries: " + tried + "/" + MAXSTRUCTUREATTEMPTS * Object.keys(regionData).length);
        }
        //runs once a second.
        //create ticking areas from the queue if any are free
        if (this.tickingAreaQueue.length > 0) {
            for(var i = 0; i < this.tickingAreaQueue.length; i++){
                for (var area of this.tickingAreaStates){
                    if (!area.busy && this.tickingAreaQueue.length > 0 && area.dimension == this.tickingAreaQueue[i].dimension) {
                        var queued = this.tickingAreaQueue[i];
                        world.getDimension(area.dimension).runCommand(`/tickingarea remove area${area.id}`);
                        var result = world.getDimension(area.dimension).runCommand(`/tickingarea add ${queued.location.x} 0 ${queued.location.z} ${queued.location.x + queued.length} 1 ${queued.location.z + queued.width} area${area.id}`);
                        if (result.successCount > 0) {
                            area.busy = true;
                            area.pending = true;
                            area.location = {
                                x: queued.location.x,
                                z: queued.location.z
                            };
                            this.tickingAreaQueue.splice(i, 1);
                        }
                        break;
                    }
                }
            }
        }
        //check for pending tickingareas to see if they're loaded
        for (var area of this.tickingAreaStates){
            if (area.pending == true && area.location != undefined) {
                if (world.getDimension(area.dimension).getBlock({
                    x: area.location.x,
                    y: 0,
                    z: area.location.z
                }) != undefined) {
                    area.pending = false;
                    processNewChunk({
                        x: Math.floor(area.location.x / 16),
                        z: Math.floor(area.location.z / 16),
                        dimension: area.dimension
                    }, area.id);
                }
            }
        }
        //try to find a spot to explore for a new structure
        for (var region of Object.keys(regionData)){
            if (regionData[region].structureAttempts < MAXSTRUCTUREATTEMPTS && regionData[region].placedStructures < MAXSTRUCTURES) {
                regionData[region].structureAttempts++;
                var regionDimension = region.indexOf("r") > -1 ? "minecraft:end" : region.indexOf("n") > -1 ? "minecraft:nether" : "minecraft:overworld";
                var regionString = region.replace("r", "").replace("o", ",").replace("n", ",").replace("e", ",");
                var regionCoords = regionString.split(",");
                //if(DEBUG) world.sendMessage("looking for random chunk in " + JSON.stringify(regionCoords) + " (attempt " + regionData[region].structureAttempts + "/" + MAXSTRUCTUREATTEMPTS + ")");
                //pick a random chunk in the region
                var pickedChunkX = parseInt(regionCoords[0]) * 16 + Math.round(Math.random() * 13); //max offset is 13 since we always load a 3x3 space for structures
                var pickedChunkZ = parseInt(regionCoords[1]) * 16 + Math.round(Math.random() * 13);
                //check if the chunk is already processed
                //if(DEBUG) world.sendMessage("picked chunk " + pickedChunkX + "," + pickedChunkZ + " which is coord " + (pickedChunkX * 16) + "," + (pickedChunkZ * 16));
                var spaceIsFresh = true;
                for(var x = pickedChunkX; x < pickedChunkX + 3; x++){
                    for(var z = pickedChunkZ; z < pickedChunkZ + 3; z++){
                        var val = getChunkData({
                            x: pickedChunkX,
                            y: 0,
                            z: pickedChunkZ
                        }).value;
                        if (val != undefined && val > 1) {
                            spaceIsFresh = false;
                            print("§cStructure tried to overlap another structure. Failed.");
                            break;
                        }
                    }
                    if (!spaceIsFresh) break;
                }
                if (spaceIsFresh) {
                    //reserve the space from other checks while it tries to place a structure
                    setChunkData({
                        x: pickedChunkX,
                        y: 0,
                        z: pickedChunkZ
                    }, 1);
                    setChunkData({
                        x: pickedChunkX,
                        y: 0,
                        z: pickedChunkZ + 1
                    }, 1);
                    setChunkData({
                        x: pickedChunkX,
                        y: 0,
                        z: pickedChunkZ + 2
                    }, 1);
                    setChunkData({
                        x: pickedChunkX + 1,
                        y: 0,
                        z: pickedChunkZ
                    }, 1);
                    setChunkData({
                        x: pickedChunkX + 1,
                        y: 0,
                        z: pickedChunkZ + 1
                    }, 1);
                    setChunkData({
                        x: pickedChunkX + 1,
                        y: 0,
                        z: pickedChunkZ + 2
                    }, 1);
                    setChunkData({
                        x: pickedChunkX + 2,
                        y: 0,
                        z: pickedChunkZ
                    }, 1);
                    setChunkData({
                        x: pickedChunkX + 2,
                        y: 0,
                        z: pickedChunkZ + 1
                    }, 1);
                    setChunkData({
                        x: pickedChunkX + 2,
                        y: 0,
                        z: pickedChunkZ + 2
                    }, 1);
                    this.loadArea({
                        x: pickedChunkX * 16,
                        y: 0,
                        z: pickedChunkZ * 16
                    }, 63, 63, regionDimension);
                    world.setDynamicProperty("r" + regionCoords[0] + "o" + regionCoords[1], JSON.stringify(regionData[region]));
                }
            }
        }
    }
    constructor(){
        this.tickingAreaQueue = [] //a list of coordinates and sizes
        ;
        this.tickingAreaStates = [
            {
                id: 0,
                busy: false,
                pending: false,
                dimension: "minecraft:overworld"
            },
            {
                id: 1,
                busy: false,
                pending: false,
                dimension: "minecraft:overworld"
            },
            {
                id: 2,
                busy: false,
                pending: false,
                dimension: "minecraft:overworld"
            },
            {
                id: 3,
                busy: false,
                pending: false,
                dimension: "minecraft:overworld"
            },
            {
                id: 4,
                busy: false,
                pending: false,
                dimension: "minecraft:overworld"
            },
            {
                id: 5,
                busy: false,
                pending: false,
                dimension: "minecraft:overworld"
            },
            {
                id: 6,
                busy: false,
                pending: false,
                dimension: "minecraft:overworld"
            },
            {
                id: 7,
                busy: false,
                pending: false,
                dimension: "minecraft:overworld"
            },
            {
                id: 8,
                busy: false,
                pending: false,
                dimension: "minecraft:overworld"
            },
            {
                id: 9,
                busy: false,
                pending: false,
                dimension: "minecraft:overworld"
            },
            {
                id: 10,
                busy: false,
                pending: false,
                dimension: "minecraft:nether"
            },
            {
                id: 11,
                busy: false,
                pending: false,
                dimension: "minecraft:nether"
            },
            {
                id: 12,
                busy: false,
                pending: false,
                dimension: "minecraft:nether"
            },
            {
                id: 13,
                busy: false,
                pending: false,
                dimension: "minecraft:nether"
            },
            {
                id: 14,
                busy: false,
                pending: false,
                dimension: "minecraft:nether"
            },
            {
                id: 15,
                busy: false,
                pending: false,
                dimension: "minecraft:nether"
            },
            {
                id: 16,
                busy: false,
                pending: false,
                dimension: "minecraft:nether"
            },
            {
                id: 17,
                busy: false,
                pending: false,
                dimension: "minecraft:nether"
            },
            {
                id: 18,
                busy: false,
                pending: false,
                dimension: "minecraft:nether"
            },
            {
                id: 19,
                busy: false,
                pending: false,
                dimension: "minecraft:nether"
            },
            {
                id: 20,
                busy: false,
                pending: false,
                dimension: "minecraft:the_end"
            },
            {
                id: 21,
                busy: false,
                pending: false,
                dimension: "minecraft:the_end"
            },
            {
                id: 22,
                busy: false,
                pending: false,
                dimension: "minecraft:the_end"
            },
            {
                id: 23,
                busy: false,
                pending: false,
                dimension: "minecraft:the_end"
            },
            {
                id: 24,
                busy: false,
                pending: false,
                dimension: "minecraft:the_end"
            },
            {
                id: 25,
                busy: false,
                pending: false,
                dimension: "minecraft:the_end"
            },
            {
                id: 26,
                busy: false,
                pending: false,
                dimension: "minecraft:the_end"
            },
            {
                id: 27,
                busy: false,
                pending: false,
                dimension: "minecraft:the_end"
            },
            {
                id: 28,
                busy: false,
                pending: false,
                dimension: "minecraft:the_end"
            },
            {
                id: 29,
                busy: false,
                pending: false,
                dimension: "minecraft:the_end"
            }
        ], this.tickingAreaQueue = [];
        var self = this;
        this.init = function() {
            var states = world.getDynamicProperty("tickState");
            if (states != undefined) {
                self.tickingAreaStates = JSON.parse(states);
            }
            var queue = world.getDynamicProperty("tickQueue");
            if (queue != undefined) {
                self.tickingAreaQueue = JSON.parse(queue);
            }
        };
        this.init();
    }
}
var manager = new TickingAreaManager();
world.beforeEvents.playerLeave.subscribe((event)=>{
    //save the state of the manager
    world.setDynamicProperty("tickState", JSON.stringify(manager.tickingAreaStates));
    world.setDynamicProperty("tickQueue", JSON.stringify(manager.tickingAreaQueue));
});
var validStructureBlocks = [
    "minecraft:sand",
    "minecraft:sandstone",
    "minecraft:grass_block"
];
system.afterEvents.scriptEventReceive.subscribe((event)=>{
    switch(event.id){
        case "wonder_ores:reset_orening":
            world.setDynamicProperty("orening_selection_made", false);
            break;
        case "wonder_ores:orePopulateChunk":
            var curx = Math.floor(event.sourceEntity.location.x / 16);
            var curz = Math.floor(event.sourceEntity.location.z / 16);
            populateWithOres({
                x: curx,
                z: curz
            }, event.sourceEntity.dimension.id);
            break;
        case "wonder_ores:test":
            var players = world.getAllPlayers();
            for (var player of players){
                scanSurroundings(player, false);
            }
            break;
        case "wonder_ores:biomeTest":
            rollStructure([
                "ice_plains",
                "foo"
            ]);
            break;
        case "wonder_ores:regions":
            world.sendMessage(JSON.stringify(regionData));
            break;
        case "wonder_ores:regionsList":
            world.sendMessage(Object.keys(regionData).length + " regions loaded");
            for(var region in regionData){
                world.sendMessage(region + ": §a" + regionData[region].placedStructures + " §6" + regionData[region].structureAttempts);
            }
            break;
        case "wonder_ores:showRegionData":
            showRegionData = true;
            break;
        case "wonder_ores:hideRegionData":
            showRegionData = false;
            break;
        case "wonder_ores:purgeTickingAreas":
            for (var area of manager.tickingAreaStates){
                area.busy = false;
                area.pending = false;
                area.location = undefined;
            }
            break;
        case "wonder_ores:inspectCurrentChunk":
            var chunkX = Math.floor(event.sourceEntity.location.x / 16);
            var chunkZ = Math.floor(event.sourceEntity.location.z / 16);
            var regionX = Math.floor(chunkX / 16);
            var regionZ = Math.floor(chunkZ / 16);
            world.sendMessage("Chunk " + chunkX + "," + chunkZ + " in region " + regionX + "," + regionZ + " has save value of " + regionData["r" + regionX + "o" + regionZ].chunks[chunkX + "," + chunkZ]);
            break;
        case "wonder_ores:listStructures":
            var chunkX = Math.floor(event.sourceEntity.location.x / 16);
            var chunkZ = Math.floor(event.sourceEntity.location.z / 16);
            var regionX = Math.floor(chunkX / 16);
            var regionZ = Math.floor(chunkZ / 16);
            world.sendMessage("Structures in region " + regionX + "," + regionZ);
            regionData[regionX + "o" + regionZ].break;
    }
});
world.afterEvents.playerJoin.subscribe((event)=>{
    checkOnPlayers(true);
});
function checkOnPlayers(regionChanged) {
    var players = world.getAllPlayers();
    for (var player of players){
        var playerx = player.location.x;
        var playerz = player.location.z;
        var playerChunkX = Math.floor(playerx / 16);
        var playerChunkZ = Math.floor(playerz / 16);
        var playerRegionX = Math.floor(playerChunkX / 16);
        var playerRegionZ = Math.floor(playerChunkZ / 16);
        //run chunk scan if players have moved
        if (playerList[player.name] == undefined) {
            playerList[player.name] = {
                dimension: player.dimension,
                chunk: playerChunkX + "," + playerChunkZ,
                region: {
                    x: playerRegionX,
                    z: playerRegionZ
                }
            };
        }
        if (playerList[player.name].chunk != playerChunkX + "," + playerChunkZ) {
            if (regionChanged == undefined) regionChanged = false;
            if (playerList[player.name].region == undefined || playerList[player.name].region.x != playerRegionX || playerList[player.name].region.z != playerRegionZ) {
                regionChanged = true;
                print("Region changed. Updated player to: " + playerRegionX + "," + playerRegionZ);
            }
            playerList[player.name] = {
                dimension: player.dimension,
                chunk: playerChunkX + "," + playerChunkZ,
                region: {
                    x: playerRegionX,
                    z: playerRegionZ
                }
            };
            scanSurroundings(player, regionChanged);
        }
    }
}
function scanSurroundings(player, regionChanged) {
    var playerx = player.location.x;
    var playerz = player.location.z;
    var chunkx = Math.floor(playerx / 16);
    var chunkz = Math.floor(playerz / 16);
    var orePopulationJobQueue = [];
    if (DEBUG) var chunksChecked = 0;
    var dimension = player.dimension.id;
    if (regionChanged || Object.keys(regionData).length == 0) {
        regionData = {};
        var players = world.getDimension(player.dimension.id).getPlayers();
        for (var p of players){
            //load surrounding region data and create it if it's missing
            if (DEBUG) world.sendMessage("region: " + JSON.stringify(playerList[p.name].region));
            if (DEBUG) var regionDisplay = "";
            for(var rx = playerList[p.name].region.x - 1; rx < playerList[p.name].region.x + 2; rx++){
                for(var rz = playerList[p.name].region.z - 1; rz < playerList[p.name].region.z + 2; rz++){
                    var region = world.getDynamicProperty("r" + rx + "o" + rz);
                    regionDisplay += rx + "o" + rz + "|";
                    if (region == undefined) {
                        print("creating new region");
                        const emptyRegion = {
                            chunks: {},
                            structureAttempts: 0,
                            placedStructures: 0,
                            structureLocations: []
                        };
                        world.setDynamicProperty("r" + rx + "o" + rz, JSON.stringify(emptyRegion));
                        if (DEBUG) world.sendMessage("saved region as " + JSON.stringify(emptyRegion));
                        region = emptyRegion;
                        regionData["r" + rx + "o" + rz] = region;
                    } else {
                        region = JSON.parse(region);
                        regionData["r" + rx + "o" + rz] = region;
                    }
                }
                regionDisplay += "\n";
            }
        }
        if (DEBUG) world.sendMessage("§8" + regionDisplay);
        if (DEBUG) world.sendMessage("§8Reloaded regionData. " + Object.keys(regionData).length + " regions are loaded to RAM.");
    }
    if (DEBUG) {
        surroundings = [];
        for(var i = -PLAYERSAFERADIUS; i <= PLAYERSAFERADIUS; i++){
            var row = [];
            for(var j = -PLAYERSAFERADIUS; j <= PLAYERSAFERADIUS; j++){
                row.push("§8■ ");
            }
            surroundings.push(row);
        }
    }
    var x = PLAYERSAFERADIUS;
    var z = PLAYERSAFERADIUS;
    var angle = 4;
    var loadedChunksInRotation = 0;
    var regionsWithNewChunks = [];
    var cachedChunksFound = 0;
    var foundLoadedChunkInRotation = true;
    var currentChunkX = chunkx + (x - PLAYERSAFERADIUS);
    while(foundLoadedChunkInRotation && currentChunkX < chunkx + PLAYERSAFERADIUS){
        //mark chunks close to the player as processed so structures don't appear in their face
        currentChunkX = chunkx + (x - PLAYERSAFERADIUS);
        var currentChunkZ = chunkz + (z - PLAYERSAFERADIUS);
        var currentRegionX = Math.floor(currentChunkX / 16);
        var currentRegionZ = Math.floor(currentChunkZ / 16);
        if (DEBUG) chunksChecked++;
        if (regionData["r" + currentRegionX + "o" + currentRegionZ] == undefined) {
            print("tried to check out of bounds chunk. Stopping scan.");
            break;
        }
        if (regionData["r" + currentRegionX + "o" + currentRegionZ].chunks[currentChunkX + "," + currentChunkZ] == undefined) {
            //if the chunk hasn't been scanned before, try to scan it
            var chunkblock = world.getDimension(dimension).getBlock({
                x: (chunkx + (x - PLAYERSAFERADIUS)) * 16,
                y: 0,
                z: (chunkz + (z - PLAYERSAFERADIUS)) * 16
            });
            if (DEBUG && surroundings[z] != undefined && surroundings[z][x] != undefined) surroundings[z][x] = chunkblock == undefined ? "§c■ " : "§6■ ";
            if (chunkblock != undefined) {
                loadedChunksInRotation++;
                if (DEBUG) surroundings[z][x] = "§a■ ";
                orePopulationJobQueue.push({
                    x: chunkx + (x - PLAYERSAFERADIUS),
                    z: chunkz + (z - PLAYERSAFERADIUS)
                });
                regionData["r" + currentRegionX + "o" + currentRegionZ].chunks[currentChunkX + "," + currentChunkZ] = 1;
                if (!regionsWithNewChunks.includes("r" + currentRegionX + "o" + currentRegionZ)) regionsWithNewChunks.push("r" + currentRegionX + "o" + currentRegionZ);
                world.setDynamicProperty("r" + currentRegionX + "o" + currentRegionZ, JSON.stringify(regionData["r" + currentRegionX + "o" + currentRegionZ]));
            }
        } else {
            if (DEBUG && surroundings[z] != undefined && surroundings[z][x] != undefined) surroundings[z][x] = "§3■ ";
            cachedChunksFound++;
            loadedChunksInRotation++;
        }
        if (x == PLAYERSAFERADIUS) console.log("x==PLAYERSAFERADIUS in region " + currentRegionX + "," + currentRegionZ);
        if (z == PLAYERSAFERADIUS) console.log("z==PLAYERSAFERADIUS in region " + currentRegionX + "," + currentRegionZ);
        if (angle == 4) {
            if (loadedChunksInRotation == 0) foundLoadedChunkInRotation = false;
            loadedChunksInRotation = 0;
            z--;
            angle = 0;
        }
        if (angle == 3) {
            x++;
            z--;
            if (x == PLAYERSAFERADIUS) angle = 4;
        }
        if (angle == 2) {
            x--;
            z--;
            if (z == PLAYERSAFERADIUS) angle = 3;
        }
        if (angle == 1) {
            x--;
            z++;
            if (x == PLAYERSAFERADIUS) angle = 2;
        }
        if (angle == 0) {
            x++;
            z++;
            if (z >= PLAYERSAFERADIUS) angle = 1;
        }
    }
    //save regions that had new chunks
    for (var r of regionsWithNewChunks){
        world.setDynamicProperty(r, JSON.stringify(regionData[r]));
    }
    if (DEBUG) {
        print("Chunks checked: " + (chunksChecked - cachedChunksFound) + ", ");
        var updateMap = '/titleraw @a actionbar {"rawtext":[{"text":"';
        for (var row of surroundings){
            for (var cell of row){
                updateMap += cell;
            }
            updateMap += '"},{"text":"\n"},{"text":"';
        }
        updateMap += '"},{"text":"\n"},{"text":"\n"},{"text":"\n"},{"text":"\n"},{"text":"\n"},{"text":"\n"},{"text":"\n"},{"text":"\n"},{"text":"\n"},{"text":"\n"}]}';
        world.getDimension(player.dimension.id).runCommand(updateMap);
    }
    //queue chunks for ore population
    system.runJob(chunkOreQueue(orePopulationJobQueue, player.dimension.id));
}
function* chunkOreQueue(chunks, dimension) {
    for (var chunk of chunks){
        populateWithOres(chunk, dimension);
        yield;
    }
}
/**
 * Place resource veins into the chunk based on the data in spawn_rules
 * @param {VectorXZ} chunk The chunk to place the ores in
 * @param {string} dimension The dimension the chunk is in
 */ function populateWithOres(chunk, dimension) {
    //world.sendMessage("populating chunk " + JSON.stringify(chunk));
    var ores = ORES_OVERWORLD;
    if (dimension == "minecraft:nether") ores = ORES_NETHER;
    if (dimension == "minecraft:the_end") ores = ORES_END;
    var blockQueue = [];
    for (var ore of ores){
        for(var i = 0; i < ore.tries; i++){
            //see if the weight causes the try to fail
            if (ore.weight < 0 || ore.weight > 1) console.error("[Wonderland] spawn weight for " + ore.block + " should be within range 0-1, was set to " + ore.weight);
            if (Math.random() <= ore.weight) {
                //choose a height to try place the resource patch
                var height = ore.min_height + guassianRand(ore.height_offset, ore.distribution_falloff) * (ore.max_height - ore.min_height);
                //world.sendMessage("Tried to place ore " + ore.block + " at y " + height);
                var clusterSize = Math.round(ore.min_size + Math.random() * (ore.max_size - ore.min_size));
                var clusterRadius = Math.cbrt(0.75 * (clusterSize / Math.PI));
                var centerx = chunk.x * 16 + Math.ceil(clusterRadius) + Math.random() * (16 - 2 * clusterRadius);
                var centerz = chunk.z * 16 + Math.ceil(clusterRadius) + Math.random() * (16 - 2 * clusterRadius);
                var block = world.getDimension(dimension).getBlock({
                    x: centerx,
                    y: height,
                    z: centerz
                });
                if (block != undefined) {
                    if (ALLOWEDOREBLOCKS.includes(block["type"].id) && ore.block != "wonder_ores:flame_ore" && ore.block != "wonder_ores:volcanic_rock" && ore.block != "wonder_ores:flame_ore_nether" && ore.block != "wonder_ores:volcanic_rock_nether" || block["type"].id == "minecraft:lava" && (ore.block == "wonder_ores:flame_ore" || ore.block == "wonder_ores:flame_ore_nether" || ore.block == "wonder_ores:volcanic_rock" || ore.block == "wonder_ores:volcanic_rock_nether")) {
                        //choose how many blocks to place in the cluster
                        //world.sendMessage("Sphere with radius " + clusterRadius + ", offset of " + centerx + "," + centerz);
                        if (block["type"].id == "minecraft:lava" && (ore.block == "wonder_ores:flame_ore" || ore.block == "wonder_ores:flame_ore_nether" || ore.block == "wonder_ores:volcanic_rock" || ore.block == "wonder_ores:volcanic_rock_nether")) {
                            if (DEBUG) world.sendMessage("Spawning lava requiring block " + ore.block + " at " + JSON.stringify(block.location));
                        }
                        var deepslate = "";
                        if (dimension == "minecraft:overworld" && height < 5 && ore.deepslate_variant) deepslate = "_deepslate";
                        var placedBlocks = 0;
                        for(var x = 0 - Math.ceil(clusterRadius); x < clusterRadius; x++){
                            for(var y = 0 - Math.ceil(clusterRadius); y < clusterRadius; y++){
                                for(var z = 0 - Math.ceil(clusterRadius); z < clusterRadius; z++){
                                    if (placedBlocks < clusterSize && getDistance(block.location, {
                                        x: centerx + x,
                                        y: height + y,
                                        z: centerz + z
                                    }) <= clusterRadius) {
                                        blockQueue.push({
                                            location: {
                                                x: centerx + x,
                                                y: height + y,
                                                z: centerz + z
                                            },
                                            block: ore.block + deepslate,
                                            dimension: dimension
                                        });
                                        placedBlocks++;
                                    }
                                }
                            }
                        }
                    //world.sendMessage("§cPlaced " + placedBlocks + " of planned " + clusterSize);
                    } else {
                    //spawned in illegal block
                    }
                }
            }
        }
    }
    //world.sendMessage("Added " + blockQueue.length + " blocks to the queue");
    if (blockQueue.length > 0) system.runJob(placeBlocksGenerator(blockQueue, dimension));
}
function* placeBlocksGenerator(blocks, dimension) {
    for (var block of blocks){
        var point = world.getDimension(dimension).getBlock(block.location);
        if (point != undefined && ALLOWEDOREBLOCKS.includes(point["type"].id)) {
            point.setType(block.block);
        }
        yield;
    }
}
function processNewChunk(chunk, tickingAreaId) {
    print("trying for a structure at " + chunk.x * 16 + "," + chunk.z * 16);
    var block = getTopBlock({
        x: chunk.x * 16,
        z: chunk.z * 16
    }, chunk.dimension);
    //if (block != undefined) print("attempt landed on " + block["type"].id + " at y " + block.location.y);
    if (validStructureBlocks.includes(block["type"].id)) {
        //summon a watcher and wait for it to report back its biome tags.
        var result = world.getDimension(chunk.dimension).spawnEntity("wonder_ores:structure_watcher", {
            x: block.location.x,
            y: block.location.y,
            z: block.location.z
        });
        if (result == undefined) {
            print("§cFAILED TO SPAWN WATCHER");
            manager.freeArea({
                x: block.location.x,
                y: 0,
                z: block.location.z
            }, false);
        } else {
            result.addTag("watcher_for_area_" + tickingAreaId);
        }
    } else {
        manager.freeArea({
            x: chunk.x * 16,
            y: 0,
            z: chunk.z * 16
        }, false);
    }
}
function processWatchers() {
    var watchers = world.getDimension("minecraft:overworld").getEntities({
        type: "wonder_ores:structure_watcher"
    }).concat(world.getDimension("minecraft:nether").getEntities({
        type: "wonder_ores:structure_watcher"
    }), world.getDimension("minecraft:the_end").getEntities({
        type: "wonder_ores:structure_watcher"
    }));
    for (var watcher of watchers){
        var cachedLocation = watcher.location;
        if (watcher.hasTag("wonderland_all_biometags_added")) {
            var fp = watcher.getDynamicProperty("footprint");
            var footprint = [
                0,
                0
            ];
            if (fp != undefined) footprint = JSON.parse(fp.toString());
            if (footprint[0] == 0) {
                //get biome tags
                var biomeTags = [];
                var watcherTags = watcher.getTags();
                for (var tag of watcherTags){
                    if (tag.indexOf("biometag_") > -1) biomeTags.push(tag);
                }
                //roll for a structure
                var result = rollStructure(biomeTags);
                if (result["footprint"][0] == 0) {
                    print("§7Watcher wasn't in a biome that can spawn wonderland structures. Despawned.");
                    manager.freeArea({
                        x: cachedLocation.x,
                        y: 0,
                        z: cachedLocation.z
                    }, false);
                    watcher.remove();
                    watcher = undefined;
                    continue;
                } else {
                    watcher.setDynamicProperty("structure_type", result["path"]);
                    watcher.setDynamicProperty("y_offset", result["yOffset"]);
                    footprint = result["footprint"];
                    watcher.setDynamicProperty("footprint", JSON.stringify(footprint));
                }
            }
            //footprint and other structure information has loaded
            //check that the location isn't close to any existing structures
            var foundNearbyStructure = false;
            var watcherChunkX = Math.floor(watcher.location.x / 16);
            var watcherChunkZ = Math.floor(watcher.location.z / 16);
            var regionsInRange = [];
            var checks = [
                {
                    x: watcherChunkX - 1,
                    y: 0,
                    z: watcherChunkZ - 1
                },
                {
                    x: watcherChunkX - 1,
                    y: 0,
                    z: watcherChunkZ + 1
                },
                {
                    x: watcherChunkX + 1,
                    y: 0,
                    z: watcherChunkZ - 1
                },
                {
                    x: watcherChunkX + 1,
                    y: 0,
                    z: watcherChunkZ + 1
                }
            ];
            for (var check of checks){
                var reg = getChunkData(check).region;
                if (!regionsInRange.includes(reg)) regionsInRange.push(reg);
            }
            print("§6checking for nearby structures. " + regionsInRange.length + " regions in range");
            var debugClosestDistance = 1000;
            for (var region of regionsInRange){
                var r = regionData["r" + region.x + "o" + region.z];
                if (r == undefined) {
                    r = world.getDynamicProperty("r" + region.x + "o" + region.z);
                }
                if (r != undefined && r.structureLocations != undefined) {
                    for (var structure of r.structureLocations){
                        print("comparing distance of " + watcherChunkX + ", " + watcherChunkZ + " and " + structure.x / 16 + "," + structure.z / 16);
                        debugClosestDistance = Math.min(getDistance({
                            x: watcherChunkX,
                            y: 0,
                            z: watcherChunkZ
                        }, {
                            x: structure.x / 16,
                            y: 0,
                            z: structure.z / 16
                        }), debugClosestDistance);
                        if (getDistance({
                            x: watcherChunkX,
                            y: 0,
                            z: watcherChunkZ
                        }, {
                            x: structure.x / 16,
                            y: 0,
                            z: structure.z / 16
                        }) <= MINSTRUCTUREDISTANCE) {
                            foundNearbyStructure = true;
                            break;
                        }
                    }
                }
                if (foundNearbyStructure) break;
            }
            print("Closest structure was " + debugClosestDistance + " chunks away");
            if (foundNearbyStructure) {
                print("§cStructure would be too close to another, cancelled placement.");
                manager.freeArea({
                    x: cachedLocation.x,
                    y: 0,
                    z: cachedLocation.z
                }, false);
                watcher.remove();
                continue;
            }
            var foundUnloaded = false;
            var cornerBlocks = [];
            var location = watcher.location;
            for(var x = 0; x < 2; x++){
                for(var z = 0; z < 2; z++){
                    var block = world.getDimension(watcher.dimension.id).getBlock({
                        x: location.x + x * footprint[0],
                        y: 0,
                        z: location.z + z * footprint[1]
                    });
                    if (block == undefined) {
                        foundUnloaded = true;
                        break;
                    }
                    cornerBlocks.push(block);
                }
                if (foundUnloaded) break;
            }
            //also get the middle block
            var block = world.getDimension(watcher.dimension.id).getBlock({
                x: location.x + footprint[0] / 2,
                y: 0,
                z: location.z + footprint[1] / 2
            });
            if (block == undefined) {
                foundUnloaded = true;
            }
            cornerBlocks.push(block);
            if (cornerBlocks.length == 5) {
                //print("watcher for structure " + watcher.getDynamicProperty("structure_type") + " is at " + JSON.stringify(watcher.location) + " with a footprint of " + footprint[0] + "," + footprint[1]);
                var yOffset = parseInt(watcher.getDynamicProperty("y_offset").toString());
                var averageCornerHeight = 0;
                var lowestPoint = 256;
                var highestPoint = -64;
                for(var corner in cornerBlocks){
                    cornerBlocks[corner] = getTopBlock({
                        x: cornerBlocks[corner].location.x,
                        z: cornerBlocks[corner].location.z
                    }, cornerBlocks[corner].dimension.id);
                    if (cornerBlocks[corner] != undefined) {
                        if (!validStructureBlocks.includes(cornerBlocks[corner]["type"].id)) {
                            print("§cCorner of structure would be on invalid block " + cornerBlocks[corner]["type"].id + ". Cancelled placement");
                            manager.freeArea({
                                x: cachedLocation.x,
                                y: 0,
                                z: cachedLocation.z
                            }, false);
                            watcher.remove();
                            watcher = undefined;
                            break;
                        }
                        averageCornerHeight += cornerBlocks[corner].location.y;
                        lowestPoint = Math.min(lowestPoint, cornerBlocks[corner].location.y);
                        highestPoint = Math.max(highestPoint, cornerBlocks[corner].location.y);
                    } else {
                        print("§ccouldn't get all 5 points for structure. Cancelling.");
                        manager.freeArea({
                            x: cachedLocation.x,
                            y: 0,
                            z: cachedLocation.z
                        }, false);
                        watcher.remove();
                        break;
                    }
                    if (highestPoint - lowestPoint > STEEPESTVALIDSLOPE) {
                        print("§6slope of " + (highestPoint - lowestPoint) + " was too steep for structure " + watcher.getDynamicProperty("structure_type") + ", cancelled structure placement");
                        manager.freeArea({
                            x: cachedLocation.x,
                            y: 0,
                            z: cachedLocation.z
                        }, false);
                        watcher.remove();
                        watcher = undefined;
                        break;
                    }
                }
                if (watcher == undefined) continue;
                averageCornerHeight = averageCornerHeight / 5;
                //work out which face is lowest, and rotate the structure so that entrances are at the lowest face
                //this has been removed due to a trapdoor issue with structure rotation, though the code would be nice for future wonderlands if the 4yo bug is fixed :'D ~Sparks
                //var northHeight: number = (cornerBlocks[0].location.y + cornerBlocks[3].location.y) / 2;
                //var southHeight: number = (cornerBlocks[2].location.y + cornerBlocks[4].location.y) / 2;
                //var eastHeight: number = (cornerBlocks[3].location.y + cornerBlocks[4].location.y) / 2;
                //var westHeight: number = (cornerBlocks[1].location.y + cornerBlocks[2].location.y) / 2;
                //var lowestFace: number = Math.min(northHeight, southHeight, eastHeight, westHeight);
                var rotation = 0;
                //if (eastHeight == lowestFace) rotation = 90;
                //if (southHeight == lowestFace) rotation = 180;
                //if (westHeight == lowestFace) rotation = 270;
                //print("§7Lowest face of terrain was a rotation of " + rotation + ". North:" + northHeight + ", East:" + eastHeight + ", South:" + southHeight + ", West:" + westHeight);
                print("§aPlaced structure " + watcher.getDynamicProperty("structure_type") + " at " + (averageCornerHeight + yOffset + 1));
                print("§7Average height of terrain is " + averageCornerHeight + " Placed on " + cornerBlocks[0]["type"].id.replace("minecraft:", "") + " at " + JSON.stringify(cornerBlocks[0].location) + "," + cornerBlocks[1]["type"].id.replace("minecraft:", "") + " at " + JSON.stringify(cornerBlocks[1].location) + "," + cornerBlocks[2]["type"].id.replace("minecraft:", "") + " at " + JSON.stringify(cornerBlocks[2].location) + "," + cornerBlocks[3]["type"].id.replace("minecraft:", "") + " at " + JSON.stringify(cornerBlocks[3].location) + ". Min height was " + lowestPoint + " and max height was " + highestPoint + " for a slope of " + (highestPoint - lowestPoint));
                watcher.runCommand("/structure load " + watcher.getDynamicProperty("structure_type") + " ~ " + (averageCornerHeight + yOffset + 1) + " ~ " + rotation + "_degrees");
                manager.freeArea({
                    x: cachedLocation.x,
                    y: 0,
                    z: cachedLocation.z
                }, true);
                watcher.remove();
                continue;
            } else {
                print("§ccouldn't get all 5 points for structure. Cancelling.");
                manager.freeArea({
                    x: cachedLocation.x,
                    y: 0,
                    z: cachedLocation.z
                }, false);
                watcher.remove();
                continue;
            }
        }
    }
}
function getTopBlock(location, dimensionId) {
    var ignoreBlocks = [
        "minecraft:snow_layer",
        "minecraft:short_grass",
        "minecraft:tall_grass",
        "minecraft:orange_tulip",
        "minecraft:pink_tulip",
        "minecraft:red_tulip",
        "minecraft:white_tulip",
        "minecraft:oxeye_daisy",
        "minecraft:allium",
        "minecraft:azure_bluet",
        "minecraft:dandelion",
        "minecraft:poppy",
        "minecraft:blue_orchid",
        "minecraft:cornflower",
        "minecraft:lily_of_the_valley",
        "minecraft:lilac",
        "minecraft:rose_bush",
        "minecraft:peony",
        "minecraft:pink_petals",
        "minecraft:vine",
        "minecraft:oak_leaves",
        "minecraft:birch_leaves",
        "minecraft:deadbush",
        "minecraft:cactus"
    ];
    for(var y = 256; y > -64; y--){
        //if(DEBUG) print("checking down: " + world.getDimension(chunk.dimension).getBlock({ x: chunk.x * 16, y: y, z: chunk.z * 16 }).type.id);
        var block = world.getDimension(dimensionId).getBlock({
            x: location["x"],
            y: y,
            z: location["z"]
        });
        if (block == undefined) {
            print("requested block was undefined: " + JSON.stringify(location));
            return undefined;
        }
        var blockId = block["type"].id;
        if (!block.isAir && !ignoreBlocks.includes(blockId)) {
            y++;
            return block;
        }
    }
}
/**
 * send a world message only if debug is on
 * @param {String} message The message to print
 */ function print(debugMessage) {
    if (DEBUG) world.sendMessage("" + debugMessage);
}
/**
 * Return the region and the int value of a chunk or undefined if the chunk's never been written to
 * @param {Vector3} location the coordinate of the chunk
 * @return {Object}
 */ function getChunkData(location) {
    //get the region of the chunk;
    var regionX = Math.floor(location.x / 16);
    var regionZ = Math.floor(location.z / 16);
    var region = {};
    var chunkValue;
    if (regionData["r" + regionX + "o" + regionZ] != undefined) {
        chunkValue = regionData["r" + regionX + "o" + regionZ].chunks[location.x + "," + location.z];
    } else {
        var saveData = world.getDynamicProperty("r" + regionX + "o" + regionZ);
        if (saveData == undefined) {
            chunkValue = undefined;
        } else {
            saveData = JSON.parse(saveData);
            if (saveData.chunks[location.x + "," + location.z] == undefined) {
                chunkValue = undefined;
            } else {
                chunkValue = saveData.chunks[location.x + "," + location.z];
            }
        }
    }
    return {
        region: {
            x: regionX,
            y: 0,
            z: regionZ
        },
        value: chunkValue
    };
}
/**
 * Write a value to a chunk. Creates the chunk and region reference if it doesn't exist, but doesn't load it into regionData
 * @param {Vector3} location The coordinate of the chunk
 * @param {number} value The value to set the chunk to
 * @return {void}
 */ function setChunkData(location, value) {
    //get the region of the chunk;
    var regionX = Math.floor(location.x / 16);
    var regionZ = Math.floor(location.z / 16);
    if (regionData["r" + regionX + "o" + regionZ] != undefined) {
        regionData["r" + regionX + "o" + regionZ].chunks[location.x + "," + location.z] = value;
    } else {
        var saveData = world.getDynamicProperty("r" + regionX + "o" + regionZ);
        if (saveData == undefined) {
            //create a new region and save it
            var chunk = {};
            chunk[location.x + "," + location.z] = value;
            world.setDynamicProperty("r" + regionX + "o" + regionZ, JSON.stringify({
                chunks: chunk,
                structureAttempts: 0,
                placedStructures: 0,
                structureLocations: []
            }));
        } else {
            saveData = JSON.parse(saveData);
            saveData.chunks[location.x + "," + location.z] = value;
            world.setDynamicProperty("r" + regionX + "o" + regionZ, JSON.stringify(saveData));
        }
    }
}
function getDistance(a = {
    x: 0,
    y: 0,
    z: 0
}, b = {
    x: 0,
    y: 0,
    z: 0
}) {
    var dx = a.x - b.x;
    var dy = a.y - b.y;
    var dz = a.z - b.z;
    return Math.round(Math.sqrt(dx * dx + dy * dy + dz * dz));
}
