diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fc8bc18 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +target/Progressive-Java-Client.jar diff --git a/README.md b/README.md index 6c4e36e..8c057a3 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,41 @@ -# Java 254 Client +# Progressive Java Client -Full RS2 build-254 client With Native OpenGL-lwjgl +Early Java desktop client for a LostCityRS / 2004SP revision 254+ servers. -## Build - -```bat -build.bat -``` - -Or with PowerShell: - -```powershell -.\build.ps1 -``` +## Current status -Requires JDK 17+. +Implemented so far: -The generated `target/Progressive-Java-Client.jar` is standalone: it contains the -required libraries and native binaries, and can be copied elsewhere by itself. +- Revision `254` configuration +- Swing desktop window +- 765x503 game canvas +- 50 TPS game loop +- WebSocket connection to the game server +- `/crc` cache checksum loading +- 254 login handshake using opcodes `14`, `16`, and `18` +- ISAAC cipher setup +- Client/server protocol constants +- Test login UI -You can also build with Maven: +Not implemented yet: -```powershell -mvn clean package -``` +- 117HD-style rendering port +- HD terrain lighting and shading +- HD textures and normal maps +- Water, lava, and animated surface effects +- Improved skybox/fog/atmosphere rendering +- Modern GPU-based scene rendering +- 60 FPS animation/camera support while keeping server TPS compatible +- True resizable and fullscreen client modes +- Original 765x503 fixed-mode compatibility -The standalone JAR is written to `target/`. +## Requirements -## Run +- Java 17+ +- Maven +- A compatible LostCity/2004Scape revision 254 server running locally or remotely (Progressive strongly advised) -```bat -run.bat -``` - -Using `run.bat` is recommended because it supplies the Java options used by -LWJGL and starts the client with the default server settings. - -You can also launch the generated JAR or a GitHub Release JAR directly: +## Build -```powershell -java -jar Progressive-Java-Client.jar -``` +```bash +mvn package diff --git a/cache/jingle1.mid b/cache/jingle1.mid new file mode 100644 index 0000000..60c7c43 Binary files /dev/null and b/cache/jingle1.mid differ diff --git a/cache/jingle2.mid b/cache/jingle2.mid new file mode 100644 index 0000000..8e344b0 Binary files /dev/null and b/cache/jingle2.mid differ diff --git a/cache/jingle3.mid b/cache/jingle3.mid new file mode 100644 index 0000000..530208c Binary files /dev/null and b/cache/jingle3.mid differ diff --git a/cache/jingle4.mid b/cache/jingle4.mid new file mode 100644 index 0000000..8e344b0 Binary files /dev/null and b/cache/jingle4.mid differ diff --git a/cache/sound0.wav b/cache/sound0.wav new file mode 100644 index 0000000..b0efe24 Binary files /dev/null and b/cache/sound0.wav differ diff --git a/cache/sound1.wav b/cache/sound1.wav new file mode 100644 index 0000000..b046718 Binary files /dev/null and b/cache/sound1.wav differ diff --git a/cache/sound2.wav b/cache/sound2.wav new file mode 100644 index 0000000..b046718 Binary files /dev/null and b/cache/sound2.wav differ diff --git a/cache/sound3.wav b/cache/sound3.wav new file mode 100644 index 0000000..b0efe24 Binary files /dev/null and b/cache/sound3.wav differ diff --git a/cache/sound4.wav b/cache/sound4.wav new file mode 100644 index 0000000..b046718 Binary files /dev/null and b/cache/sound4.wav differ diff --git a/src/main/java/com/gradwahl/rs254/discord/DiscordRichPresence.java b/src/main/java/com/gradwahl/rs254/discord/DiscordRichPresence.java new file mode 100644 index 0000000..b3ecb24 --- /dev/null +++ b/src/main/java/com/gradwahl/rs254/discord/DiscordRichPresence.java @@ -0,0 +1,127 @@ +package com.gradwahl.rs254.discord; + +import java.io.Closeable; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; + +public final class DiscordRichPresence implements Closeable { + private static final int OP_HANDSHAKE = 0; + private static final int OP_FRAME = 1; + private static final int OP_CLOSE = 2; + + private final String appId; + private final ExecutorService worker = Executors.newSingleThreadExecutor(r -> { + Thread t = new Thread(r, "discord-rpc"); + t.setDaemon(true); + return t; + }); + private final AtomicInteger nonce = new AtomicInteger(); + + private RandomAccessFile pipe; + private long startTime; + private boolean connected; + + public DiscordRichPresence(String appId) { + this.appId = appId; + } + + public void connect() { + worker.execute(() -> { + disconnectNow(); + startTime = System.currentTimeMillis() / 1000L; + for (int i = 0; i < 10; i++) { + RandomAccessFile candidate = null; + try { + candidate = new RandomAccessFile("\\\\.\\pipe\\discord-ipc-" + i, "rw"); + writePacket(candidate, OP_HANDSHAKE, "{\"v\":1,\"client_id\":\"" + escape(appId) + "\"}"); + readPacket(candidate); + pipe = candidate; + connected = true; + return; + } catch (IOException ignored) { + if (candidate != null) { + try { + candidate.close(); + } catch (IOException ignoredClose) { + } + } + } + } + System.err.println("[discord] could not connect to Discord IPC (is Discord running?)"); + }); + } + + public void updateActivity(String details, String state) { + worker.execute(() -> { + if (!connected || pipe == null) return; + String activity = "{\"details\":\"" + escape(details) + "\"" + + ",\"state\":\"" + escape(state) + "\"" + + ",\"timestamps\":{\"start\":" + startTime + "}" + + ",\"assets\":{\"large_image\":\"logo\",\"large_text\":\"LostCity RSPS\"}}"; + String payload = "{\"cmd\":\"SET_ACTIVITY\"" + + ",\"args\":{\"pid\":" + ProcessHandle.current().pid() + ",\"activity\":" + activity + "}" + + ",\"nonce\":\"" + nonce.incrementAndGet() + "\"}"; + try { + writePacket(pipe, OP_FRAME, payload); + readPacket(pipe); + } catch (IOException e) { + disconnectNow(); + } + }); + } + + public void disconnect() { + worker.execute(this::disconnectNow); + } + + private void disconnectNow() { + RandomAccessFile f = pipe; + pipe = null; + connected = false; + if (f == null) return; + try { + writePacket(f, OP_CLOSE, "{}"); + } catch (IOException ignored) { + } + try { + f.close(); + } catch (IOException ignored) { + } + } + + private static void writePacket(RandomAccessFile f, int opcode, String payload) throws IOException { + byte[] data = payload.getBytes(StandardCharsets.UTF_8); + ByteBuffer header = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN); + header.putInt(opcode); + header.putInt(data.length); + f.write(header.array()); + f.write(data); + } + + private static void readPacket(RandomAccessFile f) throws IOException { + byte[] header = new byte[8]; + f.readFully(header); + int length = ByteBuffer.wrap(header, 4, 4).order(ByteOrder.LITTLE_ENDIAN).getInt(); + if (length > 0 && length <= 65_535) { + byte[] body = new byte[length]; + f.readFully(body); + } + } + + private static String escape(String s) { + if (s == null) return ""; + return s.replace("\\", "\\\\").replace("\"", "\\\""); + } + + @Override + public void close() { + disconnect(); + worker.shutdownNow(); + } +} diff --git a/src/main/java/com/gradwahl/rs254/gl/GLRenderer.java b/src/main/java/com/gradwahl/rs254/gl/GLRenderer.java index bbfd757..fc9230e 100644 --- a/src/main/java/com/gradwahl/rs254/gl/GLRenderer.java +++ b/src/main/java/com/gradwahl/rs254/gl/GLRenderer.java @@ -29,6 +29,7 @@ import java.util.Map; import java.util.Set; import java.util.TreeMap; +import java.util.prefs.Preferences; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.imageio.ImageIO; @@ -37,10 +38,13 @@ import javax.swing.text.AttributeSet; import javax.swing.text.DefaultCaret; import javax.swing.text.Element; +import javax.swing.text.StyleConstants; import javax.swing.text.html.HTML; import javax.swing.text.html.HTMLDocument; import javax.swing.text.html.HTMLEditorKit; +import com.gradwahl.rs254.discord.DiscordRichPresence; + import static org.lwjgl.glfw.Callbacks.glfwFreeCallbacks; import static org.lwjgl.glfw.GLFW.*; import static org.lwjgl.opengl.GL33.*; @@ -71,10 +75,19 @@ public final class GLRenderer implements TriangleRenderer { private static final int SIDEBAR_ROW_H = 36; private static final int SIDEBAR_TABS = 6; private static final int TAB_ICON_SIZE = 22; + private static final String DISCORD_APP_ID = "1507449981689270283"; + private static final String[] AFK_LABELS = { + "90 Seconds", "2 Minutes", "5 Minutes", "10 Minutes", "30 Minutes", "Never" + }; + private static final int[] AFK_CYCLES = { + 4_500, 6_000, 15_000, 30_000, 90_000, -1 + }; private static final int LOSTHQ_READER_W = SIDEBAR_PANEL_W - 8; - private static final int LOSTHQ_READER_WIDE = 450; // wider canvas to capture horizontal overflow + private static final int LOSTHQ_READER_WIDE = 1280; // wide backing canvas for drag-panning occasional overflow private static final int HSCROLL_H = 10; // horizontal scrollbar height - private static final int LOSTHQ_QUEST_COMPLETE_IMAGE_W = 284; + private static final int LOSTHQ_BODY_W = LOSTHQ_READER_W - 12; + private static final int LOSTHQ_QUEST_COMPLETE_IMAGE_W = LOSTHQ_READER_W; + private static final int LOSTHQ_CONTENT_IMAGE_W = LOSTHQ_BODY_W - 4; private static final int[] XP_TABLE = buildXpTable(); private static final java.util.Map SKILL_UNLOCKS = buildSkillUnlocks(); private static java.util.Map buildSkillUnlocks() { @@ -272,6 +285,7 @@ private static int[] buildXpTable() { "Fletching", "Fishing", "Firemaking", "Crafting", "Smithing", "Mining", "Herblore", "Agility", "Thieving", "Runecrafting" }; + private static final int HSCORE_SKILL_COLUMNS = 3; // LostHQ toolkit destinations, kept in the same order as the website menu. private static final String[] LOSTHQ_ITEMS = { @@ -464,14 +478,27 @@ void main() { private static final java.awt.Font UI_FONT_TINY = INTER_MEDIUM.deriveFont(7f); // RuneLite-style client sidebar + private static final Preferences SETTINGS_PREFS = Preferences.userNodeForPackage(GLRenderer.class); + private static final DiscordRichPresence DISCORD_RPC = new DiscordRichPresence(DISCORD_APP_ID); + public static volatile int afkTimeoutCycles = AFK_CYCLES[0]; + public static volatile boolean shiftKeyDown; + public static volatile boolean settingShiftDropInventory; + public static volatile boolean settingShiftTakeGround; + public static volatile boolean settingShiftAttackNpc; + public static volatile boolean settingShiftPickpocketNpc; + public static volatile boolean settingShiftBankNpc; + public static volatile boolean settingShiftUseQuicklyBankBooth; + public static volatile boolean settingShiftExamineAnything; + public static volatile boolean settingDiscordRichPresence; + public static volatile boolean settingFps60Enabled; private boolean sidebarOpen; private int sidebarTab; private boolean sidebarGpuEnabled = true; - private boolean sidebarFpsEnabled = true; + private boolean sidebarFpsEnabled = SETTINGS_PREFS.getBoolean("fps60", false); private boolean sidebarRoofsEnabled = true; private boolean settingsFullscreen = false; - private boolean settingsShiftClick = false; - private boolean settingsDiscordRp = false; + private boolean settingsAfkDropdownOpen; + private int settingsAfkIndex = SETTINGS_PREFS.getInt("afkIndex", 0); // XP session tracking — updated by Client when XP packets arrive public static final long[] xpSessionGains = new long[25]; @@ -505,6 +532,10 @@ void main() { private volatile JEditorPane lostHqPage; private volatile URI lostHqPageUri; private volatile String lostHqHtml; + // Click-to-zoom overlay for quest-reward parchment images. + // When non-null, a full-screen modal shows the image at high resolution; + // any mouse click dismisses it. + private volatile BufferedImage lostHqZoomImage; private final Set lostHqCompletedSteps = new HashSet<>(); private final Object lostHqProgressLock = new Object(); private boolean lostHqProgressRefreshScheduled; @@ -512,6 +543,7 @@ void main() { private long lostHqProgressPageId; private volatile int lostHqScrollY; private volatile int lostHqScrollX; + private volatile int lostHqContentW = LOSTHQ_READER_W; private boolean lostHqDragging; private boolean lostHqDragMoved; private int lostHqDragLastY; @@ -573,6 +605,7 @@ public GLRenderer(int screenW, int screenH) { this.maxUiW = screenW + SIDEBAR_PANEL_W + SIDEBAR_RAIL_W; this.windowW = screenW + SIDEBAR_RAIL_W; this.windowH = screenH; + loadSettings(); } // ------------------------------------------------------------------------- @@ -647,6 +680,7 @@ public void init() { setupUIPass(); setupCallbacks(); + updateWindowSizeLimits(); updateOutputViewport(); glfwShowWindow(window); } @@ -704,6 +738,7 @@ private static int icoInt(byte[] b, int off) { /** Attach a GameShell so GLFW input events are forwarded to the game. */ public void setGameShell(GameShell gs) { this.shell = gs; + gs.setFramerate(50); } @Override @@ -767,6 +802,7 @@ public void destroy() { if (uiDirectBuf != null) MemoryUtil.memFree(uiDirectBuf); if (sidebarNativeDirect != null) MemoryUtil.memFree(sidebarNativeDirect); hiscoresFetcher.shutdownNow(); + DISCORD_RPC.disconnect(); glfwFreeCallbacks(window); glfwDestroyWindow(window); glfwTerminate(); @@ -968,6 +1004,11 @@ private void setupUIPass() { private void drawUIOverlay() { if (PixMap.uiBuffer == null) return; + // Draw the quest-reward zoom overlay into uiBuffer BEFORE upload so the + // overlay appears on the same frame the user clicked. It writes only to + // the 3D viewport area, leaving the chatbox / inventory / sidebar alone. + drawLostHqZoomOverlay(); + // Upload game UI pixels. drawSidebar() no longer touches uiBuffer, so no backup needed. glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, uiTex); @@ -1212,8 +1253,12 @@ private void drawTabIcon(int id, int cx, int cy, boolean active) { if (img != null) { int x = cx - TAB_ICON_SIZE / 2; int y = cy - TAB_ICON_SIZE / 2; + boolean glow = id == 1 && xpScreenEnabled; + if (glow) { + drawIconAlphaGlow(img, x, y, TAB_ICON_SIZE, TAB_ICON_SIZE); + } java.awt.Composite prev = sg.getComposite(); - sg.setComposite(java.awt.AlphaComposite.getInstance(java.awt.AlphaComposite.SRC_OVER, active ? 1.0f : 0.55f)); + sg.setComposite(java.awt.AlphaComposite.getInstance(java.awt.AlphaComposite.SRC_OVER, active || glow ? 1.0f : 0.55f)); sg.drawImage(img, x, y, TAB_ICON_SIZE, TAB_ICON_SIZE, null); sg.setComposite(prev); } else { @@ -1221,6 +1266,53 @@ private void drawTabIcon(int id, int cx, int cy, boolean active) { } } + private void drawIconAlphaGlow(BufferedImage img, int x, int y, int w, int h) { + BufferedImage scaled = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); + java.awt.Graphics2D g = scaled.createGraphics(); + try { + g.setRenderingHint(java.awt.RenderingHints.KEY_INTERPOLATION, + java.awt.RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR); + g.drawImage(img, 0, 0, w, h, null); + } finally { + g.dispose(); + } + + BufferedImage yellowMask = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); + BufferedImage whiteMask = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); + for (int py = 0; py < h; py++) { + for (int px = 0; px < w; px++) { + int alpha = (scaled.getRGB(px, py) >>> 24) & 0xFF; + if (alpha == 0) continue; + yellowMask.setRGB(px, py, ((alpha * 190 / 255) << 24) | 0x73738B); + whiteMask.setRGB(px, py, ((alpha * 150 / 255) << 24) | 0xFFFFFF); + } + } + + java.awt.Composite prev = sg.getComposite(); + try { + sg.setComposite(java.awt.AlphaComposite.SrcOver); + for (int r = 4; r >= 1; r--) { + float opacity = 0.12f + (4 - r) * 0.05f; + sg.setComposite(java.awt.AlphaComposite.getInstance(java.awt.AlphaComposite.SRC_OVER, opacity)); + sg.drawImage(yellowMask, x - r, y, w, h, null); + sg.drawImage(yellowMask, x + r, y, w, h, null); + sg.drawImage(yellowMask, x, y - r, w, h, null); + sg.drawImage(yellowMask, x, y + r, w, h, null); + sg.drawImage(yellowMask, x - r, y - r, w, h, null); + sg.drawImage(yellowMask, x + r, y - r, w, h, null); + sg.drawImage(yellowMask, x - r, y + r, w, h, null); + sg.drawImage(yellowMask, x + r, y + r, w, h, null); + } + sg.setComposite(java.awt.AlphaComposite.getInstance(java.awt.AlphaComposite.SRC_OVER, 0.55f)); + sg.drawImage(whiteMask, x - 1, y, w, h, null); + sg.drawImage(whiteMask, x + 1, y, w, h, null); + sg.drawImage(whiteMask, x, y - 1, w, h, null); + sg.drawImage(whiteMask, x, y + 1, w, h, null); + } finally { + sg.setComposite(prev); + } + } + private void drawSidebar() { int railX = sidebarRailX(); int panelX = sidebarPanelX(); @@ -1243,7 +1335,7 @@ private void drawSidebar() { fillUiRect(railX, 0, 1, screenH, 0xFF363636); for (int index = 0; index < SIDEBAR_TABS - 1; index++) { int y = index * SIDEBAR_ROW_H; - boolean active = (index == 1) ? xpScreenEnabled : (sidebarOpen && sidebarTab == index); + boolean active = index != 1 && sidebarOpen && sidebarTab == index; if (active) { fillUiRect(railX + 1, y, SIDEBAR_RAIL_W - 1, SIDEBAR_ROW_H, 0xFF3F3523); fillUiRect(railX + 1, y, 3, SIDEBAR_ROW_H, 0xFFE89E14); @@ -1293,7 +1385,7 @@ private void drawHiscoresPanel(int x) { // Skill selector buttons — 2 per row, 10 rows, full skill names at tiny size int panelW = sidebarPanelW(); - int columns = panelW >= 190 ? 2 : 1; + int columns = HSCORE_SKILL_COLUMNS; int buttonW = (panelW - 20 - (columns - 1) * 4) / columns; int buttonRows = (HSCORE_SKILL_LABEL.length + columns - 1) / columns; for (int i = 0; i < HSCORE_SKILL_LABEL.length; i++) { @@ -1304,7 +1396,8 @@ private void drawHiscoresPanel(int x) { boolean sel = (i == hiscoresSkill); fillUiRect(bx, by, buttonW, 11, sel ? 0xFF3F3523 : 0xFF2A2A2A); if (sel) fillUiRect(bx, by, buttonW, 1, 0xFFE89E14); - drawUiText(HSCORE_SKILL_LABEL[i], bx + 4, by + 2, 0, sel ? 0xFFE89E14 : 0xFF999999); + drawUiTextFittedFull(HSCORE_SKILL_LABEL[i], bx + 4, by + 2, + buttonW - 8, 0, sel ? 0xFFE89E14 : 0xFF999999); } int afterButtons = 52 + buttonRows * 13 + 3; @@ -1680,6 +1773,7 @@ private void loadLostHqPage(URI uri) { resetLostHqProgress(); lostHqScrollY = 0; lostHqScrollX = 0; + lostHqContentW = LOSTHQ_READER_W; lostHqSearchQuery = ""; lostHqSearchFocused = false; lostHqSearchResults = List.of(); @@ -1715,10 +1809,13 @@ else if (!lostHqSearchIsNpc && lostHqItemData == null) lostHqLauncher.execute(() -> { try { String html; + // Prefer bundled HTML — it has local fixes (column widths, link text, + // image sizes) applied to fit the narrow sidebar panel. Fall back to + // the live site only if the page isn't bundled. try { - html = fetchLostHqHtml(uri); - } catch (Exception networkEx) { html = loadBundledHtml(uri); + } catch (Exception bundledEx) { + html = fetchLostHqHtml(uri); } String finalHtml = html; // Use classpath base so relative image src attributes (img/...) load @@ -1764,12 +1861,25 @@ private String fetchLostHqHtml(URI uri) throws Exception { html = html.replaceAll("(?is)]+class=\"[^\"]*narrowscroll-(?:top|bottom)[^\"]*\"[^>]*>", ""); // Rewrite absolute /img/... paths to relative so they resolve from the classpath base html = html.replaceAll("(?i)(src=[\"'])/img/", "$1img/"); - return injectCompactCss(wrapLostHqTableCells(wrapLostHqTextNodes(normalizeTableWidths(injectXpTable(normalizeLostHqCanvases(normalizeLostHqImageWidths(html))))))); + return prepareLostHqHtml(html); } finally { conn.disconnect(); } } + private static String prepareLostHqHtml(String html) { + String prepared = normalizeLostHqInlineWidths(html); + prepared = normalizeLostHqLinkSpacing(prepared); + prepared = normalizeLostHqImageWidths(prepared); + prepared = normalizeLostHqCanvases(prepared); + prepared = injectXpTable(prepared); + prepared = normalizeLostHqTables(prepared); + prepared = wrapLostHqTextNodes(prepared); + prepared = wrapLostHqTableCells(prepared); + prepared = breakLostHqLongLinkRuns(prepared); + return injectCompactCss(prepared); + } + private static String injectCompactCss(String html) { // Swing's HTMLEditorKit only honours pixel (px) values for width — it ignores // percentage widths and !important. Use LOSTHQ_READER_W as the pixel reference @@ -1777,14 +1887,15 @@ private static String injectCompactCss(String html) { // allowing the table layout engine to distribute column widths and wrap text. int w = LOSTHQ_READER_W; String compactCss = "", ""); - return injectCompactCss(wrapLostHqTableCells(wrapLostHqTextNodes(normalizeTableWidths(injectXpTable(normalizeLostHqCanvases(normalizeLostHqImageWidths(html))))))); + return prepareLostHqHtml(html); } } @@ -2426,8 +2637,7 @@ private void drawLostHqPage(int x) { fillUiRect(x + 4, screenH - HSCROLL_H, pageW, HSCROLL_H, 0xFF1A1A1A); return; } - // Track measured content width for scrollbar sizing; default to body width. - int contentW = LOSTHQ_READER_W; + int contentW = Math.max(LOSTHQ_READER_W, lostHqContentW); java.awt.Graphics2D pageGraphics = (java.awt.Graphics2D) sg.create(); try { synchronized (page.getTreeLock()) { @@ -2439,7 +2649,10 @@ private void drawLostHqPage(int x) { page.setSize(LOSTHQ_READER_WIDE, Short.MAX_VALUE); page.setSize(LOSTHQ_READER_WIDE, Math.max(readerVisibleH, page.getPreferredSize().height)); int pw = page.getPreferredSize().width; - if (pw > LOSTHQ_READER_W) contentW = Math.min(pw, LOSTHQ_READER_WIDE); + contentW = lostHqMeasuredContentW(pw); + lostHqContentW = contentW; + int maxScrollX = Math.max(0, contentW - readerVisibleW); + if (lostHqScrollX > maxScrollX) lostHqScrollX = maxScrollX; page.paint(pageGraphics); } } catch (RuntimeException ignored) { @@ -2450,7 +2663,7 @@ private void drawLostHqPage(int x) { // Horizontal scrollbar int sbX = x + 4; int sbY = screenH - HSCROLL_H; - int maxScrollX = Math.max(0, contentW - readerVisibleW); + int maxScrollX = lostHqMaxScrollX(); fillUiRect(sbX, sbY, pageW, HSCROLL_H, 0xFF1A1A1A); int thumbW = Math.max(20, pageW * readerVisibleW / Math.max(1, contentW)); int trackRange = Math.max(1, pageW - thumbW); @@ -2461,13 +2674,81 @@ private void drawLostHqPage(int x) { } private void drawSettingsPanel(int x) { - drawUiText("CLIENT SETTINGS", x + 16, 56, 1, 0xFFE89E14); - drawToggleRow(x, 72, "GPU RENDERING", sidebarGpuEnabled); - drawToggleRow(x, 116, "SHOW FPS", sidebarFpsEnabled); - drawToggleRow(x, 160, "SHOW ROOFS", sidebarRoofsEnabled); - drawToggleRow(x, 204, "FULLSCREEN", settingsFullscreen); - drawToggleRow(x, 248, "SHIFT CLICK", settingsShiftClick); - drawToggleRow(x, 292, "DISCORD RP", settingsDiscordRp); + int panelW = sidebarPanelW(); + int y = 52; + + y = drawSettingsSectionTitle(x, y, "Afk timer"); + drawSelectBox(x + 16, y, panelW - 32, AFK_LABELS[settingsAfkIndex], settingsAfkDropdownOpen); + y += 22; + if (settingsAfkDropdownOpen) { + for (int i = 0; i < AFK_LABELS.length; i++) { + int rowY = y + i * 14; + fillUiRect(x + 16, rowY, panelW - 32, 14, i == settingsAfkIndex ? 0xFF3F3523 : 0xFF202020); + fillUiRect(x + 16, rowY, panelW - 32, 1, 0xFF363636); + drawUiText(AFK_LABELS[i], x + 22, rowY + 3, 0, i == settingsAfkIndex ? 0xFFE89E14 : 0xFFDCDCDC); + } + y += AFK_LABELS.length * 14 + 4; + } else { + y += 8; + } + + y = drawSettingsSectionTitle(x, y, "Shift Click Actions"); + y = drawSettingsToggleRow(x, y, "Drop (Inventory Items)", settingShiftDropInventory); + y = drawSettingsToggleRow(x, y, "Take (Ground Items)", settingShiftTakeGround); + y = drawSettingsToggleRow(x, y, "Attack (NPC's)", settingShiftAttackNpc); + y = drawSettingsToggleRow(x, y, "Pickpocket (NPC's)", settingShiftPickpocketNpc); + y = drawSettingsToggleRow(x, y, "Bank (Bank NPC'S)", settingShiftBankNpc); + y = drawSettingsToggleRow(x, y, "Use-Quickly (Bank Booth's)", settingShiftUseQuicklyBankBooth); + y = drawSettingsToggleRow(x, y, "Examine (Anything)", settingShiftExamineAnything); + y += 2; + + y = drawSettingsSectionTitle(x, y, "Discord Features"); + y = drawSettingsToggleRow(x, y, "Discord Rich Presence", settingDiscordRichPresence); + y += 2; + + y = drawSettingsSectionTitle(x, y, "Client Settings"); + y = drawSettingsToggleRow(x, y, "60 Fps Mode", sidebarFpsEnabled); + drawSettingsToggleRow(x, y, "Fullscreen Mode", settingsFullscreen); + } + + private void loadSettings() { + settingsAfkIndex = Math.max(0, Math.min(AFK_LABELS.length - 1, settingsAfkIndex)); + afkTimeoutCycles = AFK_CYCLES[settingsAfkIndex]; + settingFps60Enabled = sidebarFpsEnabled; + settingShiftDropInventory = SETTINGS_PREFS.getBoolean("shiftDropInventory", false); + settingShiftTakeGround = SETTINGS_PREFS.getBoolean("shiftTakeGround", false); + settingShiftAttackNpc = SETTINGS_PREFS.getBoolean("shiftAttackNpc", false); + settingShiftPickpocketNpc = SETTINGS_PREFS.getBoolean("shiftPickpocketNpc", false); + settingShiftBankNpc = SETTINGS_PREFS.getBoolean("shiftBankNpc", false); + settingShiftUseQuicklyBankBooth = SETTINGS_PREFS.getBoolean("shiftUseQuicklyBankBooth", false); + settingShiftExamineAnything = SETTINGS_PREFS.getBoolean("shiftExamineAnything", false); + settingDiscordRichPresence = SETTINGS_PREFS.getBoolean("discordRichPresence", false); + if (settingDiscordRichPresence) { + DISCORD_RPC.connect(); + } + } + + private int drawSettingsSectionTitle(int x, int y, String title) { + drawUiText(title, x + 16, y, 2, 0xFFE89E14); + fillUiRect(x + 16, y + 15, sidebarPanelW() - 32, 1, 0xFF363636); + return y + 18; + } + + private int drawSettingsToggleRow(int x, int y, String text, boolean enabled) { + int panelW = sidebarPanelW(); + drawUiTextFittedFull(text, x + 16, y + 5, panelW - 72, 0, 0xFFDCDCDC); + drawToggle(x + panelW - 48, y + 2, enabled); + return y + 20; + } + + private void drawSelectBox(int x, int y, int w, String text, boolean open) { + fillUiRect(x, y, w, 18, 0xFF202020); + fillUiRect(x, y, w, 1, 0xFF666666); + fillUiRect(x, y + 17, w, 1, 0xFF111111); + fillUiRect(x, y, 1, 18, 0xFF4A4A4A); + fillUiRect(x + w - 1, y, 1, 18, 0xFF111111); + drawUiText(text, x + 7, y + 5, 0, 0xFFDCDCDC); + drawUiText(open ? "^" : "v", x + w - 14, y + 5, 0, 0xFFE89E14); } private void drawPluginRow(int x, int y, String name, String description, boolean enabled) { @@ -2616,6 +2897,51 @@ private void drawUiText(String text, int x, int y, int scale, int argb) { sg.drawString(text, x, y + fm.getAscent()); } + private void drawUiTextFitted(String text, int x, int y, int maxWidth, int scale, int argb) { + if (text == null || text.isEmpty() || maxWidth <= 0) return; + int chosenScale = scale; + java.awt.Font font = uiFont(chosenScale); + java.awt.FontMetrics fm = sg.getFontMetrics(font); + if (fm.stringWidth(text) > maxWidth && scale > 0) { + chosenScale = 0; + font = uiFont(chosenScale); + fm = sg.getFontMetrics(font); + } + String fitted = text; + if (fm.stringWidth(fitted) > maxWidth) { + String ellipsis = "..."; + while (!fitted.isEmpty() && fm.stringWidth(fitted + ellipsis) > maxWidth) { + fitted = fitted.substring(0, fitted.length() - 1); + } + fitted = fitted.isEmpty() ? ellipsis : fitted + ellipsis; + } + drawUiText(fitted, x, y, chosenScale, argb); + } + + private void drawUiTextFittedFull(String text, int x, int y, int maxWidth, int scale, int argb) { + if (text == null || text.isEmpty() || maxWidth <= 0) return; + java.awt.Font font = uiFont(scale); + java.awt.FontMetrics fm = sg.getFontMetrics(font); + if (fm.stringWidth(text) > maxWidth) { + font = UI_FONT_TINY; + fm = sg.getFontMetrics(font); + } + float squeeze = Math.min(1f, maxWidth / (float) Math.max(1, fm.stringWidth(text))); + + java.awt.geom.AffineTransform prevTx = sg.getTransform(); + try { + int a = (argb >> 24) & 0xFF, r = (argb >> 16) & 0xFF, + g = (argb >> 8) & 0xFF, b = argb & 0xFF; + sg.setColor(new java.awt.Color(r, g, b, a)); + sg.setFont(font); + sg.translate(x, y); + sg.scale(squeeze, 1.0); + sg.drawString(text, 0, fm.getAscent()); + } finally { + sg.setTransform(prevTx); + } + } + private java.awt.Font uiFont(int scale) { return (scale >= 2) ? UI_FONT_HEAD : (scale == 0) ? UI_FONT_TINY : UI_FONT_BODY; } @@ -2768,8 +3094,9 @@ private void setupCallbacks() { double rs = lostHqReaderScale(); int pw = Math.max(1, sidebarPanelW() - 8); int visW = Math.max(1, (int) Math.ceil(pw / rs)); - int maxSX = Math.max(0, LOSTHQ_READER_WIDE - visW); - int thumbW = Math.max(20, pw * visW / LOSTHQ_READER_WIDE); + int contentW = Math.max(LOSTHQ_READER_W, lostHqContentW); + int maxSX = Math.max(0, contentW - visW); + int thumbW = Math.max(20, pw * visW / Math.max(1, contentW)); int trackRange = Math.max(1, pw - thumbW); int dx = mouseX - lostHqHScrollAncX; lostHqScrollX = Math.max(0, Math.min(maxSX, @@ -2841,6 +3168,12 @@ private void setupCallbacks() { int ly = worldMapFullscreen ? toFullscreenLogicalY(py[0]) : toLogicalY(py[0]); cursorX = lx; cursorY = ly; + // Quest-reward zoom overlay swallows the first click anywhere on screen + // (only relevant in-game; the overlay never appears at the title screen). + if (lostHqZoomImage != null && shell instanceof Client && ((Client) shell).ingame) { + lostHqZoomImage = null; + return; + } if (worldMapFullscreen) { clickWorldMap(lx, ly); return; @@ -2903,8 +3236,19 @@ private void setupCallbacks() { windowW = width; windowH = height; }); + glfwSetWindowMaximizeCallback(window, (win, maximized) -> { + updateWindowSizeLimits(); + if (!maximized && sidebarOpen) { + resizeForSidebar(); + } else { + updateOutputViewport(); + } + }); glfwSetKeyCallback(window, (win, key, scancode, action, mods) -> { + if (key == GLFW_KEY_LEFT_SHIFT || key == GLFW_KEY_RIGHT_SHIFT) { + shiftKeyDown = action != GLFW_RELEASE; + } if (key == GLFW_KEY_GRAVE_ACCENT) { if (action == GLFW_PRESS) statsOverlayVisible = !statsOverlayVisible; return; @@ -3076,6 +3420,17 @@ private int outputW() { return screenW + SIDEBAR_RAIL_W + (sidebarOpen ? SIDEBAR_PANEL_W : 0); } + private void updateWindowSizeLimits() { + if (window == NULL) return; + if (settingsFullscreen || sidebarInsideWindow()) { + glfwSetWindowSizeLimits(window, GLFW_DONT_CARE, GLFW_DONT_CARE, GLFW_DONT_CARE, GLFW_DONT_CARE); + glfwSetWindowAspectRatio(window, GLFW_DONT_CARE, GLFW_DONT_CARE); + } else { + glfwSetWindowSizeLimits(window, outputW(), screenH, GLFW_DONT_CARE, GLFW_DONT_CARE); + glfwSetWindowAspectRatio(window, outputW(), screenH); + } + } + private void setOutputViewport(int logicalWidth) { int[] width = new int[1]; int[] height = new int[1]; @@ -3101,6 +3456,7 @@ private void clickSidebar(int x, int y) { xpScreenEnabled = !xpScreenEnabled; if (sidebarOpen && sidebarTab == 1) { sidebarOpen = false; + updateWindowSizeLimits(); resizeForSidebar(); updateOutputViewport(); } @@ -3114,6 +3470,7 @@ private void clickSidebar(int x, int y) { sidebarOpen = true; sidebarTab = tab; } + updateWindowSizeLimits(); resizeForSidebar(); updateOutputViewport(); return; @@ -3126,6 +3483,7 @@ private void clickSidebar(int x, int y) { // Close button (top-right X in the panel header) if (x >= sidebarPanelX() + sidebarPanelW() - 22 && y <= 42) { sidebarOpen = false; + updateWindowSizeLimits(); resizeForSidebar(); updateOutputViewport(); return; @@ -3135,7 +3493,7 @@ private void clickSidebar(int x, int y) { case 0 -> { // Hiscores – skill selector buttons (2 per row, 10 rows, y=52..181) int px = sidebarPanelX(); int panelW = sidebarPanelW(); - int columns = panelW >= 190 ? 2 : 1; + int columns = HSCORE_SKILL_COLUMNS; int buttonW = (panelW - 20 - (columns - 1) * 4) / columns; int relX = x - (px + 10); int relY = y - 52; @@ -3205,17 +3563,135 @@ private void clickSidebar(int x, int y) { } } } - case 5 -> { // Settings toggles (each row is 44 px tall starting at y=72) - if (y >= 72 && y < 116) sidebarGpuEnabled = !sidebarGpuEnabled; - if (y >= 116 && y < 160) sidebarFpsEnabled = !sidebarFpsEnabled; - if (y >= 160 && y < 204) sidebarRoofsEnabled = !sidebarRoofsEnabled; - if (y >= 204 && y < 248) toggleFullscreen(); - if (y >= 248 && y < 292) settingsShiftClick = !settingsShiftClick; - if (y >= 292 && y < 336) settingsDiscordRp = !settingsDiscordRp; + case 5 -> { + clickSettingsPanel(x, y); } } } + private void clickSettingsPanel(int x, int y) { + int px = sidebarPanelX(); + int panelW = sidebarPanelW(); + int rowY = 52; + + rowY += 18; + if (x >= px + 16 && x < px + panelW - 16 && y >= rowY && y < rowY + 18) { + settingsAfkDropdownOpen = !settingsAfkDropdownOpen; + return; + } + rowY += 22; + if (settingsAfkDropdownOpen) { + for (int i = 0; i < AFK_LABELS.length; i++) { + int optY = rowY + i * 14; + if (x >= px + 16 && x < px + panelW - 16 && y >= optY && y < optY + 14) { + setAfkIndex(i); + settingsAfkDropdownOpen = false; + return; + } + } + rowY += AFK_LABELS.length * 14 + 4; + } else { + rowY += 8; + } + + rowY += 18; + if (toggleHit(px, rowY, x, y)) { setShiftDropInventory(!settingShiftDropInventory); return; } + rowY += 20; + if (toggleHit(px, rowY, x, y)) { setShiftTakeGround(!settingShiftTakeGround); return; } + rowY += 20; + if (toggleHit(px, rowY, x, y)) { setShiftAttackNpc(!settingShiftAttackNpc); return; } + rowY += 20; + if (toggleHit(px, rowY, x, y)) { setShiftPickpocketNpc(!settingShiftPickpocketNpc); return; } + rowY += 20; + if (toggleHit(px, rowY, x, y)) { setShiftBankNpc(!settingShiftBankNpc); return; } + rowY += 20; + if (toggleHit(px, rowY, x, y)) { setShiftUseQuicklyBankBooth(!settingShiftUseQuicklyBankBooth); return; } + rowY += 20; + if (toggleHit(px, rowY, x, y)) { setShiftExamineAnything(!settingShiftExamineAnything); return; } + rowY += 22; + + rowY += 18; + if (toggleHit(px, rowY, x, y)) { setDiscordRichPresence(!settingDiscordRichPresence); return; } + rowY += 22; + + rowY += 18; + if (toggleHit(px, rowY, x, y)) { setFps60(!sidebarFpsEnabled); return; } + rowY += 20; + if (toggleHit(px, rowY, x, y)) toggleFullscreen(); + } + + private boolean toggleHit(int px, int rowY, int mouseX, int mouseY) { + return mouseX >= px + 8 && mouseX < px + sidebarPanelW() - 8 + && mouseY >= rowY && mouseY < rowY + 20; + } + + private void setAfkIndex(int index) { + settingsAfkIndex = Math.max(0, Math.min(AFK_LABELS.length - 1, index)); + afkTimeoutCycles = AFK_CYCLES[settingsAfkIndex]; + SETTINGS_PREFS.putInt("afkIndex", settingsAfkIndex); + } + + private void setFps60(boolean enabled) { + sidebarFpsEnabled = enabled; + settingFps60Enabled = enabled; + SETTINGS_PREFS.putBoolean("fps60", enabled); + if (shell != null) { + shell.setFramerate(50); + } + } + + private void setShiftDropInventory(boolean enabled) { + settingShiftDropInventory = enabled; + SETTINGS_PREFS.putBoolean("shiftDropInventory", enabled); + } + + private void setShiftTakeGround(boolean enabled) { + settingShiftTakeGround = enabled; + SETTINGS_PREFS.putBoolean("shiftTakeGround", enabled); + } + + private void setShiftAttackNpc(boolean enabled) { + settingShiftAttackNpc = enabled; + SETTINGS_PREFS.putBoolean("shiftAttackNpc", enabled); + } + + private void setShiftPickpocketNpc(boolean enabled) { + settingShiftPickpocketNpc = enabled; + SETTINGS_PREFS.putBoolean("shiftPickpocketNpc", enabled); + } + + private void setShiftBankNpc(boolean enabled) { + settingShiftBankNpc = enabled; + SETTINGS_PREFS.putBoolean("shiftBankNpc", enabled); + } + + private void setShiftUseQuicklyBankBooth(boolean enabled) { + settingShiftUseQuicklyBankBooth = enabled; + SETTINGS_PREFS.putBoolean("shiftUseQuicklyBankBooth", enabled); + } + + private void setShiftExamineAnything(boolean enabled) { + settingShiftExamineAnything = enabled; + SETTINGS_PREFS.putBoolean("shiftExamineAnything", enabled); + } + + private void setDiscordRichPresence(boolean enabled) { + settingDiscordRichPresence = enabled; + SETTINGS_PREFS.putBoolean("discordRichPresence", enabled); + if (enabled) { + DISCORD_RPC.connect(); + DISCORD_RPC.updateActivity("Playing 2004 Singleplayer Progressive", "Loading world..."); + } else { + DISCORD_RPC.disconnect(); + } + } + + public static void updateDiscordActivity(String details, String state) { + if (settingDiscordRichPresence) { + DISCORD_RPC.updateActivity(details, state); + } + } + private void scrollLostHqPage(int amount) { JEditorPane page = lostHqPage; if (page == null) return; @@ -3232,13 +3708,23 @@ private void scrollLostHqPage(int amount) { private void scrollLostHqHorizontal(int amountScreenPx) { double readerScale = lostHqReaderScale(); - int pageW = Math.max(1, sidebarPanelW() - 8); - int visibleW = Math.max(1, (int) Math.ceil(pageW / readerScale)); - int maxScroll = Math.max(0, LOSTHQ_READER_WIDE - visibleW); + int maxScroll = lostHqMaxScrollX(); int readerAmt = (int) Math.round(amountScreenPx / readerScale); lostHqScrollX = Math.max(0, Math.min(maxScroll, lostHqScrollX + readerAmt)); } + private int lostHqMaxScrollX() { + double readerScale = lostHqReaderScale(); + int pageW = Math.max(1, sidebarPanelW() - 8); + int visibleW = Math.max(1, (int) Math.ceil(pageW / readerScale)); + return Math.max(0, Math.max(LOSTHQ_READER_W, lostHqContentW) - visibleW); + } + + private static int lostHqMeasuredContentW(int measuredW) { + int w = Math.max(LOSTHQ_READER_W, Math.min(measuredW, LOSTHQ_READER_WIDE)); + return w; + } + private void clickLostHqPage(int x, int y) { JEditorPane page = lostHqPage; URI pageUri = lostHqPageUri; @@ -3246,7 +3732,7 @@ private void clickLostHqPage(int x, int y) { double readerScale = lostHqReaderScale(); int relX = (int) ((x - sidebarPanelX() - 4) / readerScale) + lostHqScrollX; int relY = (int) ((y - 70) / readerScale) + lostHqScrollY; - if (relX < 0 || relX >= LOSTHQ_READER_WIDE || relY < 0) return; + if (relX < 0 || relX >= Math.max(LOSTHQ_READER_W, lostHqContentW) || relY < 0) return; try { int pos; synchronized (page.getTreeLock()) { @@ -3254,6 +3740,27 @@ private void clickLostHqPage(int x, int y) { } HTMLDocument doc = (HTMLDocument) page.getDocument(); Element element = doc.getCharacterElement(pos); + // First check for IMG element at click position — walk up the tree. + // Swing represents as a leaf whose name attribute is HTML.Tag.IMG. + for (Element walk = element; walk != null; walk = walk.getParentElement()) { + AttributeSet attrs = walk.getAttributes(); + Object name = attrs.getAttribute(StyleConstants.NameAttribute); + if (name != HTML.Tag.IMG) continue; + Object src = attrs.getAttribute(HTML.Attribute.SRC); + if (src == null) break; + String srcStr = src.toString(); + if (srcStr.contains("questimages/quest_complete_thumb/") + || srcStr.contains("questimages/quest_complete/")) { + // Only zoom while in-game — there's no 3D viewport to overlay onto + // at the title/login screen, so the click would do nothing useful. + if (shell instanceof Client && ((Client) shell).ingame) { + // Always load the high-res original for the zoom view. + openLostHqZoom(srcStr.replace("quest_complete_thumb/", "quest_complete/")); + } + return; + } + break; + } AttributeSet anchor = (AttributeSet) element.getAttributes().getAttribute(HTML.Tag.A); if (anchor == null) return; Object href = anchor.getAttribute(HTML.Attribute.HREF); @@ -3268,6 +3775,71 @@ private void clickLostHqPage(int x, int y) { } } + private void openLostHqZoom(String src) { + // src may be absolute (https://...) or relative (img/...). Strip everything + // up to and including "img/" so we can resolve against the classpath base. + int idx = src.indexOf("img/"); + if (idx < 0) return; + String resource = "/losthq/" + src.substring(idx); + try (InputStream in = GLRenderer.class.getResourceAsStream(resource)) { + if (in == null) return; + BufferedImage img = ImageIO.read(in); + if (img != null) lostHqZoomImage = img; + } catch (Exception ignored) { + } + } + + private void drawLostHqZoomOverlay() { + BufferedImage img = lostHqZoomImage; + if (img == null || PixMap.uiBuffer == null) return; + // Draw inside the 3D game viewport only, leaving chatbox / inventory / sidebar alone. + int x0 = vpDrawX; + int y0 = vpDrawY; + int w = vpW; + int h = vpH; + if (w <= 0 || h <= 0) return; + // Wrap the uiBuffer slice as a BufferedImage so we can draw with Graphics2D + // (it's TYPE_INT_ARGB = BGRA on little-endian, which matches how GL uploads it). + java.awt.image.DataBufferInt buf = new java.awt.image.DataBufferInt( + PixMap.uiBuffer, PixMap.uiBuffer.length); + java.awt.image.SinglePixelPackedSampleModel sm = + new java.awt.image.SinglePixelPackedSampleModel( + java.awt.image.DataBuffer.TYPE_INT, maxUiW, screenH, + new int[]{0x00FF0000, 0x0000FF00, 0x000000FF, 0xFF000000}); + java.awt.image.WritableRaster raster = + java.awt.image.Raster.createWritableRaster(sm, buf, null); + BufferedImage canvas = new BufferedImage( + java.awt.image.ColorModel.getRGBdefault(), raster, false, null); + java.awt.Graphics2D g = canvas.createGraphics(); + try { + g.setClip(x0, y0, w, h); + // Semi-transparent dark backdrop over the game viewport. + g.setComposite(java.awt.AlphaComposite.Src); + g.setColor(new java.awt.Color(0, 0, 0, 200)); + g.fillRect(x0, y0, w, h); + // Fit image inside the viewport with a small margin, preserve aspect ratio. + int maxW = (int) (w * 0.92); + int maxH = (int) (h * 0.92); + double scale = Math.min((double) maxW / img.getWidth(), + (double) maxH / img.getHeight()); + int drawW = (int) (img.getWidth() * scale); + int drawH = (int) (img.getHeight() * scale); + int drawX = x0 + (w - drawW) / 2; + int drawY = y0 + (h - drawH) / 2; + g.setRenderingHint(java.awt.RenderingHints.KEY_INTERPOLATION, + java.awt.RenderingHints.VALUE_INTERPOLATION_BICUBIC); + g.setRenderingHint(java.awt.RenderingHints.KEY_RENDERING, + java.awt.RenderingHints.VALUE_RENDER_QUALITY); + g.drawImage(img, drawX, drawY, drawW, drawH, null); + // Hint text just above the image. + g.setColor(new java.awt.Color(0xE8, 0x9E, 0x14)); + g.setFont(new java.awt.Font("SansSerif", java.awt.Font.BOLD, 11)); + g.drawString("CLICK TO CLOSE", drawX, Math.max(y0 + 12, drawY - 6)); + } finally { + g.dispose(); + } + } + private void clickWorldMap(int x, int y) { int vx = worldMapViewX(); int vy = worldMapViewY(); @@ -3291,6 +3863,7 @@ private void clickWorldMap(int x, int y) { worldMapFullscreen = false; sidebarOpen = false; mapDragging = false; + updateWindowSizeLimits(); resizeForSidebar(); updateOutputViewport(); } @@ -3334,6 +3907,7 @@ private void clickWorldMap(int x, int y) { private void toggleFullscreen() { settingsFullscreen = !settingsFullscreen; if (settingsFullscreen) { + updateWindowSizeLimits(); long monitor = glfwGetPrimaryMonitor(); org.lwjgl.glfw.GLFWVidMode mode = glfwGetVideoMode(monitor); if (mode != null) { @@ -3342,6 +3916,7 @@ private void toggleFullscreen() { } } else { glfwSetWindowMonitor(window, NULL, 100, 100, outputW(), screenH, GLFW_DONT_CARE); + updateWindowSizeLimits(); } updateOutputViewport(); } diff --git a/src/main/java/jagex2/client/Client.java b/src/main/java/jagex2/client/Client.java index a8fa74c..da582bf 100644 --- a/src/main/java/jagex2/client/Client.java +++ b/src/main/java/jagex2/client/Client.java @@ -32,6 +32,8 @@ @ObfuscatedName("client") public class Client extends GameShell { + private static final boolean FAST_STARTUP = Boolean.parseBoolean(System.getProperty("rs254.fastStartup", "true")); + @ObfuscatedName("client.ab") public int activeMapFunctionCount; @@ -496,7 +498,13 @@ public class Client extends GameShell { private static final int[] XP_DROP_ICON_ORDER = { 0, 3, 14, 2, 16, 13, 1, 15, 10, 4, 17, 7, 5, 12, 11, 6, 9, 8, 20 }; - private static final String[] SKILL_ICON_FILENAMES = { + private static final String[] XP_DROP_SMALL_SKILL_ICON_FILENAMES = { + "attack", "defence", "strength", "hitpoints", "ranged", "prayer", "magic", + "cooking", "woodcutting", "fletching", "fishing", "firemaking", "crafting", + "smithing", "mining", "herblore", "agility", "thieving", "slayer", null, + "runecraft", null, null, null, null + }; + private static final String[] XP_DROP_LEGACY_SKILL_ICON_FILENAMES = { "attack", "defence", "strength", "hitpoints", "ranged", "prayer", "magic", "cooking", "woodcutting", "fletching", "fishing", "firemaking", "crafting", "smithing", "mining", "herblore", "agility", "thieving", null, null, @@ -507,6 +515,7 @@ public class Client extends GameShell { private final int[] xpDropAmount = new int[XP_DROP_COUNT]; private final int[] xpDropStartCycle = new int[XP_DROP_COUNT]; private final Pix32[] xpDropSkillIcons = new Pix32[Stats.COUNT]; + private boolean xpDropCustomSkillIconsAttempted; private boolean xpDropSkillIconsLoaded; @ObfuscatedName("client.gi") @@ -779,6 +788,12 @@ public class Client extends GameShell { @ObfuscatedName("client.ke") public int orbitCameraZ; + // Orbit camera target at the start of the current logic tick, so the render + // loop can interpolate the camera in lockstep with the (interpolated) local + // player position in 60fps mode. Updated each tick in followCamera(). + private int prevOrbitCameraX; + private int prevOrbitCameraZ; + @ObfuscatedName("client.le") public int sendCameraDelay; @@ -1494,6 +1509,16 @@ public void setWaveVolume(int arg1) { // GL renderer — null until load() initialises it private GLRenderer glRenderer; + private String discordLastArea = ""; + private int discordLastLevel = -1; + private int discordLastUpdate = 0; + private int entityAnimationStep = 1; + private int entityAnimationStepAccumulator; + private int queuedGroundTakeX = -1; + private int queuedGroundTakeZ = -1; + private int[] queuedGroundTakeIds; + private int queuedGroundTakeCount; + private int queuedGroundTakeIndex; // ---- @@ -1637,25 +1662,29 @@ public void load() { } catch (Exception var73) { } } - this.drawProgress("Requesting models", 70); - int var20 = this.onDemand.getFileCount(0); - for (int var21 = 0; var21 < var20; var21++) { - int var22 = this.onDemand.getModelFlags(var21); - if (var22 != 0) { - this.onDemand.request(0, var21); - } - } - int var23 = this.onDemand.remaining(); - while (this.onDemand.remaining() > 0) { - int var24 = var23 - this.onDemand.remaining(); - if (var24 > 0) { - this.drawProgress("Loading models - " + var24 * 100 / var23 + "%", 70); + if (!FAST_STARTUP) { + this.drawProgress("Requesting models", 70); + int var20 = this.onDemand.getFileCount(0); + for (int var21 = 0; var21 < var20; var21++) { + int var22 = this.onDemand.getModelFlags(var21); + if (var22 != 0) { + this.onDemand.request(0, var21); + } } - this.onDemandLoop(); - try { - Thread.sleep(100L); - } catch (Exception var72) { + int var23 = this.onDemand.remaining(); + while (this.onDemand.remaining() > 0) { + int var24 = var23 - this.onDemand.remaining(); + if (var24 > 0) { + this.drawProgress("Loading models - " + var24 * 100 / var23 + "%", 70); + } + this.onDemandLoop(); + try { + Thread.sleep(100L); + } catch (Exception var72) { + } } + } else { + this.drawProgress("Preparing models", 70); } if (this.fileStreams[0] != null) { this.drawProgress("Requesting maps", 75); @@ -1684,33 +1713,35 @@ public void load() { } } } - int var27 = this.onDemand.getFileCount(0); - for (int var28 = 0; var28 < var27; var28++) { - int var29 = this.onDemand.getModelFlags(var28); - byte var30 = 0; - if ((var29 & 0x8) != 0) { - var30 = 10; - } else if ((var29 & 0x20) != 0) { - var30 = 9; - } else if ((var29 & 0x10) != 0) { - var30 = 8; - } else if ((var29 & 0x40) != 0) { - var30 = 7; - } else if ((var29 & 0x80) != 0) { - var30 = 6; - } else if ((var29 & 0x2) != 0) { - var30 = 5; - } else if ((var29 & 0x4) != 0) { - var30 = 4; - } - if ((var29 & 0x1) != 0) { - var30 = 3; - } - if (var30 != 0) { - this.onDemand.prefetchPriority(0, var30, var28); + if (!FAST_STARTUP) { + int var27 = this.onDemand.getFileCount(0); + for (int var28 = 0; var28 < var27; var28++) { + int var29 = this.onDemand.getModelFlags(var28); + byte var30 = 0; + if ((var29 & 0x8) != 0) { + var30 = 10; + } else if ((var29 & 0x20) != 0) { + var30 = 9; + } else if ((var29 & 0x10) != 0) { + var30 = 8; + } else if ((var29 & 0x40) != 0) { + var30 = 7; + } else if ((var29 & 0x80) != 0) { + var30 = 6; + } else if ((var29 & 0x2) != 0) { + var30 = 5; + } else if ((var29 & 0x4) != 0) { + var30 = 4; + } + if ((var29 & 0x1) != 0) { + var30 = 3; + } + if (var30 != 0) { + this.onDemand.prefetchPriority(0, var30, var28); + } } + this.onDemand.prefetchMaps(membersWorld); } - this.onDemand.prefetchMaps(membersWorld); if (!lowMem) { int var31 = this.onDemand.getFileCount(2); for (int var32 = 1; var32 < var31; var32++) { @@ -1937,6 +1968,9 @@ public void draw() { glRenderer.beginFrame(this.ingame && drawScene, drawScene); } drawCycle++; + // Publish the render-time interpolation state for entity model building. + ClientEntity.renderInterpOn = GLRenderer.settingFps60Enabled; + ClientEntity.renderInterp = GLRenderer.settingFps60Enabled ? super.subTickFraction : 0f; if (this.ingame) { this.gameDraw(); } else { @@ -2901,6 +2935,7 @@ public void gameLoop() { if (this.packetCycle > 750) { this.tryReconnect(); } + this.updateEntityAnimationStep(); this.movePlayers(); this.moveNpcs(); this.timeoutChat(); @@ -2978,10 +3013,14 @@ public void gameLoop() { this.out.p2(this.hoveredSlot); this.out.p1(var22); } - } else if ((this.oneMouseButton == 1 || this.isAddFriendOption(this.menuSize - 1)) && this.menuSize > 2) { + } else { + if (this.handleShiftClick()) { + // handled + } else if ((this.oneMouseButton == 1 || this.isAddFriendOption(this.menuSize - 1)) && this.menuSize > 2) { this.showContextMenu(); - } else if (this.menuSize > 0) { - this.useMenuOption(this.menuSize - 1); + } else if (this.menuSize > 0) { + this.useMenuOption(this.menuSize - 1); + } } this.selectedCycle = 10; super.mouseClickButton = 0; @@ -3028,13 +3067,15 @@ public void gameLoop() { } this.handleInputKey(); super.idleCycles++; - if (super.idleCycles > 4500) { + int afkTimeout = GLRenderer.afkTimeoutCycles; + if (afkTimeout > 0 && super.idleCycles > afkTimeout) { ClientDebugger.onIdleTimeout(super.idleCycles); this.pendingLogout = 250; super.idleCycles -= 500; // IDLE_TIMER this.out.pIsaac(144); } + updateDiscordRichPresence(); this.macroCameraCycle++; if (this.macroCameraCycle > 500) { this.macroCameraCycle = 0; @@ -3880,6 +3921,9 @@ public void handleMouseInput() { } } } + if (var2 == 1 && this.handleShiftClick()) { + return; + } if (var2 == 1 && (this.oneMouseButton == 1 || this.isAddFriendOption(this.menuSize - 1)) && this.menuSize > 2) { var2 = 2; } @@ -4167,6 +4211,10 @@ public void timeoutChat() { @ObfuscatedName("client.f(I)V") public void followCamera() { try { + // Snapshot the camera target before this tick eases it, for render-time + // interpolation (see interpOrbitCameraX/Z). + this.prevOrbitCameraX = this.orbitCameraX; + this.prevOrbitCameraZ = this.orbitCameraZ; int var2 = localPlayer.x + this.macroCameraX; int var3 = localPlayer.z + this.macroCameraZ; if (this.orbitCameraX - var2 < -500 || this.orbitCameraX - var2 > 500 || this.orbitCameraZ - var3 < -500 || this.orbitCameraZ - var3 > 500) { @@ -4615,7 +4663,59 @@ public void moveNpcs() { } @ObfuscatedName("client.a(BLz;I)V") + /** + * Whether the entity's position should be interpolated this render frame, i.e. + * 60fps mode is on and the move since last tick is a normal step (not a teleport + * or exact-move snap, which would streak across the map). + */ + private boolean shouldInterpScenePos(ClientEntity arg0) { + if (!ClientEntity.renderInterpOn) { + return false; + } + int dx = arg0.x - arg0.prevSceneX; + int dz = arg0.z - arg0.prevSceneZ; + return dx <= 64 && dx >= -64 && dz <= 64 && dz >= -64; + } + + /** Render-time interpolated scene X (falls back to the exact logic position). */ + private int interpSceneX(ClientEntity arg0) { + if (!this.shouldInterpScenePos(arg0)) { + return arg0.x; + } + return arg0.prevSceneX + Math.round((arg0.x - arg0.prevSceneX) * super.subTickFraction); + } + + /** Render-time interpolated scene Z (falls back to the exact logic position). */ + private int interpSceneZ(ClientEntity arg0) { + if (!this.shouldInterpScenePos(arg0)) { + return arg0.z; + } + return arg0.prevSceneZ + Math.round((arg0.z - arg0.prevSceneZ) * super.subTickFraction); + } + + /** Render-time interpolated orbit camera X (falls back on snaps / when off). */ + private int interpOrbitCameraX() { + int d = this.orbitCameraX - this.prevOrbitCameraX; + if (!ClientEntity.renderInterpOn || d > 256 || d < -256) { + return this.orbitCameraX; + } + return this.prevOrbitCameraX + Math.round(d * super.subTickFraction); + } + + /** Render-time interpolated orbit camera Z (falls back on snaps / when off). */ + private int interpOrbitCameraZ() { + int d = this.orbitCameraZ - this.prevOrbitCameraZ; + if (!ClientEntity.renderInterpOn || d > 256 || d < -256) { + return this.orbitCameraZ; + } + return this.prevOrbitCameraZ + Math.round(d * super.subTickFraction); + } + public void moveEntity(ClientEntity arg1, int arg2) { + // Remember where the entity was before this tick's movement so the render + // loop can interpolate its position between 50fps logic ticks (60fps mode). + arg1.prevSceneX = arg1.x; + arg1.prevSceneZ = arg1.z; if (arg1.x < 128 || arg1.z < 128 || arg1.x >= 13184 || arg1.z >= 13184) { arg1.primarySeqId = -1; arg1.spotanimId = -1; @@ -4887,7 +4987,7 @@ public void entityAnim(ClientEntity arg1) { arg1.spotanimFrame = 0; } SeqType var4 = SpotAnimType.list[arg1.spotanimId].seq; - arg1.spotanimCycle++; + arg1.spotanimCycle += this.entityAnimationStep; while (arg1.spotanimFrame < var4.numFrames && arg1.spotanimCycle > var4.getDuration(arg1.spotanimFrame)) { arg1.spotanimCycle -= var4.getDuration(arg1.spotanimFrame); arg1.spotanimFrame++; @@ -4905,7 +5005,7 @@ public void entityAnim(ClientEntity arg1) { } if (arg1.primarySeqId != -1 && arg1.primarySeqDelay == 0) { SeqType var6 = SeqType.list[arg1.primarySeqId]; - arg1.primarySeqCycle++; + arg1.primarySeqCycle += this.entityAnimationStep; while (arg1.primarySeqFrame < var6.numFrames && arg1.primarySeqCycle > var6.getDuration(arg1.primarySeqFrame)) { arg1.primarySeqCycle -= var6.getDuration(arg1.primarySeqFrame); arg1.primarySeqFrame++; @@ -4927,6 +5027,20 @@ public void entityAnim(ClientEntity arg1) { } } + @Override + protected boolean isHighFpsEnabled() { + return GLRenderer.settingFps60Enabled; + } + + private void updateEntityAnimationStep() { + // Animations always advance at native speed (one cycle per logic tick). + // In 60fps mode the extra smoothness comes from render-time keyframe + // interpolation (Model.animateInterpolated / ClientEntity.seqInterpWeight), + // not from advancing the frame counters faster. + this.entityAnimationStep = 1; + this.entityAnimationStepAccumulator = 0; + } + @ObfuscatedName("client.D(I)V") public void loadTitle() { if (this.imageTitle2 != null) { @@ -5422,7 +5536,14 @@ public void gameDrawMain() { var2 = this.cameraModifierWobbleScale[4] + 128; } int var3 = this.orbitCameraYaw + this.macroCameraAngle & 0x7FF; - this.camFollow(var2, this.orbitCameraX, this.getAvH(localPlayer.z, this.minusedlevel, localPlayer.x) - 50, this.orbitCameraZ, var2 * 3 + 600, var3); + // Interpolate the camera target (and the focus height from the local + // player's interpolated position) so the camera tracks smoothly at the + // render rate instead of stepping at 50fps. + int camX = this.interpOrbitCameraX(); + int camZ = this.interpOrbitCameraZ(); + int focusX = this.interpSceneX(localPlayer); + int focusZ = this.interpSceneZ(localPlayer); + this.camFollow(var2, camX, this.getAvH(focusZ, this.minusedlevel, focusX) - 50, camZ, var2 * 3 + 600, var3); } int var4; if (this.cutscene) { @@ -5522,8 +5643,10 @@ public void addPlayers(boolean arg0) { } this.tileLastOccupiedCycle[var7][var8] = this.sceneCycle; } - var5.y = this.getAvH(var5.z, this.minusedlevel, var5.x); - this.world.addDynamic(var5.yaw, var6, 60, var5.x, var5, var5.y, this.minusedlevel, var5.needsForwardDrawPadding, var5.z); + int rx = this.interpSceneX(var5); + int rz = this.interpSceneZ(var5); + var5.y = this.getAvH(rz, this.minusedlevel, rx); + this.world.addDynamic(var5.yaw, var6, 60, rx, var5, var5.y, this.minusedlevel, var5.needsForwardDrawPadding, rz); } else { var5.lowMemory = false; var5.y = this.getAvH(var5.z, this.minusedlevel, var5.x); @@ -5549,7 +5672,9 @@ public void addNpcs(boolean arg0) { } this.tileLastOccupiedCycle[var6][var7] = this.sceneCycle; } - this.world.addDynamic(var4.yaw, var5, (var4.size - 1) * 64 + 60, var4.x, var4, this.getAvH(var4.z, this.minusedlevel, var4.x), this.minusedlevel, var4.needsForwardDrawPadding, var4.z); + int rx = this.interpSceneX(var4); + int rz = this.interpSceneZ(var4); + this.world.addDynamic(var4.yaw, var5, (var4.size - 1) * 64 + 60, rx, var4, this.getAvH(rz, this.minusedlevel, rx), this.minusedlevel, var4.needsForwardDrawPadding, rz); } } } @@ -6132,45 +6257,75 @@ private void drawXpDrops() { } private void loadCustomXpDropIcons() { - if (this.xpDropSkillIconsLoaded) { + if (this.xpDropSkillIconsLoaded || this.xpDropCustomSkillIconsAttempted) { return; } - int size = 16; - boolean anyLoaded = false; - for (int skill = 0; skill < SKILL_ICON_FILENAMES.length; skill++) { - String name = SKILL_ICON_FILENAMES[skill]; - if (name == null) continue; - try (InputStream is = Client.class.getResourceAsStream("/skillicons/" + name + ".png")) { - if (is == null) continue; - BufferedImage src = ImageIO.read(is); - int srcW = src.getWidth(); - int srcH = src.getHeight(); - double scale = (double) size / Math.max(srcW, srcH); - int dstW = Math.max(1, (int) Math.round(srcW * scale)); - int dstH = Math.max(1, (int) Math.round(srcH * scale)); - BufferedImage scaled = new BufferedImage(dstW, dstH, BufferedImage.TYPE_INT_ARGB); - Graphics2D g2 = scaled.createGraphics(); - g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); - g2.drawImage(src, 0, 0, dstW, dstH, null); - g2.dispose(); - Pix32 pix = new Pix32(dstW, dstH); - scaled.getRGB(0, 0, dstW, dstH, pix.data, 0, dstW); - for (int j = 0; j < pix.data.length; j++) { - int argb = pix.data[j]; - int rgb = argb & 0xFFFFFF; - pix.data[j] = ((argb >>> 24) < 128) ? 0 : (rgb == 0 ? 1 : rgb); - } - this.xpDropSkillIcons[skill] = pix; - anyLoaded = true; - } catch (Exception e) { - System.err.println("[skillicons] Failed to load " + name + ": " + e.getMessage()); - } - } - if (anyLoaded) { + this.xpDropCustomSkillIconsAttempted = true; + boolean allLoaded = true; + for (int skill = 0; skill < XP_DROP_SMALL_SKILL_ICON_FILENAMES.length; skill++) { + String smallName = XP_DROP_SMALL_SKILL_ICON_FILENAMES[skill]; + if (smallName == null) continue; + Pix32 icon = this.loadXpDropIconResource("/skill_icons_small/" + smallName + ".png", false, true); + if (icon == null) { + String legacyName = XP_DROP_LEGACY_SKILL_ICON_FILENAMES[skill]; + if (legacyName != null) { + icon = this.loadXpDropIconResource("/skillicons/" + legacyName + ".png", true, false); + } + } + if (icon == null) { + allLoaded = false; + } else { + this.xpDropSkillIcons[skill] = icon; + } + } + if (allLoaded) { this.xpDropSkillIconsLoaded = true; } } + private Pix32 loadXpDropIconResource(String path, boolean allowUpscale, boolean transparentBlack) { + int size = 16; + try (InputStream is = Client.class.getResourceAsStream(path)) { + if (is == null) return null; + BufferedImage src = ImageIO.read(is); + int srcW = src.getWidth(); + int srcH = src.getHeight(); + double scale = allowUpscale + ? (double) size / Math.max(srcW, srcH) + : Math.min(1.0, (double) size / Math.max(srcW, srcH)); + int dstW = Math.max(1, (int) Math.round(srcW * scale)); + int dstH = Math.max(1, (int) Math.round(srcH * scale)); + BufferedImage scaled = new BufferedImage(dstW, dstH, BufferedImage.TYPE_INT_ARGB); + Graphics2D g2 = scaled.createGraphics(); + Object interpolation = scale == 1.0 + ? RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR + : RenderingHints.VALUE_INTERPOLATION_BICUBIC; + g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, interpolation); + g2.drawImage(src, 0, 0, dstW, dstH, null); + g2.dispose(); + Pix32 pix = new Pix32(dstW, dstH); + scaled.getRGB(0, 0, dstW, dstH, pix.data, 0, dstW); + for (int j = 0; j < pix.data.length; j++) { + int argb = pix.data[j]; + int rgb = argb & 0xFFFFFF; + pix.data[j] = ((argb >>> 24) < 128 || transparentBlack && isNearBlack(rgb)) + ? 0 + : (rgb == 0 ? 1 : rgb); + } + return pix; + } catch (Exception e) { + System.err.println("[skillicons] Failed to load " + path + ": " + e.getMessage()); + return null; + } + } + + private static boolean isNearBlack(int rgb) { + int red = rgb >> 16 & 0xFF; + int green = rgb >> 8 & 0xFF; + int blue = rgb & 0xFF; + return red <= 8 && green <= 8 && blue <= 8; + } + private void loadXpDropSkillIcons() { if (this.xpDropSkillIconsLoaded || IfType.list == null || this.tabInterfaceId[1] < 0) { return; @@ -8031,6 +8186,7 @@ public void zonePacket(int arg0, Packet arg1) { this.objStacks[this.minusedlevel][var36][var37] = null; } this.showObject(var36, var37); + this.continueQueuedGroundTake(var36, var37); } } } else if (arg0 == 37) { @@ -8927,6 +9083,187 @@ public boolean isAddFriendOption(int arg0) { return var3 == 605; } + private int shiftClickMenuIndex() { + if (!GLRenderer.shiftKeyDown) { + return -1; + } + for (int i = this.menuSize - 1; i >= 0; i--) { + if (matchesShiftClickSetting(i)) { + return i; + } + } + return -1; + } + + /** + * Executes the shift-left-click action for the current menu, if one is enabled + * and matched. Returns true if it handled the click. "Take" loots every ground + * item on the tile in one go; all other actions perform their single matched + * menu option. + */ + private boolean handleShiftClick() { + int shiftIndex = this.shiftClickMenuIndex(); + if (shiftIndex < 0) { + return false; + } + int action = this.menuAction[shiftIndex]; + if (action >= 2000) { + action -= 2000; + } + if (GLRenderer.settingShiftTakeGround && action == 617) { + this.takeAllGroundItems(this.menuParamB[shiftIndex], this.menuParamC[shiftIndex]); + return true; + } + this.useMenuOption(shiftIndex); + return true; + } + + private void takeAllGroundItems(int tileX, int tileZ) { + if (tileX < 0 || tileX >= 104 || tileZ < 0 || tileZ >= 104) { + return; + } + LinkList stack = this.objStacks[this.minusedlevel][tileX][tileZ]; + if (stack == null) { + return; + } + int[] ids = new int[32]; + int count = 0; + for (ClientObj obj = (ClientObj) stack.head(); obj != null; obj = (ClientObj) stack.next()) { + if (count == ids.length) { + int[] grown = new int[ids.length * 2]; + System.arraycopy(ids, 0, grown, 0, ids.length); + ids = grown; + } + ids[count++] = obj.id; + } + if (count == 0) { + return; + } + this.queuedGroundTakeX = tileX; + this.queuedGroundTakeZ = tileZ; + this.queuedGroundTakeIds = ids; + this.queuedGroundTakeCount = count; + this.queuedGroundTakeIndex = count - 1; + this.sendQueuedGroundTake(true); + } + + private void continueQueuedGroundTake(int tileX, int tileZ) { + if (tileX != this.queuedGroundTakeX || tileZ != this.queuedGroundTakeZ || this.queuedGroundTakeIds == null) { + return; + } + this.sendQueuedGroundTake(false); + } + + private void sendQueuedGroundTake(boolean includeMovement) { + if (this.queuedGroundTakeIds == null || this.queuedGroundTakeIndex < 0) { + this.clearQueuedGroundTake(); + return; + } + int tileX = this.queuedGroundTakeX; + int tileZ = this.queuedGroundTakeZ; + if (includeMovement) { + boolean moved = this.tryMove(0, 0, 0, tileX, 2, localPlayer.routeTileZ[0], localPlayer.routeTileX[0], tileZ, false, 0, 0); + if (!moved) { + this.tryMove(0, 1, 0, tileX, 2, localPlayer.routeTileZ[0], localPlayer.routeTileX[0], tileZ, false, 1, 0); + } + this.crossX = super.mouseClickX; + this.crossY = super.mouseClickY; + this.crossMode = 2; + this.crossCycle = 0; + } + int id = this.queuedGroundTakeIds[this.queuedGroundTakeIndex--]; + this.out.pIsaac(178); // OPOBJ3 / Take + this.out.p2(tileX + this.sceneBaseTileX); + this.out.p2(tileZ + this.sceneBaseTileZ); + this.out.p2(id); + } + + private void clearQueuedGroundTake() { + this.queuedGroundTakeX = -1; + this.queuedGroundTakeZ = -1; + this.queuedGroundTakeIds = null; + this.queuedGroundTakeCount = 0; + this.queuedGroundTakeIndex = -1; + } + + private boolean matchesShiftClickSetting(int index) { + if (index < 0 || index >= this.menuSize || this.menuOption[index] == null) { + return false; + } + int action = this.menuAction[index]; + if (action >= 2000) { + action -= 2000; + } + String option = stripMenuTags(this.menuOption[index]).toLowerCase(); + if (GLRenderer.settingShiftDropInventory && action == 100 && option.startsWith("drop ")) { + return true; + } + if (GLRenderer.settingShiftTakeGround && action == 617 && option.startsWith("take ")) { + return true; + } + if (GLRenderer.settingShiftAttackNpc && isNpcAction(action) && option.startsWith("attack ")) { + return true; + } + if (GLRenderer.settingShiftPickpocketNpc && isNpcAction(action) && option.startsWith("pickpocket ")) { + return true; + } + if (GLRenderer.settingShiftBankNpc && isNpcAction(action) && option.startsWith("bank ")) { + return true; + } + if (GLRenderer.settingShiftUseQuicklyBankBooth && isLocAction(action) && option.startsWith("use-quickly ")) { + return true; + } + // Examine is unambiguous by its option text, so match it for any object + // type (inventory item, ground item, loc, npc) rather than by action code. + return GLRenderer.settingShiftExamineAnything && option.startsWith("examine "); + } + + private static boolean isNpcAction(int action) { + return action == 242 || action == 209 || action == 309 || action == 852 || action == 793; + } + + private static boolean isLocAction(int action) { + return action == 625 || action == 721 || action == 743 || action == 357 || action == 1071; + } + + private static String stripMenuTags(String s) { + return s.replaceAll("@...@", "").trim(); + } + + private void updateDiscordRichPresence() { + if (!GLRenderer.settingDiscordRichPresence || !this.ingame || localPlayer == null) { + return; + } + int tileX = this.sceneBaseTileX + (localPlayer.x >> 7); + int tileZ = this.sceneBaseTileZ + (localPlayer.z >> 7); + String area = areaName(tileX, tileZ); + int level = localPlayer.combatLevel; + boolean areaChanged = !area.equals(this.discordLastArea); + boolean levelChanged = level != this.discordLastLevel && this.discordLastLevel != -1; + if (areaChanged || levelChanged || loopCycle - this.discordLastUpdate >= 200) { + this.discordLastArea = area; + this.discordLastLevel = level; + this.discordLastUpdate = loopCycle; + String name = localPlayer.name != null ? localPlayer.name : this.loginUser; + GLRenderer.updateDiscordActivity(name + " (Level " + level + ")", "in " + area); + } + } + + private static String areaName(int x, int z) { + if (x >= 3200 && x <= 3265 && z >= 3200 && z <= 3265) return "Lumbridge"; + if (x >= 3140 && x <= 3215 && z >= 3410 && z <= 3515) return "Varrock"; + if (x >= 2940 && x <= 3060 && z >= 3310 && z <= 3395) return "Falador"; + if (x >= 3080 && x <= 3135 && z >= 3480 && z <= 3525) return "Edgeville"; + if (x >= 3050 && x <= 3135 && z >= 3200 && z <= 3295) return "Draynor"; + if (x >= 3260 && x <= 3335 && z >= 3150 && z <= 3225) return "Al Kharid"; + if (x >= 2800 && x <= 2875 && z >= 3420 && z <= 3510) return "Catherby"; + if (x >= 2600 && x <= 2675 && z >= 3270 && z <= 3335) return "Ardougne"; + if (x >= 2940 && x <= 3015 && z >= 3350 && z <= 3405) return "Port Sarim"; + if (x >= 2940 && x <= 3015 && z >= 3200 && z <= 3265) return "Rimmington"; + if (z >= 3520) return "Wilderness"; + return "Gielinor"; + } + @ObfuscatedName("client.a(BI)V") public void useMenuOption(int arg1) { if (arg1 < 0) { diff --git a/src/main/java/jagex2/client/GameShell.java b/src/main/java/jagex2/client/GameShell.java index eaabf4a..29d167b 100644 --- a/src/main/java/jagex2/client/GameShell.java +++ b/src/main/java/jagex2/client/GameShell.java @@ -25,6 +25,14 @@ public class GameShell extends Panel implements Runnable, MouseListener, MouseMo @ObfuscatedName("a.k") public int fps; + /** + * Fraction (0..1) of the way through the current fixed logic tick at the + * moment {@link #draw()} runs. Only meaningful when {@link #isHighFpsEnabled()} + * is true; the render path uses it to interpolate animations between the + * 50fps logic updates so motion looks smooth at the monitor's refresh rate. + */ + public volatile float subTickFraction = 0f; + @ObfuscatedName("a.l") public boolean debug = false; @@ -145,6 +153,12 @@ public void run() { this.otim[var6] = System.currentTimeMillis(); } long var7 = System.currentTimeMillis(); + // High-FPS (interpolated render) bookkeeping. In this mode the game logic + // still ticks on the fixed server-compatible schedule, but draw() runs + // every monitor refresh and animations are interpolated between ticks. + long highFpsLast = System.currentTimeMillis(); + long logicAccumMs = 0L; + boolean wasHighFps = this.isHighFpsEnabled(); while (true) { long var11; do { @@ -161,59 +175,127 @@ public void run() { return; } } - int var9 = var2; - int var10 = var3; - var2 = 300; - var3 = 1; - var11 = System.currentTimeMillis(); - if (this.otim[var1] == 0L) { - var2 = var9; - var3 = var10; - } else if (var11 > this.otim[var1]) { - var2 = (int) ((long) (this.deltime * 2560) / (var11 - this.otim[var1])); - } - if (var2 < 25) { - var2 = 25; - } - if (var2 > 256) { - var2 = 256; - var3 = (int) ((long) this.deltime - (var11 - this.otim[var1]) / 10L); - } - if (var3 > this.deltime) { - var3 = this.deltime; - } - this.otim[var1] = var11; - var1 = (var1 + 1) % 10; - if (var3 > 1) { - for (int var13 = 0; var13 < 10; var13++) { - if (this.otim[var13] != 0L) { - this.otim[var13] += var3; + boolean highFpsEnabled = this.isHighFpsEnabled(); + if (highFpsEnabled != wasHighFps) { + long now = System.currentTimeMillis(); + highFpsLast = now; + logicAccumMs = 0L; + this.subTickFraction = 0f; + if (!highFpsEnabled) { + for (int i = 0; i < 10; i++) { + this.otim[i] = now; } + var1 = 0; + var2 = 256; + var3 = 1; + var4 = 0; } + wasHighFps = highFpsEnabled; } - if (var3 < this.mindel) { - var3 = this.mindel; - } - try { - Thread.sleep((long) var3); - } catch (InterruptedException var16) { - var5++; - } - while (var4 < 256) { - this.mouseClickButton = this.nextMouseClickButton; - this.mouseClickX = this.nextMouseClickX; - this.mouseClickY = this.nextMouseClickY; - this.mouseClickTime = this.nextMouseClickTime; - this.nextMouseClickButton = 0; - this.loop(); - this.keyQueueReadPos = this.keyQueueWritePos; - var4 += var2; - } - var4 &= 0xFF; - if (this.deltime > 0) { - this.fps = var2 * 1000 / (this.deltime * 256); + if (highFpsEnabled) { + // ---- decoupled path: fixed-timestep logic + interpolated draw ---- + var11 = System.currentTimeMillis(); + long elapsed = var11 - highFpsLast; + highFpsLast = var11; + if (elapsed < 0L) { + elapsed = 0L; + } + if (elapsed > 200L) { + elapsed = 200L; // clamp so a long stall can't spiral the catch-up loop + } + logicAccumMs += elapsed; + int logicMs = this.deltime > 0 ? this.deltime : 20; + int guard = 0; + while (logicAccumMs >= logicMs && guard < 10) { + this.mouseClickButton = this.nextMouseClickButton; + this.mouseClickX = this.nextMouseClickX; + this.mouseClickY = this.nextMouseClickY; + this.mouseClickTime = this.nextMouseClickTime; + this.nextMouseClickButton = 0; + this.loop(); + this.keyQueueReadPos = this.keyQueueWritePos; + logicAccumMs -= logicMs; + guard++; + } + if (logicAccumMs > logicMs) { + logicAccumMs = logicMs; // hit the guard; keep the fraction in [0,1] + } + this.subTickFraction = (float) logicAccumMs / (float) logicMs; + this.draw(); + // glfwSwapInterval(1) makes draw() block on vsync, which paces the + // render to the refresh rate. Add a tiny floor in case vsync is off + // so we don't busy-spin a core at 100%. + long frameMs = System.currentTimeMillis() - var11; + if (frameMs < 2L) { + try { + Thread.sleep(1L); + } catch (InterruptedException ignored) { + } + frameMs = System.currentTimeMillis() - var11; + } + // Report the actual render rate (≈ refresh rate), not the logic rate. + this.fps = frameMs > 0L ? (int) (1000L / frameMs) : 1000; + } else { + // ---- legacy path: 1:1 logic/draw, unchanged ---- + this.subTickFraction = 0f; + int var9 = var2; + int var10 = var3; + var2 = 300; + var3 = 1; + var11 = System.currentTimeMillis(); + if (this.otim[var1] == 0L) { + var2 = var9; + var3 = var10; + } else if (var11 > this.otim[var1]) { + var2 = (int) ((long) (this.deltime * 2560) / (var11 - this.otim[var1])); + } + if (var2 < 25) { + var2 = 25; + } + if (var2 > 256) { + var2 = 256; + var3 = (int) ((long) this.deltime - (var11 - this.otim[var1]) / 10L); + } + if (var3 > this.deltime) { + var3 = this.deltime; + } + this.otim[var1] = var11; + var1 = (var1 + 1) % 10; + if (var3 > 1) { + for (int var13 = 0; var13 < 10; var13++) { + if (this.otim[var13] != 0L) { + this.otim[var13] += var3; + } + } + } + if (var3 < this.mindel) { + var3 = this.mindel; + } + try { + Thread.sleep((long) var3); + } catch (InterruptedException var16) { + var5++; + } + while (var4 < 256) { + this.mouseClickButton = this.nextMouseClickButton; + this.mouseClickX = this.nextMouseClickX; + this.mouseClickY = this.nextMouseClickY; + this.mouseClickTime = this.nextMouseClickTime; + this.nextMouseClickButton = 0; + this.loop(); + this.keyQueueReadPos = this.keyQueueWritePos; + var4 += var2; + } + var4 &= 0xFF; + if (this.deltime > 0) { + this.fps = var2 * 1000 / (this.deltime * 256); + } + this.draw(); + // Keep the high-fps clock fresh so flipping the toggle on mid-session + // doesn't replay a huge accumulated delta as a burst of logic ticks. + highFpsLast = System.currentTimeMillis(); + logicAccumMs = 0L; } - this.draw(); } while (!this.debug); System.out.println("ntime:" + var11); for (int var14 = 0; var14 < 10; var14++) { @@ -254,6 +336,15 @@ public void setFramerate(int arg1) { this.deltime = 1000 / arg1; } + /** + * When true, {@link #run()} renders on a loop decoupled from the fixed logic + * tick (one draw per refresh, interpolated). Subclasses override this to wire + * it to the user's "60 FPS" setting; the base shell keeps the legacy behaviour. + */ + protected boolean isHighFpsEnabled() { + return false; + } + public void start() { if (this.state >= 0) { this.state = 0; diff --git a/src/main/java/jagex2/config/NpcType.java b/src/main/java/jagex2/config/NpcType.java index 4de6733..41222b2 100644 --- a/src/main/java/jagex2/config/NpcType.java +++ b/src/main/java/jagex2/config/NpcType.java @@ -226,6 +226,15 @@ public void decode(Packet arg1) { @ObfuscatedName("gc.a(II[II)Lfb;") public Model getTempModel(int arg1, int[] arg2, int arg3) { + return this.getTempModel(arg1, arg2, arg3, -1, 0); + } + + /** + * As {@link #getTempModel(int, int[], int)} but, for the single-sequence + * (non-masked) case, lag-blends FROM keyframe {@code frameFrom} TO the current + * keyframe {@code arg1} by {@code t256}/256 for 60fps smooth animation. + */ + public Model getTempModel(int arg1, int[] arg2, int arg3, int frameFrom, int t256) { Model var5 = (Model) modelCache.get(this.id); if (var5 == null) { boolean var6 = false; @@ -260,7 +269,11 @@ public Model getTempModel(int arg1, int[] arg2, int arg3) { if (arg1 != -1 && arg3 != -1) { var11.maskAnimate(arg2, arg1, arg3); } else if (arg1 != -1) { - var11.animate(arg1); + if (frameFrom != -1) { + var11.animateInterpolated(frameFrom, arg1, t256); + } else { + var11.animate(arg1); + } } if (this.resizeh != 128 || this.resizev != 128) { var11.resize(this.resizeh, this.resizeh, this.resizev); diff --git a/src/main/java/jagex2/config/ObjType.java b/src/main/java/jagex2/config/ObjType.java index 358d52e..749e550 100644 --- a/src/main/java/jagex2/config/ObjType.java +++ b/src/main/java/jagex2/config/ObjType.java @@ -407,6 +407,11 @@ public Model getModel(int arg0) { var5.recolour(this.recol_s[var6], this.recol_d[var6]); } } + // Ground items must render solid. The GL renderer has the depth buffer off + // and blending always on, so any face alpha carried by the item model shows + // as see-through (e.g. logs you can look through). Dropped items are never + // translucent, so drop the per-face alpha for the ground model. + var5.faceAlpha = null; var5.calculateNormals(this.ambient + 64, this.contrast + 768, -50, -10, -50, true); var5.useAABBMouseCheck = true; modelCache.put(var5, (long) this.id); diff --git a/src/main/java/jagex2/dash3d/ClientEntity.java b/src/main/java/jagex2/dash3d/ClientEntity.java index 351a4cc..4b61ac3 100644 --- a/src/main/java/jagex2/dash3d/ClientEntity.java +++ b/src/main/java/jagex2/dash3d/ClientEntity.java @@ -13,6 +13,11 @@ public class ClientEntity extends ModelSource { @ObfuscatedName("z.p") public int z; + /** Scene position at the start of the current logic tick, for 60fps render-time + * position interpolation. Updated each tick in Client.moveEntity. */ + public int prevSceneX; + public int prevSceneZ; + @ObfuscatedName("z.q") public int yaw; @@ -172,6 +177,79 @@ public class ClientEntity extends ModelSource { @ObfuscatedName("z.A") public String chatMessage; + // ---- Render-time animation interpolation (60fps "smooth" mode) ---- + /** Sub-tick fraction in [0,1] through the current 50fps logic tick. */ + public static float renderInterp = 0f; + /** Whether animation interpolation is active this frame. */ + public static boolean renderInterpOn = false; + /** Scratch output of {@link #seqInterpWeight}: the AnimFrame id to blend FROM. */ + public int interpFromFrame = -1; + // Lag-interpolation state: the keyframe currently displayed and the one shown + // just before it, tracked separately for the primary and secondary sequences. + private int interpObservedPrimary = -1; + private int interpPrevPrimary = -1; + private int interpObservedSecondary = -1; + private int interpPrevSecondary = -1; + + /** + * Lag interpolation: blends FROM the keyframe shown just before the current + * one TO the current keyframe, across the current frame's hold window. Returns + * the blend weight (0..256) and sets {@link #interpFromFrame} to the frame to + * blend from; the caller renders {@code animateInterpolated(interpFromFrame, + * currentFrame, weight)}. + * + *

Because it always blends between two frames that were actually displayed, + * it handles every transition the same way — normal advances, loop wraps, and + * even server-driven restarts (which jump the frame backwards) — with no + * special cases. Returns 0 with interpFromFrame = -1 when there is nothing to + * interpolate (interpolation off, or no distinct previous frame yet), so the + * caller falls back to a plain single frame. + */ + public int seqInterpWeight(SeqType seq, int frameIndex, int cycle, boolean secondary) { + this.interpFromFrame = -1; + if (seq == null || frameIndex < 0 || frameIndex >= seq.numFrames) { + return 0; + } + int curFrame = seq.frames[frameIndex]; + // Track the previous distinct keyframe. Kept up to date every render (even + // when interpolation is off) so it's correct the instant it's re-enabled. + int prev; + if (secondary) { + if (curFrame != this.interpObservedSecondary) { + this.interpPrevSecondary = this.interpObservedSecondary; + this.interpObservedSecondary = curFrame; + } + prev = this.interpPrevSecondary; + } else { + if (curFrame != this.interpObservedPrimary) { + this.interpPrevPrimary = this.interpObservedPrimary; + this.interpObservedPrimary = curFrame; + } + prev = this.interpPrevPrimary; + } + if (!renderInterpOn || renderInterp < 0f || prev == -1 || prev == curFrame) { + return 0; + } + int duration = seq.getDuration(frameIndex); + if (duration <= 0) { + return 0; + } + // Continuous position through the current frame's hold window, in [0,1]. + // Secondary cycles run 0..duration (duration+1 windows); primary cycles run + // 1..duration. At pos 0 the previous frame is shown, at pos 1 the current. + float pos = secondary + ? (cycle + renderInterp) / (float) (duration + 1) + : (cycle - 1 + renderInterp) / (float) duration; + if (pos < 0f) { + pos = 0f; + } + if (pos > 1f) { + pos = 1f; + } + this.interpFromFrame = prev; + return (int) (pos * 256f); + } + @ObfuscatedName("z.a(IIZZ)V") public void teleport(int arg0, int arg1, boolean arg3) { if (this.primarySeqId != -1 && SeqType.list[this.primarySeqId].postanim_move == 1) { diff --git a/src/main/java/jagex2/dash3d/ClientNpc.java b/src/main/java/jagex2/dash3d/ClientNpc.java index fb39e5e..a81e953 100644 --- a/src/main/java/jagex2/dash3d/ClientNpc.java +++ b/src/main/java/jagex2/dash3d/ClientNpc.java @@ -50,18 +50,28 @@ public Model getTempModel() { @ObfuscatedName("ab.c(I)Lfb;") public Model getTempModel2() { if (super.primarySeqId >= 0 && super.primarySeqDelay == 0) { - int var2 = SeqType.list[super.primarySeqId].frames[super.primarySeqFrame]; + SeqType var1 = SeqType.list[super.primarySeqId]; + int var2 = var1.frames[super.primarySeqFrame]; int var3 = -1; if (super.secondarySeqId >= 0 && super.secondarySeqId != super.readyanim) { var3 = SeqType.list[super.secondarySeqId].frames[super.secondarySeqFrame]; } - return this.type.getTempModel(var2, SeqType.list[super.primarySeqId].walkmerge, var3); + if (var3 == -1) { + int var4 = this.seqInterpWeight(var1, super.primarySeqFrame, super.primarySeqCycle, false); + return this.type.getTempModel(var2, var1.walkmerge, -1, this.interpFromFrame, var4); + } + return this.type.getTempModel(var2, var1.walkmerge, var3); } else { - int var4 = -1; + int var5 = -1; + int var6 = 0; + int var7 = -1; if (super.secondarySeqId >= 0) { - var4 = SeqType.list[super.secondarySeqId].frames[super.secondarySeqFrame]; + SeqType var8 = SeqType.list[super.secondarySeqId]; + var5 = var8.frames[super.secondarySeqFrame]; + var6 = this.seqInterpWeight(var8, super.secondarySeqFrame, super.secondarySeqCycle, true); + var7 = this.interpFromFrame; } - return this.type.getTempModel(var4, null, -1); + return this.type.getTempModel(var5, null, -1, var7, var6); } } diff --git a/src/main/java/jagex2/dash3d/ClientPlayer.java b/src/main/java/jagex2/dash3d/ClientPlayer.java index de362af..59cfa43 100644 --- a/src/main/java/jagex2/dash3d/ClientPlayer.java +++ b/src/main/java/jagex2/dash3d/ClientPlayer.java @@ -229,12 +229,20 @@ public Model getTempModel() { public Model getTempModel2() { if (this.transmog != null) { int var2 = -1; + int transmogFrom = -1; + int transmogT = 0; if (super.primarySeqId >= 0 && super.primarySeqDelay == 0) { - var2 = SeqType.list[super.primarySeqId].frames[super.primarySeqFrame]; + SeqType var3 = SeqType.list[super.primarySeqId]; + var2 = var3.frames[super.primarySeqFrame]; + transmogT = this.seqInterpWeight(var3, super.primarySeqFrame, super.primarySeqCycle, false); + transmogFrom = this.interpFromFrame; } else if (super.secondarySeqId >= 0) { - var2 = SeqType.list[super.secondarySeqId].frames[super.secondarySeqFrame]; + SeqType var4 = SeqType.list[super.secondarySeqId]; + var2 = var4.frames[super.secondarySeqFrame]; + transmogT = this.seqInterpWeight(var4, super.secondarySeqFrame, super.secondarySeqCycle, true); + transmogFrom = this.interpFromFrame; } - return this.transmog.getTempModel(var2, null, -1); + return this.transmog.getTempModel(var2, null, -1, transmogFrom, transmogT); } long var4 = this.baseId; @@ -242,9 +250,15 @@ public Model getTempModel2() { int var7 = -1; int var8 = -1; int var9 = -1; + // Lag-interpolation source frame + weight for the single-sequence (non-masked) + // case; computed alongside var6 so the apply below can blend keyframes. + int interpFrom = -1; + int interpT = 0; if (super.primarySeqId >= 0 && super.primarySeqDelay == 0) { SeqType var10 = SeqType.list[super.primarySeqId]; var6 = var10.frames[super.primarySeqFrame]; + interpT = this.seqInterpWeight(var10, super.primarySeqFrame, super.primarySeqCycle, false); + interpFrom = this.interpFromFrame; if (super.secondarySeqId >= 0 && super.secondarySeqId != super.readyanim) { var7 = SeqType.list[super.secondarySeqId].frames[super.secondarySeqFrame]; } @@ -257,7 +271,10 @@ public Model getTempModel2() { var4 += var9 - this.appearance[3] << 16; } } else if (super.secondarySeqId >= 0) { - var6 = SeqType.list[super.secondarySeqId].frames[super.secondarySeqFrame]; + SeqType secSeq = SeqType.list[super.secondarySeqId]; + var6 = secSeq.frames[super.secondarySeqFrame]; + interpT = this.seqInterpWeight(secSeq, super.secondarySeqFrame, super.secondarySeqCycle, true); + interpFrom = this.interpFromFrame; } Model var11 = (Model) modelCache.get(var4); if (var11 == null) { @@ -332,7 +349,11 @@ public Model getTempModel2() { if (var6 != -1 && var7 != -1) { var22.maskAnimate(SeqType.list[super.primarySeqId].walkmerge, var6, var7); } else if (var6 != -1) { - var22.animate(var6); + if (interpFrom != -1) { + var22.animateInterpolated(interpFrom, var6, interpT); + } else { + var22.animate(var6); + } } var22.calcBoundingCylinder(); var22.labelFaces = null; diff --git a/src/main/java/jagex2/dash3d/Model.java b/src/main/java/jagex2/dash3d/Model.java index e7b6a6f..1698fe6 100644 --- a/src/main/java/jagex2/dash3d/Model.java +++ b/src/main/java/jagex2/dash3d/Model.java @@ -27,6 +27,16 @@ public class Model extends ModelSource { @ObfuscatedName("fb.v") public static int[] tmpFaceAlpha = new int[2000]; + // Scratch buffers for vertex-level animation interpolation (60fps mode). + private static int[] interpBaseX = new int[2000]; + private static int[] interpBaseY = new int[2000]; + private static int[] interpBaseZ = new int[2000]; + private static int[] interpPoseX = new int[2000]; + private static int[] interpPoseY = new int[2000]; + private static int[] interpPoseZ = new int[2000]; + private static int[] interpBaseAlpha = new int[2000]; + public static boolean forceOpaqueFaceAlpha; + @ObfuscatedName("fb.w") public int vertexCount; @@ -1078,6 +1088,82 @@ public void animate(int arg1) { } } + /** + * Blends between two keyframes of the same animation by {@code t256}/256 + * (0 = frame A, 256 = frame B) to render smooth in-between poses for 60fps + * mode. Works at the vertex level: it builds each keyframe's full pose from + * the current rest vertices and linearly interpolates the results. Doing it + * post-skinning avoids the stateful per-frame pivot/origin transforms, which + * cannot be safely merged across two frames. Falls back to a plain + * single-frame apply when the inputs can't be interpolated. + * + *

Must be called while the model holds its un-animated rest pose (i.e. + * straight after {@code set(...)}), exactly like {@link #animate(int)}. + */ + public void animateInterpolated(int frameIdA, int frameIdB, int t256) { + if (this.labelVertices == null || frameIdA == -1) { + return; + } + if (t256 <= 0 || frameIdB == -1) { + this.animate(frameIdA); + return; + } + if (t256 >= 256) { + this.animate(frameIdB); + return; + } + AnimFrame var5 = AnimFrame.get(frameIdA); + AnimFrame var6 = AnimFrame.get(frameIdB); + if (var5 == null) { + return; + } + if (var6 == null || var6.base != var5.base) { + this.animate(frameIdA); + return; + } + int var7 = this.vertexCount; + if (interpBaseX.length < var7) { + interpBaseX = new int[var7 + 100]; + interpBaseY = new int[var7 + 100]; + interpBaseZ = new int[var7 + 100]; + interpPoseX = new int[var7 + 100]; + interpPoseY = new int[var7 + 100]; + interpPoseZ = new int[var7 + 100]; + } + // Snapshot the rest pose so frame B can be built from the same base. + System.arraycopy(this.vertexX, 0, interpBaseX, 0, var7); + System.arraycopy(this.vertexY, 0, interpBaseY, 0, var7); + System.arraycopy(this.vertexZ, 0, interpBaseZ, 0, var7); + // animate() also folds type-5 alpha transforms into faceAlpha; snapshot it + // too so building both poses doesn't apply those changes twice. + boolean var9 = this.faceAlpha != null; + if (var9) { + if (interpBaseAlpha.length < this.faceCount) { + interpBaseAlpha = new int[this.faceCount + 100]; + } + System.arraycopy(this.faceAlpha, 0, interpBaseAlpha, 0, this.faceCount); + } + // Pose A. + this.animate(frameIdA); + System.arraycopy(this.vertexX, 0, interpPoseX, 0, var7); + System.arraycopy(this.vertexY, 0, interpPoseY, 0, var7); + System.arraycopy(this.vertexZ, 0, interpPoseZ, 0, var7); + // Restore the rest pose (and alpha), then build pose B in place. + System.arraycopy(interpBaseX, 0, this.vertexX, 0, var7); + System.arraycopy(interpBaseY, 0, this.vertexY, 0, var7); + System.arraycopy(interpBaseZ, 0, this.vertexZ, 0, var7); + if (var9) { + System.arraycopy(interpBaseAlpha, 0, this.faceAlpha, 0, this.faceCount); + } + this.animate(frameIdB); + // vertex = poseA + (poseB - poseA) * t. + for (int var8 = 0; var8 < var7; var8++) { + this.vertexX[var8] = interpPoseX[var8] + (this.vertexX[var8] - interpPoseX[var8]) * t256 / 256; + this.vertexY[var8] = interpPoseY[var8] + (this.vertexY[var8] - interpPoseY[var8]) * t256 / 256; + this.vertexZ[var8] = interpPoseZ[var8] + (this.vertexZ[var8] - interpPoseZ[var8]) * t256 / 256; + } + } + @ObfuscatedName("fb.a([IIII)V") public void maskAnimate(int[] arg0, int arg2, int arg3) { if (arg2 == -1) { @@ -1783,6 +1869,11 @@ public void render2(boolean arg0, boolean arg1, int arg2) { @ObfuscatedName("fb.f(I)V") public void render3(int arg0) { + if (this.faceAlpha == null || forceOpaqueFaceAlpha) { + Pix3D.trans = 0; + } else { + Pix3D.trans = this.faceAlpha[arg0]; + } if (faceNearClipped[arg0]) { this.render3ZClip(arg0); return; @@ -1794,11 +1885,6 @@ public void render3(int arg0) { Pix3D.triZ1 = vertexScreenZ[var3] + this.minDepth; Pix3D.triZ2 = vertexScreenZ[var4] + this.minDepth; Pix3D.hclip = faceClippedX[arg0]; - if (this.faceAlpha == null) { - Pix3D.trans = 0; - } else { - Pix3D.trans = this.faceAlpha[arg0]; - } int var5; if (this.faceInfo == null) { var5 = 0; diff --git a/src/main/java/jagex2/dash3d/World.java b/src/main/java/jagex2/dash3d/World.java index aee0601..c26d344 100644 --- a/src/main/java/jagex2/dash3d/World.java +++ b/src/main/java/jagex2/dash3d/World.java @@ -197,6 +197,8 @@ public class World { @ObfuscatedName("s.M") public static boolean click; + private GroundObject deferredGroundObject; + public World(int arg0, int arg1, int[][][] arg2, int arg4) { this.maxLevel = arg4; this.maxTileX = arg0; @@ -1350,22 +1352,21 @@ public void fill(Square arg0, boolean arg1) { } } } + this.deferredGroundObject = null; if (var18) { GroundDecor var33 = var3.groundDecor; if (var33 != null) { - var33.model.worldRender(0, cameraSinX, cameraCosX, cameraSinY, cameraCosY, var33.x - cx, var33.y - cy, var33.z - cz, var33.typecode); + boolean forceOpaque = Model.forceOpaqueFaceAlpha; + Model.forceOpaqueFaceAlpha = true; + try { + var33.model.worldRender(0, cameraSinX, cameraCosX, cameraSinY, cameraCosY, var33.x - cx, var33.y - cy, var33.z - cz, var33.typecode); + } finally { + Model.forceOpaqueFaceAlpha = forceOpaque; + } } GroundObject var34 = var3.groundObject; if (var34 != null && var34.height == 0) { - if (var34.bottom != null) { - var34.bottom.worldRender(0, cameraSinX, cameraCosX, cameraSinY, cameraCosY, var34.x - cx, var34.y - cy, var34.z - cz, var34.typecode); - } - if (var34.middle != null) { - var34.middle.worldRender(0, cameraSinX, cameraCosX, cameraSinY, cameraCosY, var34.x - cx, var34.y - cy, var34.z - cz, var34.typecode); - } - if (var34.top != null) { - var34.top.worldRender(0, cameraSinX, cameraCosX, cameraSinY, cameraCosY, var34.x - cx, var34.y - cy, var34.z - cz, var34.typecode); - } + this.deferredGroundObject = var34; } } int var35 = var3.combinedPrimaryExtendDirections; @@ -1414,6 +1415,10 @@ public void fill(Square arg0, boolean arg1) { } } if (!var3.drawPrimaries) { + if (this.deferredGroundObject != null) { + this.renderGroundObject(this.deferredGroundObject, 0); + this.deferredGroundObject = null; + } break; } int var43 = var3.primaryCount; @@ -1505,6 +1510,10 @@ public void fill(Square arg0, boolean arg1) { } } if (!var3.drawPrimaries) { + if (this.deferredGroundObject != null) { + this.renderGroundObject(this.deferredGroundObject, 0); + this.deferredGroundObject = null; + } break; } } @@ -1534,15 +1543,7 @@ public void fill(Square arg0, boolean arg1) { fillLeft--; GroundObject var71 = var3.groundObject; if (var71 != null && var71.height != 0) { - if (var71.bottom != null) { - var71.bottom.worldRender(0, cameraSinX, cameraCosX, cameraSinY, cameraCosY, var71.x - cx, var71.y - cy - var71.height, var71.z - cz, var71.typecode); - } - if (var71.middle != null) { - var71.middle.worldRender(0, cameraSinX, cameraCosX, cameraSinY, cameraCosY, var71.x - cx, var71.y - cy - var71.height, var71.z - cz, var71.typecode); - } - if (var71.top != null) { - var71.top.worldRender(0, cameraSinX, cameraCosX, cameraSinY, cameraCosY, var71.x - cx, var71.y - cy - var71.height, var71.z - cz, var71.typecode); - } + this.renderGroundObject(var71, var71.height); } if (var3.backWallTypes != 0) { Decor var72 = var3.decor; @@ -1621,6 +1622,24 @@ public void fill(Square arg0, boolean arg1) { } } + private void renderGroundObject(GroundObject obj, int yOffset) { + boolean forceOpaque = Model.forceOpaqueFaceAlpha; + Model.forceOpaqueFaceAlpha = true; + try { + if (obj.bottom != null) { + obj.bottom.worldRender(0, cameraSinX, cameraCosX, cameraSinY, cameraCosY, obj.x - cx, obj.y - cy - yOffset, obj.z - cz, obj.typecode); + } + if (obj.middle != null) { + obj.middle.worldRender(0, cameraSinX, cameraCosX, cameraSinY, cameraCosY, obj.x - cx, obj.y - cy - yOffset, obj.z - cz, obj.typecode); + } + if (obj.top != null) { + obj.top.worldRender(0, cameraSinX, cameraCosX, cameraSinY, cameraCosY, obj.x - cx, obj.y - cy - yOffset, obj.z - cz, obj.typecode); + } + } finally { + Model.forceOpaqueFaceAlpha = forceOpaque; + } + } + @ObfuscatedName("s.a(Lp;IIIIIII)V") public void renderQuickGround(QuickGround arg0, int arg1, int arg2, int arg3, int arg4, int arg5, int arg6, int arg7) { int var9; diff --git a/src/main/java/sign/signlink.java b/src/main/java/sign/signlink.java index 4b645cd..9fa679b 100644 --- a/src/main/java/sign/signlink.java +++ b/src/main/java/sign/signlink.java @@ -174,23 +174,38 @@ public void run() { } public static String findcachedir() { - String[] var0 = new String[] { "c:/windows/", "c:/winnt/", "d:/windows/", "d:/winnt/", "e:/windows/", "e:/winnt/", "f:/windows/", "f:/winnt/", "c:/", "~/", "/tmp/", "" }; if (storeid < 32 || storeid > 34) { storeid = 32; } - String var1 = ".file_store_" + storeid; + String var1 = "file_store_" + storeid; + String override = System.getProperty("rs254.cache.dir"); + String userDir = System.getProperty("user.dir", "."); + String userHome = System.getProperty("user.home", "."); + String tmpDir = System.getProperty("java.io.tmpdir", "."); + String[] var0 = new String[] { + override, + userDir + File.separator + "cache", + userHome + File.separator + ".progressive-java-client" + File.separator + var1, + tmpDir + File.separator + ".progressive-java-client" + File.separator + var1, + "c:/windows/.file_store_" + storeid, + "c:/winnt/.file_store_" + storeid, + "d:/windows/.file_store_" + storeid, + "d:/winnt/.file_store_" + storeid, + "e:/windows/.file_store_" + storeid, + "e:/winnt/.file_store_" + storeid, + "f:/windows/.file_store_" + storeid, + "f:/winnt/.file_store_" + storeid, + "c:/.file_store_" + storeid + }; for (int var2 = 0; var2 < var0.length; var2++) { try { String var3 = var0[var2]; - if (var3.length() > 0) { - File var4 = new File(var3); - if (!var4.exists()) { - continue; - } + if (var3 == null || var3.length() == 0) { + continue; } - File var5 = new File(var3 + var1); - if (var5.exists() || var5.mkdir()) { - return var3 + var1 + "/"; + File var4 = new File(var3); + if ((var4.exists() || var4.mkdirs()) && var4.isDirectory() && canWriteCacheDir(var4)) { + return var4.getPath() + File.separator; } } catch (Exception var6) { } @@ -198,7 +213,36 @@ public static String findcachedir() { return null; } + private static boolean canWriteCacheDir(File arg0) { + File var1 = new File(arg0, ".write_test"); + try { + FileOutputStream var2 = new FileOutputStream(var1); + var2.write(0); + var2.close(); + var1.delete(); + File var3 = new File(arg0, "main_file_cache.dat"); + if (var3.exists()) { + RandomAccessFile var4 = new RandomAccessFile(var3, "rw"); + var4.close(); + } + for (int var5 = 0; var5 < 5; var5++) { + File var6 = new File(arg0, "main_file_cache.idx" + var5); + if (var6.exists()) { + RandomAccessFile var7 = new RandomAccessFile(var6, "rw"); + var7.close(); + } + } + return true; + } catch (Exception var8) { + var1.delete(); + return false; + } + } + public static int getuid(String arg0) { + if (arg0 == null) { + return 0; + } try { File var1 = new File(arg0 + "uid.dat"); if (!var1.exists() || var1.length() < 4L) { diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/bigchompybirdhunting.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/bigchompybirdhunting.png new file mode 100644 index 0000000..b13d830 Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/bigchompybirdhunting.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/biohazard.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/biohazard.png new file mode 100644 index 0000000..77a961f Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/biohazard.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/blackknightsfortress.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/blackknightsfortress.png new file mode 100644 index 0000000..7204e87 Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/blackknightsfortress.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/clocktower.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/clocktower.png new file mode 100644 index 0000000..73ad1dd Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/clocktower.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/cooksassistant.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/cooksassistant.png new file mode 100644 index 0000000..f5f6ddf Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/cooksassistant.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/deathplateau.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/deathplateau.png new file mode 100644 index 0000000..b1e827e Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/deathplateau.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/demonslayer.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/demonslayer.png new file mode 100644 index 0000000..141fb07 Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/demonslayer.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/digsite.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/digsite.png new file mode 100644 index 0000000..7ce38fe Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/digsite.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/dorics.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/dorics.png new file mode 100644 index 0000000..e23e859 Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/dorics.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/dragonslayer.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/dragonslayer.png new file mode 100644 index 0000000..63a0e6a Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/dragonslayer.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/druidicritual.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/druidicritual.png new file mode 100644 index 0000000..0c1c51c Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/druidicritual.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/dwarfcannon.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/dwarfcannon.png new file mode 100644 index 0000000..b7dc6cb Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/dwarfcannon.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/eadgar.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/eadgar.png new file mode 100644 index 0000000..c2a60db Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/eadgar.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/elementalworkshop.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/elementalworkshop.png new file mode 100644 index 0000000..8527721 Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/elementalworkshop.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/ernestthechicken.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/ernestthechicken.png new file mode 100644 index 0000000..1a9c22d Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/ernestthechicken.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/familycrest.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/familycrest.png new file mode 100644 index 0000000..51a83c2 Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/familycrest.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/fightarena.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/fightarena.png new file mode 100644 index 0000000..c77c39e Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/fightarena.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/fishingcontest.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/fishingcontest.png new file mode 100644 index 0000000..624f51d Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/fishingcontest.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/fremtrials.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/fremtrials.png new file mode 100644 index 0000000..0295435 Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/fremtrials.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/gertrudescat.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/gertrudescat.png new file mode 100644 index 0000000..ffc785a Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/gertrudescat.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/goblindiplomacy.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/goblindiplomacy.png new file mode 100644 index 0000000..fd09f95 Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/goblindiplomacy.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/grandtree.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/grandtree.png new file mode 100644 index 0000000..974fb55 Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/grandtree.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/hazeelcult.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/hazeelcult.png new file mode 100644 index 0000000..5cf01b6 Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/hazeelcult.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/heros.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/heros.png new file mode 100644 index 0000000..591b3b3 Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/heros.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/holygrail.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/holygrail.png new file mode 100644 index 0000000..b40cbd2 Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/holygrail.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/horror.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/horror.png new file mode 100644 index 0000000..51c4ff5 Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/horror.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/impcatcher.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/impcatcher.png new file mode 100644 index 0000000..c3c59b6 Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/impcatcher.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/junglepotion.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/junglepotion.png new file mode 100644 index 0000000..4238b63 Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/junglepotion.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/knightssword.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/knightssword.png new file mode 100644 index 0000000..c985f43 Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/knightssword.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/legends.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/legends.png new file mode 100644 index 0000000..3f61f30 Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/legends.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/lostcity.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/lostcity.png new file mode 100644 index 0000000..2990a87 Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/lostcity.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/merlinscrystal.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/merlinscrystal.png new file mode 100644 index 0000000..e93eb5d Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/merlinscrystal.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/monksfriend.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/monksfriend.png new file mode 100644 index 0000000..f33901e Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/monksfriend.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/murdermystery.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/murdermystery.png new file mode 100644 index 0000000..f7a53a9 Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/murdermystery.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/naturespirit.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/naturespirit.png new file mode 100644 index 0000000..65bdc90 Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/naturespirit.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/observatory.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/observatory.png new file mode 100644 index 0000000..7aa3a5a Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/observatory.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/piratestreasure.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/piratestreasure.png new file mode 100644 index 0000000..319417d Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/piratestreasure.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/plaguecity.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/plaguecity.png new file mode 100644 index 0000000..508bc88 Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/plaguecity.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/priestinperil.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/priestinperil.png new file mode 100644 index 0000000..93c09b2 Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/priestinperil.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/princealirescue.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/princealirescue.png new file mode 100644 index 0000000..d7e737e Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/princealirescue.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/regicide.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/regicide.png new file mode 100644 index 0000000..7f08fc3 Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/regicide.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/restlessghost.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/restlessghost.png new file mode 100644 index 0000000..cdd3b07 Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/restlessghost.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/romeojuliet.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/romeojuliet.png new file mode 100644 index 0000000..e590708 Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/romeojuliet.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/runemysteries.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/runemysteries.png new file mode 100644 index 0000000..d6b2e26 Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/runemysteries.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/scorpioncatcher.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/scorpioncatcher.png new file mode 100644 index 0000000..339e31f Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/scorpioncatcher.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/seaslug.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/seaslug.png new file mode 100644 index 0000000..189a276 Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/seaslug.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/shades.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/shades.png new file mode 100644 index 0000000..e7b8866 Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/shades.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/sheepherder.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/sheepherder.png new file mode 100644 index 0000000..5fa17e0 Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/sheepherder.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/sheepshearer.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/sheepshearer.png new file mode 100644 index 0000000..6fe2750 Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/sheepshearer.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/shieldofarrav.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/shieldofarrav.png new file mode 100644 index 0000000..5c7626c Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/shieldofarrav.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/shilovillage.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/shilovillage.png new file mode 100644 index 0000000..273a8cd Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/shilovillage.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/tbwt.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/tbwt.png new file mode 100644 index 0000000..08bee7c Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/tbwt.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/templeofikov.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/templeofikov.png new file mode 100644 index 0000000..04c98d2 Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/templeofikov.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/touristtrap.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/touristtrap.png new file mode 100644 index 0000000..da32a90 Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/touristtrap.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/treegnomevillage.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/treegnomevillage.png new file mode 100644 index 0000000..9ec8277 Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/treegnomevillage.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/tribaltotem.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/tribaltotem.png new file mode 100644 index 0000000..674ae9f Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/tribaltotem.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/trollstronghold.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/trollstronghold.png new file mode 100644 index 0000000..0fe7aa3 Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/trollstronghold.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/undergroundpass.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/undergroundpass.png new file mode 100644 index 0000000..40bb479 Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/undergroundpass.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/vampireslayer.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/vampireslayer.png new file mode 100644 index 0000000..a41fccc Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/vampireslayer.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/watchtower.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/watchtower.png new file mode 100644 index 0000000..9011997 Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/watchtower.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/waterfall.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/waterfall.png new file mode 100644 index 0000000..4fb1a7f Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/waterfall.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/witchshouse.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/witchshouse.png new file mode 100644 index 0000000..928c455 Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/witchshouse.png differ diff --git a/src/main/resources/losthq/img/questimages/quest_complete_thumb/witchspotion.png b/src/main/resources/losthq/img/questimages/quest_complete_thumb/witchspotion.png new file mode 100644 index 0000000..d9a5b43 Binary files /dev/null and b/src/main/resources/losthq/img/questimages/quest_complete_thumb/witchspotion.png differ diff --git a/src/main/resources/losthq/p_questguides_quest_bigchompybirdhunting.html b/src/main/resources/losthq/p_questguides_quest_bigchompybirdhunting.html index 57933f6..917c441 100644 --- a/src/main/resources/losthq/p_questguides_quest_bigchompybirdhunting.html +++ b/src/main/resources/losthq/p_questguides_quest_bigchompybirdhunting.html @@ -141,7 +141,7 @@

Instructions:



When you have cooked the bird, go to Rantz and talk to him.


Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)


diff --git a/src/main/resources/losthq/p_questguides_quest_biohazard.html b/src/main/resources/losthq/p_questguides_quest_biohazard.html index ed0045f..3af4e0e 100644 --- a/src/main/resources/losthq/p_questguides_quest_biohazard.html +++ b/src/main/resources/losthq/p_questguides_quest_biohazard.html @@ -93,7 +93,7 @@

Instructions:



The king is on the 2nd floor of the palace in Ardougne. The palace is located in the southwest of Ardougne. Talk to King Lathas, and he will tell you about his brother, King Tyras, and reward you.


Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)


diff --git a/src/main/resources/losthq/p_questguides_quest_blackknightsfortress.html b/src/main/resources/losthq/p_questguides_quest_blackknightsfortress.html index 575e1f9..f9ab1f4 100644 --- a/src/main/resources/losthq/p_questguides_quest_blackknightsfortress.html +++ b/src/main/resources/losthq/p_questguides_quest_blackknightsfortress.html @@ -85,7 +85,7 @@

Instructions:



Go back and talk to Sir Amik Varze. He will reward you.


Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)

This quest guide was written on RuneHQ by Henry-x. Thanks to DNKevin, Weezy patgil2003, MarilynManson, Nitr021, Ozzy, and pokemama for corrections.

This quest guide was entered into the database on Sat, Feb 07, 2004, at 10:08:20 PM by Chownuggs and CJH and was last updated on Sat, Feb 05, 2005, at 06:17:02 AM by nitro21. diff --git a/src/main/resources/losthq/p_questguides_quest_clocktower.html b/src/main/resources/losthq/p_questguides_quest_clocktower.html index 4379590..6e40068 100644 --- a/src/main/resources/losthq/p_questguides_quest_clocktower.html +++ b/src/main/resources/losthq/p_questguides_quest_clocktower.html @@ -126,7 +126,7 @@

Black Cog:



Now that all of the cogs have been placed, go up the ladder and talk to Brother Kojo to receive your reward.


Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)


diff --git a/src/main/resources/losthq/p_questguides_quest_cooksassistant.html b/src/main/resources/losthq/p_questguides_quest_cooksassistant.html index 2eac929..fbf112e 100644 --- a/src/main/resources/losthq/p_questguides_quest_cooksassistant.html +++ b/src/main/resources/losthq/p_questguides_quest_cooksassistant.html @@ -69,7 +69,7 @@

Instructions:

After you get all the ingredients, return to the cook and he will reward you.



Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)

This quest guide was written on RuneHQ by henry-x. Thanks to DNKevin and Weezy for corrections.

This quest guide was entered into the database on Sat, Feb 21, 2004, at 04:19:36 PM by Weezy and CJH and was last updated on Mon, Aug 02, 2004, at 07:38:58 AM. diff --git a/src/main/resources/losthq/p_questguides_quest_deathplateau.html b/src/main/resources/losthq/p_questguides_quest_deathplateau.html index 9315179..b19d080 100644 --- a/src/main/resources/losthq/p_questguides_quest_deathplateau.html +++ b/src/main/resources/losthq/p_questguides_quest_deathplateau.html @@ -147,7 +147,7 @@

To find the the Sherpa:

Speak with Denulth, and give him the Secret Way Map and the combination for your reward.

Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)

This quest guide was written by Im4eversmart and leaderofdarkness . Thanks to Henry_n and EverettP for corrections.

This quest guide was entered into the database on Mon, Aug 09, 2004, at 10:33:21 PM by xxtigurxx and was last updated on Mon, Dec 13, 2004, at 04:44:07 AM by MrStormy. diff --git a/src/main/resources/losthq/p_questguides_quest_demonslayer.html b/src/main/resources/losthq/p_questguides_quest_demonslayer.html index 40200b7..dee20bf 100644 --- a/src/main/resources/losthq/p_questguides_quest_demonslayer.html +++ b/src/main/resources/losthq/p_questguides_quest_demonslayer.html @@ -88,7 +88,7 @@

Instructions:



Once he's gone (Congratuations) you're done and you get the reward.


Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)

This quest guide was written on RuneHQ by Gnat88. Thanks to Keystone, Nitr021, Pirate Bob49, pokemama, and Sythion for corrections.

This quest guide was entered into the database on Tue, Mar 02, 2004, at 09:46:46 PM by Weezy and CJH and was last updated on Fri, Sept 26, 2025, at 08:53:32 PM by Halogod35. diff --git a/src/main/resources/losthq/p_questguides_quest_digsite.html b/src/main/resources/losthq/p_questguides_quest_digsite.html index 448c329..4934495 100644 --- a/src/main/resources/losthq/p_questguides_quest_digsite.html +++ b/src/main/resources/losthq/p_questguides_quest_digsite.html @@ -165,7 +165,7 @@

The Stone Tablet



Take the stone tablet back to the Archeological expert to complete the quest and get your reward.


Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)

This quest guide was written by Elyria1. Thanks to FunkyMetal, stormer, kevin rigby, ya sissy jrr for corrections.

This quest guide was entered into the database on Wed, Jun 09, 2004, at 12:23:34 PM by Pirate Bob49 and was last updated on Mon, Jul 05, 2004, at 10:48:51 PM. diff --git a/src/main/resources/losthq/p_questguides_quest_dorics.html b/src/main/resources/losthq/p_questguides_quest_dorics.html index 12148bf..f9aba9f 100644 --- a/src/main/resources/losthq/p_questguides_quest_dorics.html +++ b/src/main/resources/losthq/p_questguides_quest_dorics.html @@ -73,7 +73,7 @@

Instructions:



Congratulations! You've just completed Doric's Quest and can now use his anvils.


Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)

This quest guide was written on RuneHQ by Stormer and Ghou Lies. Thanks to Nitr021, Weezy, and Pirate Bob49 for corrections.

This quest guide was entered into the database on Mon, Feb 16, 2004, at 03:38:33 PM by Chownuggs and CJH and was last updated on Sat, Feb 05, 2005, at 06:26:44 AM by nitro21. diff --git a/src/main/resources/losthq/p_questguides_quest_dragonslayer.html b/src/main/resources/losthq/p_questguides_quest_dragonslayer.html index 3118e2e..4fecd00 100644 --- a/src/main/resources/losthq/p_questguides_quest_dragonslayer.html +++ b/src/main/resources/losthq/p_questguides_quest_dragonslayer.html @@ -182,7 +182,7 @@

Finishing Up



Speak to Oziach and tell him the dragon is dead. You can now wear a Rune Platebody and Green Dragonhide body!


Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)

This quest guide was written on RuneHQ by Catherine and Ghou Lies. Thanks to Weezy, firespyrit, evomasta, stormer, DRAVAN, Axelman, Ghou Lies, and Fran 2004 for corrections.

This quest guide was entered into the database on Fri, Feb 06, 2004, at 09:11:17 PM by Chownuggs and was last updated on Wed, Sept 10, 2025, at 12:48:26 AM by Fran 2004. diff --git a/src/main/resources/losthq/p_questguides_quest_druidicritual.html b/src/main/resources/losthq/p_questguides_quest_druidicritual.html index 3dacabb..704b238 100644 --- a/src/main/resources/losthq/p_questguides_quest_druidicritual.html +++ b/src/main/resources/losthq/p_questguides_quest_druidicritual.html @@ -72,7 +72,7 @@

Instructions:



Kaqemeex will give you your reward and then explain some things about Herblore. He also refers you to the Scribe section of the website for more info.


Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)


diff --git a/src/main/resources/losthq/p_questguides_quest_dwarfcannon.html b/src/main/resources/losthq/p_questguides_quest_dwarfcannon.html index ff3fd47..94bc1b0 100644 --- a/src/main/resources/losthq/p_questguides_quest_dwarfcannon.html +++ b/src/main/resources/losthq/p_questguides_quest_dwarfcannon.html @@ -84,7 +84,7 @@

Instructions:



If you have the runes, teleport to Camelot. Once you get there by your chosen mode of transportation, return to the commander. Talk to him once more, and at long last, you receive your reward.


Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)


diff --git a/src/main/resources/losthq/p_questguides_quest_eadgar.html b/src/main/resources/losthq/p_questguides_quest_eadgar.html index 6e1c0a0..2f8c317 100644 --- a/src/main/resources/losthq/p_questguides_quest_eadgar.html +++ b/src/main/resources/losthq/p_questguides_quest_eadgar.html @@ -185,7 +185,7 @@

Instructions:

Take the Goutweed back to Sanfew to collect your reward.

Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)

This quest guide was written by Im4eversmart and Leader of Darkness (Davidsp), and map by engekomkomme. Thanks to DRAVAN, DownStrike, Nitr021, seanj50 and Infy102 for corrections.

This quest guide was entered into the RuneHQ.com database on Wed, Oct 06, 2004, at 09:33:10 PM by Monkeymatt and was last updated on Tue, Nov 15, 2005, at 09:05:35 PM by DRAVAN. diff --git a/src/main/resources/losthq/p_questguides_quest_elementalworkshop.html b/src/main/resources/losthq/p_questguides_quest_elementalworkshop.html index 326fbf7..057c4bc 100644 --- a/src/main/resources/losthq/p_questguides_quest_elementalworkshop.html +++ b/src/main/resources/losthq/p_questguides_quest_elementalworkshop.html @@ -118,7 +118,7 @@

Instructions:



Note: You can make more Elemental Shields after you've finished the quest.

Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)


diff --git a/src/main/resources/losthq/p_questguides_quest_ernestthechicken.html b/src/main/resources/losthq/p_questguides_quest_ernestthechicken.html index 32e3a07..0b71c19 100644 --- a/src/main/resources/losthq/p_questguides_quest_ernestthechicken.html +++ b/src/main/resources/losthq/p_questguides_quest_ernestthechicken.html @@ -92,7 +92,7 @@

Instructions:



Return to Professor Oddenstein with all three items. He will fix the machine and turn Ernest back into a human. Ernest will thank and reward you.


Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)

This quest guide was written on RuneHQ by Henry-X. Thanks to Weezy and Sythion for corrections.

This quest guide was entered into the database on Thu, Feb 19, 2004, at 02:33:04 PM by Weezy and was last updated on Thu, Sept 25, 2025, at 06:47:32 PM by Halogod35. diff --git a/src/main/resources/losthq/p_questguides_quest_familycrest.html b/src/main/resources/losthq/p_questguides_quest_familycrest.html index 4261b94..08de76f 100644 --- a/src/main/resources/losthq/p_questguides_quest_familycrest.html +++ b/src/main/resources/losthq/p_questguides_quest_familycrest.html @@ -167,7 +167,7 @@

Finishing Up:




Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)

This quest guide was written by Firkløver and gigakiller. Thanks to L3tHaL_LeAdA, Keystone, thehellkeeper, Your Homey 1, gypped, and DRAVAN for corrections.

This quest guide was entered into the database on Tue, May 11, 2004, at 08:04:55 PM by Freakybat and CJH and was last updated on Sat, Aug 14, 2004, at 12:17:42 PM. diff --git a/src/main/resources/losthq/p_questguides_quest_fightarena.html b/src/main/resources/losthq/p_questguides_quest_fightarena.html index 068d547..9d21020 100644 --- a/src/main/resources/losthq/p_questguides_quest_fightarena.html +++ b/src/main/resources/losthq/p_questguides_quest_fightarena.html @@ -76,7 +76,7 @@

Instructions:



While avoiding General Khazard's attacks, talk to Jeremy Servil and then leave through the door. Then head to Lady Servil to claim your reward. (Congratulations)


Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)


diff --git a/src/main/resources/losthq/p_questguides_quest_fishingcontest.html b/src/main/resources/losthq/p_questguides_quest_fishingcontest.html index 0ee70b0..4261363 100644 --- a/src/main/resources/losthq/p_questguides_quest_fishingcontest.html +++ b/src/main/resources/losthq/p_questguides_quest_fishingcontest.html @@ -78,7 +78,7 @@

Instructions:



Return to the dwarf and he will reward you.


Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)

Note: The video shows 21 Fishing requirement, it's actually 10.

diff --git a/src/main/resources/losthq/p_questguides_quest_fremtrials.html b/src/main/resources/losthq/p_questguides_quest_fremtrials.html index c79d19b..43e169d 100644 --- a/src/main/resources/losthq/p_questguides_quest_fremtrials.html +++ b/src/main/resources/losthq/p_questguides_quest_fremtrials.html @@ -395,7 +395,7 @@

Finishing Up



Note: Talk to Olaf after finishing the quest and he will tell you a secret: The Enchanted Lyre teleports to Rellekka have been slightly tweaked: offerings of raw shark will now give you 2 teleports to Rellekka before your lyre needs re-enchanting, but offerings of sea turtles and manta rays will allow you 3 or 4 teleports before you need to re-enchant your lyre.

Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)

This quest guide was written by Zancross, Pequ, Broodgamerdx, Monkeymatt, Shakeshaft, trekkie, darkgade, and Nalian. Thanks to DRAVAN, Monkeymatt, Slow Cheetah, flatlander20, TheRulnig, drunk_faerie, odedex, Ponteaus, Broken Darkness05, fireball0236, run the walk, slamball, Albel111, xiaoweiqiang, and RPMemperor for corrections.

This quest guide was entered into the RuneHQ.com database on Wed, Nov 03, 2004, at 04:04:18 PM by MrStormy and Monkeymatt and was last updated on Tue, Nov 15, 2005, at 09:22:34 PM by DRAVAN. diff --git a/src/main/resources/losthq/p_questguides_quest_gertrudescat.html b/src/main/resources/losthq/p_questguides_quest_gertrudescat.html index 894f37a..fdff76c 100644 --- a/src/main/resources/losthq/p_questguides_quest_gertrudescat.html +++ b/src/main/resources/losthq/p_questguides_quest_gertrudescat.html @@ -80,7 +80,7 @@

Instructions:



For a indepth guide for kittens, see the Kitten Care Guide.

Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)


diff --git a/src/main/resources/losthq/p_questguides_quest_goblindiplomacy.html b/src/main/resources/losthq/p_questguides_quest_goblindiplomacy.html index ecb105e..5fa99e9 100644 --- a/src/main/resources/losthq/p_questguides_quest_goblindiplomacy.html +++ b/src/main/resources/losthq/p_questguides_quest_goblindiplomacy.html @@ -81,7 +81,7 @@

Instructions:

QUEST CONCLUSION
Talk to General Bentnoze a few times to hand over all 3 pieces of armor. You will give him the original goblin armor last, and you will have sorted out their argument.


Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)

This quest guide was written on RuneHQ by Runegirlie and deathtoyouall. Thanks to Sharqua, Darkest Ange, Nightsdeath, DRAVAN, zus, and Keystone for corrections.

This quest guide was entered into the database on Sun, Apr 11, 2004, at 05:30:14 AM by Keystone and was last updated on Tue, May 18, 2004, at 02:45:25 PM. diff --git a/src/main/resources/losthq/p_questguides_quest_grandtree.html b/src/main/resources/losthq/p_questguides_quest_grandtree.html index 62cb709..03381ae 100644 --- a/src/main/resources/losthq/p_questguides_quest_grandtree.html +++ b/src/main/resources/losthq/p_questguides_quest_grandtree.html @@ -161,7 +161,7 @@

Instructions:



Once you find it return to King and he will reward you. You will also be able to use the glider, and there is now a mine open beneath the tree, accessible by pushing some roots. To access this mine in the future, enter the Grand Tree and move the tile in the floor.


Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)

The Gnome Translation Guide

-A-
Arpos: Rocks
diff --git a/src/main/resources/losthq/p_questguides_quest_hazeelcult.html b/src/main/resources/losthq/p_questguides_quest_hazeelcult.html index 5ac131c..3bbac5d 100644 --- a/src/main/resources/losthq/p_questguides_quest_hazeelcult.html +++ b/src/main/resources/losthq/p_questguides_quest_hazeelcult.html @@ -100,7 +100,7 @@

Instructions:



Talk to Ceril and give him the armor. Then, tell him about his butler. Ceril won't believe you, so go to the 2nd floor and search the butler's cupboard for some poison. Return to Ceril and receive your reward. Quest completed.


Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)


Valve Locations
1st Valve: South wall of Carnillean Mansion — turn right diff --git a/src/main/resources/losthq/p_questguides_quest_heros.html b/src/main/resources/losthq/p_questguides_quest_heros.html index a35ddb1..0e9a61b 100644 --- a/src/main/resources/losthq/p_questguides_quest_heros.html +++ b/src/main/resources/losthq/p_questguides_quest_heros.html @@ -159,7 +159,7 @@

Instructions:



Congratulations! You have finished the Hero's Quest, and can now enter the Hero's Guild!

Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)


diff --git a/src/main/resources/losthq/p_questguides_quest_holygrail.html b/src/main/resources/losthq/p_questguides_quest_holygrail.html index 32c6faf..4285649 100644 --- a/src/main/resources/losthq/p_questguides_quest_holygrail.html +++ b/src/main/resources/losthq/p_questguides_quest_holygrail.html @@ -105,7 +105,7 @@

Instructions:



Return to Camelot and speak with King Arthur to receive your reward.


Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)

This quest guide was written by Elyria1. Thanks to stormer and faital03 for corrections.

This quest guide was entered into the database on Tue, Jun 01, 2004, at 04:44:52 PM by Pirate Bob49 and CJH and was last updated on Mon, Aug 02, 2004, at 08:45:25 AM. diff --git a/src/main/resources/losthq/p_questguides_quest_horror.html b/src/main/resources/losthq/p_questguides_quest_horror.html index bbdd6e2..692bff8 100644 --- a/src/main/resources/losthq/p_questguides_quest_horror.html +++ b/src/main/resources/losthq/p_questguides_quest_horror.html @@ -158,7 +158,7 @@

Part 3: The Monster from the Greyish-Green Lagoon

Switch between melee, range and magic as the monster switches colors. Just keep your prayer up and keep an eye on your hits. Some people recommend hiding behind the nearby rocks as it changes to forms you can't attack, then rushing back out to continue the battle.

Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)

Quest finished! When you kill the second monster you will be automatically transported out of the room and you will see the Quest Completed picture. You will get your exp, quest points and a Rusty casket.

Go up to the second story of the Lighthouse; Jossik is now there. You can buy most ales and some other stuff off him. But for now talk to him. He tells you to read what the Rusty casket says; pick the same god twice to receive a Damaged book of that god (Guthix, Saradomin or Zamorak). diff --git a/src/main/resources/losthq/p_questguides_quest_impcatcher.html b/src/main/resources/losthq/p_questguides_quest_impcatcher.html index 011b745..2efba07 100644 --- a/src/main/resources/losthq/p_questguides_quest_impcatcher.html +++ b/src/main/resources/losthq/p_questguides_quest_impcatcher.html @@ -63,7 +63,7 @@

Instructions:



After you have collected all the beads, return to Wizard Mizgog. He will reward you.


Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)

This quest guide was written on RuneHQ by henry-x.

This quest guide was entered into the database on Tue, Mar 02, 2004, at 10:25:33 PM by Weezy and was last updated on Wed, Mar 31, 2004, at 05:13:34 PM. diff --git a/src/main/resources/losthq/p_questguides_quest_junglepotion.html b/src/main/resources/losthq/p_questguides_quest_junglepotion.html index 35b0ac0..85c3166 100644 --- a/src/main/resources/losthq/p_questguides_quest_junglepotion.html +++ b/src/main/resources/losthq/p_questguides_quest_junglepotion.html @@ -80,7 +80,7 @@

Instructions:



Once you return, Trifitus will thank you, reward you, and train you in Herblore. (Congratulations)


Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)


diff --git a/src/main/resources/losthq/p_questguides_quest_knightssword.html b/src/main/resources/losthq/p_questguides_quest_knightssword.html index 089727d..7f00af4 100644 --- a/src/main/resources/losthq/p_questguides_quest_knightssword.html +++ b/src/main/resources/losthq/p_questguides_quest_knightssword.html @@ -80,7 +80,7 @@

Instructions:



Take it back to the squire for your reward. I had trouble parting with my sword because it looked so cool... lol.


Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)

This quest guide was written on RuneHQ by Gnat88. Thanks to Urger, noob hunters, Weezy, IglooGuy, and stormer for corrections.

This quest guide was entered into the database on Sat, Feb 28, 2004, at 06:03:08 PM by Monkeychris and was last updated on Tue, Apr 20, 2004, at 11:17:05 PM. diff --git a/src/main/resources/losthq/p_questguides_quest_legends.html b/src/main/resources/losthq/p_questguides_quest_legends.html index 40c484b..b69faa0 100644 --- a/src/main/resources/losthq/p_questguides_quest_legends.html +++ b/src/main/resources/losthq/p_questguides_quest_legends.html @@ -243,7 +243,7 @@

Option 2 - (Longer but Easier Boss fight)



Speak to Radimus Erkle inside the Legends' Guild. He will offer you 4 lots of 7,650XP. After you claimed all of the XP:


Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)

This quest guide was written by j1j2j3 and trekkie. Thanks to avster and blue komoto for pictures; monkeymatt for converting maps; pokemama for edits, information on enchanted vials, and maps; Kidd, Dravan, Freakybat, Mage101, DNKevin, pj, stormer, Nitr021, and dogs major for corrections.

This quest guide was entered into the database on Sat, Apr 10, 2004, at 07:23:28 PM by Kidd and CJH and was last updated on Sat, Mar 19, 2005, at 01:58:58 PM by dravan. diff --git a/src/main/resources/losthq/p_questguides_quest_lostcity.html b/src/main/resources/losthq/p_questguides_quest_lostcity.html index 4ba94be..edbe355 100644 --- a/src/main/resources/losthq/p_questguides_quest_lostcity.html +++ b/src/main/resources/losthq/p_questguides_quest_lostcity.html @@ -105,7 +105,7 @@

Instructions:





Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)


diff --git a/src/main/resources/losthq/p_questguides_quest_merlinscrystal.html b/src/main/resources/losthq/p_questguides_quest_merlinscrystal.html index 799e2dc..0d4773a 100644 --- a/src/main/resources/losthq/p_questguides_quest_merlinscrystal.html +++ b/src/main/resources/losthq/p_questguides_quest_merlinscrystal.html @@ -87,7 +87,7 @@

Instructions:



Proceed to the top floor of the southeast end of the castle and use your Excalibur with the crystal and it will shatter to free Merlin, he tells you to speak with King Arthur for your reward.


Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)


diff --git a/src/main/resources/losthq/p_questguides_quest_monksfriend.html b/src/main/resources/losthq/p_questguides_quest_monksfriend.html index 2a7231d..e8dc6db 100644 --- a/src/main/resources/losthq/p_questguides_quest_monksfriend.html +++ b/src/main/resources/losthq/p_questguides_quest_monksfriend.html @@ -74,7 +74,7 @@

Instructions:



Talk to him and he'll give you the runes. Then, you party. Unfortunately, you can't burst the party balloons, but those monks really know how to get down, party, and boogie like there's no tomorrow.


Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)


diff --git a/src/main/resources/losthq/p_questguides_quest_murdermystery.html b/src/main/resources/losthq/p_questguides_quest_murdermystery.html index aa8eb8e..edc8d3c 100644 --- a/src/main/resources/losthq/p_questguides_quest_murdermystery.html +++ b/src/main/resources/losthq/p_questguides_quest_murdermystery.html @@ -93,7 +93,7 @@

Instructions:



Talk to the guard and tell him you have evidence identifying the culprit. He'll ask you about each item and collect the evidence from you. He'll say the culprit will be placed under house arrest until the trial, and thank you for your help.


Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)


diff --git a/src/main/resources/losthq/p_questguides_quest_naturespirit.html b/src/main/resources/losthq/p_questguides_quest_naturespirit.html index de77f08..4df1de0 100644 --- a/src/main/resources/losthq/p_questguides_quest_naturespirit.html +++ b/src/main/resources/losthq/p_questguides_quest_naturespirit.html @@ -112,7 +112,7 @@

Instructions:

Kill three Ghasts and go back and talk to him.




Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)

This quest guide was written by Rekeri. Thanks to Sporhund, Stormer, ossie000, the_peleton, Ju Juitsu, Cricket55416, malku_raj, DRAVAN, trekkie, and SchmackyEvil for corrections.

diff --git a/src/main/resources/losthq/p_questguides_quest_observatory.html b/src/main/resources/losthq/p_questguides_quest_observatory.html index 8d417f2..52dbf56 100644 --- a/src/main/resources/losthq/p_questguides_quest_observatory.html +++ b/src/main/resources/losthq/p_questguides_quest_observatory.html @@ -97,7 +97,7 @@

Instructions:

Aquarius: 25 Water Runes
Pisces: 3 Cooked Tunas

Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)


diff --git a/src/main/resources/losthq/p_questguides_quest_piratestreasure.html b/src/main/resources/losthq/p_questguides_quest_piratestreasure.html index ef51b5d..2013db9 100644 --- a/src/main/resources/losthq/p_questguides_quest_piratestreasure.html +++ b/src/main/resources/losthq/p_questguides_quest_piratestreasure.html @@ -80,7 +80,7 @@

Instructions:



You will get 450 coins, a gold ring, and an emerald, which are the rewards for completing the quest. The quest is now finished.


Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)

This quest guide was written on RuneHQ by Henry-x. Thanks to Evadek and Weezy for corrections.

This quest guide was entered into the database on Sat, Feb 07, 2004, at 09:10:19 PM by Chownuggs and was last updated on Wed, Mar 31, 2004, at 05:14:06 PM. diff --git a/src/main/resources/losthq/p_questguides_quest_plaguecity.html b/src/main/resources/losthq/p_questguides_quest_plaguecity.html index d16fe06..2e826c6 100644 --- a/src/main/resources/losthq/p_questguides_quest_plaguecity.html +++ b/src/main/resources/losthq/p_questguides_quest_plaguecity.html @@ -101,7 +101,7 @@

Instructions:



Go back to the dungeon and talk to Edmond, he will say thank you and reward you.


Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)


diff --git a/src/main/resources/losthq/p_questguides_quest_priestinperil.html b/src/main/resources/losthq/p_questguides_quest_priestinperil.html index a5e7849..1b4e194 100644 --- a/src/main/resources/losthq/p_questguides_quest_priestinperil.html +++ b/src/main/resources/losthq/p_questguides_quest_priestinperil.html @@ -104,7 +104,7 @@

Instructions:



You have now completed the quest, talk to the priest again and he will tell you about the underworld beyond the rift which will take you to a new land.


Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)

This quest guide was written by monkeymatt9. Thanks to xxtigurxx, chrisbarker, MuH-K0o0o, trekkie, faigel, and Gnat88 for corrections.

diff --git a/src/main/resources/losthq/p_questguides_quest_princealirescue.html b/src/main/resources/losthq/p_questguides_quest_princealirescue.html index 57b8a85..9e6e414 100644 --- a/src/main/resources/losthq/p_questguides_quest_princealirescue.html +++ b/src/main/resources/losthq/p_questguides_quest_princealirescue.html @@ -112,7 +112,7 @@

Rescuing Prince Ali



Return to Hassen in Al Kharid and talk to him... congratz! You're now a friend of Al Kharid and can pass through the gate for free.


Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)

This quest guide was written on RuneHQ by xxteargodxx. Thanks to Fran 2004 for corrections.

This quest guide was entered into the database on Thu, Mar 04, 2004, at 12:28:14 AM by Weezy and was last updated on Thu, Sep 25, 2025, at 04:14:03 AM by Halogod35. diff --git a/src/main/resources/losthq/p_questguides_quest_regicide.html b/src/main/resources/losthq/p_questguides_quest_regicide.html index 3a25c4a..3e7b279 100644 --- a/src/main/resources/losthq/p_questguides_quest_regicide.html +++ b/src/main/resources/losthq/p_questguides_quest_regicide.html @@ -177,7 +177,7 @@

Here are some dangerous traps you will encounter:





Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)

Dragon Halberd costs anywhere from 325k to 350k depending on the store's stocks.

diff --git a/src/main/resources/losthq/p_questguides_quest_restlessghost.html b/src/main/resources/losthq/p_questguides_quest_restlessghost.html index 560b656..45aa88b 100644 --- a/src/main/resources/losthq/p_questguides_quest_restlessghost.html +++ b/src/main/resources/losthq/p_questguides_quest_restlessghost.html @@ -71,7 +71,7 @@

Instructions:



Go back to the tomb and use the skull on the ghost's coffin. The ghost will vanish and whisper "thank you." Of course, a minor thank you isn't going to cut it — so naturally, the ghost also gives you your reward. Quest complete.


Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)

This quest guide was written on RuneHQ by Gnat88. Thanks to Urger and Weezy for corrections.

This quest guide was entered into the database on Sat, Feb 28, 2004, at 04:46:45 PM by Monkeychris and was last updated on Wed, Mar 31, 2004, at 05:00:31 PM. diff --git a/src/main/resources/losthq/p_questguides_quest_romeojuliet.html b/src/main/resources/losthq/p_questguides_quest_romeojuliet.html index 10b26cc..cd52230 100644 --- a/src/main/resources/losthq/p_questguides_quest_romeojuliet.html +++ b/src/main/resources/losthq/p_questguides_quest_romeojuliet.html @@ -68,7 +68,7 @@

Instructions:



You'll get your reward after talking to Romeo. Who cares about a bad ending, as long as you get your reward, right?


Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)

This quest guide was written on RuneHQ by Gnat88.

This quest guide was entered into the database on Tue, Mar 02, 2004, at 09:58:20 PM by Weezy and was last updated on Wed, Mar 31, 2004, at 05:15:59 PM. diff --git a/src/main/resources/losthq/p_questguides_quest_runemysteries.html b/src/main/resources/losthq/p_questguides_quest_runemysteries.html index 97feb52..e7880f2 100644 --- a/src/main/resources/losthq/p_questguides_quest_runemysteries.html +++ b/src/main/resources/losthq/p_questguides_quest_runemysteries.html @@ -70,7 +70,7 @@

Instructions:



Congratulations — you have completed the first RS2-introduced quest!

Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)

This quest guide was written on RuneHQ by evadek. Thanks to Monkeychris, Rebelbeta, Sharker998, Archangel Malachi, Neek, Tomk4k, Therichsweede, and Poison for corrections.

This quest guide was entered into the database on Mon, Dec 01, 2003, at 06:12:25 PM by MrStormy and was last updated on Wed, Mar 31, 2004, at 05:01:06 PM. diff --git a/src/main/resources/losthq/p_questguides_quest_scorpioncatcher.html b/src/main/resources/losthq/p_questguides_quest_scorpioncatcher.html index c7b3d51..b697170 100644 --- a/src/main/resources/losthq/p_questguides_quest_scorpioncatcher.html +++ b/src/main/resources/losthq/p_questguides_quest_scorpioncatcher.html @@ -100,7 +100,7 @@

Instructions:



When you have all of the Scorpions, head back to the Wizard and he will give you your reward.


Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)


diff --git a/src/main/resources/losthq/p_questguides_quest_seaslug.html b/src/main/resources/losthq/p_questguides_quest_seaslug.html index 73d2fba..fe5b7d5 100644 --- a/src/main/resources/losthq/p_questguides_quest_seaslug.html +++ b/src/main/resources/losthq/p_questguides_quest_seaslug.html @@ -73,7 +73,7 @@

Instructions:



Go back to Holgart and return to land. Talk to Caroline to claim your reward.


Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)


diff --git a/src/main/resources/losthq/p_questguides_quest_shades.html b/src/main/resources/losthq/p_questguides_quest_shades.html index c6069a0..906a739 100644 --- a/src/main/resources/losthq/p_questguides_quest_shades.html +++ b/src/main/resources/losthq/p_questguides_quest_shades.html @@ -151,7 +151,7 @@

Instructions:

He will congratulate you on setting the shade to rest

Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)

Note: For the permanent cure, you need 20% sanctity and then you must use serum 207 on the sacred flame then give it to the people as serum 207(p). It is recommended to use this on Razmire so that you have ready access to his Stores in the future.

diff --git a/src/main/resources/losthq/p_questguides_quest_sheepherder.html b/src/main/resources/losthq/p_questguides_quest_sheepherder.html index 8e14e88..4eeccef 100644 --- a/src/main/resources/losthq/p_questguides_quest_sheepherder.html +++ b/src/main/resources/losthq/p_questguides_quest_sheepherder.html @@ -79,7 +79,7 @@

Instructions:



If you've survived this rapid mouse-clicking experience, return to the Councilor and he'll reward you for your heroic efforts.


Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)


diff --git a/src/main/resources/losthq/p_questguides_quest_sheepshearer.html b/src/main/resources/losthq/p_questguides_quest_sheepshearer.html index 62e6219..230e946 100644 --- a/src/main/resources/losthq/p_questguides_quest_sheepshearer.html +++ b/src/main/resources/losthq/p_questguides_quest_sheepshearer.html @@ -63,7 +63,7 @@

Instructions:



Return to Fred with your 20 balls of wool and give them to him. He'll thank you and reward you for your help. You'll receive 1 Quest point, 60 gold coins, and 150 Crafting XP.


Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)

This quest guide was written on RuneHQ by Stormer. Thanks to Weezy and tj for corrections.

This quest guide was entered into the database on Mon, Feb 16, 2004, at 03:26:27 PM by Chownuggs and was last updated on Fri, Apr 23, 2004, at 12:35:12 PM. diff --git a/src/main/resources/losthq/p_questguides_quest_shieldofarrav.html b/src/main/resources/losthq/p_questguides_quest_shieldofarrav.html index 93179a8..9b1d228 100644 --- a/src/main/resources/losthq/p_questguides_quest_shieldofarrav.html +++ b/src/main/resources/losthq/p_questguides_quest_shieldofarrav.html @@ -116,7 +116,7 @@

Finishing the Quest



Head northwest into the palace courtyard and into the palace. Go to the throne room, just to the east of the main entrance. Talk to King Roald to recieve your reward.


Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)

This quest guide was written on RuneHQ by halojunkie. Thanks to Fran 2004 for corrections.

This quest guide was entered into the database on Tue, Apr 13, 2004, at 05:38:26 PM by DRAVAN, and was last updated on Fri, Sep 19, 2025, at 09:56:34 PM by Halogod35. diff --git a/src/main/resources/losthq/p_questguides_quest_shilovillage.html b/src/main/resources/losthq/p_questguides_quest_shilovillage.html index 7406002..adc8fd9 100644 --- a/src/main/resources/losthq/p_questguides_quest_shilovillage.html +++ b/src/main/resources/losthq/p_questguides_quest_shilovillage.html @@ -122,7 +122,7 @@

Instructions:



To finish the quest, return to the tomb where you previously found the blue scroll, by climbing the rocks and crossing the bridge. Just use the corpse on the dolmen. Well done—quest complete!


Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)


diff --git a/src/main/resources/losthq/p_questguides_quest_tbwt.html b/src/main/resources/losthq/p_questguides_quest_tbwt.html index 2e76777..62668fb 100644 --- a/src/main/resources/losthq/p_questguides_quest_tbwt.html +++ b/src/main/resources/losthq/p_questguides_quest_tbwt.html @@ -187,7 +187,7 @@

Tiadeche

Head back to Tai Bwo Wannai and speak to Timfraku.

Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)

This quest guide was written by Im4eversmart. Thanks to DRAVAN, Dracon, Brenden, alex200599, Headbiter, Demonichell, Agamemnus, Eq_S_Guy, and Ghoulies for corrections.

This quest guide was entered into the RuneHQ.com database on Tue, Sep 14, 2004, at 09:50:55 PM by DRAVAN and was last updated on Wed, Nov 02, 2005, at 08:56:14 PM by DRAVAN. diff --git a/src/main/resources/losthq/p_questguides_quest_templeofikov.html b/src/main/resources/losthq/p_questguides_quest_templeofikov.html index 2d7baa5..75fc3c2 100644 --- a/src/main/resources/losthq/p_questguides_quest_templeofikov.html +++ b/src/main/resources/losthq/p_questguides_quest_templeofikov.html @@ -171,7 +171,7 @@

Instructions:



*The Boots of Lightness reduce your weight by 4 kg, making them great for running because you regain energy faster. It's not a bad idea to grab a few of these on your first trip back to the bank.

Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)

This quest guide was written by Pirate and mage017. Thanks to Your Homey 1 for corrections.

This quest guide was entered into the database on Sun, Apr 25, 2004, at 06:17:58 PM by Freakybat and CJH and was last updated on Mon, Aug 02, 2004, at 08:42:56 AM. diff --git a/src/main/resources/losthq/p_questguides_quest_touristtrap.html b/src/main/resources/losthq/p_questguides_quest_touristtrap.html index c965a6b..783ac7f 100644 --- a/src/main/resources/losthq/p_questguides_quest_touristtrap.html +++ b/src/main/resources/losthq/p_questguides_quest_touristtrap.html @@ -111,7 +111,7 @@

Instructions:



Once outside go north to Irena and talk to her.


Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)


diff --git a/src/main/resources/losthq/p_questguides_quest_treegnomevillage.html b/src/main/resources/losthq/p_questguides_quest_treegnomevillage.html index ecc037e..a514881 100644 --- a/src/main/resources/losthq/p_questguides_quest_treegnomevillage.html +++ b/src/main/resources/losthq/p_questguides_quest_treegnomevillage.html @@ -82,7 +82,7 @@

Instructions:



Once he is dead (congratulations!), you'll receive the orbs. Return once again to Elkoy, then to Bolren. He'll tell you about the Spirit Trees and give you your reward.


Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)


diff --git a/src/main/resources/losthq/p_questguides_quest_tribaltotem.html b/src/main/resources/losthq/p_questguides_quest_tribaltotem.html index e700ddd..58fcec7 100644 --- a/src/main/resources/losthq/p_questguides_quest_tribaltotem.html +++ b/src/main/resources/losthq/p_questguides_quest_tribaltotem.html @@ -74,7 +74,7 @@

Instructions:



Return to Kangai, and he will reward you.


Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)


diff --git a/src/main/resources/losthq/p_questguides_quest_trollstronghold.html b/src/main/resources/losthq/p_questguides_quest_trollstronghold.html index b7c1210..104f18e 100644 --- a/src/main/resources/losthq/p_questguides_quest_trollstronghold.html +++ b/src/main/resources/losthq/p_questguides_quest_trollstronghold.html @@ -134,7 +134,7 @@

Instructions:

Follow the path east then south, you'll know where you are, head back to Burthrope and go to smithy, and speak to Dunstan for your reward.

Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)

This quest guide was written by greatgecko,mhoi, and DRAVAN. Thanks to Penneepster for corrections.

This quest guide was entered into the database on Tue, Aug 31, 2004, at 10:20:22 PM by DRAVAN and was last updated on Mon, Dec 06, 2004, at 10:18:36 PM by MrStormy. diff --git a/src/main/resources/losthq/p_questguides_quest_undergroundpass.html b/src/main/resources/losthq/p_questguides_quest_undergroundpass.html index 1d5c537..e2150a9 100644 --- a/src/main/resources/losthq/p_questguides_quest_undergroundpass.html +++ b/src/main/resources/losthq/p_questguides_quest_undergroundpass.html @@ -227,7 +227,7 @@

Final Battle



You will be flung into a part of the underground pass. Either Teleport to Ardounge or talk to the scout again, and he'll take you outside. Speak to the King and inform him you made it through.


Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)


diff --git a/src/main/resources/losthq/p_questguides_quest_vampireslayer.html b/src/main/resources/losthq/p_questguides_quest_vampireslayer.html index 5702846..79d84a9 100644 --- a/src/main/resources/losthq/p_questguides_quest_vampireslayer.html +++ b/src/main/resources/losthq/p_questguides_quest_vampireslayer.html @@ -72,7 +72,7 @@

Instructions:



Once he's dead, you have finished the quest. (Congratulations!)

Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)

This quest guide was written on RuneHQ by Gnat88. Thanks to Weezy, stormer, Fran 2004 for corrections.

This quest guide was entered into the database on Sat, Feb 28, 2004, at 06:37:37 PM by Monkeychris and was last updated on Sun, Sep 21, 2025, at 06:42:25 PM by Halogod35. diff --git a/src/main/resources/losthq/p_questguides_quest_watchtower.html b/src/main/resources/losthq/p_questguides_quest_watchtower.html index 9ba84ab..0de85f9 100644 --- a/src/main/resources/losthq/p_questguides_quest_watchtower.html +++ b/src/main/resources/losthq/p_questguides_quest_watchtower.html @@ -119,7 +119,7 @@

Instructions:



Go back to the wizard and talk to him. Flip the switch on the West wall and…


Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)

This quest guide was written by payman and irish_buddha. Thanks to goatluver500, L3tHaL LeAdA, and Grand Gaia for corrections.

This quest guide was entered into the database on Tue, May 18, 2004, at 04:26:49 PM by DRAVAN and CJH and was last updated on Mon, Aug 02, 2004, at 08:52:16 AM. diff --git a/src/main/resources/losthq/p_questguides_quest_waterfall.html b/src/main/resources/losthq/p_questguides_quest_waterfall.html index adc8cdb..833dbd8 100644 --- a/src/main/resources/losthq/p_questguides_quest_waterfall.html +++ b/src/main/resources/losthq/p_questguides_quest_waterfall.html @@ -102,7 +102,7 @@

Instructions:



Go up to the trophy, use the urn with the trophy and you have completed the quest! If you are unsuccessful you must put your armour and weapon back in the bank and go back to the grave and get the amulet again.


Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)


diff --git a/src/main/resources/losthq/p_questguides_quest_witchshouse.html b/src/main/resources/losthq/p_questguides_quest_witchshouse.html index c4b6cdd..6c0fc38 100644 --- a/src/main/resources/losthq/p_questguides_quest_witchshouse.html +++ b/src/main/resources/losthq/p_questguides_quest_witchshouse.html @@ -81,7 +81,7 @@

Instructions:



After defeating the experiment (congratulations), grab the ball and open the door. For the last time, sneak toward the house, then exit and return the ball to the boy for your reward.


Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)


diff --git a/src/main/resources/losthq/p_questguides_quest_witchspotion.html b/src/main/resources/losthq/p_questguides_quest_witchspotion.html index 8f27ab9..1e63141 100644 --- a/src/main/resources/losthq/p_questguides_quest_witchspotion.html +++ b/src/main/resources/losthq/p_questguides_quest_witchspotion.html @@ -70,7 +70,7 @@

Instructions:



After getting the ingredients, return to Hetty's house and talk to her. She will tell you the cauldron is done and ask you to drink it. Click on the cauldron to drink it, and the quest is finished.


Congratulations, Quest Complete!


- Quest Complete!
+ Quest Complete!
(Click to expand)

This quest guide was written on RuneHQ by Henry-X. Thanks to Weezy, Corruptus, and Fran 2004 for corrections.

This quest guide was entered into the database on Sat, Feb 07, 2004, at 12:00:31 PM by Chownuggs and was last updated on Sun, Sep 21, 2025, at 07:44:28 PM by Halogod35. diff --git a/src/main/resources/losthq/p_skillguides_skill_crafting.html b/src/main/resources/losthq/p_skillguides_skill_crafting.html index 3c9ddd4..1c74235 100644 --- a/src/main/resources/losthq/p_skillguides_skill_crafting.html +++ b/src/main/resources/losthq/p_skillguides_skill_crafting.html @@ -41,7 +41,7 @@

Crafting Skill Guide

Crafting is rumoured to be a slow, useless, and boring skill, but once you get into it there's lots to do! You can make everything from pots to collect flour in, to dragonhide armour for rangers. Crafting can be found all over the RuneScape world. In the amulets people wear, the vials you use in herblore and the Mystic Staves Magicians battle with. These items are all made in crafting.

- + @@ -58,7 +58,7 @@

Crafting Skill Guide

@@ -78,7 +78,7 @@

Crafting Skill Guide

@@ -87,7 +87,7 @@

Crafting Skill Guide

@@ -106,7 +106,7 @@

Crafting Skill Guide

@@ -120,7 +120,7 @@

Crafting Skill Guide

@@ -132,7 +132,7 @@

Crafting Skill Guide

@@ -141,7 +141,7 @@

Crafting Skill Guide

@@ -164,7 +164,7 @@

Crafting Skill Guide

@@ -176,7 +176,7 @@

Crafting Skill Guide

@@ -185,7 +185,7 @@

Crafting Skill Guide

@@ -203,7 +203,7 @@

Crafting Skill Guide

@@ -246,7 +246,7 @@

Crafting Skill Guide

@@ -267,7 +267,7 @@

Crafting Skill Guide

@@ -288,7 +288,7 @@

Crafting Skill Guide

@@ -308,7 +308,7 @@

Crafting Skill Guide

@@ -323,7 +323,7 @@

Crafting Skill Guide

@@ -339,7 +339,7 @@

Crafting Skill Guide

@@ -360,7 +360,7 @@

Crafting Skill Guide

@@ -384,7 +384,7 @@

Crafting Skill Guide

@@ -400,7 +400,7 @@

Crafting Skill Guide

@@ -427,7 +427,7 @@

Crafting Skill Guide

@@ -451,7 +451,7 @@

Crafting Skill Guide

@@ -481,7 +481,7 @@

Crafting Skill Guide

@@ -512,7 +512,7 @@

Crafting Skill Guide

@@ -542,7 +542,7 @@

Crafting Skill Guide

@@ -557,7 +557,7 @@

Crafting Skill Guide

@@ -584,8 +584,7 @@

Crafting Skill Guide

@@ -664,9 +660,7 @@

Crafting Skill Guide

@@ -701,8 +695,7 @@

Crafting Skill Guide

@@ -751,8 +742,7 @@

Crafting Skill Guide

Level Ingredients 1 -
+
Spinning Wheel
-
+
Potter's wheel
1 -
+
Pottery Oven
- Furnace + Furnace
Furnace
- Furnace + Furnace
Furnace
-
+
Potter's wheel
7 -
+
Pottery Oven
- Furnace + Furnace
Furnace
-
+
Potter's wheel
8 -
+
Pottery Oven
10 -
+
Spinning Wheel
- Furnace + Furnace
Furnace
- Furnace + Furnace
Furnace
- Furnace + Furnace
Furnace
- Furnace + Furnace
Furnace
- Furnace + Furnace
Furnace
- Furnace + Furnace
Furnace
- Furnace + Furnace
Furnace
- Furnace + Furnace
Furnace
- Furnace + Furnace
Furnace
- Furnace + Furnace
Furnace
- Furnace + Furnace
Furnace
- Furnace + Furnace
Furnace
- Furnace + Furnace
Furnace
- Furnace + Furnace
Furnace
- Furnace + Furnace
Furnace
60 - - + @@ -605,9 +604,7 @@

Crafting Skill Guide

63 - - - + @@ -636,8 +633,7 @@

Crafting Skill Guide

68 - - + @@ -655,7 +651,7 @@

Crafting Skill Guide

- Furnace + Furnace
Furnace
71 - - - + @@ -683,7 +677,7 @@

Crafting Skill Guide

- Furnace + Furnace
Furnace
75 - - + @@ -713,9 +706,7 @@

Crafting Skill Guide

77 - - - + @@ -742,7 +733,7 @@

Crafting Skill Guide

- Furnace + Furnace
Furnace
82 - - + @@ -763,9 +753,7 @@

Crafting Skill Guide

84 - - - + @@ -779,7 +767,7 @@

Pottery

Bowls, pots and pie dishes can all be made by crafting. Clay is obtained by mining.
To make the clay workable it needs to be softened first. Use a jug of water with the clay and it will be turned into soft clay. Take the clay to a potter's wheel. There is one in the barbarian village.

- + @@ -789,7 +777,7 @@

Leather

Leather is made from cow hides, so you will need to find a cow field and kill some cows.

Use the clay on the potter's wheel and select what object you would like to make. If you have the correct crafting level you will make an unfired piece of pottery. If you want to make several of the same object, simply right-click and select the number you wish to produce. Use it on the pottery oven and as long as it doesn't crack upon heating, you will have made yourself a nice new piece of pottery.
- + @@ -815,7 +803,7 @@

Holy and Unholy symbols

You need a crafting level of 16 to craft silver bars. To make silver bars see the mining and smithing guides.

Once you have a cow hide, take it to the tannery in Al Kharid. You can then craft your leather into leather armour, boots or gloves. You will need to get a needle and some thread from a crafting shop. Right-click the needle in your inventory and use it on a piece of leather, then as long as you have some thread the screen below will be opened, and you will be given a choice of leather objects to make.
- + @@ -823,7 +811,7 @@

Holy and Unholy symbols

You will need a holy symbol mould which can be bought from a crafting shop. Then use your silver bar on a furnace and you will be able to make a holy symbol of Saradomin.
You can either sell your holy symbol to the general store or you can make it into an object that makes your prayers last longer. If you want to use your holy symbol you will need some string. To make string, buy a pair of shears from a general store. Use the shears on a sheep to get some wool.

- + @@ -833,7 +821,7 @@

Holy and Unholy symbols

Runescape members may also come across an unholy symbol mould which can be used to make unholy symbols in the same way and requires a crafting level of 17 to use. When they get the mould they will also be told how to get their unholy symbols enchanted.

Jewelery and amulets

Use the wool on a spinning wheel to spin it into a ball. Then you can use your ball of wool on your holy symbol to give it a string. Finally you will need to get your holy symbol blessed. This can only be done by taking it to a monk called Brother Jered or by using a completed prayer book. Brother Jered is upstairs in the monastery. You will need a prayer level of 31 to get in to talk to him.
- + @@ -842,11 +830,11 @@

Jewelery and amulets


Putting gems in your gold jewelry can increase the smithing level required quite a lot, but it also increases the value of what you are making.

- All the gold jewelry can be sold for a good price, but amulets and rings with jewels in them can also be enchanted at a high enough magic level to give various bonuses. First of all your amulet will need a string, this is made in the same way as the string for the holy amulets of Saradomin. Then it can be enchanted using the magic skill. + All the gold jewelry can be sold for a good price, but amulets and rings with jewels in them can also be enchanted at a high enough magic level to give various bonuses. First of all your amulet will need a string, this is made in the same way as the string for the holy amulets of Saradomin. Then it can be enchanted using the magic skill.

Glassmaking

To make gold bars see the mining and smithing guides. You can make amulets, necklaces, and rings from gold bars. Buy the appropriate moulds from a crafting shop.

Use a gold bar on a furnace and select the object you would like to make.
- + diff --git a/src/main/resources/losthq/p_skillguides_skill_smithing.html b/src/main/resources/losthq/p_skillguides_skill_smithing.html index 8609b12..6057d5c 100644 --- a/src/main/resources/losthq/p_skillguides_skill_smithing.html +++ b/src/main/resources/losthq/p_skillguides_skill_smithing.html @@ -60,11 +60,11 @@

Ores / smithing levels chart

Some of these requirements are shown below. There are even more bars to make at higher levels.

To make some glass, you will first need to get some seaweed. This can be obtained by big net fishing, or found on Entrana Island. Heat the seaweed to get soda ash.

Then use a bucket on a sandpit, to get a bucket of sand. With both soda ash and sand in your inventory, you can then use a furnace on either one to make molten glass.
- + - +
Bar Ores required
per bar
Level
required
Level
@@ -183,7 +183,7 @@

Forging items



Select a bar from your inventory, then select an anvil. You will be given a screen to decide what sort of equipment you would like to make. You will be shown how many bars are needed to make types of object. If this is written in green then you have enough bars to make the item, if it is written in red then you do not.

- +

The name of the objects will be written in black or white. If the name is written in white then you have the smithing level required to make it. If it is written in black then you do not.

@@ -207,17 +207,17 @@

Forging items

The following is a table showing at what level you can forge various items.

- + - @@ -333,11 +333,11 @@

Forging items

- diff --git a/src/main/resources/sideicons/floating_xp.png b/src/main/resources/sideicons/floating_xp.png index 4ac12f7..8e2ff8f 100644 Binary files a/src/main/resources/sideicons/floating_xp.png and b/src/main/resources/sideicons/floating_xp.png differ diff --git a/src/main/resources/sideicons/guides_tools.png b/src/main/resources/sideicons/guides_tools.png index 73dcbab..61ae60e 100644 Binary files a/src/main/resources/sideicons/guides_tools.png and b/src/main/resources/sideicons/guides_tools.png differ diff --git a/src/main/resources/sideicons/highscores.png b/src/main/resources/sideicons/highscores.png index 9060efe..737df09 100644 Binary files a/src/main/resources/sideicons/highscores.png and b/src/main/resources/sideicons/highscores.png differ diff --git a/src/main/resources/sideicons/settings.png b/src/main/resources/sideicons/settings.png index 63fe326..08b8bcb 100644 Binary files a/src/main/resources/sideicons/settings.png and b/src/main/resources/sideicons/settings.png differ diff --git a/src/main/resources/sideicons/world_map.png b/src/main/resources/sideicons/world_map.png index 8af319e..43c9cb7 100644 Binary files a/src/main/resources/sideicons/world_map.png and b/src/main/resources/sideicons/world_map.png differ diff --git a/src/main/resources/sideicons/xp_tracker.png b/src/main/resources/sideicons/xp_tracker.png index 992f858..35ee42f 100644 Binary files a/src/main/resources/sideicons/xp_tracker.png and b/src/main/resources/sideicons/xp_tracker.png differ diff --git a/src/main/resources/skill_icons_small/agility.png b/src/main/resources/skill_icons_small/agility.png new file mode 100644 index 0000000..42d1d93 Binary files /dev/null and b/src/main/resources/skill_icons_small/agility.png differ diff --git a/src/main/resources/skill_icons_small/attack.png b/src/main/resources/skill_icons_small/attack.png new file mode 100644 index 0000000..76a4f0f Binary files /dev/null and b/src/main/resources/skill_icons_small/attack.png differ diff --git a/src/main/resources/skill_icons_small/combat.png b/src/main/resources/skill_icons_small/combat.png new file mode 100644 index 0000000..7468bbe Binary files /dev/null and b/src/main/resources/skill_icons_small/combat.png differ diff --git a/src/main/resources/skill_icons_small/construction.png b/src/main/resources/skill_icons_small/construction.png new file mode 100644 index 0000000..fad449f Binary files /dev/null and b/src/main/resources/skill_icons_small/construction.png differ diff --git a/src/main/resources/skill_icons_small/cooking.png b/src/main/resources/skill_icons_small/cooking.png new file mode 100644 index 0000000..e69e66e Binary files /dev/null and b/src/main/resources/skill_icons_small/cooking.png differ diff --git a/src/main/resources/skill_icons_small/crafting.png b/src/main/resources/skill_icons_small/crafting.png new file mode 100644 index 0000000..1aca856 Binary files /dev/null and b/src/main/resources/skill_icons_small/crafting.png differ diff --git a/src/main/resources/skill_icons_small/defence.png b/src/main/resources/skill_icons_small/defence.png new file mode 100644 index 0000000..d35edc7 Binary files /dev/null and b/src/main/resources/skill_icons_small/defence.png differ diff --git a/src/main/resources/skill_icons_small/farming.png b/src/main/resources/skill_icons_small/farming.png new file mode 100644 index 0000000..8e9c4ac Binary files /dev/null and b/src/main/resources/skill_icons_small/farming.png differ diff --git a/src/main/resources/skill_icons_small/firemaking.png b/src/main/resources/skill_icons_small/firemaking.png new file mode 100644 index 0000000..4e8fe3d Binary files /dev/null and b/src/main/resources/skill_icons_small/firemaking.png differ diff --git a/src/main/resources/skill_icons_small/fishing.png b/src/main/resources/skill_icons_small/fishing.png new file mode 100644 index 0000000..ba48409 Binary files /dev/null and b/src/main/resources/skill_icons_small/fishing.png differ diff --git a/src/main/resources/skill_icons_small/fletching.png b/src/main/resources/skill_icons_small/fletching.png new file mode 100644 index 0000000..402e90f Binary files /dev/null and b/src/main/resources/skill_icons_small/fletching.png differ diff --git a/src/main/resources/skill_icons_small/herblore.png b/src/main/resources/skill_icons_small/herblore.png new file mode 100644 index 0000000..3b67900 Binary files /dev/null and b/src/main/resources/skill_icons_small/herblore.png differ diff --git a/src/main/resources/skill_icons_small/hitpoints.png b/src/main/resources/skill_icons_small/hitpoints.png new file mode 100644 index 0000000..a44fa5c Binary files /dev/null and b/src/main/resources/skill_icons_small/hitpoints.png differ diff --git a/src/main/resources/skill_icons_small/hunter.png b/src/main/resources/skill_icons_small/hunter.png new file mode 100644 index 0000000..d6157f0 Binary files /dev/null and b/src/main/resources/skill_icons_small/hunter.png differ diff --git a/src/main/resources/skill_icons_small/magic.png b/src/main/resources/skill_icons_small/magic.png new file mode 100644 index 0000000..871221f Binary files /dev/null and b/src/main/resources/skill_icons_small/magic.png differ diff --git a/src/main/resources/skill_icons_small/mining.png b/src/main/resources/skill_icons_small/mining.png new file mode 100644 index 0000000..8b67d55 Binary files /dev/null and b/src/main/resources/skill_icons_small/mining.png differ diff --git a/src/main/resources/skill_icons_small/overall.png b/src/main/resources/skill_icons_small/overall.png new file mode 100644 index 0000000..4417881 Binary files /dev/null and b/src/main/resources/skill_icons_small/overall.png differ diff --git a/src/main/resources/skill_icons_small/prayer.png b/src/main/resources/skill_icons_small/prayer.png new file mode 100644 index 0000000..1e624e5 Binary files /dev/null and b/src/main/resources/skill_icons_small/prayer.png differ diff --git a/src/main/resources/skill_icons_small/ranged.png b/src/main/resources/skill_icons_small/ranged.png new file mode 100644 index 0000000..c9dc83d Binary files /dev/null and b/src/main/resources/skill_icons_small/ranged.png differ diff --git a/src/main/resources/skill_icons_small/runecraft.png b/src/main/resources/skill_icons_small/runecraft.png new file mode 100644 index 0000000..d899270 Binary files /dev/null and b/src/main/resources/skill_icons_small/runecraft.png differ diff --git a/src/main/resources/skill_icons_small/sailing.png b/src/main/resources/skill_icons_small/sailing.png new file mode 100644 index 0000000..089b8ff Binary files /dev/null and b/src/main/resources/skill_icons_small/sailing.png differ diff --git a/src/main/resources/skill_icons_small/slayer.png b/src/main/resources/skill_icons_small/slayer.png new file mode 100644 index 0000000..2444216 Binary files /dev/null and b/src/main/resources/skill_icons_small/slayer.png differ diff --git a/src/main/resources/skill_icons_small/smithing.png b/src/main/resources/skill_icons_small/smithing.png new file mode 100644 index 0000000..eb999cb Binary files /dev/null and b/src/main/resources/skill_icons_small/smithing.png differ diff --git a/src/main/resources/skill_icons_small/strength.png b/src/main/resources/skill_icons_small/strength.png new file mode 100644 index 0000000..3c69b7b Binary files /dev/null and b/src/main/resources/skill_icons_small/strength.png differ diff --git a/src/main/resources/skill_icons_small/thieving.png b/src/main/resources/skill_icons_small/thieving.png new file mode 100644 index 0000000..e7dfc16 Binary files /dev/null and b/src/main/resources/skill_icons_small/thieving.png differ diff --git a/src/main/resources/skill_icons_small/woodcutting.png b/src/main/resources/skill_icons_small/woodcutting.png new file mode 100644 index 0000000..21e9536 Binary files /dev/null and b/src/main/resources/skill_icons_small/woodcutting.png differ
Item Level Requirement to Smith
Bronze + Bron Iron Steel - Mithril - Adamant + Mith + Adam Rune
Level Requirement to Smith
Bronze + Bron Iron Steel - Mithril - Adamant + Mith + Adam Rune