Treecapitator Help

Discussion in 'CommandHelper' started by Chloe Sanders, Dec 1, 2017.

  1. Chloe Sanders

    Chloe Sanders New Member

    This one is going to be a little more complicated >.< I think I know how I'm going to do this the main problem is I dont know how to search for blocks near a location.

    How do I search for blocks of a specific type in a 2 block radius?
  2. PseudoKnight

    PseudoKnight Well-Known Member Developer

    Ya, recursion can be tricky. (And if you do it wrong you'll end up freezing your server.) It's hard to explain any parts of this without doing the whole thing. There's multiple ways of doing this, but I'll give you one. (i haven't actually done this before, so I might have not thought of something important) Essentially you'll want to save a starting location, put it in a new array of locations. Then create a loop using the array of locations. Check adjacent blocks to the side and upwards (not downwards!) using get_block_at(x, y, z, world) format by increasing and/or decreasing one those values by 1 until you've checked all appropriate sides. Before you check the block type you have to make sure that the location you're checking is not already in your array of locations. If one of those blocks matches, then you break_block() it and push that location to your array. You might also want a distance check or an array size check to make sure you're not going too far (multiple trees intersecting each other). Also, you might want to save the first log type and leaf type you encounter so that you only remove those blocks in the future (multiple tree types intersecting). You could also put the block breaks in an execution queue so that it animates upwards.
  3. Chloe Sanders

    Chloe Sanders New Member

    I see how I can do this with what you have told me but it feels really messy like how I would need to manually code the log radius for each tree seperatly due to certain types of trees need special treatment. For example an acacia tree would need to be checked for logs in a radius of 2 since it can have diagonal logs that wont be picked up by blocks north, east, south, west and up. Do you think it would be possible to have some code that would automatically check all blocks in a radius based off a command without having to manually do it? For example I made this command:
    PHP:
    register_command('radius', array(
      'description': 'Radius Check',
      'usage': '/radius <block radius>',
      'permission': 'radius.test',
      'noPermMsg': colorize('&4You do not have permission to use this command'),
      'executor': closure(@alias, @sender, @args) {
        if(array_index_exists(@args, 0)) {
          if(@args[0] >= 10) {
            msg (colorize('&cA radius of&f ' . @args[0] . ' &cis too large!'))
          } else {
            msg (colorize('&6All blocks in a radius of ' . @args[0] . ' at x:&a ' . pcursor()[0] . ' &6y: &a' . pcursor()[1] . ' &6z: &a' . pcursor()[2] . '&6:'))
            //code to search all blocks in a <block radius>
          }
        } else {
          msg (colorize('&cCommand Usage: &f/radius <block radius>'))
        }
      }
      )
    )
    If I could get the code checking for blocks accuratly doing /radius 1, /radius 2, /radius 3 etc... it would make adding a treecapitator much easier
  4. PseudoKnight

    PseudoKnight Well-Known Member Developer

    If the first log is acacia, use 4 more checks for diagonal-up blocks.
  5. Chloe Sanders

    Chloe Sanders New Member

    So there is no easier way of doing something like this?
    PHP:
    proc(_treecheck, @location, @block) {
      @location['x'] += -1
      if (get_block_at(@location) == @block['type'] . ":" . @block['data']) {
        break_block(@location)
        _treecheck(@location,@block)
      }
      @location['x'] += +2
      if (get_block_at(@location) == @block['type'] . ":" . @block['data']) {
        break_block(@location)
        _treecheck(@location,@block)
      }
      @location['x'] += -1
      @location['z'] += -1
      if (get_block_at(@location) == @block['type'] . ":" . @block['data']) {
        break_block(@location)
        _treecheck(@location,@block)
      }
      @location['z'] += +2
      if (get_block_at(@location) == @block['type'] . ":" . @block['data']) {
        break_block(@location)
        _treecheck(@location,@block)
      }
      @location['x'] += -1
      if (get_block_at(@location) == @block['type'] . ":" . @block['data']) {
        break_block(@location)
        _treecheck(@location,@block)
      }
    }
    bind(block_break, null, null, @tree_drop) {
      array @trees = array('162:1': null);
      if(array_index_exists(@trees, @tree_drop['block']['type'] . ":" . @tree_drop['block']['data'])) {
        _treecheck(@tree_drop['location'],@tree_drop['block'])
      }
    }
    its hurting my head just thinking about it :confused: this currently only does north, east, south, west and 1 diaganal direction not done it for blocks upwards and such yet as its slowly making me go insane :p
  6. PseudoKnight

    PseudoKnight Well-Known Member Developer

    Ya, I don't blame you. This is not an easy thing to code. You're currently not checking if you've already checked a location, so this would loop forever, freezing your server. And yes, there are ways of making this simpler (easier? easy can sometimes just be brute forcing, which is not always good). There's a lot of redundancy there you can wrap in a proc. Also you don't have to modify the location reference, but if you do, it will be helpful to create a copy of it for the array of locations. (eg. @location = @location[])
  7. Chloe Sanders

    Chloe Sanders New Member

    Actually I have as it will only continue to loop if the original block is found "if (get_block_at(@location) == @block['type'] . ":" . @block['data']) {"

    I actually made a treecapitator ages ago using a plugin called "Skript" but I wasn't a huge fan of the plugin and you always had to download loads of addon plugins for basic features. It was much easier for me in skript however as there is a feature where you loop all blocks at a certain location in a given radius - just to showcase that here was the code
    PHP:
    script load:
      set {csk_trees::oak_tree::trunk} to "17:0|17:4|17:8"
      set {csk_trees::oak_tree::leaves} to "18:4|18:8"
      set {csk_trees::spruce_tree::trunk} to "17:1|17:5|17:9"
      set {csk_trees::spruce_tree::leaves} to "18:1|18:9"
      set {csk_trees::birch_tree::trunk} to "17:2|17:6|17:10"
      set {csk_trees::birch_tree::leaves} to "18:2|18:10"
      set {csk_trees::jungle_tree::trunk} to "17:3|17:7|17:11"
      set {csk_trees::jungle_tree::leaves} to "18:3|18:11"
      set {csk_trees::acacia_tree::trunk} to "162:0|162:4|162:8"
      set {csk_trees::acacia_tree::leaves} to "161:0|161:8"
      set {csk_trees::dark_oak_tree::trunk} to "162:1|162:5|162:9"
      set {csk_trees::dark_oak_tree::leaves} to "161:1|161:9"
    function treechop(location:location, tree:text, y:number, tool:item, tree_radius:number=1, leave_radius:number=2):
      loop blocks in radius {_tree_radius} of {_location}:
        if {csk_trees::%{_tree}%::trunk} contains "%id of loop-block%:%data value of loop-block%":
          location of loop-block is not {_location}
          y coord of loop-block is greater than or equal to {_y}
          break loop-block using {_tool}
          treechop(location of loop-block, "%{_tree}%", {_y}, {_tool}, {_tree_radius}, {_leave_radius})
      loop blocks in radius {_leave_radius} of {_location}:
        if {csk_trees::%{_tree}%::leaves} contains "%id of loop-block%:%data value of loop-block%":
          break loop-block using {_tool}
    break of any log:
      tool of player is any axe
      set {_tree_radius} to 1
      set {_leave_radius} to 1
      if {csk_trees::oak_tree::trunk} contains "%id of block%:%data value of block%":
        set {_tree} to "oak_tree"
      else if {csk_trees::spruce_tree::trunk} contains "%id of block%:%data value of block%":
        set {_tree} to "spruce_tree"
      else if {csk_trees::birch_tree::trunk} contains "%id of block%:%data value of block%":
        set {_tree} to "birch_tree"
      else if {csk_trees::jungle_tree::trunk} contains "%id of block%:%data value of block%":
        set {_tree} to "jungle_tree"
      else if {csk_trees::acacia::trunk} contains "%id of block%:%data value of block%":
        set {_tree} to "acacia_tree"
      else if {csk_trees::dark_oak_tree::trunk} contains "%id of block%:%data value of block%":
        set {_tree} to "dark_oak_tree"
        set {_tree_radius} to 2
        set {_leave_radius} to 2
      treechop(location of block, "%{_tree}%", y coord of block, tool of player, {_tree_radius}, {_leave_radius})
    It managed to support all tree types and it is what I have planned for this script I was just wondering if there is a loop all blocks at a location feature in command helper. I'll try and do it your way but I may need help
  8. PseudoKnight

    PseudoKnight Well-Known Member Developer

    A cuboid iterator is a commonly used procedure for that (3 nested for() loops for x, y, z). It's a little more efficient than a radius if you need to define your shape more specifically. In this case a 3x2x3 cuboid. It's less efficient than my 5 block check (9 with acacia) because it checks 17 blocks, but it'd work.

    I see how you're avoiding infinite recursion. I can work with that.

    This actually got me interested in trying some things.
    PHP:
    proc _try_break(@x, @y, @z, @blocks) {
        @block = get_block_at(@x, @y, @z);
        if(array_contains(@blocks, @block)) {
            @loc = array(@x, @y, @z, pworld());
            @sound = if(@block == @blocks['log'], 'BLOCK_WOOD_BREAK', 'BLOCK_GRASS_BREAK');
            break_block(@loc);
            play_sound(@loc, array('sound': @sound, 'volume': 0.1));
            _treecheck(@x, @y, @z, @blocks);
        }
    }

    proc _treecheck(@x, @y, @z, @blocks, @checkDiagonal = true) {
        // Add a delay to animate the breaking. It's pretty and distributes the load.
        set_timeout(100, closure() {
            _try_break(@x, @y + 1, @z, @blocks);
            _try_break(@x - 1, @y, @z, @blocks);
            _try_break(@x + 1, @y, @z, @blocks);
            _try_break(@x, @y, @z - 1, @blocks);
            _try_break(@x, @y, @z + 1, @blocks);
            if(@checkDiagonal && @blocks['log'] == '162:0') {
                // check diagonal for acacia trunk once
                _treecheck(@x, @y + 1, @z, @blocks, false);
            }
        });
    }

    bind(block_break, null, null, @event) {
        @type = @event['block']['type'];
        if(@type == 17 || @type == 162) {
            @data = @event['block']['data'];
            @blocks = array(
                'log': @type.':'[email protected]data,
                'leaves': if(@type == 17, 18, 161).':'.(@data + 8),
            );
            @loc = @event['location'];
            _treecheck(@loc['x'], @loc['y'], @loc['z'], @blocks);
        }
    }
    There's still the problem of this breaking merged trees, but while you can solve that a little bit by passing a source location and checking a radius based on the tree type, it's impossible to solve completely. It's one of those reasons I never really liked those plugins. (Plus punching trees is fun too. XD)
    Last edited: Dec 3, 2017
    Chloe Sanders likes this.
  9. Chloe Sanders

    Chloe Sanders New Member

    oooh this looks helpful :) going to have to test it out tomorrow and see how far I can get with this ;)
  10. Chloe Sanders

    Chloe Sanders New Member

    @PseudoKnight
    How would I save information to a file and grab it in the future? Like how would I keep variables for say making an economy plugin, home plugin, etc... I didn't keep wanting to create new threads I've sort of got the treecapitator working now but I'd like to take a break from it as I dont think I'm good enough with command helper yet.

    Wasn't sure if it was 'res_create_resource()' as in the description it says it will be deprecated in the future
  11. PseudoKnight

    PseudoKnight Well-Known Member Developer

    No, res_create_resource() is an advanced tool for string building and seeding random numbers. The easiest solution is to use persistence functions like get_value() and store_value(). It stores in an sqlite database by default, but that can be configured to something else like Serialized, for example, which is what I recommend if you want something fast but not thread safe or manually editable.
  12. Oboist

    Oboist Member

    Some tool i wrote for my server (wher player breaks a log, it tries to find the whole tree. If it finds "non-natural" blocks or no leaves found around, it stops (because clearly it's some building, not a tree)).
    It is part of bigger cleaning tool, so i just write all blocks that needs cleaning to one file. (so _reblock() replaces all stashed blocks)

    But, of course, it is not for quick tree breaking, its for cleaning floating tree pieces leaved by players. Really makes server prettier.

    PHP:

    bind(block_break, null, null, @event){
    @loc = @event[location]
    if(@loc[world] == 'world' && sk_regions_at(@loc) == array() && (@event[block][type] == '17' || @event[block][type] == '162')){
    @treeid = time()
    @tree = _treesearch(@treeid, array(), @loc)
    if(import(leaves[email protected]treeid) == true && import(notatree[email protected]treeid) != true){
    foreach(@block in @tree){
    chd_write('../values/'.reblock.'.txt', '{|}'.integer(@block[0]).'.'.integer(@block[1]).'.'.integer(@block[2]).'{|}'.0, 'APPEND')
    }}}

    proc _treesearch(@treeid, @tree, @loc){
    if(import(notatree[email protected]treeid) == true, return())
    array_push(@tree, @loc)
    for(@x = @loc['x'] - 1, @x <= @loc['x'] + 1, @x++){
    for(@y = @loc['y'] - 1, @y <= @loc['y'] + 1, @y++){
    for(@z = @loc['z'] - 1, @z <= @loc['z'] + 1, @z++){
        @newloc = array(0: @x, 1: @y, 2: @z, 3: 'world', x: @x, y: @y, z: @z, world: 'world')
        @block = split(':', get_block_at(@newloc))[0]
        if(!array_contains(array(0, 1, 2, 3, 6, 8, 9, 10, 11, 12, 13, 17, 18, 31, 32, 37, 38, 39, 40, 51, 60, 78, 79, 81, 82, 86, 99, 100, 103, 106, 110, 111, 119, 127, 141, 142, 159, 161, 162, 172, 174, 175), @block)){
            export(notatree[email protected]treeid, true)
        } else {
            if(@block == 18 || @block == 161){
                export(leaves[email protected]treeid, true)
            } else if(@block == 17 || @block == 162){
                if(!array_contains_ic(@tree, @newloc), @tree = _treesearch(@treeid, @tree, @newloc))
            }
        }
    }}}
    return(@tree)
    }

    proc _reblock(){
    @arr = split('{|}', read('../values/reblock.txt'))
    for(@x = array_size(@arr) - 1, @x > 0 , @x -= 2){
    @trgt = split('.', @arr[@x - 1])
    if(sk_regions_at(array(1: @trgt[0], 2: @trgt[1], 3: @trgt[2], 4: 'world', x: @trgt[0], y: @trgt[1], z: @trgt[2], world: 'world')) == array()){set_block_at(@trgt[0], @trgt[1], @trgt[2], @arr[@x], 'world')}}
    chd_write('../values/'.reblock.'.txt', '0.1.0{|}1')
    console('[REBLOCK] DONE!', false)
    }

    register_command('reblock', array(executor: closure(@alias, @pl, @args){_reblock()}))
    and, in my case, i use
    set_cron('0 4 * * *', closure(_reblock()))
    for daily cleaning.

    Maybe i only should add execution queue in _reblock...
    Last edited: Jan 7, 2018
  13. ThgilFoDrol

    ThgilFoDrol New Member

    I'm late to the party, but this is functionally similar to an ore broadcast feature I wrote a while back. This mechanism reports to staff if people mine a vein of ore described in the config, giving the ore, quantity, and light level. It tracks ore blocks that have been reported already to avoid an infinite loop, and it also ignores ore blocks placed by players, unless they're in creative mode.

    Gif demo (56k warning, 50mb):
    [​IMG]

    There's probably some optimization you can do if you re-purpose it for trees, but here's the code:
    events.ms
    PHP:
    include('procs.ms');
    bind('block_break', array('priority': 'monitor'), null, @event,
        @ores = _ncanticheat_get_ores();

        # player is in survival, and the block broken is an ore that we track
       if(pmode(@event['player']) == 'SURVIVAL' && array_index_exists(@ores, string(@event['block']['type']))){
            @counter = 0;
            @matches = array();
            @eventblock = associative_array( # prevent duplicates in final array where you have two of this array, with integer keys and without
               'x': @event['location']['x'],
                'y': @event['location']['y'],
                'z': @event['location']['z'],
                'world': @event['location']['world'],
            );
            @matches[] = @eventblock;
            @storagekey = 'ncanticheat.oredetect.blocks';
            @tracked = import(@storagekey, array()); # store the previously tracked ore blocks

            if(array_contains(@tracked, @eventblock)){ # if mined block was already tracked
               # stop tracking the block, as we assume player was successful in mining it
               array_remove_values(@tracked, @eventblock);
                export(@storagekey, @tracked); # update tracked block list
               # exit
               return();
            }

            do{
                @surrounding = array();
                @newadditions = 0;

               # iterate over the list of connected ores we found from the previous loop
               foreach(@match in @matches){
                    @surrounding = array_merge(@surrounding, _ncanticheat_get_surrounding_blocks(@match)); # push a list of matching surrounding blocks, inc. duplicates
               }

               # iterate over surrounding blocks
               foreach(@block in @surrounding){
                    if(!array_contains(@matches, @block) && !array_contains(@tracked, @block)){ # this block wasn't checked before
                       @matches[] = @block; # push to matches
                       @newadditions += 1;
                    }
                }
                @counter += 1;
            } while(@counter < 4 && @newadditions != 0);

            @tracked = array_merge(@tracked, @matches); # push new matched blocks to tracked block list
            array_remove_values(@tracked, @eventblock); # stop tracking the block the player just broke
            export(@storagekey, @tracked); # update tracked block list

            @quantity = string(array_size(@matches));
            if(@counter == 4){ # max loop count was reached
               # there may be more ores we didn't record
               @quantity .= '+';
            }
            @ore = @ores[string(@event['block']['type'])];
            @ploc = array_normalize(ploc(@event['player']))[0..3];
            @ploc[1] += 1; # why the hell is ploc returning a block below the player
            broadcast(concat(colorize('&c[!] '[email protected]ore['color']), @event['player'], ' found ', @quantity, ' ', @ore['name'], ' ore (light: ', get_light_at(@ploc), ').'), 'herochat.speak.staff');

            #broadcast('Took '.subtract(time(), @start).'ms');
       }
    )

    bind('block_place', array('priority': 'monitor'), null, @event,
        @ores = _ncanticheat_get_ores();
        @storagekey = 'ncanticheat.oredetect.blocks';
        @tracked = import(@storagekey, array()); # retrieve the previously tracked ore blocks

        @eventblock = associative_array(
            'x': @event['location']['x'],
            'y': @event['location']['y'],
            'z': @event['location']['z'],
            'world': @event['location']['world'],
        );

        if(pmode(@event['player']) == 'CREATIVE'){ # ignore blocks placed by creative mode people
           return();
        }

        # check if we track the block type that was placed
        if(array_index_exists(@ores, string(@event['type']))){
           # add to tracked blocks list
           @tracked[] = @eventblock;
           export(@storagekey, @tracked); # update tracked blocks list
       }
    )
    procs.ms
    PHP:
    # Given a keyed loc array, return an array of surrounding blocks with the same id as keyed loc arrays
    proc _ncanticheat_get_surrounding_blocks(@locarray){
        @givenblockid = get_block_at(@locarray);
        @surrounding = array();

        for(@x = @locarray['x'] - 1, @x <= @locarray['x'] + 1, @x++){
            for(@y = @locarray['y'] - 1, @y <= @locarray['y'] + 1, @y++){
                for(@z = @locarray['z'] - 1, @z <= @locarray['z'] + 1, @z++){
                    @candidateblock = associative_array(
                        'x': @x,
                        'y': @y,
                        'z': @z,
                        'world': @locarray['world'];
                    );

                    # check if candidate block id matches block id of given block
                   if(@givenblockid == get_block_at(@candidateblock)){
                        # matches, so we add to array
                       @surrounding[] = @candidateblock;
                    }
                }
            }
        }

        return(@surrounding);
    }

    # Return array mapping ore ids to names
    proc _ncanticheat_get_ores(){
        @ores = associative_array(
            '129': associative_array(
                'name': 'emerald',
                'color': '&a',
            ),
            '56': associative_array(
                'name': 'diamond',
                'color': '&b',
            ),
            '15': associative_array(
                'name': 'iron',
                'color': '&7',
            ),
            '14': associative_array(
                'name': 'gold',
                'color': '&e',
            ),
        );

        return(@ores);
    }
    Link to github repo:
    https://github.com/thgilfodrol/NoobcraftGC-CH/tree/master/anticheat
  14. PseudoKnight

    PseudoKnight Well-Known Member Developer

    I did something like that a long time ago too. Here's a modified version.
    PHP:
    bind(block_break, array('priority': 'monitor'), array('name': 'DIAMOND_ORE'), @event) {
        @loc = @event['location'];
        if(@loc['y'] < 17) {
            @report = true;
            for(@x = @loc['x'] - 1, @x <= @loc['x'] + 1, @x++) {
                for(@y = @loc['y'] - 1, @y <= @loc['y'] + 1, @y++) {
                    for(@z = @loc['z'] - 1, @z <= @@loc['z'] + 1, @z++) {
                        if(get_block_at(@x, @y, @z) === '56:0') {
                            @report = false;
                        }
                    }
                }
            }
            if(@report) {
                @msg = color('red').'[!] '.display_name().color('reset').' found diamonds';
                broadcast(@msg, 'group.moderator');
                console(@msg, false);
            }
        }
    }
    Obviously this cuts a lot of corners to make it simple and fast (eg. only diamonds), but it only reports if it's the last block in a patch. Yes, it reports if players place down ore and then mine it again, but as long as this is for reporting and not enforcement, you can fudge a little.
    Last edited: May 15, 2018 at 5:55 AM
  15. ThgilFoDrol

    ThgilFoDrol New Member

    I did something similar to that for a year before moving on to that current solution (i.e. moved from something simple and fast to something more feature complete). I was worried that it'd be too slow and cause the server to hang while doing all that iterating, but in benchmarks the time for me was around 20-80ms for a typical ore vein upon discovery, and 1-3ms for subsequent blocks in the tracked ore vein. I decided to cap the do-while loop to 4 iterations because it rarely went over in practice (e.g. broadcast would be: found 14+ ore, 17+ ore, 11+ ore, etc when exceeding the iteration cap) and higher caps resulted in 200-300ms delays with little benefit.

    I figured it was a good tradeoff because we usually don't have a lot of people mining at the same time, but I'm sure there must be a faster way than what I have there too.