/** Copyright (C) <2015> <coolAlias> This file is part of coolAlias' Zelda Sword Skills Minecraft Mod; as such, you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package zeldaswordskills.entity.player; import java.util.ArrayList; import java.util.Collections; import java.util.EnumMap; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.UUID; import net.minecraft.entity.Entity; import net.minecraft.entity.passive.EntityCow; import net.minecraft.entity.passive.EntityHorse; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.init.Items; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; import net.minecraft.util.BlockPos; import net.minecraft.util.ChatComponentTranslation; import net.minecraft.util.MathHelper; import net.minecraft.world.chunk.Chunk; import net.minecraftforge.common.util.Constants; import net.minecraftforge.fml.relauncher.Side; import net.minecraftforge.fml.relauncher.SideOnly; import zeldaswordskills.ZSSAchievements; import zeldaswordskills.ZSSMain; import zeldaswordskills.block.BlockWarpStone; import zeldaswordskills.item.ZSSItems; import zeldaswordskills.network.PacketDispatcher; import zeldaswordskills.network.bidirectional.LearnSongPacket; import zeldaswordskills.network.bidirectional.PlaySoundPacket; import zeldaswordskills.ref.Sounds; import zeldaswordskills.songs.AbstractZeldaSong; import zeldaswordskills.songs.ZeldaSongs; import zeldaswordskills.util.PlayerUtils; import zeldaswordskills.util.SongNote; import zeldaswordskills.util.WarpPoint; import zeldaswordskills.util.WorldUtils; public class ZSSPlayerSongs { private final EntityPlayer player; private final Set<AbstractZeldaSong> knownSongs = new HashSet<AbstractZeldaSong>(); /** Coordinates of most recently activated Warp Stones */ private final Map<BlockWarpStone.EnumWarpSong, WarpPoint> warpPoints = new EnumMap<BlockWarpStone.EnumWarpSong, WarpPoint>(BlockWarpStone.EnumWarpSong.class); /** Song to be learned from the learning GUI is set by the block or entity triggering the GUI */ @SideOnly(Side.CLIENT) public AbstractZeldaSong songToLearn; /** Used to prevent Song GUI from opening during right-click interaction, e.g. when converting Princess Zelda */ public boolean preventSongGui; /** Notes set by the player to play the Scarecrow's Song */ private final List<SongNote> scarecrowNotes = new ArrayList<SongNote>(); /** World time marking one week after player first played the Scarecrow Song */ private long scarecrowTime; /** World time at which this player will next be able to use the Song of Healing */ private long nextSongHealTime; /** UUID of last horse ridden, for playing Epona's Song (persistent across world saves) */ private UUID horseUUID = null; /** Entity ID of last horse ridden, should be more efficient when getting entity */ private int horseId = -1; /** Last chunk coordinates of horse ridden, in case chunk not loaded */ private int horseChunkX, horseChunkZ; /** Unsaved collection of cows affected by Epona's Song used for getting Lon Lon Milk */ private Map<Integer, Long> lonlonCows = new HashMap<Integer, Long>(); /** Set of all NPCs this player has cured */ private final Set<String> curedNpcs = new HashSet<String>(); public ZSSPlayerSongs(EntityPlayer player) { this.player = player; } public static ZSSPlayerSongs get(EntityPlayer player) { return ZSSPlayerInfo.get(player).getPlayerSongs(); } /** * Returns true if the player knows the song */ public boolean isSongKnown(AbstractZeldaSong song) { return knownSongs.contains(song); } /** * Adds the song to the player's repertoire if able. * When called on the server, sends a packet to update the client. * @param notes Only used when learning the Scarecrow Song, otherwise null * @return false if song already known or {@link AbstractZeldaSong#canLearn canLearn} returned false */ public boolean learnSong(AbstractZeldaSong song, List<SongNote> notes) { boolean addSong = true; if (isSongKnown(song)) { return false; } else if (!song.canLearn(player)) { if (!player.worldObj.isRemote) { PlayerUtils.sendTranslatedChat(player, "chat.zss.song.nolearn", new ChatComponentTranslation(song.getTranslationString())); } return false; } else if (song == ZeldaSongs.songScarecrow) { if (notes == null || notes.size() != 8 || !ZeldaSongs.areNotesUnique(notes)) { ZSSMain.logger.warn("Trying to add Scarecrow's Song with invalid list: " + notes); return false; } // first time add notes only if (scarecrowNotes.isEmpty()) { addSong = false; scarecrowNotes.addAll(notes); scarecrowTime = player.worldObj.getTotalWorldTime() + (24000 * 7); } else if (player.worldObj.getTotalWorldTime() > scarecrowTime) { // validate notes before adding the song for good for (int i = 0; i < scarecrowNotes.size() && addSong; ++i) { addSong = (scarecrowNotes.get(i) == notes.get(i)); } } else if (!player.worldObj.isRemote) { // only play chat once PlayerUtils.sendTranslatedChat(player, "chat.zss.song.scarecrow.later"); } } if (addSong) { knownSongs.add(song); player.triggerAchievement(ZSSAchievements.ocarinaSong); if (song == ZeldaSongs.songScarecrow) { player.triggerAchievement(ZSSAchievements.ocarinaScarecrow); } if (knownSongs.size() > 15) { player.triggerAchievement(ZSSAchievements.ocarinaMaestro); } if (!player.worldObj.isRemote) { PacketDispatcher.sendTo(new PlaySoundPacket(Sounds.SUCCESS, 1.0F, 1.0F), (EntityPlayerMP) player); PlayerUtils.sendTranslatedChat(player, "chat.zss.song.learned", new ChatComponentTranslation(song.getTranslationString())); PacketDispatcher.sendTo(new LearnSongPacket(song, notes), (EntityPlayerMP) player); } } return true; } /** * Returns true if the song was removed from the player's repertoire */ public boolean removeSong(AbstractZeldaSong song) { if (knownSongs.contains(song)) { knownSongs.remove(song); if (song == ZeldaSongs.songScarecrow) { scarecrowNotes.clear(); scarecrowTime = 0; } if (player instanceof EntityPlayerMP) { PacketDispatcher.sendTo(new LearnSongPacket(song, true), (EntityPlayerMP) player); } return true; } return false; } /** * Completely wipes all songs (including Scarecrow's Song) from player's repertoire */ public void resetKnownSongs() { knownSongs.clear(); scarecrowNotes.clear(); scarecrowTime = 0; if (player instanceof EntityPlayerMP) { PacketDispatcher.sendTo(new LearnSongPacket(true), (EntityPlayerMP) player); } } /** * Checks the player's known songs to see if any match the notes played * @return The song matching the notes played or null */ public AbstractZeldaSong getKnownSongFromNotes(List<SongNote> notesPlayed) { for (AbstractZeldaSong song : knownSongs) { if (song == ZeldaSongs.songScarecrow) { if (notesPlayed != null && notesPlayed.size() == scarecrowNotes.size()) { for (int i = 0; i < scarecrowNotes.size(); ++i) { if (notesPlayed.get(i) != scarecrowNotes.get(i)) { return null; } } return song; } } else if (song.areCorrectNotes(notesPlayed)) { return song; } } return null; } /** * Call each time a warp stone is activated to set the warp coordinates for that block type */ public void onActivatedWarpStone(BlockPos pos, BlockWarpStone.EnumWarpSong warpSong) { if (warpPoints.containsKey(warpSong.getMetadata())) { warpPoints.remove(warpSong.getMetadata()); } warpPoints.put(warpSong, new WarpPoint(player.worldObj.provider.getDimensionId(), pos)); } /** * Returns the chunk coordinates to warp to for the various warp songs, or null if not yet set */ public WarpPoint getWarpPoint(AbstractZeldaSong song) { return warpPoints.get(BlockWarpStone.EnumWarpSong.bySong(song)); } /** * Returns true if the player can open the Scarecrow Song gui: i.e., * notes have not been set or song not yet learned and enough time has passed, * with appropriate chat messages for failed conditions. */ public boolean canOpenScarecrowGui(boolean addChat) { if (scarecrowNotes.isEmpty()) { return true; } else if (isSongKnown(ZeldaSongs.songScarecrow)) { if (addChat) { PlayerUtils.sendTranslatedChat(player, "chat.zss.song.scarecrow.known"); } return false; } else if (player.worldObj.getTotalWorldTime() < scarecrowTime) { if (addChat) { PlayerUtils.sendTranslatedChat(player, "chat.zss.song.scarecrow.later"); } return false; } else { return true; } } /** * Returns true if the player can currently benefit from the Song of Healing */ public boolean canHealFromSong() { return player.getHealth() < player.getMaxHealth() && player.worldObj.getTotalWorldTime() > nextSongHealTime; } /** * Sets the next time the player can benefit from the Song of Healing */ public void setNextHealTime() { nextSongHealTime = player.worldObj.getTotalWorldTime() + 24000; } /** * Returns a copy of the notes set for the Scarecrow song, if any */ public List<SongNote> getScarecrowNotes() { return Collections.unmodifiableList(scarecrowNotes); } /** * Returns last horse ridden or null if unavailable for some reason */ public EntityHorse getLastHorseRidden() { Entity entity = (horseId < 0 ? null : player.worldObj.getEntityByID(horseId)); if (entity == null && horseUUID != null) { entity = getHorseByUUID(); } // Check horse's last known chunk coordinates if (entity == null) { entity = getHorseFromChunk(horseChunkX, horseChunkZ); int n = 1; // search n surrounding chunks if horse is still null for (int i = -n; entity == null && i <= n; ++i) { for (int k = -n; entity == null && k <= n; ++k) { if (i != 0 || k != 0) { entity = getHorseFromChunk(horseChunkX + i, horseChunkZ + k); } } } } if (entity instanceof EntityHorse && entity.isEntityAlive()) { return (EntityHorse) entity; } // don't reset id fields, as horse may simply be in an unloaded chunk return null; } /** * Searches for horse by UUID; if found, sets horseId */ private Entity getHorseByUUID() { if (horseUUID == null) { return null; } Entity entity = WorldUtils.getEntityByUUID(player.worldObj, horseUUID); if (entity instanceof EntityHorse) { horseId = entity.getEntityId(); } return entity; } /** * Loads the chunk if necessary and searches for the player's horse by UUID */ private Entity getHorseFromChunk(int chunkX, int chunkZ) { Chunk chunk = player.worldObj.getChunkFromChunkCoords(chunkX, chunkZ); if (chunk != null && chunk.isLoaded()) { return getHorseByUUID(); } return null; } /** * Sets the horse as this player's last horse ridden, for Epona's Song */ public void setHorseRidden(EntityHorse horse) { if (horse.getEntityId() == horseId) { setHorseCoordinates(horse); } else if (horse.isTame() && horse.getOwnerId().equals(player.getUniqueID().toString())) { this.horseId = horse.getEntityId(); this.horseUUID = horse.getPersistentID(); setHorseCoordinates(horse); } } /** * Sets last ridden horse's last chunk coordinates */ private void setHorseCoordinates(EntityHorse horse) { this.horseChunkX = (MathHelper.floor_double(horse.posX) >> 4); this.horseChunkZ = (MathHelper.floor_double(horse.posZ) >> 4); } /** * Marks the cow as affected by Epona's Song for purposes of getting Lon Lon Milk */ public void addLonLonCow(EntityCow cow) { NBTTagCompound data = cow.getEntityData(); // Quick and dirty - not really worth a new IEEP if (!cow.isChild() && (!data.hasKey("zss_lon_milk") || cow.worldObj.getWorldTime() > data.getLong("zss_lon_milk"))) { lonlonCows.put(cow.getEntityId(), cow.worldObj.getWorldTime() + 6000); // 5 minutes to milk the cow, using regular world time cow.worldObj.setEntityState(cow, (byte) 18); // show the lovely hearts } } /** * Returns true if the player was able to successfully acquire Lon Lon Milk from this cow */ public boolean milkLonLonCow(EntityPlayer player, EntityCow cow) { boolean match = false; Iterator<Entry<Integer, Long>> iterator = lonlonCows.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry<Integer, Long> entry = iterator.next(); if (cow.worldObj.getWorldTime() > entry.getValue()) { iterator.remove(); // remove expired entries } else if (entry.getKey() == cow.getEntityId()) { match = true; iterator.remove(); // also remove the matched entry } } ItemStack stack = player.getHeldItem(); if (match && stack != null && stack.getItem() == Items.glass_bottle && !cow.isChild()) { NBTTagCompound data = cow.getEntityData(); if (!data.hasKey("zss_lon_milk") || cow.worldObj.getWorldTime() > data.getLong("zss_lon_milk")) { if (stack.stackSize > 1) { --stack.stackSize; PlayerUtils.addItemToInventory(player, new ItemStack(ZSSItems.lonlonMilk)); } else { player.setCurrentItemOrArmor(0, new ItemStack(ZSSItems.lonlonMilk)); } data.setLong("zss_lon_milk", cow.worldObj.getWorldTime() + 24000); // once per day, regular world time return true; } } return false; } /** * Returns whether this player has already cured an Npc with the given name */ public boolean hasCuredNpc(String name) { return curedNpcs.contains(name); } /** * Call after curing an Npc to save that information * @return false if the Npc has already been marked as cured by this player */ public boolean onCuredNpc(String name) { return curedNpcs.add(name); } public void saveNBTData(NBTTagCompound compound) { NBTTagList songs = new NBTTagList(); for (AbstractZeldaSong song : knownSongs) { NBTTagCompound tag = new NBTTagCompound(); // using unlocalized name instead of ordinal in case enum order/size ever changes tag.setString("song", song.getUnlocalizedName()); songs.appendTag(tag); } compound.setTag("KnownSongs", songs); if (!scarecrowNotes.isEmpty()) { int[] notes = new int[scarecrowNotes.size()]; for (int i = 0; i < scarecrowNotes.size(); ++i) { notes[i] = scarecrowNotes.get(i).ordinal(); } compound.setIntArray("ScarecrowNotes", notes); } compound.setLong("ScarecrowTime", scarecrowTime); compound.setLong("NextSongHealTime", nextSongHealTime); if (horseUUID != null) { compound.setLong("HorseUUIDMost", horseUUID.getMostSignificantBits()); compound.setLong("HorseUUIDLeast", horseUUID.getLeastSignificantBits()); compound.setInteger("HorseChunkX", horseChunkX); compound.setInteger("HorseChunkZ", horseChunkZ); } if (!warpPoints.isEmpty()) { NBTTagList warpList = new NBTTagList(); for (BlockWarpStone.EnumWarpSong warpSong : warpPoints.keySet()) { WarpPoint warp = warpPoints.get(warpSong); if (warp != null) { NBTTagCompound warpTag = warp.writeToNBT(); warpTag.setInteger("WarpKey", warpSong.getMetadata()); warpList.appendTag(warpTag); } else { ZSSMain.logger.warn("NULL warp point stored in map with key " + warpSong.getMetadata()); } } compound.setTag("WarpList", warpList); } if (!curedNpcs.isEmpty()) { NBTTagList npcs = new NBTTagList(); for (String name : curedNpcs) { NBTTagCompound npc = new NBTTagCompound(); npc.setString("NpcName", name); npcs.appendTag(npc); } compound.setTag("CuredNpcs", npcs); } } public void loadNBTData(NBTTagCompound compound) { NBTTagList songs = compound.getTagList("KnownSongs", Constants.NBT.TAG_COMPOUND); knownSongs.clear(); for (int i = 0; i < songs.tagCount(); ++i) { NBTTagCompound tag = songs.getCompoundTagAt(i); AbstractZeldaSong song = ZeldaSongs.getSongByName(tag.getString("song")); if (song != null) { knownSongs.add(song); } } if (compound.hasKey("ScarecrowNotes")) { try { int[] notes = compound.getIntArray("ScarecrowNotes"); for (int n : notes) { scarecrowNotes.add(SongNote.values()[n]); } } catch (Exception e) { ZSSMain.logger.error("Exception thrown while loading Scarecrow's Song notes: " + e.getMessage()); } } scarecrowTime = compound.getLong("ScarecrowTime"); nextSongHealTime = compound.getLong("NextHealSongTime"); if (compound.hasKey("HorseChunkX") && compound.hasKey("HorseChunkZ")) { horseChunkX = compound.getInteger("HorseChunkX"); horseChunkZ = compound.getInteger("HorseChunkZ"); } if (compound.hasKey("HorseUUIDMost") && compound.hasKey("HorseUUIDLeast")) { horseUUID = new UUID(compound.getLong("HorseUUIDMost"), compound.getLong("HorseUUIDLeast")); } if (compound.hasKey("WarpList")) { NBTTagList warpList = compound.getTagList("WarpList", Constants.NBT.TAG_COMPOUND); for (int i = 0; i < warpList.tagCount(); ++i) { NBTTagCompound warpTag = warpList.getCompoundTagAt(i); WarpPoint warp = WarpPoint.readFromNBT(warpTag); warpPoints.put(BlockWarpStone.EnumWarpSong.byMetadata(warpTag.getInteger("WarpKey")), warp); } } if (compound.hasKey("CuredNpcs")) { NBTTagList npcs = compound.getTagList("CuredNpcs", Constants.NBT.TAG_COMPOUND); for (int i = 0; i < npcs.tagCount(); ++i) { NBTTagCompound npc = npcs.getCompoundTagAt(i); curedNpcs.add(npc.getString("NpcName")); } } } }