/** Runes of Wizardry Mod for Minecraft * Licensed under the GNU GPL version 3 * * this file was created by Xilef11 on 2015-11-14 */ package com.zpig333.runesofwizardry.core.rune; import java.util.Arrays; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; import net.minecraft.block.state.IBlockState; import net.minecraft.entity.item.EntityItem; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.init.SoundEvents; import net.minecraft.item.ItemStack; import net.minecraft.tileentity.TileEntity; import net.minecraft.util.EnumFacing; import net.minecraft.util.EnumParticleTypes; import net.minecraft.util.SoundCategory; import net.minecraft.util.math.AxisAlignedBB; import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.Vec3i; import net.minecraft.util.text.TextComponentTranslation; import net.minecraft.world.World; import net.minecraft.world.WorldServer; import org.apache.logging.log4j.Level; import com.zpig333.runesofwizardry.api.DustRegistry; import com.zpig333.runesofwizardry.api.IDust; import com.zpig333.runesofwizardry.api.IRune; import com.zpig333.runesofwizardry.api.RuneEntity; import com.zpig333.runesofwizardry.block.BlockDustPlaced; import com.zpig333.runesofwizardry.core.ConfigHandler; import com.zpig333.runesofwizardry.core.WizardryLogger; import com.zpig333.runesofwizardry.core.WizardryRegistry; import com.zpig333.runesofwizardry.tileentity.TileEntityDustActive; import com.zpig333.runesofwizardry.tileentity.TileEntityDustDead; import com.zpig333.runesofwizardry.tileentity.TileEntityDustPlaced; import com.zpig333.runesofwizardry.util.ArrayUtils; import com.zpig333.runesofwizardry.util.Utils; /** internal Utility/logic methods for Runes * @author Xilef11 * */ public class RunesUtil { //no instance of this private RunesUtil(){} /** * Checks if a Rune is properly defined. Will throw an exception if something is wrong * @param rune the IRune to validate * @throws InvalidRuneException when the given rune is not correctly defined */ public static RuneStats validateRune(IRune rune){ ItemStack[][] pattern = rune.getPattern(); int rows = pattern.length; List<ItemStack> dusts = new LinkedList<ItemStack>(); //rows must be a multiple of 4 if(rows % TileEntityDustPlaced.ROWS !=0) throw new InvalidRuneException(rune,"The number of rows ("+rows+") is not a multiple of "+TileEntityDustPlaced.ROWS); StringBuilder badRowsBuilder = new StringBuilder(); for(int i=0;i<rows;i++){ ItemStack[] row = pattern[i]; if(row ==null)throw new InvalidRuneException(rune, "Found a null row: "+i); //columns must be a multiple of 4 if((row.length % TileEntityDustPlaced.COLS) ==0){ //Make sure every stack is an IDust and contains 1 item for(int j=0;j<row.length;j++){ ItemStack stack = row[j]; if(stack==null) throw new InvalidRuneException(rune, "Some ItemStacks in this Rune's pattern are null. please use ItemStack.EMPTY"); if(!stack.isEmpty()){//null stacks are OK if(!(stack.getItem() instanceof IDust)) throw new InvalidRuneException(rune,"The Item at position "+i+", "+j+" is not an IDust"); if(stack.getCount()!=1) throw new InvalidRuneException(rune,"The number of dusts at position "+i+", "+j+" must be 1"); //add to dust cost calculation if(!stack.isEmpty())dusts.add(stack.copy()); } } }else{ //if multiple rows have a bad # of columns, only 1 exception will be thrown for all of them badRowsBuilder.append(i); badRowsBuilder.append(" "); } } String badRows = badRowsBuilder.toString(); if(!badRows.equals("")){ throw new InvalidRuneException(rune, "The number of columns is not a multiple of "+TileEntityDustPlaced.COLS+" for the rows # "+badRows); } //stats dusts = Utils.sortAndMergeStacks(dusts); return new RuneStats(dusts, pattern[0].length/TileEntityDustPlaced.COLS, pattern.length/TileEntityDustPlaced.ROWS, rune.getEntityPosition().getX(), rune.getEntityPosition().getY()); } /** * Finds and activates (if appropriate) a rune starting at pos * @param pos the position around which to search for a rune * @param world the world in which to search for a rune */ public static void activateRune(World world, BlockPos pos, EntityPlayer player){ if(world.isRemote)return;//work on the server only TileEntity initial = world.getTileEntity(pos); if(initial instanceof TileEntityDustPlaced){ TileEntityDustPlaced ted = (TileEntityDustPlaced) initial; if(ted.isInRune())return;//maybe add message or something }else{ WizardryLogger.logError("activateRune was called on a BlockPos that isn't placed dust!"); //return; } PatternFinder finder = new PatternFinder(world, pos); finder.search(); ItemStack pattern[][] = finder.toArray(); RuneFacing match = matchPattern(pattern); if(match==null){ player.sendMessage(new TextComponentTranslation("runesofwizardry.message.norune")); if(ConfigHandler.hardcoreActivation){ for(BlockPos p:finder.getDustPositions()){ killDusts(world, p); } } return; } if(!match.rune.canBeActivatedByPlayer(player, world, pos)){ WizardryLogger.logInfo("Player "+player.getName()+" did not have permission to activate "+match.rune.getName()+" at "+world+" pos "+pos); return; } //sacrifice ItemStack[] sacrifice=null; boolean negated=false; Set<EntityItem> sacList=new HashSet<EntityItem>(); for(BlockPos p: finder.getDustPositions()){ sacList.addAll(world.getEntitiesWithinAABB(EntityItem.class, new AxisAlignedBB(p,p.add(1,1,1)))); } List<ItemStack> stacks= new LinkedList<ItemStack>(); for(EntityItem e: sacList){ ItemStack s =e.getEntityItem(); if(s.getItem()==WizardryRegistry.sacrifice_negator){ negated=true; }else{ //add all items that are not the sacrifice negator stacks.add(s.copy());//copy the stack just in case a rune needs it } } WizardryLogger.logInfo("Found sacrifice: "+Arrays.deepToString(stacks.toArray(new ItemStack[0]))); //check if sacrifice matches rune if(stacks.isEmpty())stacks=null; if(!negated){ if(!match.rune.sacrificeMatches(stacks)){ //translation OK player.sendMessage(new TextComponentTranslation("runesofwizardry.message.badsacrifice", new TextComponentTranslation(match.rune.getName()))); if(ConfigHandler.hardcoreSacrifices){ for(EntityItem e:sacList){ if(world instanceof WorldServer){ ((WorldServer)world).spawnParticle(EnumParticleTypes.SMOKE_NORMAL, false, e.posX, e.posY, e.posZ, 1, 0d, 0.5d, 0d, 0d); } e.setDead(); } for(BlockPos p:finder.getDustPositions()){ killDusts(world, p); } } return; } //kill the items for(EntityItem e:sacList){ if(world instanceof WorldServer){ //SPELL_MOB or SPELL_WITCH or SMOKE_LARGE are also options ((WorldServer)world).spawnParticle(EnumParticleTypes.SMOKE_NORMAL, false, e.posX, e.posY, e.posZ, 1, 0d, 0.5d, 0d, 0d); } e.setDead(); } //if(!sacList.isEmpty())world.playSoundAtEntity(player, "mob.chicken.plop", 0.5F, 0.8F + (world.rand.nextFloat() - world.rand.nextFloat()) * 0.8F); if(!sacList.isEmpty())world.playSound(null,player.posX, player.posY, player.posZ, SoundEvents.ENTITY_CHICKEN_EGG, SoundCategory.AMBIENT, 0.5F, 0.8F + (world.rand.nextFloat() - world.rand.nextFloat()) * 0.8F); } sacrifice = stacks==null? null : stacks.toArray(new ItemStack[stacks.size()]); //find the "top-left" corner BlockPos topLeft; BlockPos entityPos;//BlockPos seems to only have ints, maybe we need to use something else? //NORTH is Z-, EAST is X+, UP is Y+ Vec3i offset = match.rune.getEntityPosition(); switch(match.top){ case NORTH: topLeft = finder.getNW(); entityPos = topLeft.add(offset.getX(), 0, offset.getY()); break; case EAST: topLeft = finder.getNE(); entityPos = topLeft.add(-(offset.getY()), 0, offset.getX()); break; case SOUTH: topLeft = finder.getSE(); entityPos = topLeft.add(-(offset.getX()),0,-(offset.getY())); break; case WEST:topLeft = finder.getSW(); entityPos = topLeft.add(offset.getY(),0,-(offset.getX())); break; default: throw new IllegalStateException("A rune is facing in an invalid direction: "+match.rune.getName()+" at "+pos+" facing "+match.top); } WizardryLogger.logInfo("Top-left block is :"+topLeft+" and entity Pos is: "+entityPos); //check that the entity position is valid if(!finder.getDustPositions().contains(entityPos)){ throw new IllegalStateException("Tried to create a Rune with invalid entity position"); } TileEntity tile = world.getTileEntity(entityPos); if(!(tile instanceof TileEntityDustPlaced)){ throw new IllegalStateException("The TileEntity at "+entityPos+" isn't placed dust!"); } TileEntityDustPlaced toReplace = (TileEntityDustPlaced)tile; ItemStack[][] contents = toReplace.getContents(); //place the rune world.removeTileEntity(entityPos); world.setBlockState(entityPos, WizardryRegistry.dust_placed.getDefaultState().withProperty(BlockDustPlaced.PROPERTYSTATE, BlockDustPlaced.STATE_ACTIVE)); TileEntity te = world.getTileEntity(entityPos); if(!(te instanceof TileEntityDustActive))throw new IllegalStateException("TileEntity not formed!"); TileEntityDustActive entity = (TileEntityDustActive)te; entity.setContents(contents); //create the entity RuneEntity runeEnt = match.rune.createRune(match.rotatedPattern,match.top, finder.getDustPositions(), entity); entity.setRune(runeEnt); for(BlockPos p:finder.getDustPositions()){ TileEntityDustPlaced t = (TileEntityDustPlaced)world.getTileEntity(p); t.setRune(runeEnt); } //entity.setRune(runeEnt); entity.updateRendering(); WizardryLogger.logInfo("Formed Rune: "+match.rune.getName()+" facing "+match.top+" by "+player.getDisplayNameString()); runeEnt.onRuneActivatedbyPlayer(player,sacrifice,negated); } /** * Returns the IRune that matches a given ItemStack[][] pattern, or null if there isn't one * @return null if there is no match, the IRune match otherwise. */ private static RuneFacing matchPattern(ItemStack[][] dusts){ for(IRune rune : DustRegistry.getAllRunes()){ ItemStack[][] pattern = rune.getPattern(); //NORTH check if(PatternUtils.patternsEqual(pattern, dusts)&&rune.patternMatchesExtraCondition(dusts)) return new RuneFacing(rune, EnumFacing.NORTH,dusts); //EAST dusts = ArrayUtils.rotateCCW(dusts); if(PatternUtils.patternsEqual(pattern, dusts)&&rune.patternMatchesExtraCondition(dusts)) return new RuneFacing(rune, EnumFacing.EAST,dusts); //SOUTH dusts = ArrayUtils.rotateCCW(dusts); if(PatternUtils.patternsEqual(pattern, dusts)&&rune.patternMatchesExtraCondition(dusts)) return new RuneFacing(rune, EnumFacing.SOUTH,dusts); //WEST dusts = ArrayUtils.rotateCCW(dusts); if(PatternUtils.patternsEqual(pattern, dusts)&&rune.patternMatchesExtraCondition(dusts)) return new RuneFacing(rune, EnumFacing.WEST,dusts); //rotate the dusts back to north - this is what was causing the wierdness... dusts = ArrayUtils.rotateCCW(dusts); } return null; } /** * This method changes the TileEntityDustActive associated to a rune to a TileEntityDustPlaced with the same contents, effectively deactivating the rune. * @param rune the rune to deactivate */ public static void deactivateRune(RuneEntity rune){ ItemStack[][] contents = rune.entity.getContents(); BlockPos pos = rune.getPos(); World world = rune.entity.getWorld(); world.removeTileEntity(pos); world.setBlockState(pos,WizardryRegistry.dust_placed.getDefaultState()); TileEntity te = world.getTileEntity(pos); if(te instanceof TileEntityDustPlaced){ ((TileEntityDustPlaced)te).setContents(contents); }else{ //throw new IllegalStateException("TileEntity wasn't placed dust: "+te); Throwable t = new IllegalStateException("TileEntity wasn't placed dust: "+te); WizardryLogger.logException(Level.ERROR, t, "Error deactivating rune (1)"); } //set all entities as not in a rune for(BlockPos p:rune.dustPositions){ TileEntity te1 = world.getTileEntity(p); if(te1 instanceof TileEntityDustPlaced){ ((TileEntityDustPlaced)te1).setRune(null); IBlockState state = world.getBlockState(pos); world.notifyBlockUpdate(pos, state, state, 3); }else{ //throw new IllegalStateException("TileEntity wasn't placed dust: "+te1); Throwable t = new IllegalStateException("TileEntity wasn't placed dust: "+te1); WizardryLogger.logException(Level.ERROR, t, "Error deactivating rune (2)"); } } } /** * Sets all the dust blocks connected to a Rune to dead dust * @param rune the rune for wich to kill all dusts */ public static void killAllDustsInRune(RuneEntity rune){ World world = rune.entity.getWorld(); if(!world.isRemote){ for(BlockPos p: rune.dustPositions){ killDusts(world, p); } } } /** * Replaces all dusts in the TileEntityDustPlaced given by {@code worldIn} and {@code pos} to dead dust * @param worldIn * @param pos */ public static void killDusts(World worldIn,BlockPos pos){ if(worldIn.isRemote)return;//no need to do work on both client and server if we're going to update TileEntity en = worldIn.getTileEntity(pos); if(en instanceof TileEntityDustPlaced){ TileEntityDustPlaced ted = (TileEntityDustPlaced)en; ItemStack[][] contents = ted.getContents(); for(int i=0;i<contents.length;i++){ for(int j=0;j<contents[i].length;j++){ if(!contents[i][j].isEmpty())contents[i][j]=new ItemStack(WizardryRegistry.dust_dead); } } if(ConfigHandler.deadDustDecay){ worldIn.removeTileEntity(pos); worldIn.setBlockState(pos, WizardryRegistry.dust_placed.getDefaultState().withProperty(BlockDustPlaced.PROPERTYSTATE, BlockDustPlaced.STATE_DEAD)); en = worldIn.getTileEntity(pos); if(!(en instanceof TileEntityDustDead))throw new IllegalStateException("TileEntity not formed!"); TileEntityDustDead ded = (TileEntityDustDead)en; ded.setContents(contents); } //TODO particles? IBlockState state = worldIn.getBlockState(pos); worldIn.notifyBlockUpdate(pos, state, state, 3); }else{ WizardryLogger.logError("killDustForEntity was called with a BlockPos that does not have a TileEntityDustPlaced! :"+pos); } } /** * Represents a pair of IRune and EnumFacing, where the EnumFacing represents the direction of the "top" of the IRune pattern */ private static class RuneFacing{ public IRune rune; public EnumFacing top; public ItemStack[][] rotatedPattern; public RuneFacing(IRune rune, EnumFacing top,ItemStack[][] rotatedPattern){ this.rune=rune; this.top=top; this.rotatedPattern=rotatedPattern; } } /** * This class serves to document various properties of the rune calculated during validation for efficiency * @author Xilef11 * */ public static class RuneStats{ public final List<ItemStack> dustCosts; public final int xsize,ysize; public final int centerx,centery; private RuneStats(List<ItemStack> dustCosts, int xsize, int ysize, int centerx, int centery) { this.dustCosts = dustCosts; this.xsize = xsize; this.ysize = ysize; this.centerx = centerx; this.centery = centery; } } /** This exception is thrown by {@link RunesUtil#validateRune(IRune)} when the rune is invalid * * @author Xilef11 * */ public static class InvalidRuneException extends RuntimeException{ private static final long serialVersionUID = -2125761965795670536L; /** constructs an InvalidException with the message and name of the rune * * @param rune the rune that caused the exception * @param message details on the error */ public InvalidRuneException(IRune rune, String message){ super(rune.getName()+": "+message); } } }