From 9d7c98cddbcab38a347fce662d1cd8a88e1a9b2b Mon Sep 17 00:00:00 2001 From: BuildTools Date: Tue, 28 Nov 2023 18:49:21 +0500 Subject: [PATCH] v3.6.4 --- .../excellentenchants/ExcellentEnchants.java | 1 + .../excellentenchants/Placeholders.java | 1 + .../command/EnchantCommand.java | 3 +- .../excellentenchants/config/Config.java | 2 + .../enchantment/EnchantPopulator.java | 5 +- .../enchantment/impl/ExcellentEnchant.java | 17 ++ .../menu/EnchantmentsListMenu.java | 167 ++++++++++++++---- Core/src/main/resources/lang/messages_en.yml | 0 .../src/main/resources/menu/enchants_list.yml | 72 -------- 9 files changed, 156 insertions(+), 112 deletions(-) delete mode 100644 Core/src/main/resources/lang/messages_en.yml delete mode 100644 Core/src/main/resources/menu/enchants_list.yml diff --git a/Core/src/main/java/su/nightexpress/excellentenchants/ExcellentEnchants.java b/Core/src/main/java/su/nightexpress/excellentenchants/ExcellentEnchants.java index ad15f58..fd79fda 100644 --- a/Core/src/main/java/su/nightexpress/excellentenchants/ExcellentEnchants.java +++ b/Core/src/main/java/su/nightexpress/excellentenchants/ExcellentEnchants.java @@ -108,6 +108,7 @@ public class ExcellentEnchants extends NexPlugin { public void loadLang() { this.getLangManager().loadMissing(Lang.class); this.getLangManager().loadEnum(FitItemType.class); + this.getLangManager().loadEnum(ObtainType.class); this.getLang().saveChanges(); } diff --git a/Core/src/main/java/su/nightexpress/excellentenchants/Placeholders.java b/Core/src/main/java/su/nightexpress/excellentenchants/Placeholders.java index dcb3bc0..78215ad 100644 --- a/Core/src/main/java/su/nightexpress/excellentenchants/Placeholders.java +++ b/Core/src/main/java/su/nightexpress/excellentenchants/Placeholders.java @@ -8,6 +8,7 @@ public class Placeholders extends su.nexmedia.engine.utils.Placeholders { public static final String URL_ENGINE_ITEMS = "https://github.com/nulli0n/NexEngine-spigot/wiki/Configuration-Tips#item-sections"; public static final String GENERIC_TYPE = "%type%"; + public static final String GENERIC_NAME = "%name%"; public static final String GENERIC_ITEM = "%item%"; public static final String GENERIC_LEVEL = "%level%"; public static final String GENERIC_AMOUNT = "%amount%"; diff --git a/Core/src/main/java/su/nightexpress/excellentenchants/command/EnchantCommand.java b/Core/src/main/java/su/nightexpress/excellentenchants/command/EnchantCommand.java index 879d3aa..9d59d76 100644 --- a/Core/src/main/java/su/nightexpress/excellentenchants/command/EnchantCommand.java +++ b/Core/src/main/java/su/nightexpress/excellentenchants/command/EnchantCommand.java @@ -9,7 +9,6 @@ import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.NotNull; import su.nexmedia.engine.api.command.AbstractCommand; import su.nexmedia.engine.api.command.CommandResult; -import su.nexmedia.engine.lang.LangManager; import su.nexmedia.engine.utils.*; import su.nexmedia.engine.utils.random.Rnd; import su.nightexpress.excellentenchants.ExcellentEnchants; @@ -90,7 +89,7 @@ public class EnchantCommand extends AbstractCommand { plugin.getMessage(sender == player ? Lang.COMMAND_ENCHANT_DONE_SELF : Lang.COMMAND_ENCHANT_DONE_OTHERS) .replace(Placeholders.forPlayer(player)) .replace(Placeholders.GENERIC_ITEM, ItemUtil.getItemName(item)) - .replace(Placeholders.GENERIC_ENCHANT, LangManager.getEnchantment(enchantment)) + .replace(Placeholders.GENERIC_ENCHANT, EnchantUtils.getLocalized(enchantment)) .replace(Placeholders.GENERIC_LEVEL, NumberUtil.toRoman(level)) .send(sender); } diff --git a/Core/src/main/java/su/nightexpress/excellentenchants/config/Config.java b/Core/src/main/java/su/nightexpress/excellentenchants/config/Config.java index d4df3c3..b8576f0 100644 --- a/Core/src/main/java/su/nightexpress/excellentenchants/config/Config.java +++ b/Core/src/main/java/su/nightexpress/excellentenchants/config/Config.java @@ -19,6 +19,8 @@ import java.util.stream.Stream; public class Config { + public static final String DIR_MENU = "/menu/"; + public static final JOption TASKS_ARROW_TRAIL_TICKS_INTERVAL = JOption.create("Tasks.Arrow_Trail.Tick_Interval", 1L, "Sets how often (in ticks) arrow trail particle effects will be spawned behind the arrow." diff --git a/Core/src/main/java/su/nightexpress/excellentenchants/enchantment/EnchantPopulator.java b/Core/src/main/java/su/nightexpress/excellentenchants/enchantment/EnchantPopulator.java index d093c32..4147d61 100644 --- a/Core/src/main/java/su/nightexpress/excellentenchants/enchantment/EnchantPopulator.java +++ b/Core/src/main/java/su/nightexpress/excellentenchants/enchantment/EnchantPopulator.java @@ -69,10 +69,7 @@ public class EnchantPopulator { Set enchants = EnchantRegistry.getOfTier(tier); enchants.removeIf(enchant -> { - if (enchant.getObtainChance(this.getObtainType()) <= 0D) return true; - if (!enchant.canEnchantItem(this.getItem())) return true; - - return this.getObtainType() == ObtainType.ENCHANTING && (enchant.isTreasure() || enchant.isCursed()); + return !enchant.isObtainable(this.getObtainType()) || !enchant.canEnchantItem(this.getItem()); }); this.candidates.put(tier, enchants); diff --git a/Core/src/main/java/su/nightexpress/excellentenchants/enchantment/impl/ExcellentEnchant.java b/Core/src/main/java/su/nightexpress/excellentenchants/enchantment/impl/ExcellentEnchant.java index 9db2208..fd21885 100644 --- a/Core/src/main/java/su/nightexpress/excellentenchants/enchantment/impl/ExcellentEnchant.java +++ b/Core/src/main/java/su/nightexpress/excellentenchants/enchantment/impl/ExcellentEnchant.java @@ -195,6 +195,13 @@ public abstract class ExcellentEnchant extends Enchantment implements IEnchantme return description; } + @NotNull + public List formatDescription() { + return new ArrayList<>(this.getDescription().stream() + .map(line -> Config.ENCHANTMENTS_DESCRIPTION_FORMAT.get().replace(Placeholders.GENERIC_DESCRIPTION, line)) + .toList()); + } + @NotNull public List formatDescription(int level) { return new ArrayList<>(this.getDescription(level).stream() @@ -202,6 +209,10 @@ public abstract class ExcellentEnchant extends Enchantment implements IEnchantme .toList()); } + public boolean hasConflicts() { + return !this.getConflicts().isEmpty(); + } + @NotNull public Set getConflicts() { return this.getDefaults().getConflicts(); @@ -234,6 +245,12 @@ public abstract class ExcellentEnchant extends Enchantment implements IEnchantme return get != 0 ? this.fineLevel(get, ObtainType.ENCHANTING) : 0; } + public boolean isObtainable(@NotNull ObtainType obtainType) { + if (obtainType == ObtainType.ENCHANTING && (this.isTreasure() || this.isCursed())) return false; + + return this.getObtainChance(obtainType) > 0D; + } + public double getObtainChance(@NotNull ObtainType obtainType) { return this.getDefaults().getObtainChance().getOrDefault(obtainType, 0D); } diff --git a/Core/src/main/java/su/nightexpress/excellentenchants/enchantment/menu/EnchantmentsListMenu.java b/Core/src/main/java/su/nightexpress/excellentenchants/enchantment/menu/EnchantmentsListMenu.java index f4a6aea..4042613 100644 --- a/Core/src/main/java/su/nightexpress/excellentenchants/enchantment/menu/EnchantmentsListMenu.java +++ b/Core/src/main/java/su/nightexpress/excellentenchants/enchantment/menu/EnchantmentsListMenu.java @@ -1,9 +1,12 @@ package su.nightexpress.excellentenchants.enchantment.menu; +import org.bukkit.Material; import org.bukkit.NamespacedKey; import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryType; import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.NotNull; +import su.nexmedia.engine.api.config.JOption; import su.nexmedia.engine.api.config.JYML; import su.nexmedia.engine.api.menu.AutoPaged; import su.nexmedia.engine.api.menu.MenuItemType; @@ -12,49 +15,50 @@ import su.nexmedia.engine.api.menu.click.ItemClick; import su.nexmedia.engine.api.menu.impl.ConfigMenu; import su.nexmedia.engine.api.menu.impl.MenuOptions; import su.nexmedia.engine.api.menu.impl.MenuViewer; +import su.nexmedia.engine.api.menu.item.MenuItem; import su.nexmedia.engine.utils.Colorizer; +import su.nexmedia.engine.utils.ItemReplacer; import su.nexmedia.engine.utils.ItemUtil; import su.nexmedia.engine.utils.PDCUtil; -import su.nexmedia.engine.utils.StringUtil; import su.nightexpress.excellentenchants.ExcellentEnchants; -import su.nightexpress.excellentenchants.Placeholders; +import su.nightexpress.excellentenchants.config.Config; import su.nightexpress.excellentenchants.enchantment.impl.ExcellentEnchant; import su.nightexpress.excellentenchants.enchantment.registry.EnchantRegistry; +import su.nightexpress.excellentenchants.enchantment.type.ObtainType; import su.nightexpress.excellentenchants.enchantment.util.EnchantUtils; import java.util.*; import java.util.function.Predicate; +import java.util.stream.IntStream; + +import static su.nexmedia.engine.utils.Colors2.*; +import static su.nightexpress.excellentenchants.Placeholders.*; public class EnchantmentsListMenu extends ConfigMenu implements AutoPaged { - private static final String PATH = "/menu/enchants_list.yml"; + private static final String FILE = "enchants.yml"; private static final String PLACEHOLDER_CONFLICTS = "%conflicts%"; private static final String PLACEHOLDER_CHARGES = "%charges%"; private static final String PLACEHOLDER_OBTAINING = "%obtaining%"; - private final ItemStack enchantIcon; - private final List enchantLoreConflicts; - private final List enchantLoreCharges; - private final List enchantLoreObtaining; - private final int[] enchantSlots; - private final NamespacedKey keyLevel; private final Map> iconCache; + private String enchantName; + private List enchantLoreMain; + private List enchantLoreConflicts; + private List enchantLoreCharges; + private List enchantLoreObtaining; + private int[] enchantSlots; + public EnchantmentsListMenu(@NotNull ExcellentEnchants plugin) { - super(plugin, JYML.loadOrExtract(plugin, PATH)); + super(plugin, new JYML(plugin.getDataFolder() + Config.DIR_MENU, FILE)); this.keyLevel = new NamespacedKey(plugin, "list_display_level"); this.iconCache = new HashMap<>(); - this.enchantIcon = cfg.getItem("Enchantments.Icon"); - this.enchantLoreConflicts = Colorizer.apply(cfg.getStringList("Enchantments.Lore.Conflicts")); - this.enchantLoreCharges = Colorizer.apply(cfg.getStringList("Enchantments.Lore.Charges")); - this.enchantLoreObtaining = Colorizer.apply(cfg.getStringList("Enchantments.Lore.Obtaining")); - this.enchantSlots = cfg.getIntArray("Enchantments.Slots"); - this.registerHandler(MenuItemType.class) - .addClick(MenuItemType.CLOSE, (viewer, event) -> plugin.runTask(task -> viewer.getPlayer().closeInventory())) + .addClick(MenuItemType.CLOSE, ClickHandler.forClose(this)) .addClick(MenuItemType.PAGE_NEXT, ClickHandler.forNextPage(this)) .addClick(MenuItemType.PAGE_PREVIOUS, ClickHandler.forPreviousPage(this)); @@ -67,6 +71,85 @@ public class EnchantmentsListMenu extends ConfigMenu implemen this.iconCache.clear(); } + // ---------- + + @Override + public boolean isCodeCreation() { + return true; + } + + @Override + protected void loadAdditional() { + this.enchantName = JOption.create("Enchantment.Name", ENCHANTMENT_NAME_FORMATTED).read(cfg); + + this.enchantLoreMain = JOption.create("Enchantment.Lore.Main", + Arrays.asList( + ENCHANTMENT_DESCRIPTION, + DARK_GRAY + "(click to switch level)", + "", + LIGHT_YELLOW + BOLD + "Info:", + LIGHT_YELLOW + "▪ " + LIGHT_GRAY + "Tier: " + LIGHT_YELLOW + ENCHANTMENT_TIER, + LIGHT_YELLOW + "▪ " + LIGHT_GRAY + "Applies to: " + LIGHT_YELLOW + ENCHANTMENT_FIT_ITEM_TYPES, + LIGHT_YELLOW + "▪ " + LIGHT_GRAY + "Levels: " + LIGHT_YELLOW + ENCHANTMENT_LEVEL_MIN + GRAY + " - " + LIGHT_YELLOW + ENCHANTMENT_LEVEL_MAX, + PLACEHOLDER_CHARGES, + PLACEHOLDER_CONFLICTS, + PLACEHOLDER_OBTAINING + )).read(cfg); + + this.enchantLoreConflicts = JOption.create("Enchantment.Lore.Conflicts", + Arrays.asList( + "", + LIGHT_RED + BOLD + "Conflicts:", + LIGHT_RED + "✘ " + LIGHT_GRAY + GENERIC_NAME + )).read(cfg); + + this.enchantLoreCharges = JOption.create("Enchantment.Lore.Charges", + Arrays.asList( + LIGHT_YELLOW + "▪ " + LIGHT_GRAY + "Charges: " + LIGHT_YELLOW + ENCHANTMENT_CHARGES_MAX_AMOUNT + "⚡" + LIGHT_GRAY + " (" + WHITE + ENCHANTMENT_CHARGES_FUEL_ITEM + LIGHT_GRAY + ")" + )).read(cfg); + + this.enchantLoreObtaining = JOption.create("Enchantment.Lore.Obtaining", + Arrays.asList( + "", + LIGHT_GREEN + BOLD + "Obtaining:", + LIGHT_GREEN + "✔ " + LIGHT_GRAY + GENERIC_TYPE + )).read(cfg); + + this.enchantSlots = new JOption("Enchantment.Slots", + (cfg, path, def) -> cfg.getIntArray(path), + () -> IntStream.range(0, 27).toArray() + ).setWriter(JYML::setIntArray).read(cfg); + } + + @Override + @NotNull + protected MenuOptions createDefaultOptions() { + return new MenuOptions(DARK_GRAY + BOLD + "Custom Enchants", 36, InventoryType.CHEST); + } + + @Override + @NotNull + protected List createDefaultItems() { + List list = new ArrayList<>(); + + ItemStack nextPageStack = ItemUtil.createCustomHead("eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZjMyY2E2NjA1NmI3Mjg2M2U5OGY3ZjMyYmQ3ZDk0YzdhMGQ3OTZhZjY5MWM5YWMzYTkxMzYzMzEzNTIyODhmOSJ9fX0="); + ItemUtil.mapMeta(nextPageStack, meta -> { + meta.setDisplayName(WHITE + "Next Page" + LIGHT_GRAY + " (→)"); + }); + + ItemStack prevPageStack = ItemUtil.createCustomHead("eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvODY5NzFkZDg4MWRiYWY0ZmQ2YmNhYTkzNjE0NDkzYzYxMmY4Njk2NDFlZDU5ZDFjOTM2M2EzNjY2YTVmYTYifX19"); + ItemUtil.mapMeta(prevPageStack, meta -> { + meta.setDisplayName(LIGHT_GRAY + "(←) " + WHITE + "Previous Page"); + }); + + list.add(new MenuItem(nextPageStack).setSlots(35).setType(MenuItemType.PAGE_NEXT).setPriority(5)); + list.add(new MenuItem(prevPageStack).setSlots(27).setType(MenuItemType.PAGE_PREVIOUS).setPriority(5)); + + return list; + } + + // ----------- + @Override public void onPrepare(@NotNull MenuViewer viewer, @NotNull MenuOptions options) { super.onPrepare(viewer, options); @@ -118,26 +201,42 @@ public class EnchantmentsListMenu extends ConfigMenu implemen @NotNull private ItemStack buildEnchantIcon(@NotNull ExcellentEnchant enchant, int level) { - ItemStack icon = new ItemStack(this.enchantIcon); - ItemUtil.mapMeta(icon, meta -> { - List lore = meta.getLore(); - if (lore == null) lore = new ArrayList<>(); + ItemStack icon = new ItemStack(Material.ENCHANTED_BOOK); - List conflicts = enchant.getConflicts().isEmpty() ? Collections.emptyList() : new ArrayList<>(this.enchantLoreConflicts); - List conflictNames = enchant.getConflicts().stream().map(EnchantUtils::getLocalized).filter(Objects::nonNull).toList(); - conflicts = StringUtil.replace(conflicts, Placeholders.ENCHANTMENT_NAME, true, conflictNames); + List conflicts = new ArrayList<>(); + if (enchant.hasConflicts()) { + for (String line : this.enchantLoreConflicts) { + if (line.contains(GENERIC_NAME)) { + enchant.getConflicts().stream().map(EnchantUtils::getLocalized).filter(Objects::nonNull).forEach(conf -> { + conflicts.add(line.replace(GENERIC_NAME, conf)); + }); + } + else conflicts.add(line); + } + } - List charges = enchant.isChargesEnabled() ? new ArrayList<>(this.enchantLoreCharges) : Collections.emptyList(); - List obtaining = new ArrayList<>(this.enchantLoreObtaining); + List obtaining = new ArrayList<>(); + for (String line : this.enchantLoreObtaining) { + if (line.contains(GENERIC_TYPE)) { + for (ObtainType obtainType : ObtainType.values()) { + if (enchant.isObtainable(obtainType)) { + obtaining.add(line.replace(GENERIC_TYPE, plugin.getLangManager().getEnum(obtainType))); + } + } + } + else obtaining.add(line); + } - lore = StringUtil.replaceInList(lore, PLACEHOLDER_CONFLICTS, conflicts); - lore = StringUtil.replaceInList(lore, PLACEHOLDER_CHARGES, charges); - lore = StringUtil.replaceInList(lore, PLACEHOLDER_OBTAINING, obtaining); - lore = StringUtil.replace(lore, Placeholders.ENCHANTMENT_DESCRIPTION, true, enchant.getDescription()); - - meta.setLore(lore); - ItemUtil.replace(meta, enchant.getPlaceholders(level).replacer()); - }); + ItemReplacer.create(icon).hideFlags().trimmed() + .setDisplayName(this.enchantName) + .setLore(this.enchantLoreMain) + .replaceLoreExact(PLACEHOLDER_CHARGES, enchant.isChargesEnabled() ? new ArrayList<>(this.enchantLoreCharges) : Collections.emptyList()) + .replaceLoreExact(PLACEHOLDER_CONFLICTS, conflicts) + .replaceLoreExact(PLACEHOLDER_OBTAINING, obtaining) + .replaceLoreExact(ENCHANTMENT_DESCRIPTION, enchant.formatDescription()) + .replace(enchant.getPlaceholders(level)) + .replace(Colorizer::apply) + .writeMeta(); return icon; } diff --git a/Core/src/main/resources/lang/messages_en.yml b/Core/src/main/resources/lang/messages_en.yml deleted file mode 100644 index e69de29..0000000 diff --git a/Core/src/main/resources/menu/enchants_list.yml b/Core/src/main/resources/menu/enchants_list.yml deleted file mode 100644 index 3a5f635..0000000 --- a/Core/src/main/resources/menu/enchants_list.yml +++ /dev/null @@ -1,72 +0,0 @@ -Title: ' #a267f3&lCustom Enchants' -Size: 36 -Inventory_Type: CHEST -Use_Mini_Message: false - -Enchantments: - Icon: - Material: ENCHANTED_BOOK - Name: '%enchantment_name_formatted%' - Lore: - - '#aeb6bf%enchantment_description%' - - '' - - '#bcff9a&lInfo:' - - '#bcff9a▪ #ddeceeTier: #bcff9a%enchantment_tier%' - - '#bcff9a▪ #ddeceeApplies to: #bcff9a%enchantment_fit_item_types%' - - '#bcff9a▪ #ddeceeLevels: #bcff9a%enchantment_level_min% #ddecee- #bcff9a%enchantment_level_max%' - - '%conflicts%' - - '%charges%' - - '%obtaining%' - - '' - - '#a5ff9a&lActions:' - - '#a5ff9a▪ #ddeceeLeft-Click: #a5ff9aSwitch Level' - Lore: - Conflicts: - - '' - - '#ff9a9a[!] #ddeceeCan not be used together with:' - - '#ff9a9a▸ %enchantment_name%' - Charges: - - '' - - '#d39aff&lCharges:' - - '#d39aff◈ #ddeceeMaximum: #d39aff%enchantment_charges_max_amount%⚡' - - '#d39aff◈ #ddeceePer Use: #d39aff-%enchantment_charges_consume_amount%⚡' - - '#d39aff◈ #ddeceePer Recharge: #d39aff+%enchantment_charges_recharge_amount%⚡' - - '#d39aff◈ #ddeceeFuel Item: #d39aff%enchantment_charges_fuel_item%' - Obtaining: - - '' - - '#9af7ff&lObtain Chance:' - - '#9af7ff┃ #ddeceeEnchanting Table: #9af7ff%enchantment_obtain_chance_enchanting%%' - - '#9af7ff┃ #ddeceeVillager Trade: #9af7ff%enchantment_obtain_chance_villager%%' - - '#9af7ff┃ #ddeceeLoot Generation: #9af7ff%enchantment_obtain_chance_loot_generation%%' - - '#9af7ff┃ #ddeceeFishing: #9af7ff%enchantment_obtain_chance_fishing%%' - - '#9af7ff┃ #ddeceeMob Spawning: #9af7ff%enchantment_obtain_chance_mob_spawning%%' - Slots: 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26 - -Content: - return: - Priority: 5 - Item: - Material: PLAYER_HEAD - Head_Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYmU5YWU3YTRiZTY1ZmNiYWVlNjUxODEzODlhMmY3ZDQ3ZTJlMzI2ZGI1OWVhM2ViNzg5YTkyYzg1ZWE0NiJ9fX0= - Name: '#ffee9a(↓) &fClose Menu' - Lore: [] - Slots: 31 - Type: CLOSE - - page_next: - Slots: 35 - Type: PAGE_NEXT - Item: - Material: PLAYER_HEAD - Head_Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZjMyY2E2NjA1NmI3Mjg2M2U5OGY3ZjMyYmQ3ZDk0YzdhMGQ3OTZhZjY5MWM5YWMzYTkxMzYzMzEzNTIyODhmOSJ9fX0= - Name: '#ffee9a(→) &fNext Page' - Priority: 5 - - page_previous: - Slots: 27 - Type: PAGE_PREVIOUS - Item: - Material: PLAYER_HEAD - Head_Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvODY5NzFkZDg4MWRiYWY0ZmQ2YmNhYTkzNjE0NDkzYzYxMmY4Njk2NDFlZDU5ZDFjOTM2M2EzNjY2YTVmYTYifX19 - Name: '#ffee9a(←) &fPrevious Page' - Priority: 5 \ No newline at end of file