aboutsummaryrefslogtreecommitdiff
path: root/src/features/mining
diff options
context:
space:
mode:
Diffstat (limited to 'src/features/mining')
-rw-r--r--src/features/mining/coords.json202
-rw-r--r--src/features/mining/index.js469
-rw-r--r--src/features/mining/metadata.json8
3 files changed, 679 insertions, 0 deletions
diff --git a/src/features/mining/coords.json b/src/features/mining/coords.json
new file mode 100644
index 0000000..246731b
--- /dev/null
+++ b/src/features/mining/coords.json
@@ -0,0 +1,202 @@
+[
+ [
+ -7,
+ 26,
+ -2
+ ],
+ [
+ -15,
+ 26,
+ 31
+ ],
+ [
+ -17,
+ 26,
+ 19
+ ],
+ [
+ 47,
+ 25,
+ 33
+ ],
+ [
+ 36,
+ 26,
+ 45
+ ],
+ [
+ 48,
+ 27,
+ 45
+ ],
+ [
+ 45,
+ 27,
+ -13
+ ],
+ [
+ -38,
+ 26,
+ 21
+ ],
+ [
+ 42,
+ 26,
+ 27
+ ],
+ [
+ 29,
+ 27,
+ -7
+ ],
+ [
+ 22,
+ 26,
+ -15
+ ],
+ [
+ -7,
+ 27,
+ -26
+ ],
+ [
+ -2,
+ 26,
+ -6
+ ],
+ [
+ 43,
+ 27,
+ -21
+ ],
+ [
+ 10,
+ 26,
+ -11
+ ],
+ [
+ 17,
+ 26,
+ 49
+ ],
+ [
+ 19,
+ 26,
+ -17
+ ],
+ [
+ -35,
+ 27,
+ 35
+ ],
+ [
+ 25,
+ 27,
+ 5
+ ],
+ [
+ -37,
+ 24,
+ 46
+ ],
+ [
+ -24,
+ 26,
+ 49
+ ],
+ [
+ -7,
+ 26,
+ 48
+ ],
+ [
+ -14,
+ 27,
+ -24
+ ],
+ [
+ -18,
+ 27,
+ 44
+ ],
+ [
+ -1,
+ 26,
+ -23
+ ],
+ [
+ 41,
+ 25,
+ -37
+ ],
+ [
+ 19,
+ 26,
+ -38
+ ],
+ [
+ -7,
+ 27,
+ 27
+ ],
+ [
+ 42,
+ 26,
+ 19
+ ],
+ [
+ 42,
+ 26,
+ 19
+ ],
+ [
+ -33,
+ 27,
+ 31
+ ],
+ [
+ 6,
+ 27,
+ 25
+ ],
+ [
+ -2,
+ 26,
+ -17
+ ],
+ [
+ -15,
+ 27,
+ 5
+ ],
+ [
+ -20,
+ 27,
+ -12
+ ],
+ [
+ -25,
+ 26,
+ 30
+ ],
+ [
+ 28,
+ 27,
+ -35
+ ],
+ [
+ -19,
+ 27,
+ -22
+ ],
+ [
+ 4,
+ 26,
+ -15
+ ],
+ [
+ 36,
+ 26,
+ 17
+ ]
+] \ No newline at end of file
diff --git a/src/features/mining/index.js b/src/features/mining/index.js
new file mode 100644
index 0000000..0e9c7c0
--- /dev/null
+++ b/src/features/mining/index.js
@@ -0,0 +1,469 @@
+/// <reference types="../../../../CTAutocomplete" />
+/// <reference lib="es2015" />
+import Feature from "../../featureClass/class";
+import { m } from "../../../mappings/mappings";
+import * as stringUtils from "../../utils/stringUtils";
+import * as utils from "../../utils/utils"
+import HudTextElement from "../hud/HudTextElement";
+import LocationSetting from "../settings/settingThings/location";
+import ToggleSetting from "../settings/settingThings/toggle";
+import { numberWithCommas, timeSince2 } from "../../utils/numberUtils";
+import { fetch } from "../../utils/networkUtils";
+import socketConnection from "../../socketConnection";
+import { drawCoolWaypoint } from "../../utils/renderUtils";
+
+class Mining extends Feature {
+ constructor() {
+ super()
+ }
+
+ isInCH() {
+ if (!this.FeatureManager || !this.FeatureManager.features["dataLoader"]) return false
+ return this.FeatureManager.features["dataLoader"].class.area === "Crystal Hollows"
+ }
+
+ onEnable() {
+ this.initVariables()
+
+ this.hudElements = []
+
+ this.guessBalHp = new ToggleSetting("Show bal hp", "This will attempt to show remaining hp on bal (guessed)", true, "bal_hp", this)
+ this.balRespawnHud = new ToggleSetting("Show bal hp and respawn timer HUD", "This will add a HUD element with bal's hp and respawn timer", true, "bal_hud", this)
+ this.balHudElement = new HudTextElement()
+ .setToggleSetting(this.balRespawnHud)
+ .setLocationSetting(new LocationSetting("HUD Location", "Allows you to edit the location of the bal info", "bal_hud_location", this, [10, 50, 1, 1])
+ .requires(this.balRespawnHud)
+ .editTempText("&6Bal&7> &f172/250"))
+ this.hudElements.push(this.balHudElement)
+ this.balPetAlert = new ToggleSetting("Bal pet alert", "Give a notification on screen + sound when you drop a bal pet", true, "bal_alert", this)
+
+ this.showUnlockedGemstoneSlots = new ToggleSetting("Show unlocked gemstone slots", "This will show the unlocked gemstone slots of an item.", true, "unlocked_gemstones", this)
+ this.showContainedGemstoneSlots = new ToggleSetting("Show contained gemstones", "This will show the gemstones currently on an item.", true, "contained_gemstones", this)
+
+
+ this.compactProgressHud = new ToggleSetting("Show compact blocks in the current session", "This will add a HUD element with the compact progress", true, "compact_progress_hud", this)
+ this.compactHudElement = new HudTextElement()
+ .setToggleSetting(this.compactProgressHud)
+ .setLocationSetting(new LocationSetting("HUD Location", "Allows you to edit the location of the compact progress", "compact_progress_location", this, [10, 50, 1, 1])
+ .requires(this.compactProgressHud)
+ .editTempText("&6Compact Session&7> &f12,345"))
+ this.hudElements.push(this.compactHudElement)
+ this.compactProgressHudOnlyWhenMoreThan0 = new ToggleSetting("Only show compact progress when it is above 0", "So that you dont need to disable it when you start doing something else", true, "compact_progress_disable_0", this).requires(this.compactProgressHud)
+
+ this.gemstoneMoneyHud = new ToggleSetting("Show $/h made from gemstone mining", "This will add a HUD element with the gemstone $/h", true, "gemstone_money_hud", this)
+ this.gemstoneMoneyHudElement = new HudTextElement()
+ .setToggleSetting(this.gemstoneMoneyHud)
+ .setLocationSetting(new LocationSetting("HUD Location", "Allows you to edit the location of the gemstone $/h", "gemstone_money_location", this, [10, 60, 1, 1])
+ .requires(this.gemstoneMoneyHud)
+ .editTempText("&6$/h&7> &f$12,345,678\n&6$ made&7> &f$123,456,789\n&6Time tracked&7> &f123m"))
+ this.gemstoneMoneyHudMoneyOnly = new ToggleSetting("Force npc price", "(Eg if u are ironman)", false, "gemstone_money_hud_npc", this).requires(this.gemstoneMoneyHud)
+ this.hudElements.push(this.gemstoneMoneyHudElement)
+
+ this.nextChEvent = new ToggleSetting("Show the current and next crystal hollows event", "(syncs the data between all users in ch)", true, "chevent_hud", this)
+ this.nextChEventElement = new HudTextElement()
+ .setToggleSetting(this.nextChEvent)
+ .setLocationSetting(new LocationSetting("HUD Location", "Allows you to edit the location of the hud element", "chevent_hud_location", this, [10, 70, 1, 1])
+ .requires(this.nextChEvent)
+ .editTempText("&6Event&7> &fGONE WITH THE WIND &7->&f 2X POWDER"))
+ this.hudElements.push(this.nextChEventElement)
+
+
+ this.metalDetectorSolver = new ToggleSetting("Metal detector solver", "", true, "metal_detector_solver", this).contributor("tenios", ["- idea that chests are in same locations every time", "- locations of chests", "- finding offset of divan mines"])
+ this.alertTools = new ToggleSetting("Alert when all 4 tools in inventory", "", true, "alert_tools", this)
+
+ this.seenBalDamages = []
+ this.balHP = 250
+ this.lastBalAlive = 0
+ this.balDespawnDebounce = 0
+
+ this.predictedChestLocations = []
+
+ this.totalCompact = 0
+ this.compactProgress = 0
+ this.compactItems = 0
+
+ this.armourstandClass = Java.type("net.minecraft.entity.item.EntityArmorStand").class
+
+ this.registerEvent("tick", this.tick)
+ this.registerEvent("itemTooltip", this.itemTooltipEvent).registeredWhen(() => this.showContainedGemstoneSlots.getValue() || this.showUnlockedGemstoneSlots.getValue())
+ this.registerEvent("renderWorld", this.renderWorld).registeredWhen(() => this.guessBalHp.getValue() || this.metalDetectorSolver.getValue())
+
+ this.registerChat("&r&c&o&r&6&lRARE DROP! &r&eA Bal Pet dropped!&r", () => {
+ if (this.balPetAlert.getValue()) {
+ World.playSound("random.orb", 1, 1)
+ Client.showTitle("§r§c§o§r§6§lRARE DROP! §r§eA Bal Pet dropped!§r", "", 20, 50, 20)
+ }
+ })
+ this.registerChat("&r&c&oThe bosses outer shell looks to be weakening!&r", () => {
+ this.balHP = 200
+ })
+ this.registerChat("&r&c&oHalf way there! The boss is starting to become weaker!&r", () => {
+ this.balHP = 125
+ })
+ this.registerChat("&r&c&oNearly there! The boss is shaking it can't last much longer!&r", () => {
+ this.balHP = 75
+ })
+ this.registerChat("&r&c&oThe boss looks weak and tired and retreats into the lava...&r", () => {
+ this.balHP = 0
+ })
+
+ let startingTime = -1
+ let money = 0
+ let gemstoneCosts = {}
+ let lastMined = 0
+ this.registerChat("&r&d&lPRISTINE! &r&fYou found &r${*} &r&aFlawed ${type} Gemstone &r&8x${num}&r&f!&r", (type, num, event) => {
+
+ let id = "FLAWED_" + type.toUpperCase() + "_GEM"
+ let number = parseInt(num)
+
+ lastMined = Date.now()
+
+ if (!this.gemstoneMoneyHud.getValue()) return
+
+ if (startingTime === 0) return
+ if (startingTime === -1) {
+ startingTime = 0
+ fetch("https://api.hypixel.net/skyblock/bazaar").json().then(data => {
+ startingTime = Date.now()
+
+ Object.keys(data.products).forEach(id => {
+ if (id.startsWith("FLAWED_")) {
+ gemstoneCosts[id] = Math.max(240, data.products[id].quick_status.sellPrice)
+ if (this.gemstoneMoneyHudMoneyOnly.getValue()) {
+ gemstoneCosts[id] = 240
+ }
+ // console.log(id + ": " + gemstoneCosts[id])
+ }
+ })
+ })
+ return
+ }
+
+ money += gemstoneCosts[id] * number
+
+ let moneyPerHour = Math.floor(money / ((Date.now() - startingTime) / (1000 * 60 * 60)))
+ let moneyMade = Math.floor(money)
+ let timeTracked = timeSince2(startingTime)
+
+ this.gemstoneMoneyHudElement.setText("&6$/h&7> &f$" + numberWithCommas(moneyPerHour) + "\n&6$ made&7> &f$" + numberWithCommas(moneyMade) + "\n&6Time tracked&7> &f" + timeTracked)
+ })
+ this.registerStep(false, 10, () => {
+ if (lastMined && Date.now() - lastMined > 2 * 60000) {
+ money = 0
+ startingTime = -1
+ lastMined = 0
+ this.gemstoneMoneyHudElement.setText("")
+ }
+
+ this.nextChEventElement.setText("&6Event&7> &f" + socketConnection.chEvent.join(" &7->&f "))
+ })
+
+ let lastWorldChange = 0
+
+ this.registerEvent("worldLoad", () => {
+ lastWorldChange = Date.now()
+ })
+
+ this.registerChat("&r&r&r ${spaces}&r&${color}&l${event} ENDED!&r", (spaces, color, event) => {
+ if (Date.now() - lastWorldChange < 5000) return
+ if (!this.isInCH()) return
+
+ socketConnection.sendCHEventData(event.trim(), false)
+ })
+ this.registerChat("&r&r&r ${spaces}&r&${color}&l${event} STARTED!&r", (spaces, color, event) => {
+ if (Date.now() - lastWorldChange < 5000) return
+ if (!this.isInCH()) return
+
+ socketConnection.sendCHEventData(event.trim(), true)
+ })
+
+ this.chestCoords = JSON.parse(FileLib.read("SoopyV2", "features/mining/coords.json"))
+
+ let lastLoc = [0, 0, 0]
+
+ this.baseCoordinates = undefined
+
+ this.lastSearchedForBase = 0
+
+ let ignoreLocation = undefined
+
+ this.registerEvent("worldLoad", () => {
+ lastLoc = [0, 0, 0]
+
+ this.baseCoordinates = undefined
+
+ this.lastSearchedForBase = 0
+
+ this.predictedChestLocations = []
+
+ ignoreLocation = undefined
+ })
+
+ let registerActionBar = this.registerCustom("actionbar", (dist) => {
+ let lapis = false
+ let diamond = false
+ let emerald = false
+ let gold = false
+ Player.getInventory().getItems().forEach(i => {
+ if (i && i.getName().includes("Scavenged Lapis")) {
+ lapis = true
+ }
+ if (i && i.getName().includes("Scavenged Diamond")) {
+ diamond = true
+ }
+ if (i && i.getName().includes("Scavenged Emerald")) {
+ emerald = true
+ }
+ if (i && i.getName().includes("Scavenged Golden")) {
+ gold = true
+ }
+ })
+
+ if (this.alertTools.getValue() && lapis && diamond && gold && emerald) Client.showTitle("§cALL TOOLS", "", 10, 40, 20)
+
+ if (!this.metalDetectorSolver.getValue()) return
+ let distance = parseFloat(dist)
+ if (!this.baseCoordinates) this.findBaseCoordinates();
+
+ if (lastLoc[0] !== Player.getX() || lastLoc[1] !== Player.getY() || lastLoc[2] !== Player.getZ()) {
+ lastLoc = [Player.getX(), Player.getY(), Player.getZ()]
+ return
+ }
+
+ this.predictedChestLocations = []
+
+ this.chestCoords.forEach((coordinates) => {
+ let currentDistance = Math.hypot(Player.getX() - (this.baseCoordinates[0] - coordinates[0]), Player.getY() - (this.baseCoordinates[1] - coordinates[1] + 1), Player.getZ() - (this.baseCoordinates[2] - coordinates[2]))
+
+ if (Math.round(currentDistance * 10) / 10 === distance) {
+
+ if ([this.baseCoordinates[0] - coordinates[0], this.baseCoordinates[1] - coordinates[1], this.baseCoordinates[2] - coordinates[2]].join(",") === ignoreLocation) {
+ ignoreLocation = undefined
+ return
+ }
+
+ if (this.predictedChestLocations.length === 0) {
+ World.playSound("note.pling", 100, 2)
+ }
+
+ this.predictedChestLocations.push([this.baseCoordinates[0] - coordinates[0], this.baseCoordinates[1] - coordinates[1], this.baseCoordinates[2] - coordinates[2]])
+ }
+ });
+ })
+ registerActionBar.trigger.setCriteria('TREASURE: ${rest}').setParameter('contains');
+
+ this.registerChat("&r&aYou found${*}with your &r&cMetal Detector&r&a!&r", () => {
+ if (this.predictedChestLocations[0]) ignoreLocation = this.predictedChestLocations[0].join(",")
+ this.predictedChestLocations = []
+ })
+ }
+
+ findBaseCoordinates() {
+ if (Date.now() - this.lastSearchedForBase < 15000) return;
+ let x = ~~Player.getX();
+ let y = ~~Player.getY();
+ let z = ~~Player.getZ();
+ for (let i = x - 50; i < x + 50; i++) {
+ for (let j = y + 30; j >= y - 30; j--) {
+ for (let k = z - 50; k < z + 50; k++) {
+ if (World.getBlockAt(i, j, k).getType().getID() === 156 && World.getBlockAt(i, j + 13, k).getType().getID() === 166) {
+ this.baseCoordinates = this.getBaseCoordinates(i, j + 13, k);
+ return;
+ }
+ }
+ }
+ }
+ this.lastSearchedForBase = Date.now();
+ }
+
+ getBaseCoordinates(x, y, z) {
+ let loop = true;
+ let posX = x;
+ let posY = y;
+ let posZ = z;
+ if (World.getBlockAt(x, y, z).getType().getID() !== 166) return [x, y, z];
+ while (loop) {
+ loop = false;
+ if (World.getBlockAt(posX + 1, posY, posZ).getType().getID() == 166) {
+ posX++;
+ loop = true;
+ }
+ if (World.getBlockAt(posX, posY - 1, posZ).getType().getID() == 166) {
+ posY--;
+ loop = true;
+ }
+ if (World.getBlockAt(posX, posY, posZ + 1).getType().getID() == 166) {
+ posZ++;
+ loop = true;
+ }
+ }
+ return [posX, posY, posZ];
+ }
+
+ itemTooltipEvent(lore, item, event) {
+ this.addLore(item)
+ }
+
+ /**
+ * @param {Item} item
+ */
+ addLore(item) {
+ if (!item) return
+ if (this.showUnlockedGemstoneSlots.getValue()) {
+ let gems = item.getNBT().getCompoundTag("tag").getCompoundTag("ExtraAttributes").getCompoundTag("gems")
+ if (gems) {
+ let unlockedGems = gems.getTagMap().get("unlocked_slots")
+
+ if (unlockedGems) {
+
+ if (unlockedGems[m.tagCount]() === 0) {
+ utils.addLore(item, ChatLib.addColor("&d&lGemstones Unlocked: &f"), ChatLib.addColor("&cNone!"))
+ } else {
+ let gemstoneString = ""
+
+ for (let i = 0; i < unlockedGems[m.tagCount](); i++) {
+ let gem = String(unlockedGems[m.getStringTagAt](i)).split("_")
+
+ let name = stringUtils.firstLetterCapital(gem[0].toLowerCase())
+
+ gemstoneString += (gemstoneString === "" ? "" : "&7, &a") + name
+ }
+ utils.addLore(item, ChatLib.addColor("&d&lGemstones Unlocked: &f"), ChatLib.addColor("&a" + gemstoneString))
+ }
+ }
+ }
+ }
+ if (this.showContainedGemstoneSlots.getValue()) {
+ let gems = item.getNBT().getCompoundTag("tag").getCompoundTag("ExtraAttributes").getCompoundTag("gems")
+ if (gems) {
+ let unlockedGems = gems.getTagMap()
+
+ let gemStr = ""
+
+ unlockedGems.keySet().forEach(gem => {
+ if (gem !== "unlocked_slots" && !gem.endsWith("_gem")) {
+ gem = gem.split("_")
+
+ let gemName = stringUtils.firstLetterCapital(gems.getString(gem.join("_") + "_gem").toLowerCase()) || stringUtils.firstLetterCapital(gem[0].toLowerCase())
+
+ let name = stringUtils.firstLetterCapital(gems.getString(gem.join("_")).toLowerCase()) + " " + gemName
+
+
+ gemStr += (gemStr === "" ? "" : "&7, &a") + name
+ }
+ });
+
+ if (gemStr !== "") {
+ utils.addLore(item, ChatLib.addColor("&d&lGemstones: &f"), ChatLib.addColor("&a" + gemStr))
+ }
+ }
+ }
+ }
+
+ renderWorld() {
+ if (this.guessBalHp.getValue()) {
+ if (this.balEntity) Tessellator.drawString(this.balHP + "/250", this.balEntity.getX(), this.balEntity.getY() + 12, this.balEntity.getZ())
+ }
+ if (!this.metalDetectorSolver.getValue()) return
+ this.predictedChestLocations.forEach(loc => {
+ drawCoolWaypoint(loc[0], loc[1], loc[2], 0, 255, 0, { name: "TREASURE", phase: true })
+ })
+ }
+
+ tick() {
+ let oldCompactItems = this.compactItems
+ let oldTotalCompact = this.totalCompact
+ this.totalCompact = 0
+ this.compactItems = 0
+ let slots = [0, 1, 2, 3, 4, 5, 6, 7, 8]
+
+ slots.forEach(a => {
+ item = Player.getInventory().getStackInSlot(a)
+ if (!item) return
+ if (item.getNBT()?.getCompoundTag("tag")?.getCompoundTag("ExtraAttributes")?.getInteger("compact_blocks")) {
+ this.compactItems++
+ this.totalCompact += item.getNBT().getCompoundTag("tag").getCompoundTag("ExtraAttributes").getInteger("compact_blocks")
+ }
+ })
+
+ if (oldCompactItems === this.compactItems) {
+ this.compactProgress += this.totalCompact - oldTotalCompact
+ }
+ if (this.compactItems === 0) {
+ this.compactProgress = 0
+ }
+
+ if (this.compactProgress === 0 && this.compactProgressHudOnlyWhenMoreThan0.getValue()) {
+ this.compactHudElement.setText("")
+ } else {
+ this.compactHudElement.setText("&6Compact Session&7> &f" + numberWithCommas(this.compactProgress))
+ }
+
+
+ if (!this.FeatureManager.features["dataLoader"]) return
+ if (this.guessBalHp.getValue() || this.balRespawnHud.getValue()) {
+ if (this.FeatureManager.features["dataLoader"].class.area === "Crystal Hollows" && this.FeatureManager.features["dataLoader"].class.areaFine === "Khazad-dm") {
+
+ this.balEntity = undefined
+ World.getAllEntities().filter(a => a.getName() === "Magma Cube").filter(a => a.getEntity()[m.getSlimeSize]() > 10).forEach((bal) => {
+ //Bal found
+ this.balEntity = bal
+ })
+ if (this.balEntity) {
+ this.balDespawnDebounce = 0
+ if (this.lastBalAlive !== 0) {
+ this.lastBalAlive = 0
+ }
+ World.getAllEntitiesOfType(this.armourstandClass).forEach(e => {
+ if (Math.abs(e.getX() - this.balEntity.getX()) <= 5 && Math.abs(e.getZ() - this.balEntity.getZ()) <= 5 && Math.abs(e.getY() - (this.balEntity.getY() + 12)) <= 5) {
+ if (!this.seenBalDamages.includes(e.getUUID())) {
+ this.balHP--
+ this.seenBalDamages.push(e.getUUID())
+ }
+ }
+ })
+ } else {
+ this.balDespawnDebounce++
+ if (this.balDespawnDebounce > 10) {
+ this.seenBalDamages = []
+ this.balHP = 250
+ if (this.lastBalAlive === 0) this.lastBalAlive = Date.now()
+ }
+ }
+ }
+ }
+
+ if (this.balRespawnHud.getValue() && this.FeatureManager.features["dataLoader"].class.area === "Crystal Hollows" && this.FeatureManager.features["dataLoader"].class.areaFine === "Khazad-dm") {
+ if (this.balEntity) {
+ this.balHudElement.setText("&6Bal&7> &f" + this.balHP + "/250")
+ } else {
+ this.balHudElement.setText("&6Bal&7> &f" + Math.max(0, Math.floor((290000 - (Date.now() - this.lastBalAlive)) / 1000)) + "s")
+ }
+ } else {
+ this.balHudElement.setText("")
+ }
+ }
+
+ initVariables() {
+ this.hudElements = undefined
+ this.guessBalHp = undefined
+ this.balRespawnHud = undefined
+ this.balHudElement = undefined
+ this.balEntity = undefined
+ this.balDespawnDebounce = undefined
+ this.lastBalAlive = undefined
+ this.balHP = undefined
+ this.seenBalDamages = undefined
+ this.armourstandClass = undefined
+ this.balPetAlert = undefined
+ }
+
+ onDisable() {
+ this.hudElements.forEach(h => h.delete())
+ this.initVariables()
+ }
+}
+
+module.exports = {
+ class: new Mining()
+} \ No newline at end of file
diff --git a/src/features/mining/metadata.json b/src/features/mining/metadata.json
new file mode 100644
index 0000000..ce9ed40
--- /dev/null
+++ b/src/features/mining/metadata.json
@@ -0,0 +1,8 @@
+{
+ "name": "Mining",
+ "description": "A bunch of features to assist with mining",
+ "isHidden": false,
+ "isTogglable": true,
+ "defaultEnabled": true,
+ "sortA": 1
+} \ No newline at end of file