package openblocks.common;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.mojang.authlib.GameProfile;
import cpw.mods.fml.common.eventhandler.ASMEventHandler;
import cpw.mods.fml.common.eventhandler.EventPriority;
import cpw.mods.fml.common.eventhandler.IEventListener;
import cpw.mods.fml.common.eventhandler.ListenerList;
import cpw.mods.fml.common.eventhandler.SubscribeEvent;
import cpw.mods.fml.relauncher.ReflectionHelper;
import java.io.File;
import java.lang.ref.WeakReference;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import net.minecraft.block.Block;
import net.minecraft.entity.item.EntityItem;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.init.Blocks;
import net.minecraft.inventory.IInventory;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.util.ChatComponentText;
import net.minecraft.util.ChatComponentTranslation;
import net.minecraft.util.EnumChatFormatting;
import net.minecraft.util.IChatComponent;
import net.minecraft.util.MathHelper;
import net.minecraft.world.GameRules;
import net.minecraft.world.World;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.common.util.FakePlayer;
import net.minecraftforge.event.entity.player.PlayerDropsEvent;
import openblocks.Config;
import openblocks.OpenBlocks;
import openblocks.api.GraveDropsEvent;
import openblocks.api.GraveSpawnEvent;
import openblocks.common.GameRuleManager.GameRule;
import openblocks.common.PlayerInventoryStore.ExtrasFiller;
import openblocks.common.tileentity.TileEntityGrave;
import openmods.Log;
import openmods.inventory.GenericInventory;
import openmods.inventory.legacy.ItemDistribution;
import openmods.utils.BlockNotifyFlags;
import openmods.utils.Coord;
import openmods.utils.TagUtils;
import openmods.world.DelayedActionTickHandler;
import org.apache.logging.log4j.Level;
public class PlayerDeathHandler {
private static final Comparator<Coord> SEARCH_COMPARATOR = new Comparator<Coord>() {
private int coordSum(Coord c) {
return Math.abs(c.x) + Math.abs(c.y) + Math.abs(c.z);
}
private int coordMax(Coord c) {
return Math.max(Math.max(Math.abs(c.x), Math.abs(c.y)), Math.abs(c.z));
}
@Override
public int compare(Coord a, Coord b) {
// first order by Manhattan distance
int diff = coordSum(a) - coordSum(b);
if (diff != 0) return diff;
// then by distance from axis
return coordMax(b) - coordMax(a);
}
};
private static class SearchOrder implements Iterable<Coord> {
public final int size;
private final List<Coord> coords;
public SearchOrder(int size) {
this.size = size;
List<Coord> coords = Lists.newArrayList();
for (int x = -size; x <= size; x++)
for (int y = -size; y <= size; y++)
for (int z = -size; z <= size; z++)
coords.add(new Coord(x, y, z));
Collections.sort(coords, SEARCH_COMPARATOR);
this.coords = ImmutableList.copyOf(coords);
}
@Override
public Iterator<Coord> iterator() {
return coords.iterator();
}
}
private static SearchOrder searchOrder;
private static Iterable<Coord> getSearchOrder(int size) {
if (searchOrder == null || searchOrder.size != size) searchOrder = new SearchOrder(size);
return searchOrder;
}
private abstract static class GravePlacementChecker {
public boolean canPlace(World world, EntityPlayer player, int x, int y, int z) {
if (!world.blockExists(x, y, z)) return false;
if (!world.canMineBlock(player, x, y, z)) return false;
Block block = world.getBlock(x, y, z);
return checkBlock(world, x, y, z, block);
}
public abstract boolean checkBlock(World world, int x, int y, int z, Block block);
}
private static final GravePlacementChecker POLITE = new GravePlacementChecker() {
@Override
public boolean checkBlock(World world, int x, int y, int z, Block block) {
return (block.isAir(world, x, y, z) || block.isReplaceable(world, x, y, z));
}
};
private static final GravePlacementChecker BRUTAL = new GravePlacementChecker() {
@Override
public boolean checkBlock(World world, int x, int y, int z, Block block) {
return block.getBlockHardness(world, x, y, z) >= 0 && world.getTileEntity(x, y, z) == null;
}
};
private static class GraveCallable implements Runnable {
private final IChatComponent cause;
private final GameProfile stiffId;
private final int posX, posY, posZ;
private final List<EntityItem> loot;
private final WeakReference<World> world;
private final WeakReference<EntityPlayer> exPlayer;
public GraveCallable(World world, EntityPlayer exPlayer, List<EntityItem> loot) {
this.posX = MathHelper.floor_double(exPlayer.posX);
this.posY = MathHelper.floor_double(exPlayer.posY);
this.posZ = MathHelper.floor_double(exPlayer.posZ);
this.world = new WeakReference<World>(world);
this.exPlayer = new WeakReference<EntityPlayer>(exPlayer);
this.stiffId = exPlayer.getGameProfile();
final IChatComponent day = formatDate(world);
final IChatComponent deathCause = exPlayer.func_110142_aN().func_151521_b();
this.cause = new ChatComponentTranslation("openblocks.misc.grave_msg", deathCause, day);
this.loot = ImmutableList.copyOf(loot);
}
private static IChatComponent formatDate(World world) {
final long time = world.getTotalWorldTime();
final String day = String.format("%.1f", time / 24000.0);
final IChatComponent dayComponent = new ChatComponentText(day);
dayComponent.getChatStyle().setColor(EnumChatFormatting.WHITE).setBold(true);
return dayComponent;
}
private void setCommonStoreInfo(NBTTagCompound meta, boolean placed) {
meta.setString(PlayerInventoryStore.TAG_PLAYER_NAME, stiffId.getName());
meta.setString(PlayerInventoryStore.TAG_PLAYER_UUID, stiffId.getId().toString());
meta.setTag("PlayerLocation", TagUtils.store(posX, posY, posZ));
meta.setBoolean("Placed", placed);
}
private boolean tryPlaceGrave(World world, final int x, final int y, final int z, String gravestoneText, IChatComponent deathMessage) {
world.setBlock(x, y, z, OpenBlocks.Blocks.grave, 0, BlockNotifyFlags.ALL);
TileEntity tile = world.getTileEntity(x, y, z);
if (tile == null || !(tile instanceof TileEntityGrave)) {
Log.warn("Failed to place grave @ %d,%d,%d: invalid tile entity: %s(%s)", x, y, z, tile, tile != null? tile.getClass() : "?");
return false;
}
TileEntityGrave grave = (TileEntityGrave)tile;
IInventory loot = getLoot();
if (Config.backupGraves) backupGrave(world, loot, new ExtrasFiller() {
@Override
public void addExtras(NBTTagCompound meta) {
setCommonStoreInfo(meta, true);
meta.setTag("GraveLocation", TagUtils.store(x, y, z));
}
});
Log.info("Grave for (%s,%s) was spawned at (%d,%d,%d)", stiffId.getId(), stiffId.getName(), x, y, z);
grave.setUsername(gravestoneText);
grave.setLoot(loot);
grave.setDeathMessage(deathMessage);
return true;
}
protected IInventory getLoot() {
IInventory loot = new GenericInventory("tmpplayer", false, this.loot.size());
for (EntityItem entityItem : this.loot) {
ItemStack stack = entityItem.getEntityItem();
if (stack != null) ItemDistribution.insertItemIntoInventory(loot, stack.copy());
}
return loot;
}
private boolean trySpawnGrave(EntityPlayer player, World world) {
final Coord location = findLocation(world, player);
String gravestoneText = stiffId.getName();
final GraveSpawnEvent evt = location == null
? new GraveSpawnEvent(player, loot, gravestoneText, cause)
: new GraveSpawnEvent(player, location.x, location.y, location.z, loot, gravestoneText, cause);
if (MinecraftForge.EVENT_BUS.post(evt)) {
Log.warn("Grave event for player %s cancelled, no grave will spawn", stiffId);
return false;
}
if (!evt.hasLocation()) {
Log.warn("No location for grave found, no grave will spawn", stiffId);
return false;
}
final int x = evt.getX();
final int y = evt.getY();
final int z = evt.getZ();
Log.log(debugLevel(), "Grave for %s will be spawned at (%d,%d,%d)", stiffId, x, y, z);
if (Config.graveBase && canSpawnBase(world, player, x, y - 1, z)) {
world.setBlock(x, y - 1, z, Blocks.dirt);
}
return tryPlaceGrave(world, evt.getX(), evt.getY(), evt.getZ(), evt.gravestoneText, evt.clickText);
}
private static boolean canSpawnBase(World world, EntityPlayer player, int x, int y, int z) {
return world.blockExists(x, y, z)
&& world.getBlock(x, y, z).isAir(world, x, y, z)
&& world.canMineBlock(player, x, y, z);
}
private Coord findLocation(World world, EntityPlayer player, GravePlacementChecker checker) {
final int limitedPosY = Math.min(Math.max(posY, Config.minGraveY), Config.maxGraveY);
final int searchSize = Config.graveSpawnRange / 2;
for (Coord c : getSearchOrder(searchSize)) {
final int y = limitedPosY + c.y;
if (y > Config.maxGraveY || y < Config.minGraveY) continue;
final int x = posX + c.x;
final int z = posZ + c.z;
if (checker.canPlace(world, player, x, y, z)) return new Coord(x, y, z);
}
return null;
}
private Coord findLocation(World world, EntityPlayer player) {
Coord location = findLocation(world, player, POLITE);
if (location != null) return location;
if (Config.destructiveGraves) {
Log.warn("Failed to place grave for player %s, going berserk", stiffId);
return findLocation(world, player, BRUTAL);
}
return null;
}
private void backupGrave(World world, IInventory loot, ExtrasFiller filler) {
try {
File backup = PlayerInventoryStore.instance.storeInventory(loot, stiffId.getName(), "grave", world, filler);
Log.log(debugLevel(), "Grave backup for player %s saved to %s", stiffId, backup);
} catch (Throwable t) {
Log.warn("Failed to store grave backup for player %s", stiffId);
}
}
@Override
public void run() {
EntityPlayer player = exPlayer.get();
if (player == null) {
Log.warn("Lost player while placing player %s grave", stiffId);
return;
}
World world = this.world.get();
if (world == null) {
Log.warn("Lost world while placing player %s grave", stiffId);
return;
}
if (!trySpawnGrave(player, world)) {
if (Config.backupGraves) {
IInventory loot = getLoot();
backupGrave(world, loot, new ExtrasFiller() {
@Override
public void addExtras(NBTTagCompound meta) {
setCommonStoreInfo(meta, false);
}
});
}
for (EntityItem drop : loot)
world.spawnEntityInWorld(drop);
}
}
}
private static Level debugLevel() {
return Config.debugGraves? Level.INFO : Level.DEBUG;
}
@SubscribeEvent(priority = EventPriority.LOW, receiveCanceled = true)
public void onPlayerDrops(PlayerDropsEvent event) {
World world = event.entityPlayer.worldObj;
if (world.isRemote) return;
if (Config.debugGraves) dumpDebugInfo(event);
final EntityPlayer player = event.entityPlayer;
if (OpenBlocks.Blocks.grave == null) {
Log.log(debugLevel(), "OpenBlocks graves disabled, not placing (player '%s')", player);
return;
}
if (player instanceof FakePlayer) {
Log.debug("'%s' (%s) is a fake player, grave will not be spawned", player, player.getClass());
return;
}
if (event.isCanceled()) {
Log.warn("Event for player '%s' cancelled, grave will not be spawned", player);
return;
}
final List<EntityItem> drops = event.drops;
if (drops.isEmpty()) {
Log.log(debugLevel(), "No drops from player '%s', grave will not be spawned'", player);
return;
}
final GameRules gameRules = world.getGameRules();
if (gameRules.getGameRuleBooleanValue("keepInventory") ||
!gameRules.getGameRuleBooleanValue(GameRule.SPAWN_GRAVES)) {
Log.log(debugLevel(), "Graves disabled by gamerule (player '%s')", player);
return;
}
final GraveDropsEvent dropsEvent = new GraveDropsEvent(player);
for (EntityItem drop : drops)
dropsEvent.addItem(drop);
if (MinecraftForge.EVENT_BUS.post(dropsEvent)) {
Log.warn("Grave drops event for player '%s' cancelled, grave will not be spawned'", player);
return;
}
final List<EntityItem> graveLoot = Lists.newArrayList();
drops.clear(); // will be rebuilt based from event
for (GraveDropsEvent.ItemAction entry : dropsEvent.drops) {
switch (entry.action) {
case DELETE:
if (Config.debugGraves) Log.info("Item %s is going to be deleted", entry.item);
break;
case DROP:
if (Config.debugGraves) Log.info("Item %s is going to be dropped", entry.item);
drops.add(entry.item);
break;
default:
case STORE:
graveLoot.add(entry.item);
}
}
if (graveLoot.isEmpty()) {
Log.log(debugLevel(), "No grave drops left for player '%s' after event filtering, grave will not be spawned'", player);
return;
}
if (!tryConsumeGrave(player, Iterables.concat(graveLoot, drops))) {
Log.log(debugLevel(), "No grave in drops for player '%s', grave will not be spawned'", player);
return;
}
Log.log(debugLevel(), "Scheduling grave placement for player '%s':'%s' with %d item(s) stored and %d item(s) dropped",
player, player.getGameProfile(), graveLoot.size(), drops.size());
DelayedActionTickHandler.INSTANCE.addTickCallback(world, new GraveCallable(world, player, graveLoot));
}
// TODO: candidate for scripting
private static boolean tryConsumeGrave(EntityPlayer player, Iterable<EntityItem> graveLoot) {
if (!Config.requiresGraveInInv || player.capabilities.isCreativeMode) return true;
final Item graveItem = Item.getItemFromBlock(OpenBlocks.Blocks.grave);
if (graveItem == null) return true;
final Iterator<EntityItem> lootIter = graveLoot.iterator();
while (lootIter.hasNext()) {
final EntityItem drop = lootIter.next();
final ItemStack itemStack = drop.getEntityItem();
if (itemStack != null &&
itemStack.getItem() == graveItem &&
itemStack.stackSize > 0) {
if (--itemStack.stackSize <= 0) {
lootIter.remove();
} else {
drop.setEntityItemStack(itemStack);
}
return true;
}
}
return false;
}
private static void dumpDebugInfo(PlayerDropsEvent event) {
Log.info("Trying to spawn grave for player '%s':'%s'", event.entityPlayer, event.entityPlayer.getGameProfile());
int i = 0;
for (EntityItem e : event.drops)
Log.info("\tGrave drop %d: %s -> %s", i++, e.getClass(), e.getEntityItem());
final ListenerList listeners = event.getListenerList();
try {
int busId = 0;
while (true) {
Log.info("Dumping event %s listeners on bus %d", event.getClass(), busId);
for (IEventListener listener : listeners.getListeners(busId)) {
if (listener instanceof ASMEventHandler) {
try {
final ASMEventHandler handler = (ASMEventHandler)listener;
Object o = ReflectionHelper.getPrivateValue(ASMEventHandler.class, handler, "handler");
Log.info("\t'%s' (handler %s, priority: %s)", handler, o.getClass(), handler.getPriority());
continue;
} catch (Throwable e) {
Log.log(Level.INFO, e, "Exception while getting field");
}
}
Log.info("\t%s", listener.getClass());
}
busId++;
}
} catch (ArrayIndexOutOfBoundsException terribleLoopExitCondition) {}
}
}