2018-08-08 03:16:53 +00:00
/ *
* This file is part of Baritone .
*
* Baritone is free software : you can redistribute it and / or modify
2018-09-17 22:11:40 +00:00
* it under the terms of the GNU Lesser General Public License as published by
2018-08-08 03:16:53 +00:00
* the Free Software Foundation , either version 3 of the License , or
* ( at your option ) any later version .
*
2018-08-08 04:15:22 +00:00
* Baritone is distributed in the hope that it will be useful ,
2018-08-08 03:16:53 +00:00
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
2018-09-17 22:11:40 +00:00
* GNU Lesser General Public License for more details .
2018-08-08 03:16:53 +00:00
*
2018-09-17 22:11:40 +00:00
* You should have received a copy of the GNU Lesser General Public License
2018-08-08 03:16:53 +00:00
* along with Baritone . If not , see < https : //www.gnu.org/licenses/>.
* /
2018-08-22 20:15:56 +00:00
package baritone.pathing.movement.movements ;
2018-08-07 14:52:49 +00:00
2019-02-14 03:23:16 +00:00
import baritone.Baritone ;
2018-11-13 17:50:29 +00:00
import baritone.api.IBaritone ;
2018-10-09 01:37:52 +00:00
import baritone.api.pathing.movement.MovementStatus ;
import baritone.api.utils.BetterBlockPos ;
2018-11-13 17:50:29 +00:00
import baritone.api.utils.input.Input ;
2018-10-09 01:37:52 +00:00
import baritone.pathing.movement.CalculationContext ;
import baritone.pathing.movement.Movement ;
import baritone.pathing.movement.MovementHelper ;
import baritone.pathing.movement.MovementState ;
2018-11-26 06:19:04 +00:00
import baritone.utils.BlockStateInterface ;
2018-12-12 06:26:55 +00:00
import baritone.utils.pathing.MutableMoveResult ;
2019-06-25 00:53:25 +00:00
import com.google.common.collect.ImmutableSet ;
2018-08-12 15:40:44 +00:00
import java.util.ArrayList ;
2018-08-25 23:30:12 +00:00
import java.util.List ;
2019-06-25 00:53:25 +00:00
import java.util.Set ;
2021-06-23 17:24:32 +00:00
import net.minecraft.client.player.LocalPlayer ;
import net.minecraft.core.BlockPos ;
import net.minecraft.core.Direction ;
import net.minecraft.world.level.block.Block ;
import net.minecraft.world.level.block.Blocks ;
import net.minecraft.world.level.block.state.BlockState ;
2018-08-12 15:40:44 +00:00
2018-08-07 14:52:49 +00:00
public class MovementDiagonal extends Movement {
2018-08-07 21:14:36 +00:00
2018-08-12 04:27:46 +00:00
private static final double SQRT_2 = Math . sqrt ( 2 ) ;
2019-06-10 19:43:02 +00:00
public MovementDiagonal ( IBaritone baritone , BetterBlockPos start , Direction dir1 , Direction dir2 , int dy ) {
2021-06-23 19:29:34 +00:00
this ( baritone , start , start . relative ( dir1 ) , start . relative ( dir2 ) , dir2 , dy ) ;
2018-08-07 21:14:36 +00:00
// super(start, start.offset(dir1).offset(dir2), new BlockPos[]{start.offset(dir1), start.offset(dir1).up(), start.offset(dir2), start.offset(dir2).up(), start.offset(dir1).offset(dir2), start.offset(dir1).offset(dir2).up()}, new BlockPos[]{start.offset(dir1).offset(dir2).down()});
2018-08-07 14:52:49 +00:00
}
2019-06-10 19:43:02 +00:00
private MovementDiagonal ( IBaritone baritone , BetterBlockPos start , BetterBlockPos dir1 , BetterBlockPos dir2 , Direction drr2 , int dy ) {
2021-06-23 19:29:34 +00:00
this ( baritone , start , dir1 . relative ( drr2 ) . above ( dy ) , dir1 , dir2 ) ;
2018-08-07 14:52:49 +00:00
}
2018-11-13 17:50:29 +00:00
private MovementDiagonal ( IBaritone baritone , BetterBlockPos start , BetterBlockPos end , BetterBlockPos dir1 , BetterBlockPos dir2 ) {
2021-06-23 19:29:34 +00:00
super ( baritone , start , end , new BetterBlockPos [ ] { dir1 , dir1 . above ( ) , dir2 , dir2 . above ( ) , end , end . above ( ) } ) ;
2018-08-07 14:52:49 +00:00
}
2020-08-17 23:15:56 +00:00
@Override
protected boolean safeToCancel ( MovementState state ) {
2020-08-20 20:40:16 +00:00
//too simple. backfill does not work after cornering with this
2022-05-31 13:43:58 +00:00
//return context.precomputedData.canWalkOn(ctx, ctx.playerFeet().down());
2021-06-23 17:24:32 +00:00
LocalPlayer player = ctx . player ( ) ;
2020-08-20 20:40:16 +00:00
double offset = 0 . 25 ;
2021-06-23 17:24:32 +00:00
double x = player . position ( ) . x ;
double y = player . position ( ) . y - 1 ;
double z = player . position ( ) . z ;
2020-08-20 20:40:16 +00:00
//standard
2021-01-30 03:49:11 +00:00
if ( ctx . playerFeet ( ) . equals ( src ) ) {
2020-08-20 20:40:16 +00:00
return true ;
}
//both corners are walkable
if ( MovementHelper . canWalkOn ( ctx , new BlockPos ( src . x , src . y - 1 , dest . z ) )
2021-01-30 03:49:11 +00:00
& & MovementHelper . canWalkOn ( ctx , new BlockPos ( dest . x , src . y - 1 , src . z ) ) ) {
return true ;
2020-08-20 20:40:16 +00:00
}
//we are in a likely unwalkable corner, check for a supporting block
2020-08-24 21:06:19 +00:00
if ( ctx . playerFeet ( ) . equals ( new BetterBlockPos ( src . x , src . y , dest . z ) )
2021-01-30 03:49:11 +00:00
| | ctx . playerFeet ( ) . equals ( new BetterBlockPos ( dest . x , src . y , src . z ) ) ) {
return ( MovementHelper . canWalkOn ( ctx , new BetterBlockPos ( x + offset , y , z + offset ) )
| | MovementHelper . canWalkOn ( ctx , new BetterBlockPos ( x + offset , y , z - offset ) )
| | MovementHelper . canWalkOn ( ctx , new BetterBlockPos ( x - offset , y , z + offset ) )
| | MovementHelper . canWalkOn ( ctx , new BetterBlockPos ( x - offset , y , z - offset ) ) ) ;
2020-08-20 20:40:16 +00:00
}
2020-08-24 21:06:19 +00:00
return true ;
2021-01-30 03:49:11 +00:00
}
2020-08-17 23:15:56 +00:00
2018-08-07 14:52:49 +00:00
@Override
2019-01-08 22:56:21 +00:00
public double calculateCost ( CalculationContext context ) {
2018-12-12 06:26:55 +00:00
MutableMoveResult result = new MutableMoveResult ( ) ;
cost ( context , src . x , src . y , src . z , dest . x , dest . z , result ) ;
if ( result . y ! = dest . y ) {
return COST_INF ; // doesn't apply to us, this position is incorrect
}
return result . cost ;
2018-09-23 05:00:28 +00:00
}
2019-06-25 00:53:25 +00:00
@Override
protected Set < BetterBlockPos > calculateValidPositions ( ) {
BetterBlockPos diagA = new BetterBlockPos ( src . x , src . y , dest . z ) ;
BetterBlockPos diagB = new BetterBlockPos ( dest . x , src . y , src . z ) ;
2019-09-10 06:06:11 +00:00
if ( dest . y < src . y ) {
2021-06-23 19:29:34 +00:00
return ImmutableSet . of ( src , dest . above ( ) , diagA , diagB , dest , diagA . below ( ) , diagB . below ( ) ) ;
2019-06-25 00:53:25 +00:00
}
2019-09-10 06:06:11 +00:00
if ( dest . y > src . y ) {
2021-06-23 19:29:34 +00:00
return ImmutableSet . of ( src , src . above ( ) , diagA , diagB , dest , diagA . above ( ) , diagB . above ( ) ) ;
2019-09-10 06:06:11 +00:00
}
2019-06-25 00:53:25 +00:00
return ImmutableSet . of ( src , dest , diagA , diagB ) ;
}
2018-12-12 06:26:55 +00:00
public static void cost ( CalculationContext context , int x , int y , int z , int destX , int destZ , MutableMoveResult res ) {
2022-06-07 20:52:24 +00:00
if ( ! MovementHelper . canWalkThrough ( context , destX , y + 1 , destZ ) ) {
2018-12-12 06:26:55 +00:00
return ;
2018-08-07 14:52:49 +00:00
}
2019-10-07 00:02:41 +00:00
BlockState destInto = context . get ( destX , y , destZ ) ;
2023-01-27 21:30:51 +00:00
BlockState fromDown ;
2019-09-10 06:06:11 +00:00
boolean ascend = false ;
2019-10-07 00:02:41 +00:00
BlockState destWalkOn ;
2018-12-12 06:26:55 +00:00
boolean descend = false ;
2022-08-29 14:59:01 +00:00
boolean frostWalker = false ;
2022-06-07 20:52:24 +00:00
if ( ! MovementHelper . canWalkThrough ( context , destX , y , destZ , destInto ) ) {
2019-09-10 06:06:11 +00:00
ascend = true ;
2022-06-07 20:52:24 +00:00
if ( ! context . allowDiagonalAscend | | ! MovementHelper . canWalkThrough ( context , x , y + 2 , z ) | | ! MovementHelper . canWalkOn ( context , destX , y , destZ , destInto ) | | ! MovementHelper . canWalkThrough ( context , destX , y + 2 , destZ ) ) {
2018-12-12 06:26:55 +00:00
return ;
}
2019-09-10 06:06:11 +00:00
destWalkOn = destInto ;
2023-01-12 20:39:55 +00:00
fromDown = context . get ( x , y - 1 , z ) ;
2019-09-10 06:06:11 +00:00
} else {
destWalkOn = context . get ( destX , y - 1 , destZ ) ;
2023-01-12 20:39:55 +00:00
fromDown = context . get ( x , y - 1 , z ) ;
2023-01-11 22:56:52 +00:00
boolean standingOnABlock = MovementHelper . mustBeSolidToWalkOn ( context , x , y - 1 , z , fromDown ) ;
2022-08-29 16:47:54 +00:00
frostWalker = standingOnABlock & & MovementHelper . canUseFrostWalker ( context , destWalkOn ) ;
2023-01-11 22:56:52 +00:00
if ( ! frostWalker & & ! MovementHelper . canWalkOn ( context , destX , y - 1 , destZ , destWalkOn ) ) {
2019-09-10 06:06:11 +00:00
descend = true ;
2022-06-07 20:52:24 +00:00
if ( ! context . allowDiagonalDescend | | ! MovementHelper . canWalkOn ( context , destX , y - 2 , destZ ) | | ! MovementHelper . canWalkThrough ( context , destX , y - 1 , destZ , destWalkOn ) ) {
2019-09-10 06:06:11 +00:00
return ;
}
}
2022-09-30 20:40:23 +00:00
frostWalker & = ! context . assumeWalkOnWater ; // do this after checking for descends because jesus can't prevent the water from freezing, it just prevents us from relying on the water freezing
2018-08-07 14:52:49 +00:00
}
2018-08-13 20:40:50 +00:00
double multiplier = WALK_ONE_BLOCK_COST ;
2018-08-25 23:30:12 +00:00
// For either possible soul sand, that affects half of our walking
2018-09-23 05:00:28 +00:00
if ( destWalkOn . getBlock ( ) = = Blocks . SOUL_SAND ) {
2018-08-17 19:24:40 +00:00
multiplier + = ( WALK_ONE_OVER_SOUL_SAND_COST - WALK_ONE_BLOCK_COST ) / 2 ;
2022-08-29 14:59:01 +00:00
} else if ( frostWalker ) {
// frostwalker lets us walk on water without the penalty
2018-12-20 06:01:47 +00:00
} else if ( destWalkOn . getBlock ( ) = = Blocks . WATER ) {
2018-12-21 05:22:18 +00:00
multiplier + = context . walkOnWaterOnePenalty * SQRT_2 ;
2018-08-17 19:24:40 +00:00
}
2023-01-11 22:56:52 +00:00
Block fromDownBlock = fromDown . getBlock ( ) ;
if ( fromDownBlock = = Blocks . LADDER | | fromDownBlock = = Blocks . VINE ) {
2018-12-12 06:26:55 +00:00
return ;
2018-12-04 22:38:08 +00:00
}
2023-01-11 22:56:52 +00:00
if ( fromDownBlock = = Blocks . SOUL_SAND ) {
2018-08-17 19:24:40 +00:00
multiplier + = ( WALK_ONE_OVER_SOUL_SAND_COST - WALK_ONE_BLOCK_COST ) / 2 ;
2018-08-13 14:05:28 +00:00
}
2019-06-10 19:43:02 +00:00
BlockState cuttingOver1 = context . get ( x , y - 1 , destZ ) ;
2019-03-08 20:30:43 +00:00
if ( cuttingOver1 . getBlock ( ) = = Blocks . MAGMA_BLOCK | | MovementHelper . isLava ( cuttingOver1 ) ) {
2018-12-12 06:26:55 +00:00
return ;
2018-08-12 15:24:53 +00:00
}
2019-06-10 19:43:02 +00:00
BlockState cuttingOver2 = context . get ( destX , y - 1 , z ) ;
2019-03-08 20:30:43 +00:00
if ( cuttingOver2 . getBlock ( ) = = Blocks . MAGMA_BLOCK | | MovementHelper . isLava ( cuttingOver2 ) ) {
2018-12-12 06:26:55 +00:00
return ;
2018-08-12 15:24:53 +00:00
}
2019-09-10 06:06:11 +00:00
boolean water = false ;
2019-10-07 00:02:41 +00:00
BlockState startState = context . get ( x , y , z ) ;
2019-10-01 00:52:47 +00:00
Block startIn = startState . getBlock ( ) ;
if ( MovementHelper . isWater ( startState ) | | MovementHelper . isWater ( destInto ) ) {
2019-09-10 06:06:11 +00:00
if ( ascend ) {
return ;
}
// Ignore previous multiplier
// Whatever we were walking on (possibly soul sand) doesn't matter as we're actually floating on water
// Not even touching the blocks below
multiplier = context . waterWalkSpeed ;
water = true ;
}
2019-06-10 19:43:02 +00:00
BlockState pb0 = context . get ( x , y , destZ ) ;
BlockState pb2 = context . get ( destX , y , z ) ;
2019-09-10 06:06:11 +00:00
if ( ascend ) {
2022-06-07 20:52:24 +00:00
boolean ATop = MovementHelper . canWalkThrough ( context , x , y + 2 , destZ ) ;
boolean AMid = MovementHelper . canWalkThrough ( context , x , y + 1 , destZ ) ;
boolean ALow = MovementHelper . canWalkThrough ( context , x , y , destZ , pb0 ) ;
boolean BTop = MovementHelper . canWalkThrough ( context , destX , y + 2 , z ) ;
boolean BMid = MovementHelper . canWalkThrough ( context , destX , y + 1 , z ) ;
boolean BLow = MovementHelper . canWalkThrough ( context , destX , y , z , pb2 ) ;
2019-09-10 06:06:11 +00:00
if ( ( ! ( ATop & & AMid & & ALow ) & & ! ( BTop & & BMid & & BLow ) ) // no option
2019-10-01 00:52:47 +00:00
| | MovementHelper . avoidWalkingInto ( pb0 ) // bad
| | MovementHelper . avoidWalkingInto ( pb2 ) // bad
2022-06-07 20:52:24 +00:00
| | ( ATop & & AMid & & MovementHelper . canWalkOn ( context , x , y , destZ , pb0 ) ) // we could just ascend
| | ( BTop & & BMid & & MovementHelper . canWalkOn ( context , destX , y , z , pb2 ) ) // we could just ascend
2019-09-10 06:06:11 +00:00
| | ( ! ATop & & AMid & & ALow ) // head bonk A
| | ( ! BTop & & BMid & & BLow ) ) { // head bonk B
return ;
}
res . cost = multiplier * SQRT_2 + JUMP_ONE_BLOCK_COST ;
res . x = destX ;
res . z = destZ ;
res . y = y + 1 ;
return ;
}
2018-09-27 00:25:18 +00:00
double optionA = MovementHelper . getMiningDurationTicks ( context , x , y , destZ , pb0 , false ) ;
double optionB = MovementHelper . getMiningDurationTicks ( context , destX , y , z , pb2 , false ) ;
2018-08-12 04:27:46 +00:00
if ( optionA ! = 0 & & optionB ! = 0 ) {
2018-09-27 00:25:18 +00:00
// check these one at a time -- if pb0 and pb2 were nonzero, we already know that (optionA != 0 && optionB != 0)
// so no need to check pb1 as well, might as well return early here
2018-12-12 06:26:55 +00:00
return ;
2018-09-27 00:25:18 +00:00
}
2019-06-10 19:43:02 +00:00
BlockState pb1 = context . get ( x , y + 1 , destZ ) ;
2018-09-27 00:25:18 +00:00
optionA + = MovementHelper . getMiningDurationTicks ( context , x , y + 1 , destZ , pb1 , true ) ;
if ( optionA ! = 0 & & optionB ! = 0 ) {
// same deal, if pb1 makes optionA nonzero and option B already was nonzero, pb3 can't affect the result
2018-12-12 06:26:55 +00:00
return ;
2018-08-12 04:27:46 +00:00
}
2019-06-10 19:43:02 +00:00
BlockState pb3 = context . get ( destX , y + 1 , z ) ;
2019-03-08 20:30:43 +00:00
if ( optionA = = 0 & & ( ( MovementHelper . avoidWalkingInto ( pb2 ) & & pb2 . getBlock ( ) ! = Blocks . WATER ) | | MovementHelper . avoidWalkingInto ( pb3 ) ) ) {
2018-09-27 00:25:18 +00:00
// at this point we're done calculating optionA, so we can check if it's actually possible to edge around in that direction
2018-12-12 06:26:55 +00:00
return ;
2018-08-12 04:27:46 +00:00
}
2018-09-27 00:25:18 +00:00
optionB + = MovementHelper . getMiningDurationTicks ( context , destX , y + 1 , z , pb3 , true ) ;
if ( optionA ! = 0 & & optionB ! = 0 ) {
// and finally, if the cost is nonzero for both ways to approach this diagonal, it's not possible
2018-12-12 06:26:55 +00:00
return ;
2018-09-27 00:25:18 +00:00
}
2019-03-08 20:30:43 +00:00
if ( optionB = = 0 & & ( ( MovementHelper . avoidWalkingInto ( pb0 ) & & pb0 . getBlock ( ) ! = Blocks . WATER ) | | MovementHelper . avoidWalkingInto ( pb1 ) ) ) {
2018-09-27 00:25:18 +00:00
// and now that option B is fully calculated, see if we can edge around that way
2018-12-12 06:26:55 +00:00
return ;
2018-08-12 04:27:46 +00:00
}
if ( optionA ! = 0 | | optionB ! = 0 ) {
2018-08-13 14:05:28 +00:00
multiplier * = SQRT_2 - 0 . 001 ; // TODO tune
2018-11-26 23:01:34 +00:00
if ( startIn = = Blocks . LADDER | | startIn = = Blocks . VINE ) {
// edging around doesn't work if doing so would climb a ladder or vine instead of moving sideways
2018-12-12 06:26:55 +00:00
return ;
}
} else {
// only can sprint if not edging around
2018-12-21 05:22:18 +00:00
if ( context . canSprint & & ! water ) {
2018-12-12 06:26:55 +00:00
// If we aren't edging around anything, and we aren't in water
// We can sprint =D
// Don't check for soul sand, since we can sprint on that too
multiplier * = SPRINT_MULTIPLIER ;
2018-11-26 23:01:34 +00:00
}
2018-08-12 04:27:46 +00:00
}
2018-12-12 06:26:55 +00:00
res . cost = multiplier * SQRT_2 ;
if ( descend ) {
res . cost + = Math . max ( FALL_N_BLOCKS_COST [ 1 ] , CENTER_AFTER_FALL_COST ) ;
res . y = y - 1 ;
} else {
res . y = y ;
2018-08-13 20:40:50 +00:00
}
2018-12-12 06:26:55 +00:00
res . x = destX ;
res . z = destZ ;
2018-08-12 04:27:46 +00:00
}
2018-09-04 23:04:44 +00:00
@Override
public MovementState updateState ( MovementState state ) {
super . updateState ( state ) ;
2018-10-09 00:57:22 +00:00
if ( state . getStatus ( ) ! = MovementStatus . RUNNING ) {
2018-09-08 00:36:06 +00:00
return state ;
2018-09-08 04:32:25 +00:00
}
2018-09-04 23:04:44 +00:00
2018-11-13 17:50:29 +00:00
if ( ctx . playerFeet ( ) . equals ( dest ) ) {
2019-06-25 00:53:25 +00:00
return state . setStatus ( MovementStatus . SUCCESS ) ;
2021-06-23 19:29:34 +00:00
} else if ( ! playerInValidPosition ( ) & & ! ( MovementHelper . isLiquid ( ctx , src ) & & getValidPositions ( ) . contains ( ctx . playerFeet ( ) . above ( ) ) ) ) {
2019-06-25 00:53:25 +00:00
return state . setStatus ( MovementStatus . UNREACHABLE ) ;
2018-09-04 23:04:44 +00:00
}
2021-06-23 19:29:34 +00:00
if ( dest . y > src . y & & ctx . player ( ) . position ( ) . y < src . y + 0 . 1 & & ctx . player ( ) . horizontalCollision ) {
2019-09-10 06:06:11 +00:00
state . setInput ( Input . JUMP , true ) ;
}
2019-02-07 04:54:07 +00:00
if ( sprint ( ) ) {
2018-11-13 17:50:29 +00:00
state . setInput ( Input . SPRINT , true ) ;
2018-09-04 23:04:44 +00:00
}
2018-11-13 17:50:29 +00:00
MovementHelper . moveTowards ( ctx , state , dest ) ;
2018-09-04 23:04:44 +00:00
return state ;
}
2019-06-25 00:53:25 +00:00
private boolean sprint ( ) {
2019-03-05 05:30:04 +00:00
if ( MovementHelper . isLiquid ( ctx , ctx . playerFeet ( ) ) & & ! Baritone . settings ( ) . sprintInWater . value ) {
2019-02-07 04:54:07 +00:00
return false ;
}
for ( int i = 0 ; i < 4 ; i + + ) {
if ( ! MovementHelper . canWalkThrough ( ctx , positionsToBreak [ i ] ) ) {
return false ;
}
}
return true ;
}
2018-08-12 04:27:46 +00:00
@Override
protected boolean prepared ( MovementState state ) {
return true ;
2018-08-07 14:52:49 +00:00
}
2018-08-12 15:40:44 +00:00
@Override
2018-11-26 06:19:04 +00:00
public List < BlockPos > toBreak ( BlockStateInterface bsi ) {
2018-08-12 15:40:44 +00:00
if ( toBreakCached ! = null ) {
return toBreakCached ;
}
2018-08-25 23:35:41 +00:00
List < BlockPos > result = new ArrayList < > ( ) ;
2018-08-12 15:40:44 +00:00
for ( int i = 4 ; i < 6 ; i + + ) {
2018-11-26 06:19:04 +00:00
if ( ! MovementHelper . canWalkThrough ( bsi , positionsToBreak [ i ] . x , positionsToBreak [ i ] . y , positionsToBreak [ i ] . z ) ) {
2018-08-12 15:40:44 +00:00
result . add ( positionsToBreak [ i ] ) ;
}
}
toBreakCached = result ;
return result ;
}
@Override
2018-11-26 06:19:04 +00:00
public List < BlockPos > toWalkInto ( BlockStateInterface bsi ) {
2018-08-12 15:40:44 +00:00
if ( toWalkIntoCached = = null ) {
toWalkIntoCached = new ArrayList < > ( ) ;
}
2018-08-25 23:30:12 +00:00
List < BlockPos > result = new ArrayList < > ( ) ;
2018-08-12 15:40:44 +00:00
for ( int i = 0 ; i < 4 ; i + + ) {
2018-11-26 06:19:04 +00:00
if ( ! MovementHelper . canWalkThrough ( bsi , positionsToBreak [ i ] . x , positionsToBreak [ i ] . y , positionsToBreak [ i ] . z ) ) {
2018-08-12 15:40:44 +00:00
result . add ( positionsToBreak [ i ] ) ;
}
}
toWalkIntoCached = result ;
return toWalkIntoCached ;
}
2018-08-07 14:52:49 +00:00
}