├── io ├── build.gradle └── src │ └── main │ └── kotlin │ └── scape │ └── editor │ └── io │ └── RSBuffer.kt ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gui ├── src │ └── main │ │ ├── resources │ │ ├── icons │ │ │ ├── dat_32.png │ │ │ ├── gz_32.png │ │ │ ├── icon.png │ │ │ ├── idx_32.png │ │ │ ├── file_32.png │ │ │ ├── search_24.png │ │ │ ├── file_store_32.png │ │ │ ├── close_icon_light.png │ │ │ ├── maximize_icon_light.png │ │ │ └── minimize_icon_light.png │ │ ├── data │ │ │ └── .scape-settings.dat │ │ ├── scenes │ │ │ ├── TaskScene.fxml │ │ │ ├── PluginScene.fxml │ │ │ └── StoreScene.fxml │ │ ├── task_style.css │ │ └── style.css │ │ ├── kotlin │ │ └── scape │ │ │ └── editor │ │ │ └── gui │ │ │ ├── plugin │ │ │ ├── extension │ │ │ │ ├── IPluginExtension.kt │ │ │ │ └── ConfigExtension.kt │ │ │ ├── PluginClassLoader.kt │ │ │ ├── IPlugin.kt │ │ │ ├── PluginDescriptor.kt │ │ │ └── PluginManager.kt │ │ │ ├── model │ │ │ ├── ValueModel.kt │ │ │ ├── NamedValueModel.kt │ │ │ ├── StoreModel.kt │ │ │ ├── KeyModel.kt │ │ │ ├── PluginWrapper.kt │ │ │ └── StoreEntryModel.kt │ │ │ ├── event │ │ │ └── LoadCacheEvent.kt │ │ │ ├── Launcher.kt │ │ │ ├── util │ │ │ ├── StringExtensions.kt │ │ │ ├── DataOutputStreamExtensions.kt │ │ │ ├── SwingImageExtensions.kt │ │ │ ├── MapExtensions.kt │ │ │ ├── RSPropertySetExtensions.kt │ │ │ ├── BufferedImageExtensions.kt │ │ │ └── FXDialogUtil.kt │ │ │ ├── App.kt │ │ │ ├── controller │ │ │ ├── TaskController.kt │ │ │ ├── BaseController.kt │ │ │ └── PluginController.kt │ │ │ └── Settings.kt │ │ └── java │ │ └── scape │ │ └── editor │ │ └── fx │ │ ├── component │ │ └── NumericTextField.java │ │ └── TupleCellFactory.java └── build.gradle ├── util ├── build.gradle └── src │ └── main │ └── java │ └── scape │ └── editor │ └── util │ ├── HashUtils.java │ ├── ByteBufferUtils.java │ └── CompressionUtils.java ├── fs ├── build.gradle └── src │ └── main │ └── java │ └── scape │ └── editor │ └── fs │ ├── graphics │ ├── RSImageArchive.java │ ├── RSTexture.java │ ├── draw │ │ └── RSRaster.java │ ├── RSSprite.java │ ├── RSWidget.java │ └── RSFont.java │ ├── RSFileSystem.java │ ├── RSArchive.java │ └── RSFileStore.java ├── settings.gradle ├── .gitignore ├── LICENSE ├── README.md ├── gradlew.bat └── gradlew /io/build.gradle: -------------------------------------------------------------------------------- 1 | description = 'Scape Editor IO' 2 | 3 | sourceSets { 4 | main.java.srcDirs = ['src/main/kotlin'] 5 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scape-tools/scape-editor/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gui/src/main/resources/icons/dat_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scape-tools/scape-editor/HEAD/gui/src/main/resources/icons/dat_32.png -------------------------------------------------------------------------------- /gui/src/main/resources/icons/gz_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scape-tools/scape-editor/HEAD/gui/src/main/resources/icons/gz_32.png -------------------------------------------------------------------------------- /gui/src/main/resources/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scape-tools/scape-editor/HEAD/gui/src/main/resources/icons/icon.png -------------------------------------------------------------------------------- /gui/src/main/resources/icons/idx_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scape-tools/scape-editor/HEAD/gui/src/main/resources/icons/idx_32.png -------------------------------------------------------------------------------- /gui/src/main/resources/icons/file_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scape-tools/scape-editor/HEAD/gui/src/main/resources/icons/file_32.png -------------------------------------------------------------------------------- /gui/src/main/resources/icons/search_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scape-tools/scape-editor/HEAD/gui/src/main/resources/icons/search_24.png -------------------------------------------------------------------------------- /gui/src/main/kotlin/scape/editor/gui/plugin/extension/IPluginExtension.kt: -------------------------------------------------------------------------------- 1 | package scape.editor.gui.plugin.extension 2 | 3 | interface IPluginExtension -------------------------------------------------------------------------------- /gui/src/main/resources/icons/file_store_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scape-tools/scape-editor/HEAD/gui/src/main/resources/icons/file_store_32.png -------------------------------------------------------------------------------- /gui/src/main/resources/data/.scape-settings.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scape-tools/scape-editor/HEAD/gui/src/main/resources/data/.scape-settings.dat -------------------------------------------------------------------------------- /gui/src/main/resources/icons/close_icon_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scape-tools/scape-editor/HEAD/gui/src/main/resources/icons/close_icon_light.png -------------------------------------------------------------------------------- /gui/src/main/kotlin/scape/editor/gui/model/ValueModel.kt: -------------------------------------------------------------------------------- 1 | package scape.editor.gui.model 2 | 3 | class ValueModel(val model: KeyModel, val key: String, val value: Any) -------------------------------------------------------------------------------- /gui/src/main/resources/icons/maximize_icon_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scape-tools/scape-editor/HEAD/gui/src/main/resources/icons/maximize_icon_light.png -------------------------------------------------------------------------------- /gui/src/main/resources/icons/minimize_icon_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scape-tools/scape-editor/HEAD/gui/src/main/resources/icons/minimize_icon_light.png -------------------------------------------------------------------------------- /util/build.gradle: -------------------------------------------------------------------------------- 1 | 2 | dependencies { 3 | compile group: 'org.apache.commons', name: 'commons-compress', version:'1.9' 4 | } 5 | 6 | description = 'Scape Editor Utilities' 7 | -------------------------------------------------------------------------------- /gui/src/main/kotlin/scape/editor/gui/event/LoadCacheEvent.kt: -------------------------------------------------------------------------------- 1 | package scape.editor.gui.event 2 | 3 | import scape.editor.fs.RSFileSystem 4 | 5 | class LoadCacheEvent(val fs: RSFileSystem) -------------------------------------------------------------------------------- /gui/src/main/kotlin/scape/editor/gui/Launcher.kt: -------------------------------------------------------------------------------- 1 | package scape.editor.gui 2 | 3 | object Launcher { 4 | 5 | @JvmStatic 6 | fun main(args: Array) { 7 | App.main(args) 8 | } 9 | 10 | } -------------------------------------------------------------------------------- /gui/src/main/kotlin/scape/editor/gui/plugin/PluginClassLoader.kt: -------------------------------------------------------------------------------- 1 | package scape.editor.gui.plugin 2 | 3 | import java.net.URL 4 | import java.net.URLClassLoader 5 | 6 | class PluginClassLoader(val url: URL) : URLClassLoader(arrayOf(url)) -------------------------------------------------------------------------------- /fs/build.gradle: -------------------------------------------------------------------------------- 1 | 2 | description = 'Scape Editor RS2 File System' 3 | 4 | dependencies { 5 | compile project(':io') 6 | compile project(':util') 7 | } 8 | 9 | sourceSets { 10 | main.java.srcDirs = ['src/main/java', 'src/example/java'] 11 | } -------------------------------------------------------------------------------- /gui/src/main/kotlin/scape/editor/gui/plugin/IPlugin.kt: -------------------------------------------------------------------------------- 1 | package scape.editor.gui.plugin 2 | 3 | interface IPlugin { 4 | 5 | fun fxml(): String 6 | 7 | fun stylesheets(): Array 8 | 9 | fun applicationIcon(): String 10 | 11 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jul 19 00:12:54 CDT 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.5.1-all.zip 7 | -------------------------------------------------------------------------------- /gui/src/main/kotlin/scape/editor/gui/util/StringExtensions.kt: -------------------------------------------------------------------------------- 1 | package scape.editor.gui.util 2 | 3 | fun String.getFileNameWithoutExtension(): String { 4 | val pos = indexOf(".") 5 | 6 | if (pos != -1) { 7 | return substring(0, pos) 8 | } 9 | 10 | return this 11 | } -------------------------------------------------------------------------------- /gui/src/main/kotlin/scape/editor/gui/plugin/PluginDescriptor.kt: -------------------------------------------------------------------------------- 1 | package scape.editor.gui.plugin 2 | 3 | @Retention(AnnotationRetention.RUNTIME) 4 | @Target(AnnotationTarget.CLASS) 5 | annotation class PluginDescriptor(val name: String, val description: String = "", val authors: Array = [], val version : String = "1.0.0") -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'scape-editor' 2 | 3 | include ':fs' 4 | include ':gui' 5 | include ':io' 6 | include ':util' 7 | 8 | project(':fs').projectDir = "$rootDir/fs" as File 9 | project(':gui').projectDir = "$rootDir/gui" as File 10 | project(':io').projectDir = "$rootDir/io" as File 11 | project(':util').projectDir = "$rootDir/util" as File -------------------------------------------------------------------------------- /gui/src/main/kotlin/scape/editor/gui/util/DataOutputStreamExtensions.kt: -------------------------------------------------------------------------------- 1 | package scape.editor.gui.util 2 | 3 | import java.io.DataOutputStream 4 | import java.io.IOException 5 | 6 | @Throws(IOException::class) 7 | fun DataOutputStream.write24Int(value: Int) { 8 | this.writeByte((value shr 16).toByte().toInt()) 9 | this.writeByte((value shr 8).toByte().toInt()) 10 | this.writeByte(value.toByte().toInt()) 11 | } -------------------------------------------------------------------------------- /gui/src/main/kotlin/scape/editor/gui/model/NamedValueModel.kt: -------------------------------------------------------------------------------- 1 | package scape.editor.gui.model 2 | 3 | import javafx.beans.property.SimpleObjectProperty 4 | import javafx.beans.property.SimpleStringProperty 5 | 6 | class NamedValueModel(val name: String, val value: Any) { 7 | 8 | val nameProperty = SimpleStringProperty(name) 9 | val valueProperty = SimpleObjectProperty(ValueModel(KeyModel(-1, "", -1), name, value)) 10 | 11 | } -------------------------------------------------------------------------------- /gui/src/main/kotlin/scape/editor/gui/util/SwingImageExtensions.kt: -------------------------------------------------------------------------------- 1 | package scape.editor.gui.util 2 | 3 | import java.awt.Image 4 | import java.awt.image.BufferedImage 5 | 6 | fun Image.imageToBufferedImage(): BufferedImage { 7 | val bufferedImage = BufferedImage(getWidth(null), getHeight(null), BufferedImage.TYPE_INT_ARGB) 8 | val g2 = bufferedImage.createGraphics() 9 | g2.drawImage(this, 0, 0, null) 10 | g2.dispose() 11 | return bufferedImage 12 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.xml 2 | 3 | .classpath 4 | .project 5 | /.settings/ 6 | 7 | target 8 | bin 9 | 10 | *.class 11 | *.iml 12 | .gradle/ 13 | gui/out/ 14 | util/build/libs/util-2.0.0.jar 15 | util/build/tmp/jar/MANIFEST.MF 16 | classes/ 17 | build/kotlin-build/version.txt 18 | api/build/tmp/jar/MANIFEST.MF 19 | api/build/ 20 | gui/build/ 21 | util/src/main/java/META-INF/MANIFEST.MF 22 | build/ 23 | dump/ 24 | test/ 25 | 26 | cache/ 27 | 28 | fs/.DS_Store 29 | 30 | .DS_Store 31 | -------------------------------------------------------------------------------- /gui/src/main/kotlin/scape/editor/gui/model/StoreModel.kt: -------------------------------------------------------------------------------- 1 | package scape.editor.gui.model 2 | 3 | import javafx.beans.property.SimpleIntegerProperty 4 | import javafx.beans.property.SimpleStringProperty 5 | import javafx.scene.image.ImageView 6 | import scape.editor.gui.Settings 7 | 8 | class StoreModel(val id: Int, val name: String) { 9 | 10 | val idProperty = SimpleIntegerProperty(id) 11 | val nameProperty = SimpleStringProperty(name) 12 | val icon = ImageView(Settings.getIcon("file_store_32.png")) 13 | 14 | } -------------------------------------------------------------------------------- /gui/src/main/kotlin/scape/editor/gui/model/KeyModel.kt: -------------------------------------------------------------------------------- 1 | package scape.editor.gui.model 2 | 3 | import javafx.beans.property.SimpleIntegerProperty 4 | import javafx.beans.property.SimpleStringProperty 5 | 6 | class KeyModel(val id: Int, val name: String = "null", val instance: Any){ 7 | 8 | val idProperty = SimpleIntegerProperty(id) 9 | val nameProperty = SimpleStringProperty(name) 10 | 11 | var map = mutableMapOf() 12 | 13 | override fun toString() : String { 14 | return id.toString() 15 | } 16 | 17 | } -------------------------------------------------------------------------------- /gui/src/main/kotlin/scape/editor/gui/model/PluginWrapper.kt: -------------------------------------------------------------------------------- 1 | package scape.editor.gui.model 2 | 3 | import javafx.beans.property.SimpleStringProperty 4 | import scape.editor.gui.plugin.IPlugin 5 | import scape.editor.gui.plugin.PluginClassLoader 6 | import scape.editor.gui.plugin.PluginDescriptor 7 | 8 | class PluginWrapper(val plugin: IPlugin, val loader: PluginClassLoader, val path: String, val meta: PluginDescriptor) { 9 | val nameProperty = SimpleStringProperty(meta.name) 10 | 11 | val versionProperty = SimpleStringProperty(meta.version) 12 | 13 | override fun toString(): String { 14 | return meta.name 15 | } 16 | } -------------------------------------------------------------------------------- /gui/src/main/kotlin/scape/editor/gui/util/MapExtensions.kt: -------------------------------------------------------------------------------- 1 | package scape.editor.gui.util 2 | 3 | import java.lang.reflect.Modifier 4 | 5 | fun MutableMap.mapToInstance(instance: Any) { 6 | try { 7 | val fields = instance.javaClass.declaredFields 8 | for (field in fields) { 9 | 10 | if (Modifier.isStatic(field.modifiers)) { 11 | continue 12 | } 13 | 14 | field.isAccessible = true 15 | 16 | val name = field.name 17 | 18 | if (containsKey(name)) { 19 | field.set(instance, get(name)) 20 | } 21 | } 22 | } catch (ex: Exception) { 23 | ex.printStackTrace() 24 | } 25 | } -------------------------------------------------------------------------------- /gui/src/main/java/scape/editor/fx/component/NumericTextField.java: -------------------------------------------------------------------------------- 1 | package scape.editor.fx.component; 2 | 3 | import javafx.scene.control.TextField; 4 | 5 | public class NumericTextField extends TextField 6 | { 7 | @Override 8 | public void replaceText(int start, int end, String text) 9 | { 10 | if (validate(text)) 11 | { 12 | super.replaceText(start, end, text); 13 | } 14 | } 15 | 16 | @Override 17 | public void replaceSelection(String text) 18 | { 19 | if (validate(text)) 20 | { 21 | super.replaceSelection(text); 22 | } 23 | } 24 | 25 | private boolean validate(String text) 26 | { 27 | return text.matches("-?[0-9]*"); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018-2019 Nshusa 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -------------------------------------------------------------------------------- /util/src/main/java/scape/editor/util/HashUtils.java: -------------------------------------------------------------------------------- 1 | package scape.editor.util; 2 | 3 | public final class HashUtils { 4 | 5 | private HashUtils() { 6 | 7 | } 8 | 9 | public static int hashName(String name) { 10 | int hash = 0; 11 | name = name.toUpperCase(); 12 | for (int i = 0; i < name.length(); i++) { 13 | hash = (hash * 61 + name.charAt(i)) - 32; 14 | } 15 | return hash; 16 | } 17 | 18 | public static long hashSpriteName(String name) { 19 | name = name.toUpperCase(); 20 | long hash = 0; 21 | for (int index = 0; index < name.length(); index++) { 22 | hash = hash * 61 + name.charAt(index) - 32; 23 | hash = hash + (hash >> 56) & 0xFFFFFFFFFFFFFFL; 24 | } 25 | 26 | return hash; 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /gui/src/main/kotlin/scape/editor/gui/util/RSPropertySetExtensions.kt: -------------------------------------------------------------------------------- 1 | package scape.editor.gui.util 2 | 3 | import scape.editor.gui.plugin.extension.ConfigExtension 4 | import java.lang.reflect.Modifier 5 | import java.util.HashMap 6 | 7 | fun ConfigExtension.Companion.RSPropertySet.mapInstanceFields(instance: Any): ConfigExtension.Companion.RSPropertySet { 8 | val map = HashMap() 9 | val set = ConfigExtension.Companion.RSPropertySet() 10 | set.properties = map 11 | 12 | try { 13 | val fields = instance.javaClass.declaredFields 14 | 15 | for (field in fields) { 16 | 17 | if (Modifier.isStatic(field.modifiers)) { 18 | continue 19 | } 20 | 21 | field.isAccessible = true 22 | 23 | val name = field.name 24 | 25 | val value = field.get(instance) ?: continue 26 | 27 | map[name] = value 28 | } 29 | } catch (ex: Exception) { 30 | ex.printStackTrace() 31 | } 32 | 33 | return set 34 | } -------------------------------------------------------------------------------- /gui/src/main/kotlin/scape/editor/gui/util/BufferedImageExtensions.kt: -------------------------------------------------------------------------------- 1 | package scape.editor.gui.util 2 | 3 | import java.awt.Color 4 | import java.awt.Toolkit 5 | import java.awt.image.BufferedImage 6 | import java.awt.image.FilteredImageSource 7 | import java.awt.image.RGBImageFilter 8 | 9 | fun BufferedImage.setColorTransparent(color: Color): BufferedImage { 10 | val filter = object : RGBImageFilter() { 11 | 12 | var markerRGB = color.rgb or -0x1000000 13 | 14 | override fun filterRGB(x: Int, y: Int, rgb: Int): Int { 15 | return if (rgb or -0x1000000 == markerRGB) { 16 | 0x00FFFFFF and rgb 17 | } else { 18 | rgb 19 | } 20 | } 21 | } 22 | 23 | val ip = FilteredImageSource(source, filter) 24 | return Toolkit.getDefaultToolkit().createImage(ip).imageToBufferedImage() 25 | } 26 | 27 | fun BufferedImage.toType(type: Int): BufferedImage { 28 | val image = BufferedImage(this.width, this.height, type) 29 | image.graphics.drawImage(this, 0, 0, null) 30 | return image 31 | } -------------------------------------------------------------------------------- /gui/src/main/kotlin/scape/editor/gui/model/StoreEntryModel.kt: -------------------------------------------------------------------------------- 1 | package scape.editor.gui.model 2 | 3 | import javafx.beans.property.SimpleIntegerProperty 4 | import javafx.beans.property.SimpleStringProperty 5 | import javafx.scene.image.ImageView 6 | import scape.editor.gui.Settings 7 | import java.text.DecimalFormat 8 | 9 | class StoreEntryModel(val id: Int, val name: String, val size: Int) { 10 | 11 | val idProperty = SimpleIntegerProperty(id) 12 | var nameProperty = SimpleStringProperty(name) 13 | 14 | val sizeProperty = SimpleStringProperty(readableFileSize(size.toLong())) 15 | 16 | var icon = ImageView(Settings.getIcon("dat_32.png")) 17 | 18 | private fun readableFileSize(size: Long): String { 19 | if (size <= 0) { 20 | return "0" 21 | } 22 | val units = arrayOf("B", "kB", "MB", "GB", "TB") 23 | val digitGroups = (Math.log10(size.toDouble()) / Math.log10(1024.0)).toInt() 24 | return DecimalFormat("#,##0.#").format(size / Math.pow(1024.0, digitGroups.toDouble())) + " " + units[digitGroups] 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Scape Editor 2 | **Scape Editor** - Is a RuneScape 2 file system editor which can read/modify data within RuneScape's file system between the years 2005-2007. (Before the major engine overhaul update after revision 400) 3 | 4 | # File Store Editor 5 | ![https://i.imgur.com/poAcSnf.png](https://i.imgur.com/poAcSnf.png) 6 | 7 | # Archive Editor 8 | ![enter image description here](https://i.imgur.com/pWZRO4g.png) 9 | 10 | # 3D Model Viewer 11 | ![enter image description here](https://i.imgur.com/vOzLdVW.gif) 12 | 13 | # Interface Editor 14 | ![enter image description here](https://i.imgur.com/DNkwPVR.gif) 15 | 16 | # Sprite Editor 17 | ![enter image description here](https://i.imgur.com/aY0Kus4.png) 18 | 19 | # Texure Packer 20 | ![enter image description here](https://i.imgur.com/7I1ud9L.png) 21 | 22 | # Npc Definition Editor 23 | ![enter image description here](https://i.imgur.com/hQdDtnE.png) 24 | 25 | # Item Definition Editor 26 | ![enter image description here](https://i.imgur.com/JzltTX2.png) 27 | 28 | # Object Definition Editor 29 | ![enter image description here](https://i.imgur.com/IVbVlOv.png) 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /gui/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'application' 3 | id 'org.openjfx.javafxplugin' version '0.0.7' 4 | } 5 | 6 | dependencies { 7 | runtimeOnly "org.openjfx:javafx-graphics:$javafx.version:win" 8 | runtimeOnly "org.openjfx:javafx-graphics:$javafx.version:linux" 9 | runtimeOnly "org.openjfx:javafx-graphics:$javafx.version:mac" 10 | 11 | compile group: 'com.google.code.gson', name: 'gson', version:'2.8.4' 12 | compile group: 'commons-lang', name: 'commons-lang', version: '2.6' 13 | compile group: 'org.imgscalr', name: 'imgscalr-lib', version: '4.2' 14 | compile group: 'com.google.guava', name: 'guava', version:'18.0' 15 | 16 | compile project(':io') 17 | compile project(':fs') 18 | compile project(':util') 19 | } 20 | 21 | javafx { 22 | version = "12.0.1" 23 | modules = [ 'javafx.controls', 'javafx.fxml', 'javafx.swing' ] 24 | } 25 | 26 | mainClassName = 'scape.editor.gui.Launcher' 27 | description = 'Scape Editor User-Interface' 28 | 29 | defaultTasks 'run' 30 | 31 | configurations { 32 | jar.archiveName = 'scape-editor.jar' 33 | } 34 | 35 | jar { 36 | manifest { 37 | attributes 'Main-Class': mainClassName 38 | } 39 | from { 40 | configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /util/src/main/java/scape/editor/util/ByteBufferUtils.java: -------------------------------------------------------------------------------- 1 | package scape.editor.util; 2 | 3 | import java.nio.ByteBuffer; 4 | 5 | public final class ByteBufferUtils { 6 | 7 | private ByteBufferUtils() { 8 | 9 | } 10 | 11 | public static void write24Int(ByteBuffer buffer, int value) { 12 | buffer.put((byte) (value >> 16)).put((byte) (value >> 8)).put((byte) value); 13 | } 14 | 15 | public static int getUMedium(ByteBuffer buffer) { 16 | return (buffer.getShort() & 0xFFFF) << 8 | buffer.get() & 0xFF; 17 | } 18 | 19 | public static int getUShort(ByteBuffer buffer) { 20 | return buffer.getShort() & 0xffff; 21 | } 22 | 23 | public static int readU24Int(ByteBuffer buffer) { 24 | return (buffer.get() & 0x0ff) << 16 | (buffer.get() & 0x0ff) << 8 | (buffer.get() & 0x0ff); 25 | } 26 | 27 | public static int getSmart(ByteBuffer buffer) { 28 | int peek = buffer.get(buffer.position()) & 0xFF; 29 | if (peek < 128) { 30 | return buffer.get() & 0xFF; 31 | } 32 | return (buffer.getShort() & 0xFFFF) - 32768; 33 | } 34 | 35 | public static String getString(ByteBuffer buffer) { 36 | final StringBuilder bldr = new StringBuilder(); 37 | byte b; 38 | while (buffer.hasRemaining() && (b = buffer.get()) != 10) { 39 | bldr.append((char) b); 40 | } 41 | return bldr.toString(); 42 | } 43 | 44 | } -------------------------------------------------------------------------------- /gui/src/main/kotlin/scape/editor/gui/App.kt: -------------------------------------------------------------------------------- 1 | package scape.editor.gui 2 | 3 | import javafx.application.Application 4 | import javafx.fxml.FXMLLoader 5 | import javafx.scene.Parent 6 | import javafx.scene.Scene 7 | import javafx.scene.image.Image 8 | import javafx.stage.Stage 9 | import javafx.stage.StageStyle 10 | import scape.editor.fs.RSFileSystem 11 | import scape.editor.gui.plugin.PluginManager 12 | 13 | class App : Application() { 14 | 15 | override fun init() { 16 | Settings.load() 17 | } 18 | 19 | override fun start(stage: Stage) { 20 | mainStage = stage 21 | 22 | val root : Parent = FXMLLoader.load(App::class.java.getResource("/scenes/StoreScene.fxml")) 23 | stage.title = "Scape Editor [build $VERSION]" 24 | val scene = Scene(root) 25 | scene.stylesheets.add(App::class.java.getResource("/style.css").toExternalForm()) 26 | stage.scene = scene 27 | stage.icons.add(Image(App::class.java.getResourceAsStream("/icons/icon.png"))) 28 | stage.centerOnScreen() 29 | stage.isResizable = false 30 | stage.initStyle(StageStyle.UNDECORATED) 31 | stage.show() 32 | } 33 | 34 | override fun stop() { 35 | if (fs.isLoaded) { 36 | Settings.save(fs.root) 37 | } 38 | } 39 | 40 | companion object { 41 | val VERSION = "3.1.0" 42 | 43 | val fs = RSFileSystem() 44 | 45 | lateinit var mainStage : Stage 46 | 47 | @JvmStatic 48 | fun main(args : Array) { 49 | launch(App::class.java) 50 | } 51 | } 52 | 53 | } -------------------------------------------------------------------------------- /fs/src/main/java/scape/editor/fs/graphics/RSImageArchive.java: -------------------------------------------------------------------------------- 1 | package scape.editor.fs.graphics; 2 | 3 | import scape.editor.fs.RSArchive; 4 | import scape.editor.util.HashUtils; 5 | 6 | import java.io.IOException; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | public final class RSImageArchive { 11 | 12 | private int hash; 13 | private final List sprites = new ArrayList<>(); 14 | 15 | public RSImageArchive(int hash) { 16 | this.hash = hash; 17 | } 18 | 19 | public static RSImageArchive decode(RSArchive archive, int hash) { 20 | RSImageArchive imageArchive = new RSImageArchive(hash); 21 | 22 | for (int i = 0; ; i++) { 23 | try { 24 | RSSprite decoded = RSSprite.decode(archive, hash, i); 25 | 26 | if (decoded == null) { 27 | break; 28 | } 29 | 30 | imageArchive.sprites.add(decoded); 31 | } catch (IOException e) { 32 | break; 33 | } 34 | } 35 | 36 | return imageArchive; 37 | } 38 | 39 | public static RSImageArchive decode(RSArchive archive, String name) { 40 | return decode(archive, HashUtils.hashName(name)); 41 | } 42 | 43 | public int getHash() { 44 | return hash; 45 | } 46 | 47 | public void setHash(int hash) { 48 | this.hash = hash; 49 | } 50 | 51 | public void setName(String name) { 52 | this.hash = HashUtils.hashName(name); 53 | } 54 | 55 | public List getSprites() { 56 | return sprites; 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /gui/src/main/kotlin/scape/editor/gui/util/FXDialogUtil.kt: -------------------------------------------------------------------------------- 1 | package scape.editor.gui.util 2 | 3 | import javafx.application.Platform 4 | import javafx.scene.control.Alert 5 | import javafx.scene.control.Label 6 | import javafx.scene.control.TextArea 7 | import javafx.scene.layout.GridPane 8 | import javafx.scene.layout.Priority 9 | import java.io.PrintWriter 10 | import java.io.StringWriter 11 | import java.lang.Exception 12 | 13 | object FXDialogUtil { 14 | fun showException(ex: Exception) { 15 | val alert = Alert(Alert.AlertType.ERROR) 16 | alert.title = "Exception" 17 | alert.headerText = "Oops, there's an exception!" 18 | alert.contentText = "Something went wrong!" 19 | 20 | val sw = StringWriter() 21 | val pw = PrintWriter(sw) 22 | ex.printStackTrace(pw) 23 | val exceptionText = sw.toString() 24 | 25 | val label = Label("The exception stacktrace was:") 26 | 27 | val textArea = TextArea(exceptionText) 28 | textArea.isEditable = false 29 | textArea.isWrapText = true 30 | 31 | textArea.maxWidth = java.lang.Double.MAX_VALUE 32 | textArea.maxHeight = java.lang.Double.MAX_VALUE 33 | GridPane.setVgrow(textArea, Priority.ALWAYS) 34 | GridPane.setHgrow(textArea, Priority.ALWAYS) 35 | 36 | val expContent = GridPane() 37 | expContent.maxWidth = java.lang.Double.MAX_VALUE 38 | expContent.add(label, 0, 0) 39 | expContent.add(textArea, 0, 1) 40 | 41 | alert.dialogPane.expandableContent = expContent 42 | 43 | Platform.runLater { 44 | alert.showAndWait() 45 | } 46 | } 47 | 48 | fun showError(msg: String) { 49 | val alert = Alert(Alert.AlertType.ERROR) 50 | alert.alertType = Alert.AlertType.ERROR 51 | alert.title = "Exception" 52 | alert.headerText = "Oops, there's an exception!" 53 | alert.contentText = msg 54 | 55 | Platform.runLater { 56 | alert.showAndWait() 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /gui/src/main/resources/scenes/TaskScene.fxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 25 | 30 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /gui/src/main/kotlin/scape/editor/gui/controller/TaskController.kt: -------------------------------------------------------------------------------- 1 | package scape.editor.gui.controller 2 | 3 | import javafx.animation.PauseTransition 4 | import javafx.concurrent.Task 5 | import javafx.fxml.FXML 6 | import javafx.scene.control.ProgressBar 7 | import javafx.scene.input.MouseEvent 8 | import javafx.scene.layout.VBox 9 | import javafx.scene.paint.Color 10 | import javafx.scene.text.Text 11 | import javafx.stage.Stage 12 | import javafx.util.Duration 13 | import java.net.URL 14 | import java.util.* 15 | 16 | open class TaskController : BaseController() { 17 | 18 | @FXML 19 | lateinit var taskPane: VBox 20 | 21 | @FXML 22 | lateinit var progressText: Text 23 | 24 | @FXML 25 | lateinit var title: Text 26 | 27 | @FXML 28 | lateinit var progressBar: ProgressBar 29 | 30 | override fun initialize(location: URL, resources: ResourceBundle?) { 31 | title.fill = Color.WHITE 32 | } 33 | 34 | @FXML 35 | override fun handleMouseDragged(event: MouseEvent) { 36 | currentStage = taskPane.scene.window as Stage 37 | currentStage.x = event.screenX - xOffset 38 | currentStage.y = event.screenY - yOffset 39 | } 40 | 41 | open fun createTask(task: Task<*>) { 42 | progressText.textProperty().unbind() 43 | progressText.textProperty().bind(task.messageProperty()) 44 | progressBar.progressProperty().unbind() 45 | progressBar.progressProperty().bind(task.progressProperty()) 46 | progressText.fill = Color.WHITE 47 | progressBar.isVisible = true 48 | 49 | Thread(task).start() 50 | 51 | task.setOnSucceeded { 52 | 53 | progressText.textProperty().unbind() 54 | 55 | val pause = PauseTransition(Duration.seconds(2.0)) 56 | 57 | pause.setOnFinished { 58 | getStage().close() 59 | BaseController.sceneCount-- 60 | } 61 | 62 | pause.play() 63 | } 64 | 65 | task.setOnFailed { 66 | 67 | val pause = PauseTransition(Duration.seconds(2.0)) 68 | 69 | pause.setOnFinished { 70 | progressText.textProperty().unbind() 71 | progressText.text = "Oops, something went wrong!" 72 | } 73 | 74 | pause.play() 75 | 76 | } 77 | } 78 | 79 | private fun getStage() : Stage { 80 | return taskPane.scene.window as Stage 81 | } 82 | 83 | 84 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS="-Xmx64m" 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /gui/src/main/kotlin/scape/editor/gui/plugin/PluginManager.kt: -------------------------------------------------------------------------------- 1 | package scape.editor.gui.plugin 2 | 3 | import com.google.common.eventbus.EventBus 4 | import javafx.fxml.Initializable 5 | import scape.editor.gui.model.PluginWrapper 6 | import java.lang.reflect.Modifier 7 | import java.nio.file.Files 8 | import java.nio.file.Path 9 | import java.nio.file.Paths 10 | import java.util.jar.JarFile 11 | 12 | object PluginManager { 13 | 14 | val plugins = mutableMapOf() 15 | 16 | private val eventBus = EventBus() 17 | 18 | fun loadPlugins() { 19 | val paths = findPlugins() 20 | for (path in paths) { 21 | try { 22 | val loader = PluginClassLoader(path.toUri().toURL()) 23 | 24 | val jar = JarFile(path.toFile()) 25 | 26 | val entries = jar.entries() 27 | while(entries.hasMoreElements()) { 28 | val next = entries.nextElement() 29 | 30 | if (next.isDirectory || !next.name.endsWith(".class")) { 31 | continue 32 | } 33 | 34 | var classpath = next.name 35 | 36 | classpath = classpath.replace("/", ".") 37 | classpath = classpath.replace(".class", "") 38 | 39 | val clazz = loader.loadClass(classpath) 40 | 41 | if (Modifier.isAbstract(clazz.modifiers) || !clazz.isAnnotationPresent(PluginDescriptor::class.java) || Initializable::class.java.isAssignableFrom(clazz) ) { 42 | continue 43 | } 44 | 45 | val plugin = clazz.newInstance() 46 | 47 | if (plugin is IPlugin) { 48 | val annotation = plugin.javaClass.getAnnotation(PluginDescriptor::class.java) 49 | plugins[path.toString()] = PluginWrapper(plugin, loader, path.toString(), annotation) 50 | register(plugin) 51 | } 52 | 53 | } 54 | 55 | } catch (ex: Exception) { 56 | ex.printStackTrace() 57 | } 58 | } 59 | 60 | println("Loaded: ${plugins.size} plugins") 61 | 62 | } 63 | 64 | private fun findPlugins() : Set { 65 | val set = mutableSetOf() 66 | val root = Files.createDirectories(Paths.get(System.getProperty("user.home"), "scape-editor")) 67 | val pluginDir = Files.createDirectories(Paths.get(root.resolve("plugins").toUri())) 68 | 69 | Files.walk(pluginDir).filter { it.fileName.toString().contains(".jar")}.distinct().forEach { set.add(it)} 70 | return set 71 | } 72 | 73 | fun register(plugin: IPlugin) { 74 | this.eventBus.register(plugin) 75 | } 76 | 77 | fun unregister(plugin: IPlugin) { 78 | this.eventBus.unregister(plugin) 79 | } 80 | 81 | fun post(event: Any) { 82 | eventBus.post(event) 83 | } 84 | 85 | } -------------------------------------------------------------------------------- /io/src/main/kotlin/scape/editor/io/RSBuffer.kt: -------------------------------------------------------------------------------- 1 | package scape.editor.fs.io 2 | 3 | class RSBuffer private constructor(private var buffer: ByteArray) { 4 | 5 | var position: Int = 0 6 | 7 | fun readUByte(): Int { 8 | return buffer[position++].toInt() and 0xFF 9 | } 10 | 11 | fun readByte(): Byte { 12 | return buffer[position++] 13 | } 14 | 15 | fun readShort(): Int { 16 | position += java.lang.Short.BYTES 17 | return (buffer[position - 2].toInt() shl 8) + (buffer[position - 1].toInt() and 0xFF) 18 | } 19 | 20 | fun readUShort(): Int { 21 | position += java.lang.Short.BYTES 22 | return (buffer[position - 2].toInt() and 0xFF shl 8) + (buffer[position - 1].toInt() and 0xFF) 23 | } 24 | 25 | fun readInt(): Int { 26 | position += Integer.BYTES 27 | return (buffer[position - 4].toInt() and 0xFF shl 24) + (buffer[position - 3].toInt() and 0xFF shl 16) + (buffer[position - 2].toInt() and 0xFF shl 8) + (buffer[position - 1].toInt() and 0xFF) 28 | } 29 | 30 | fun writeByte(value: Int) { 31 | validateCapacity(java.lang.Byte.BYTES) 32 | buffer[position++] = value.toByte() 33 | } 34 | 35 | fun writeShort(value: Int) { 36 | validateCapacity(java.lang.Short.BYTES) 37 | buffer[position++] = (value shr 8).toByte() 38 | buffer[position++] = value.toByte() 39 | } 40 | 41 | fun writeInt(value: Int) { 42 | validateCapacity(Integer.BYTES) 43 | buffer[position++] = (value shr 24).toByte() 44 | buffer[position++] = (value shr 16).toByte() 45 | buffer[position++] = (value shr 8).toByte() 46 | buffer[position++] = value.toByte() 47 | } 48 | 49 | fun writeString10(s: String) { 50 | validateCapacity(s.length) 51 | System.arraycopy(s.toByteArray(), 0, buffer, position, s.length) 52 | position += s.length 53 | buffer[position++] = 10 54 | } 55 | 56 | private fun validateCapacity(length: Int) { 57 | if (position + length >= buffer.size) { 58 | val data = ByteArray(buffer.size * 2) 59 | System.arraycopy(buffer, 0, data, 0, buffer.size) 60 | this.buffer = data 61 | } 62 | } 63 | 64 | fun readUSmart(): Int { 65 | val value = buffer[position].toInt() and 0xff 66 | return if (value < 128) { 67 | readUByte() - 64 68 | } else { 69 | readUShort() - 49152 70 | } 71 | } 72 | 73 | fun readString10(): String { 74 | val startOffset = position 75 | while (buffer[position++].toInt() != 10); 76 | return String(buffer, startOffset, position - startOffset - 1) 77 | } 78 | 79 | fun capacity(): Int { 80 | return buffer.size 81 | } 82 | 83 | fun toArray(): ByteArray { 84 | val data = ByteArray(position) 85 | System.arraycopy(buffer, 0, data, 0, position) 86 | return data 87 | } 88 | 89 | companion object { 90 | 91 | private const val DEFAULT_CAPACITY = 5000 92 | 93 | @JvmStatic 94 | fun wrap(data: ByteArray): RSBuffer { 95 | return RSBuffer(data) 96 | } 97 | 98 | @JvmStatic 99 | fun init(capacity: Int = DEFAULT_CAPACITY): RSBuffer { 100 | return RSBuffer(ByteArray(capacity)) 101 | } 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /gui/src/main/resources/task_style.css: -------------------------------------------------------------------------------- 1 | .root { 2 | -fx-background-color : #121212; 3 | } 4 | 5 | #minBtn { 6 | -fx-graphic: url('icons/minimize_icon_light.png'); 7 | } 8 | 9 | #minBtn:hover { 10 | -fx-background-color: #292929; 11 | } 12 | 13 | #minBtn:pressed { 14 | -fx-background-color: #1F1F1F; 15 | } 16 | 17 | #maxBtn { 18 | -fx-graphic: url('icons/maximize_icon_light.png'); 19 | } 20 | 21 | #maxBtn:pressed { 22 | -fx-background-color: #1F1F1F; 23 | } 24 | 25 | #closeBtn { 26 | -fx-graphic: url('icons/close_icon_light.png'); 27 | } 28 | 29 | #closeBtn:hover { 30 | -fx-background-color: #E81123; 31 | } 32 | 33 | #closeBtn:pressed { 34 | -fx-background-color: #F1707A; 35 | } 36 | 37 | .tab-pane .tab-header-area .tab-header-background { 38 | -fx-opacity: 0; 39 | } 40 | 41 | .tab-pane 42 | { 43 | -fx-tab-min-width:90px; 44 | } 45 | 46 | .tab { 47 | -fx-background-insets: 0 1 0 1,0,0; 48 | } 49 | .tab-pane .tab 50 | { 51 | -fx-background-color: #e6e6e6; 52 | } 53 | 54 | .tab-pane .tab:selected 55 | { 56 | -fx-background-color: #3c3c3c; 57 | } 58 | 59 | .tab .tab-label { 60 | -fx-alignment: CENTER; 61 | -fx-text-fill: #828282; 62 | -fx-font-size: 12px; 63 | -fx-font-weight: bold; 64 | } 65 | 66 | .tab:selected .tab-label { 67 | -fx-alignment: CENTER; 68 | -fx-text-fill: #e6e6e6; 69 | } 70 | 71 | .titled-pane * { 72 | -fx-background-color: transparent; 73 | } 74 | 75 | .titled-pane { 76 | -fx-text-fill: white; 77 | } 78 | 79 | .titled-pane > .title 80 | { 81 | -fx-background-color: #121212; 82 | -fx-text-fill: white; 83 | -fx-alignment: center; 84 | } 85 | 86 | .titled-pane > *.content 87 | { 88 | -fx-box-border: transparent; 89 | } 90 | 91 | .context-menu { 92 | -fx-background-color: #333333; 93 | } 94 | 95 | .menu-item { 96 | -fx-background-color: #333333; 97 | } 98 | 99 | .menu-item:hover { 100 | -fx-background-color: #292929; 101 | } 102 | 103 | .menu-item .label { 104 | -fx-text-fill: white; 105 | } 106 | 107 | .tooltip { 108 | -fx-background-color: #292929; 109 | } 110 | 111 | *.scroll-bar:horizontal { 112 | -fx-opacity: 0; 113 | } 114 | 115 | *.scroll-bar:vertical *.thumb { 116 | -fx-background-color: #333333; 117 | -fx-padding: 10px; 118 | } 119 | 120 | *.scroll-bar:vertical *.thumb:hover { 121 | -fx-background-color: #404040; 122 | } 123 | 124 | *.scroll-bar:vertical *.increment-button, 125 | *.scroll-bar:vertical *.decrement-button { 126 | -fx-background-color: #121212; 127 | -fx-padding: 5px; 128 | -fx-border-color: #121212; 129 | -fx-background-insets: 0, 0, 0; 130 | } 131 | 132 | *.scroll-bar:vertical *.increment-arrow, 133 | *.scroll-bar:vertical *.decrement-arrow { 134 | -fx-shape: " "; 135 | -fx-padding:5px; 136 | } 137 | 138 | *.scroll-bar:vertical *.track { 139 | -fx-background-color: #181818; 140 | -fx-padding: 10px; 141 | } 142 | 143 | .progress-bar > .bar { 144 | -fx-background-color: linear-gradient( 145 | from 0px .75em to .75em 0px, 146 | repeat, 147 | -fx-accent 0%, 148 | -fx-accent 49%, 149 | derive(-fx-accent, 30%) 50%, 150 | derive(-fx-accent, 30%) 99% 151 | ); 152 | -fx-background-insets: 0; 153 | } 154 | 155 | .progress-bar > .track { 156 | -fx-background-color: #34495e; 157 | -fx-text-box-border: #34495e; 158 | -fx-control-inner-background: #34495e; 159 | } 160 | 161 | #appTitle { 162 | -fx-text-fill: white; 163 | } 164 | 165 | .menu-bar { 166 | -fx-background-color: #121212; 167 | -fx-text-fill: white; 168 | } 169 | 170 | .menu .label { 171 | -fx-text-fill: white; 172 | } 173 | 174 | .button { 175 | -fx-background-color: #181818; 176 | -fx-text-fill: white; 177 | } 178 | 179 | .button:hover { 180 | -fx-background-color: #282828; 181 | } -------------------------------------------------------------------------------- /gui/src/main/resources/scenes/PluginScene.fxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 50 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 |
65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 88 | 89 | 90 | 91 | 92 |
93 |
94 | -------------------------------------------------------------------------------- /gui/src/main/kotlin/scape/editor/gui/Settings.kt: -------------------------------------------------------------------------------- 1 | package scape.editor.gui 2 | 3 | import javafx.scene.image.Image 4 | import org.apache.commons.lang.SystemUtils 5 | import scape.editor.util.HashUtils 6 | import java.io.* 7 | import java.nio.file.Files 8 | import java.nio.file.Path 9 | import java.nio.file.Paths 10 | import java.util.zip.GZIPInputStream 11 | 12 | object Settings { 13 | 14 | private val icons = HashMap() 15 | private val storeNames = HashMap() 16 | private val storeEntryNames = HashMap, String>() 17 | private val hashes = HashMap() 18 | 19 | fun load() { 20 | loadIcons() 21 | loadSettings() 22 | } 23 | 24 | fun loadSettings() { 25 | val path: String 26 | 27 | if (!App.fs.isLoaded || !Files.exists(App.fs.root.resolve(".scape-settings.dat"))) { 28 | path = "/data/.scape-settings.dat" 29 | } else { 30 | path = App.fs.root.resolve(".scape-settings.dat").toString() 31 | } 32 | 33 | try { 34 | val stream : InputStream 35 | if (!App.fs.isLoaded || !Files.exists(App.fs.root.resolve(".scape-settings.dat"))) { 36 | stream = App::class.java.getResourceAsStream(path) 37 | } else { 38 | stream = FileInputStream(File(path)) 39 | } 40 | 41 | DataInputStream(stream).use { dis -> 42 | val stores = dis.readUnsignedByte() 43 | for (i in 0 until stores) { 44 | val id = dis.readUnsignedByte() 45 | val name = dis.readUTF() 46 | 47 | storeNames[id] = name 48 | } 49 | 50 | val storeEntries = dis.readInt() 51 | for (i in 0 until storeEntries) { 52 | val store = dis.readUnsignedByte() 53 | val file = dis.readInt() 54 | val name = dis.readUTF() 55 | storeEntryNames[Pair(store, file)] = name 56 | } 57 | val names = dis.readInt() 58 | for (i in 0 until names) { 59 | putNameForHash(dis.readUTF()) 60 | } 61 | } 62 | } catch (ex: Exception) { 63 | Files.deleteIfExists(Paths.get(path)) 64 | ex.printStackTrace() 65 | } 66 | } 67 | 68 | fun save(path: Path = App.fs.root) { 69 | val bos = ByteArrayOutputStream() 70 | DataOutputStream(bos).use { dos -> 71 | dos.writeByte(storeNames.entries.size) 72 | for (entrySet in storeNames.entries) { 73 | dos.writeByte(entrySet.key) 74 | dos.writeUTF(entrySet.value) 75 | } 76 | 77 | dos.writeInt(storeEntryNames.entries.size) 78 | for (entrySet in storeEntryNames.entries) { 79 | val key = entrySet.key 80 | val storeId = key.first 81 | val fileId = key.second 82 | 83 | dos.writeByte(storeId) 84 | dos.writeInt(fileId) 85 | dos.writeUTF(entrySet.value) 86 | } 87 | 88 | dos.writeInt(hashes.size) 89 | for (name in hashes.values) { 90 | dos.writeUTF(name) 91 | } 92 | 93 | } 94 | 95 | val file = path.resolve(".scape-settings.dat").toFile() 96 | 97 | if (SystemUtils.IS_OS_WINDOWS) { 98 | Files.deleteIfExists(file.toPath()) // fix java bug not being able to override hidden files 99 | Runtime.getRuntime().exec("attrib +h ${file.path}") 100 | } 101 | 102 | FileOutputStream(file).use { fos -> 103 | fos.write(bos.toByteArray()) 104 | } 105 | } 106 | 107 | fun getNameFromHash(hash: Int) : String? { 108 | return hashes[hash] 109 | } 110 | 111 | fun putNameForHash(name: String) { 112 | hashes[HashUtils.hashName(name)] = name 113 | } 114 | 115 | private fun loadIcons() { 116 | try { 117 | icons["file_store_32.png"] = Image(App::class.java.getResourceAsStream("/icons/file_store_32.png")) 118 | icons["dat_32.png"] = Image(App::class.java.getResourceAsStream("/icons/dat_32.png")) 119 | icons["file_32.png"] = Image(App::class.java.getResourceAsStream("/icons/file_32.png")) 120 | icons["gz_32.png"] = Image(App::class.java.getResourceAsStream("/icons/gz_32.png")) 121 | icons["idx_32.png"] = Image(App::class.java.getResourceAsStream("/icons/idx_32.png")) 122 | } catch (ex: IOException) { 123 | println("Failed to load icons.") 124 | } 125 | } 126 | 127 | fun putStoreName(storeId: Int, name: String) { 128 | storeNames[storeId] = name 129 | } 130 | 131 | fun getStoreReferenceName(storeId: Int) : String? { 132 | return storeNames[storeId] 133 | } 134 | 135 | fun getStoreEntryReferenceName(storeId: Int, fileId: Int) : String? { 136 | return storeEntryNames[Pair(storeId, fileId)] 137 | } 138 | 139 | fun putStoreEntryReferenceName(storeId: Int, fileId: Int, name: String) { 140 | storeEntryNames[Pair(storeId, fileId)] = name 141 | } 142 | 143 | fun getIcon(data: ByteArray) : Image? { 144 | if (isGzip(data)) { 145 | return Settings.getIcon("gz_32.png") 146 | } else if (data.isNotEmpty()) { 147 | return Settings.getIcon("dat_32.png") 148 | } 149 | 150 | return Settings.getIcon("file_32.png") 151 | } 152 | 153 | fun getIcon(name: String) : Image? { 154 | return icons[name] 155 | } 156 | 157 | fun isGzip(bytes: ByteArray): Boolean { 158 | if (bytes.size < 2) { 159 | return false 160 | } 161 | 162 | val head = bytes[0].toInt() and 0xff or (bytes[1].toInt() shl 8 and 0xff00) 163 | return GZIPInputStream.GZIP_MAGIC == head 164 | } 165 | 166 | } -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS='"-Xmx64m"' 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /util/src/main/java/scape/editor/util/CompressionUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2010-2011 Graham Edgecombe 3 | Copyright (c) 2011-2016 Major and other apollo contributors 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | */ 17 | 18 | package scape.editor.util; 19 | 20 | import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream; 21 | import org.apache.commons.compress.compressors.bzip2.BZip2CompressorOutputStream; 22 | 23 | import java.io.*; 24 | import java.nio.ByteBuffer; 25 | import java.util.zip.DeflaterOutputStream; 26 | import java.util.zip.GZIPInputStream; 27 | import java.util.zip.GZIPOutputStream; 28 | 29 | /** 30 | * A utility class for performing compression/decompression. 31 | * 32 | * @author Graham 33 | */ 34 | public final class CompressionUtils { 35 | 36 | /** 37 | * Bzip2s the specified array, removing the header. 38 | * 39 | * @param uncompressed The uncompressed array. 40 | * @return The compressed array. 41 | * @throws IOException If there is an error compressing the array. 42 | */ 43 | public static byte[] bzip2(byte[] uncompressed) throws IOException { 44 | ByteArrayOutputStream bout = new ByteArrayOutputStream(); 45 | 46 | try (BZip2CompressorOutputStream os = new BZip2CompressorOutputStream(bout, 1)) { 47 | os.write(uncompressed); 48 | os.finish(); 49 | 50 | byte[] compressed = bout.toByteArray(); 51 | byte[] newCompressed = new byte[compressed.length - 4]; // Strip the header 52 | System.arraycopy(compressed, 4, newCompressed, 0, newCompressed.length); 53 | return newCompressed; 54 | } 55 | } 56 | 57 | /** 58 | * Debzip2s the compressed array and places the result into the decompressed array. 59 | * 60 | * @param compressed The compressed array, without the header. 61 | * @param decompressed The decompressed array. 62 | * @throws IOException If there is an error decompressing the array. 63 | */ 64 | public static void debzip2(byte[] compressed, byte[] decompressed) throws IOException { 65 | byte[] newCompressed = new byte[compressed.length + 4]; 66 | newCompressed[0] = 'B'; 67 | newCompressed[1] = 'Z'; 68 | newCompressed[2] = 'h'; 69 | newCompressed[3] = '1'; 70 | System.arraycopy(compressed, 0, newCompressed, 4, compressed.length); 71 | 72 | try (DataInputStream is = new DataInputStream(new BZip2CompressorInputStream(new ByteArrayInputStream(newCompressed)))) { 73 | is.readFully(decompressed); 74 | } 75 | } 76 | 77 | /** 78 | * Degzips the compressed array and places the results into the decompressed array. 79 | * 80 | * @param compressed The compressed array. 81 | * @param decompressed The decompressed array. 82 | * @throws IOException If an I/O error occurs. 83 | */ 84 | public static void degzip(byte[] compressed, byte[] decompressed) throws IOException { 85 | try (DataInputStream is = new DataInputStream(new GZIPInputStream(new ByteArrayInputStream(compressed)))) { 86 | is.readFully(decompressed); 87 | } 88 | } 89 | 90 | /** 91 | * Degzips all of the datain the specified {@link ByteBuffer}. 92 | * 93 | * @param compressed The compressed buffer. 94 | * @return The decompressed array. 95 | * @throws IOException If there is an error decompressing the buffer. 96 | */ 97 | public static byte[] degzip(ByteBuffer compressed) throws IOException { 98 | try (InputStream is = new GZIPInputStream(new ByteArrayInputStream(compressed.array())); 99 | ByteArrayOutputStream out = new ByteArrayOutputStream()) { 100 | byte[] buffer = new byte[1024]; 101 | 102 | while (true) { 103 | int read = is.read(buffer, 0, buffer.length); 104 | if (read == -1) { 105 | break; 106 | } 107 | 108 | out.write(buffer, 0, read); 109 | } 110 | 111 | return out.toByteArray(); 112 | } 113 | } 114 | 115 | /** 116 | * Gzips the specified array. 117 | * 118 | * @param uncompressed The uncompressed array. 119 | * @return The compressed array. 120 | * @throws IOException If there is an error compressing the array. 121 | */ 122 | public static byte[] gzip(byte[] uncompressed) throws IOException { 123 | ByteArrayOutputStream bout = new ByteArrayOutputStream(); 124 | 125 | try (DeflaterOutputStream os = new GZIPOutputStream(bout)) { 126 | os.write(uncompressed); 127 | os.finish(); 128 | return bout.toByteArray(); 129 | } 130 | } 131 | 132 | public static boolean isGZipped(InputStream in) { 133 | if (!in.markSupported()) { 134 | in = new BufferedInputStream(in); 135 | } 136 | in.mark(2); 137 | int magic = 0; 138 | try { 139 | magic = in.read() & 0xff | ((in.read() << 8) & 0xff00); 140 | in.reset(); 141 | } catch (IOException e) { 142 | e.printStackTrace(System.err); 143 | return false; 144 | } 145 | return magic == GZIPInputStream.GZIP_MAGIC; 146 | } 147 | 148 | /** 149 | * Default private constructor to prevent instantiation. 150 | */ 151 | private CompressionUtils() { 152 | 153 | } 154 | 155 | } -------------------------------------------------------------------------------- /gui/src/main/kotlin/scape/editor/gui/controller/BaseController.kt: -------------------------------------------------------------------------------- 1 | package scape.editor.gui.controller 2 | 3 | import javafx.application.Platform 4 | import javafx.concurrent.Task 5 | import javafx.event.ActionEvent 6 | import javafx.fxml.FXML 7 | import javafx.fxml.FXMLLoader 8 | import javafx.fxml.Initializable 9 | import javafx.scene.Node 10 | import javafx.scene.Parent 11 | import javafx.scene.Scene 12 | import javafx.scene.image.Image 13 | import javafx.scene.input.MouseEvent 14 | import javafx.stage.DirectoryChooser 15 | import javafx.stage.Screen 16 | import javafx.stage.Stage 17 | import javafx.stage.StageStyle 18 | import scape.editor.gui.App 19 | import scape.editor.gui.Settings 20 | import java.io.File 21 | 22 | abstract class BaseController : Initializable { 23 | 24 | var currentPlugin = Any() 25 | 26 | var xOffset:Double = 0.toDouble() 27 | var yOffset:Double = 0.toDouble() 28 | 29 | @FXML 30 | private fun onMouseClicked(event: MouseEvent) { 31 | currentStage = (event.target as Node).scene.window as Stage 32 | xOffset = 0.0 33 | yOffset = 0.0 34 | } 35 | 36 | @FXML 37 | private fun minimizeProgram(e: ActionEvent) { 38 | val node = e.source as Node 39 | currentStage = node.scene.window as Stage 40 | currentStage.isIconified = true 41 | } 42 | 43 | fun openScene(sceneName: String) { 44 | val loader = FXMLLoader(App::class.java.getResource("/scenes/$sceneName.fxml")) 45 | val root = loader.load() 46 | 47 | val stage = Stage() 48 | val scene = Scene(root) 49 | stage.scene = scene 50 | stage.isResizable = false 51 | stage.initStyle(StageStyle.UNDECORATED) 52 | stage.scene.stylesheets.add(App::class.java.getResource("/style.css").toExternalForm()) 53 | stage.icons.add(Image(App::class.java.getResourceAsStream("/icons/icon.png"))) 54 | stage.show() 55 | sceneCount++ 56 | } 57 | 58 | fun switchScene(sceneName: String, instance: Any = Any()) { 59 | 60 | val loader = FXMLLoader(App::class.java.getResource("/scenes/$sceneName.fxml")) 61 | val root = loader.load() 62 | val base = loader.getController() as BaseController 63 | base.currentPlugin = instance 64 | 65 | val stage = Stage() 66 | val scene = Scene(root) 67 | stage.scene = scene 68 | stage.isResizable = false 69 | stage.initStyle(StageStyle.UNDECORATED) 70 | stage.scene.stylesheets.add(App::class.java.getResource("/style.css").toExternalForm()) 71 | stage.icons.add(Image(App::class.java.getResourceAsStream("/icons/icon.png"))) 72 | 73 | val currentStage = BaseController.currentStage 74 | currentStage.isResizable = stage.isResizable 75 | currentStage.scene.stylesheets.clear() 76 | currentStage.scene.stylesheets.addAll(stage.scene.stylesheets) 77 | currentStage.icons.clear() 78 | currentStage.icons.addAll(stage.icons) 79 | currentStage.scene = stage.scene 80 | currentStage.centerOnScreen() 81 | } 82 | 83 | protected fun runTask(title: String, task: Task<*>) { 84 | val loader = FXMLLoader(App::class.java.getResource("/scenes/TaskScene.fxml")) 85 | val root = loader.load() 86 | 87 | val controller = loader.getController() 88 | 89 | controller.title.text = title 90 | controller.createTask(task) 91 | 92 | val stage = Stage() 93 | val scene = Scene(root) 94 | stage.scene = scene 95 | stage.isResizable = false 96 | stage.initStyle(StageStyle.UNDECORATED) 97 | stage.scene.stylesheets.add(App::class.java.getResource("/task_style.css").toExternalForm()) 98 | stage.icons.add(Image(App::class.java.getResourceAsStream("/icons/icon.png"))) 99 | 100 | val screenWidth = Screen.getPrimary().visualBounds.width 101 | val screenHeight = Screen.getPrimary().visualBounds.height 102 | 103 | var x = Math.random() * screenWidth 104 | var y = Math.random() * screenHeight 105 | 106 | stage.show() 107 | sceneCount++ 108 | 109 | if (x > (screenWidth - stage.width)) { 110 | x = screenWidth - stage.width 111 | } 112 | 113 | if (y > (screenHeight - stage.height)) { 114 | y = screenHeight - stage.height 115 | } 116 | 117 | stage.x = x 118 | stage.y = y 119 | } 120 | 121 | @FXML 122 | fun openFS() { 123 | if (App.fs.isLoaded) { 124 | onPopulate() 125 | return 126 | } 127 | 128 | val chooser = DirectoryChooser() 129 | chooser.title = "Select directory containing cache" 130 | chooser.initialDirectory = File("./") 131 | val selectedDir = chooser.showDialog(App.mainStage) ?: return 132 | App.fs.root = selectedDir.toPath() 133 | 134 | if (!App.fs.load()) { 135 | return 136 | } 137 | 138 | Settings.loadSettings() 139 | 140 | onPopulate() 141 | } 142 | 143 | open fun onPopulate() { 144 | 145 | } 146 | 147 | @FXML 148 | open fun closeProgram(e: ActionEvent) { 149 | if (e.source is Node) { 150 | val node = e.source as Node 151 | currentStage = node.scene.window as Stage 152 | 153 | if (sceneCount > 1) { 154 | currentStage.close() 155 | } else { 156 | Platform.exit() 157 | } 158 | sceneCount-- 159 | } else { 160 | if (sceneCount > 1) { 161 | currentStage.close() 162 | } else { 163 | Platform.exit() 164 | } 165 | } 166 | } 167 | 168 | @FXML 169 | open fun handleMouseDragged(event: MouseEvent) { 170 | currentStage.x = event.screenX - xOffset 171 | currentStage.y = event.screenY - yOffset 172 | } 173 | @FXML 174 | open fun handleMousePressed(event:MouseEvent) { 175 | currentStage = (event.target as Node).scene.window as Stage 176 | xOffset = event.sceneX 177 | yOffset = event.sceneY 178 | } 179 | 180 | @FXML 181 | private fun clearProgram() { 182 | if (App.fs.isLoaded) { 183 | App.fs.reset() 184 | } 185 | onClear() 186 | } 187 | 188 | open fun onClear() { 189 | 190 | } 191 | 192 | companion object { 193 | var currentStage = App.mainStage 194 | var sceneCount = 1 195 | } 196 | 197 | } -------------------------------------------------------------------------------- /gui/src/main/resources/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | -fx-font-family: "Calibri"; 3 | -fx-font-size: 12; 4 | } 5 | 6 | .label { 7 | -fx-text-fill: gray; 8 | } 9 | 10 | #title { 11 | -fx-font-size: 16; 12 | } 13 | 14 | .cod-gray { 15 | -fx-background-color : #121212; 16 | } 17 | 18 | .check-box { 19 | -fx-text-fill: white; 20 | } 21 | 22 | /* align table column text center */ 23 | .table-column { 24 | -fx-alignment: CENTER; 25 | } 26 | 27 | /* search bar */ 28 | .tf-search { 29 | -fx-background-image:url('icons/search_24.png'); 30 | -fx-background-repeat: no-repeat; 31 | -fx-background-position: left center; 32 | } 33 | 34 | .text-field { 35 | -fx-background-color: white; 36 | -fx-text-fill: black; 37 | -fx-prompt-text-fill: black; 38 | } 39 | 40 | .list-view { 41 | -fx-background-color: #1F1F1F; 42 | } 43 | 44 | .list-cell:even { 45 | -fx-text-fill: gray; 46 | -fx-background-color: #1F1F1F; 47 | } 48 | 49 | .list-cell:odd { 50 | -fx-text-fill: gray; 51 | -fx-background-color: #1F1F1F; 52 | } 53 | 54 | .list-cell:filled:hover { 55 | -fx-text-fill: white; 56 | } 57 | 58 | .list-cell:filled:selected:focused, .list-cell:filled:selected { 59 | -fx-text-fill: white; 60 | } 61 | 62 | .table-view { 63 | -fx-background-color: #181818; 64 | } 65 | 66 | .table-view .column-header { 67 | -fx-background-color: #181818; 68 | } 69 | 70 | .table-row-cell { 71 | -fx-background-color: #181818; 72 | } 73 | 74 | .table-view .table-cell { 75 | -fx-text-fill: white; 76 | -fx-padding: 5; 77 | } 78 | 79 | /* Row hovered */ 80 | .table-view:row-selection .table-row-cell:filled:hover { 81 | -fx-background-color: #282828; 82 | } 83 | 84 | .table-row-cell:selected { 85 | -fx-background-color: #282828; 86 | } 87 | 88 | /* Removes vertical cell borders */ 89 | .table-view { 90 | -fx-table-cell-border-color: transparent; 91 | } 92 | 93 | .table-view:focused .table-row-cell:filled:focused:selected { 94 | -fx-background-color: #282828; 95 | } 96 | 97 | .table-row-cell { 98 | -fx-table-cell-border-color: transparent; 99 | } 100 | 101 | .table-view .column-header .label { 102 | -fx-text-fill: gray; 103 | } 104 | 105 | .table-view .filler { 106 | -fx-background-color: #181818; 107 | -fx-padding: 1em; 108 | } 109 | 110 | .table-view .scroll-bar:horizontal .increment-arrow, 111 | .table-view .scroll-bar:horizontal .decrement-arrow, 112 | .table-view .scroll-bar:horizontal .increment-button, 113 | .table-view .scroll-bar:horizontal .decrement-button { 114 | -fx-padding:0; 115 | } 116 | 117 | .context-menu { 118 | -fx-background-color: #333333; 119 | } 120 | 121 | .white { 122 | -fx-fill: white; 123 | } 124 | 125 | .menu-item { 126 | -fx-background-color: #333333; 127 | } 128 | 129 | .menu-item:hover { 130 | -fx-background-color: #292929; 131 | } 132 | 133 | .menu-item .label { 134 | -fx-text-fill: white; 135 | } 136 | 137 | *.scroll-bar:horizontal { 138 | -fx-opacity: 0; 139 | } 140 | 141 | *.scroll-bar:vertical *.thumb { 142 | -fx-background-color: #333333; 143 | -fx-padding: 10px; 144 | } 145 | 146 | *.scroll-bar:vertical *.thumb:hover { 147 | -fx-background-color: #404040; 148 | } 149 | 150 | *.scroll-bar:vertical *.increment-button, 151 | *.scroll-bar:vertical *.decrement-button { 152 | -fx-background-color: #121212; 153 | -fx-padding: 5px; 154 | -fx-border-color: #121212; 155 | -fx-background-insets: 0, 0, 0; 156 | } 157 | 158 | *.scroll-bar:vertical *.increment-arrow, 159 | *.scroll-bar:vertical *.decrement-arrow { 160 | -fx-shape: " "; 161 | -fx-padding:5px; 162 | } 163 | 164 | *.scroll-bar:vertical *.track { 165 | -fx-background-color: #181818; 166 | -fx-padding: 10px; 167 | } 168 | 169 | #pluginBtn { 170 | -fx-text-fill: gray; 171 | } 172 | 173 | #pluginBtn:hover { 174 | -fx-text-fill: white; 175 | } 176 | 177 | .menu-button:hover, .menu-button.showing { 178 | -fx-background-color: #292929; 179 | } 180 | 181 | #minBtn { 182 | -fx-graphic: url('icons/minimize_icon_light.png'); 183 | } 184 | 185 | .button { 186 | -fx-text-fill: white; 187 | -fx-background-color: transparent; 188 | } 189 | 190 | .button:hover { 191 | -fx-background-color: #292929; 192 | } 193 | 194 | .button:pressed { 195 | -fx-background-color: #1F1F1F; 196 | } 197 | 198 | #maxBtn { 199 | -fx-graphic: url('icons/maximize_icon_light.png'); 200 | } 201 | 202 | #closeBtn { 203 | -fx-graphic: url('icons/close_icon_light.png'); 204 | } 205 | 206 | #closeBtn:hover { 207 | -fx-background-color: #E81123; 208 | } 209 | 210 | #closeBtn:pressed { 211 | -fx-background-color: #F1707A; 212 | } 213 | 214 | .titled-pane { 215 | -fx-text-fill: white; 216 | } 217 | 218 | .titled-pane > .title 219 | { 220 | -fx-background-color: #181818; 221 | -fx-text-fill: white; 222 | } 223 | 224 | .titled-pane > .title > .arrow-button .arrow 225 | { 226 | -fx-background-color: none; 227 | } 228 | 229 | .titled-pane > *.content 230 | { 231 | -fx-background-color: #181818; 232 | -fx-box-border: transparent; 233 | } 234 | 235 | .tree-table-view { 236 | -fx-background-color: transparent; 237 | -fx-focus-color: transparent; 238 | -fx-box-border: transparent; 239 | -fx-faint-focus-color: transparent; 240 | } 241 | 242 | .tree-table-view .column-header-background .label { 243 | -fx-text-fill: gray; 244 | } 245 | 246 | .tree-table-row-cell { 247 | -fx-background-color: #121212; 248 | -fx-border-color: transparent; 249 | -fx-table-cell-border-color:#121212; 250 | } 251 | 252 | .tree-table-view .tree-table-cell { 253 | -fx-text-fill: gray; 254 | } 255 | 256 | .tree-table-view .tree-table-cell:hover { 257 | -fx-text-fill: white; 258 | } 259 | 260 | .tree-table-view .corner { 261 | -fx-background-color: transparent; 262 | } 263 | 264 | .tree-table-view .column-header { 265 | -fx-background-color: #1F1F1F; 266 | } 267 | 268 | .tab-pane .tab-header-background { 269 | -fx-background-color: transparent; 270 | } 271 | 272 | .tab-pane .tab 273 | { 274 | -fx-background-color: silver; 275 | 276 | } 277 | 278 | .tab-pane .tab:selected 279 | { 280 | -fx-background-color: #3c3c3c; 281 | } 282 | 283 | .tab:selected .tab-label { 284 | -fx-alignment: CENTER; 285 | -fx-text-fill: white; 286 | } 287 | 288 | /* Remove blue focus border on a selected tab pane */ 289 | .tab-pane:focused > .tab-header-area > .headers-region > .tab:selected .focus-indicator { 290 | -fx-border-width: 0; 291 | } 292 | 293 | .tree-view, .tree-cell { 294 | -fx-background-color: #1F1F1F; 295 | -fx-text-fill: gray; 296 | } 297 | 298 | .tree-cell:selected { 299 | -fx-text-fill: white; 300 | } 301 | 302 | .tree-cell:hover { 303 | -fx-text-fill: white; 304 | } 305 | 306 | .combo-box { 307 | -fx-focus-color: transparent; 308 | -fx-faint-focus-color: transparent; 309 | } 310 | 311 | .combo-box .list-cell 312 | { 313 | -fx-background-color: white; 314 | -fx-text-fill: black; 315 | -fx-padding: 3 0 2 7; 316 | -fx-cell-size: 1.66667em; 317 | } 318 | 319 | .combo-box-popup .list-view .list-cell:filled:hover 320 | { 321 | -fx-background-color: #121212; 322 | -fx-text-fill: white; 323 | } 324 | 325 | .tool-bar { 326 | -fx-background-color: #121212; 327 | } 328 | 329 | .tool-bar .button { 330 | -fx-text-fill: black; 331 | -fx-background-color: #bdc3c7; 332 | } 333 | 334 | .tool-bar .button:hover { 335 | -fx-background-color: #ecf0f1; 336 | } -------------------------------------------------------------------------------- /gui/src/main/kotlin/scape/editor/gui/plugin/extension/ConfigExtension.kt: -------------------------------------------------------------------------------- 1 | package scape.editor.gui.plugin.extension 2 | 3 | import javafx.application.Platform 4 | import javafx.collections.ObservableList 5 | import javafx.scene.control.Alert 6 | import javafx.scene.control.Label 7 | import javafx.scene.control.TextArea 8 | import javafx.scene.layout.GridPane 9 | import javafx.scene.layout.Priority 10 | import scape.editor.fs.RSArchive 11 | import scape.editor.fs.RSFileStore 12 | import scape.editor.fs.io.RSBuffer 13 | import scape.editor.gui.App 14 | import scape.editor.gui.model.KeyModel 15 | import scape.editor.gui.plugin.PluginDescriptor 16 | import scape.editor.gui.util.getFileNameWithoutExtension 17 | import scape.editor.gui.util.mapInstanceFields 18 | import scape.editor.gui.util.mapToInstance 19 | import java.io.IOException 20 | 21 | import java.util.* 22 | import java.io.PrintWriter 23 | import java.io.StringWriter 24 | import java.lang.Exception 25 | 26 | 27 | abstract class ConfigExtension: IPluginExtension { 28 | 29 | open abstract fun getFileName(): String 30 | 31 | open fun getStoreId(): Int { 32 | return RSFileStore.ARCHIVE_FILE_STORE 33 | } 34 | 35 | open fun getFileId(): Int { 36 | return RSArchive.CONFIG_ARCHIVE 37 | } 38 | 39 | open fun useMetaFile(): Boolean { 40 | return true 41 | } 42 | 43 | open fun readLength(buffer: RSBuffer): Int { 44 | return buffer.readUShort() 45 | } 46 | 47 | open fun writeLength(buffer: RSBuffer, size: Int) { 48 | buffer.writeShort(size) 49 | } 50 | 51 | open fun writeOffset(metaBuf: RSBuffer, dataBuf: RSBuffer, lastPos: Int) { 52 | metaBuf.writeShort(dataBuf.position - lastPos) 53 | } 54 | 55 | open fun setInitialDataBufOffset(buffer: RSBuffer) { 56 | buffer.position = 2 57 | } 58 | 59 | protected abstract fun decode(currentIndex: Int, buffer: RSBuffer) 60 | 61 | open fun onLoad(list: ObservableList, archive: RSArchive) { 62 | try { 63 | val dataBuf = RSBuffer.wrap(archive.readFile(getDataFileName()).array()) 64 | 65 | var length: Int 66 | 67 | if (useMetaFile()) { 68 | val metaBuf = RSBuffer.wrap(archive.readFile(getMetaFileName()).array()) 69 | length = readLength(metaBuf) 70 | setInitialDataBufOffset(dataBuf) 71 | } else { 72 | length = readLength(dataBuf) 73 | } 74 | 75 | for (i in 0 until length) { 76 | val instance = this.javaClass.getConstructor().newInstance() 77 | instance.decode(i, dataBuf) 78 | val set = RSPropertySet().mapInstanceFields(instance) 79 | val model = KeyModel(i, set.getOrDefault("name", "null"), instance) 80 | model.map = TreeMap(set.properties) 81 | list.add(model) 82 | } 83 | 84 | onFinish(dataBuf) 85 | } catch (ex: IOException) { 86 | ex.printStackTrace() 87 | } 88 | } 89 | 90 | open fun showError(ex: Exception) { 91 | val alert = Alert(Alert.AlertType.ERROR) 92 | alert.title = "Error" 93 | alert.headerText = "Look, there's an error!" 94 | 95 | val sw = StringWriter() 96 | val pw = PrintWriter(sw) 97 | ex.printStackTrace(pw) 98 | val exceptionText = sw.toString() 99 | 100 | val label = Label("The exception stacktrace was:") 101 | 102 | val textArea = TextArea(exceptionText) 103 | textArea.isEditable = false 104 | textArea.isWrapText = true 105 | 106 | textArea.maxWidth = java.lang.Double.MAX_VALUE 107 | textArea.maxHeight = java.lang.Double.MAX_VALUE 108 | GridPane.setVgrow(textArea, Priority.ALWAYS) 109 | GridPane.setHgrow(textArea, Priority.ALWAYS) 110 | 111 | val expContent = GridPane() 112 | expContent.maxWidth = java.lang.Double.MAX_VALUE 113 | expContent.add(label, 0, 0) 114 | expContent.add(textArea, 0, 1) 115 | 116 | alert.dialogPane.expandableContent = expContent 117 | 118 | Platform.runLater { 119 | alert.showAndWait() 120 | } 121 | } 122 | 123 | open fun onSave(list: ObservableList, archive: RSArchive) { 124 | try { 125 | if (list.isEmpty()) { 126 | return 127 | } 128 | 129 | val metaBuf = RSBuffer.init() 130 | val dataBuf = RSBuffer.init() 131 | 132 | writeLength(dataBuf, list.size) 133 | 134 | if (useMetaFile()) { 135 | writeLength(metaBuf, list.size) 136 | } 137 | 138 | for (i in 0 until list.size) { 139 | val item = list[i] 140 | val instance = item.instance as ConfigExtension 141 | item.map.mapToInstance(instance) 142 | 143 | var lastPos = dataBuf.position 144 | 145 | instance.encode(dataBuf) 146 | 147 | if (useMetaFile()) { 148 | writeOffset(metaBuf, dataBuf, lastPos) 149 | } 150 | } 151 | 152 | archive.writeFile(getDataFileName(), dataBuf.toArray()) 153 | 154 | if (useMetaFile()) { 155 | archive.writeFile(getMetaFileName(), metaBuf.toArray()) 156 | } 157 | 158 | val store = App.fs.getStore(getStoreId()) ?: return 159 | val encoded = archive.encode() ?: return 160 | 161 | if (store.writeFile(getFileId(), encoded)) { 162 | val alert = Alert(Alert.AlertType.INFORMATION) 163 | alert.title = "Info" 164 | alert.headerText = "Success!" 165 | Platform.runLater { alert.show() } 166 | } 167 | 168 | } catch (ex: IOException) { 169 | ex.printStackTrace() 170 | } 171 | } 172 | 173 | private fun onFinish(buffer: RSBuffer) { 174 | var name = this.javaClass.simpleName 175 | 176 | if (this.javaClass.isAnnotationPresent(PluginDescriptor::class.java)) { 177 | val plugin = this.javaClass.getAnnotation(PluginDescriptor::class.java) 178 | name = plugin.name 179 | } 180 | 181 | if (buffer.position != buffer.capacity()) { 182 | val alert = Alert(Alert.AlertType.WARNING) 183 | alert.title = "Warning" 184 | alert.headerText = String.format("%s was not fully loaded pos=%d capacity=%d", name, buffer.position, buffer.capacity()) 185 | Platform.runLater { alert.show() } 186 | } 187 | 188 | } 189 | 190 | protected abstract fun encode(buffer: RSBuffer) 191 | 192 | open fun dataFileNameSuffix(): String { 193 | return DATA_SUFFIX 194 | } 195 | 196 | open fun metaFileNameSuffix(): String { 197 | return META_SUFFIX 198 | } 199 | 200 | private fun getDataFileName(): String { 201 | return "${getFileName().getFileNameWithoutExtension()}.${dataFileNameSuffix()}" 202 | } 203 | 204 | private fun getMetaFileName(): String { 205 | return "${getFileName().getFileNameWithoutExtension()}.${metaFileNameSuffix()}" 206 | } 207 | 208 | companion object { 209 | 210 | const val DATA_SUFFIX = "dat" 211 | const val META_SUFFIX = "idx" 212 | 213 | class RSPropertySet { 214 | 215 | var properties: Map = HashMap() 216 | 217 | fun getOrDefault(name: String, value: T): T { 218 | return (properties as java.util.Map).getOrDefault(name, value) as T 219 | } 220 | } 221 | 222 | } 223 | 224 | } -------------------------------------------------------------------------------- /gui/src/main/resources/scenes/StoreScene.fxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 59 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 |
109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 |
141 |
142 | -------------------------------------------------------------------------------- /gui/src/main/kotlin/scape/editor/gui/controller/PluginController.kt: -------------------------------------------------------------------------------- 1 | package scape.editor.gui.controller 2 | 3 | import javafx.application.Platform 4 | import javafx.collections.FXCollections 5 | import javafx.collections.transformation.FilteredList 6 | import javafx.collections.transformation.SortedList 7 | import javafx.concurrent.Task 8 | import javafx.event.ActionEvent 9 | import javafx.fxml.FXML 10 | import javafx.fxml.FXMLLoader 11 | import javafx.scene.Node 12 | import javafx.scene.Parent 13 | import javafx.scene.Scene 14 | import javafx.scene.control.* 15 | import javafx.scene.image.Image 16 | import javafx.stage.FileChooser 17 | import javafx.stage.Stage 18 | import javafx.stage.StageStyle 19 | import scape.editor.gui.App 20 | import scape.editor.gui.model.PluginWrapper 21 | import scape.editor.gui.plugin.PluginManager 22 | import scape.editor.gui.util.FXDialogUtil 23 | import java.io.File 24 | import java.io.FileInputStream 25 | import java.io.FileOutputStream 26 | import java.lang.Exception 27 | import java.net.URL 28 | import java.nio.file.Files 29 | import java.nio.file.Paths 30 | import java.util.* 31 | 32 | class PluginController : BaseController() { 33 | 34 | @FXML 35 | lateinit var tableView : TableView 36 | 37 | @FXML 38 | lateinit var nameCol : TableColumn 39 | 40 | @FXML 41 | lateinit var versionCol : TableColumn 42 | 43 | @FXML 44 | lateinit var searchTf : TextField 45 | 46 | private val data = FXCollections.observableArrayList() 47 | 48 | override fun initialize(location: URL?, resources: ResourceBundle?) { 49 | this.setupContextMenu() 50 | 51 | nameCol.setCellValueFactory { it.value.nameProperty} 52 | versionCol.setCellValueFactory { it.value.versionProperty } 53 | 54 | val filteredList = FilteredList(data) { true} 55 | searchTf.textProperty().addListener { _, _, newValue -> filteredList.setPredicate { 56 | if (newValue == null || newValue.isEmpty()) { 57 | return@setPredicate true 58 | } 59 | 60 | val lowercase = newValue.toLowerCase() 61 | 62 | if (it.meta.name.toLowerCase().contains(lowercase)) { 63 | return@setPredicate true 64 | } 65 | 66 | return@setPredicate false 67 | } 68 | } 69 | 70 | val sortedList = SortedList(filteredList) 71 | sortedList.comparatorProperty().bind(tableView.comparatorProperty()) 72 | 73 | tableView.items = sortedList 74 | 75 | refreshList() 76 | } 77 | 78 | private fun setupContextMenu() { 79 | val menuItem1 = MenuItem("Add Plugin") 80 | menuItem1.setOnAction { this.addPlugin() } 81 | 82 | val menuItem2 = MenuItem("Remove Plugin") 83 | menuItem2.setOnAction { this.removePlugin() } 84 | 85 | val menuItem3 = MenuItem("Refresh") 86 | menuItem3.setOnAction { this.refreshList() } 87 | 88 | val contextMenu = ContextMenu(menuItem1, menuItem2, menuItem3) 89 | this.tableView.contextMenu = contextMenu 90 | } 91 | 92 | private fun refreshList() { 93 | data.clear() 94 | 95 | if (PluginManager.plugins.isNotEmpty()) { 96 | PluginManager.plugins.forEach { 97 | it.value.loader.close() 98 | PluginManager.unregister(it.value.plugin) 99 | } 100 | PluginManager.plugins.clear() 101 | } 102 | 103 | PluginManager.loadPlugins() 104 | for (plugin in PluginManager.plugins) { 105 | data.add(plugin.value) 106 | } 107 | } 108 | 109 | private fun open(value: PluginWrapper) { 110 | try { 111 | val stylesheetPaths = mutableListOf() 112 | val jarPath = value.path 113 | 114 | 115 | 116 | for(cssPath in value.plugin.stylesheets()) { 117 | val cssResource = buildResourcePath(jarPath, cssPath) 118 | stylesheetPaths.add(cssResource) 119 | } 120 | 121 | val fxmlResource = buildResourcePath(jarPath, value.plugin.fxml()) 122 | 123 | val loader = FXMLLoader(URL(fxmlResource)) 124 | loader.classLoader = value.loader 125 | val root = loader.load() as Parent 126 | val base = loader.getController() as BaseController 127 | base.currentPlugin = value.plugin 128 | 129 | val stage = Stage() 130 | val scene = Scene(root) 131 | stage.scene = scene 132 | stage.isResizable = false 133 | stage.initStyle(StageStyle.UNDECORATED) 134 | stage.scene.stylesheets.addAll(stylesheetPaths) 135 | stage.icons.add(Image(URL(buildResourcePath(jarPath, value.plugin.applicationIcon())).openStream())) 136 | 137 | val currentStage = BaseController.currentStage 138 | currentStage.isResizable = stage.isResizable 139 | currentStage.scene.stylesheets.clear() 140 | currentStage.scene.stylesheets.addAll(stage.scene.stylesheets) 141 | currentStage.icons.clear() 142 | currentStage.icons.addAll(stage.icons) 143 | currentStage.scene = stage.scene 144 | currentStage.centerOnScreen() 145 | } catch (ex: Exception) { 146 | ex.printStackTrace() 147 | FXDialogUtil.showException(ex) 148 | } 149 | } 150 | 151 | private fun buildResourcePath(jarPath: String, resourcePath: String): String { 152 | val path = jarPath.replace("\\", "/") 153 | return "jar:file:$path!/$resourcePath" 154 | } 155 | 156 | @FXML 157 | fun open(event: ActionEvent) { 158 | val node = event.source as Node 159 | currentStage = node.scene.window as Stage 160 | 161 | val selectedItem = tableView.selectionModel.selectedItem ?: return 162 | open(selectedItem) 163 | } 164 | 165 | fun addPlugin() { 166 | val chooser = FileChooser() 167 | val filter = FileChooser.ExtensionFilter("Jar files (*.jar)", "*.jar") 168 | chooser.title = "Select your plugins to add" 169 | chooser.initialDirectory = File("./") 170 | chooser.extensionFilters.add(filter) 171 | 172 | val pluginPath = Paths.get(System.getProperty("user.home"), "scape-editor", "plugins") 173 | 174 | if (!Files.exists(pluginPath)) { 175 | if (!pluginPath.toFile().mkdirs()) { 176 | return 177 | } 178 | } 179 | 180 | val selectedFiles = chooser.showOpenMultipleDialog(App.mainStage) ?: return 181 | 182 | val task = object: Task() { 183 | override fun call(): Boolean { 184 | 185 | for(selectedFile in selectedFiles) { 186 | val destPath = pluginPath.resolve(selectedFile.name).toFile() 187 | FileInputStream(selectedFile).use { input -> 188 | FileOutputStream(destPath).use { out -> 189 | out.write(input.readAllBytes()) 190 | } 191 | } 192 | } 193 | 194 | PluginManager.loadPlugins() 195 | 196 | Platform.runLater { 197 | refreshList() 198 | } 199 | 200 | return true 201 | } 202 | 203 | } 204 | 205 | Thread(task).start() 206 | 207 | } 208 | 209 | fun removePlugin() { 210 | val selectedItem = this.tableView.selectionModel.selectedItem ?: return 211 | 212 | val path = Paths.get(selectedItem.path) 213 | 214 | if (Files.exists(path)) { 215 | try { 216 | val wrapper = PluginManager.plugins[selectedItem.path] ?: return 217 | wrapper.loader.close() 218 | PluginManager.unregister(wrapper.plugin) 219 | PluginManager.plugins.remove(selectedItem.path) 220 | Files.delete(path) 221 | this.refreshList() 222 | } catch (ex: Exception) { 223 | ex.printStackTrace() 224 | } 225 | 226 | } 227 | } 228 | 229 | } -------------------------------------------------------------------------------- /fs/src/main/java/scape/editor/fs/RSFileSystem.java: -------------------------------------------------------------------------------- 1 | package scape.editor.fs; 2 | 3 | import java.io.Closeable; 4 | import java.io.File; 5 | import java.io.IOException; 6 | import java.io.RandomAccessFile; 7 | import java.nio.ByteBuffer; 8 | import java.nio.file.Files; 9 | import java.nio.file.Path; 10 | import java.util.*; 11 | 12 | public class RSFileSystem implements Closeable { 13 | 14 | private Path root; 15 | 16 | private final RSFileStore[] fileStores = new RSFileStore[255]; 17 | 18 | private boolean loaded; 19 | 20 | public boolean load() { 21 | try { 22 | if (!Files.exists(root)) { 23 | Files.createDirectory(root); 24 | } 25 | 26 | final Path dataPath = root.resolve("main_file_cache.dat"); 27 | 28 | if (!Files.exists(dataPath)) { 29 | return false; 30 | } 31 | 32 | for (int i = 0; i < 255; i++) { 33 | Path indexPath = root.resolve("main_file_cache.idx" + i); 34 | if (Files.exists(indexPath)) { 35 | fileStores[i] = new RSFileStore(i, new RandomAccessFile(dataPath.toFile(), "rw").getChannel(), new RandomAccessFile(indexPath.toFile(), "rw").getChannel()); 36 | } 37 | } 38 | loaded = true; 39 | } catch (Exception ex) { 40 | ex.printStackTrace(); 41 | return false; 42 | } 43 | return true; 44 | } 45 | 46 | public boolean createStore(int storeId) throws IOException { 47 | if (storeId < 0 || storeId >= fileStores.length) { 48 | return false; 49 | } 50 | 51 | if (fileStores[storeId] != null) { 52 | fileStores[storeId].close(); 53 | } 54 | 55 | final Path dataPath = root.resolve("main_file_cache.dat"); 56 | 57 | if (!Files.exists(dataPath)) { 58 | Files.createFile(dataPath); 59 | } 60 | 61 | final Path path = root.resolve("main_file_cache.idx" + storeId); 62 | 63 | if (!Files.exists(path)) { 64 | Files.createFile(path); 65 | } else { 66 | Files.deleteIfExists(path); 67 | } 68 | fileStores[storeId] = new RSFileStore(storeId + 1, new RandomAccessFile(dataPath.toFile(), "rw").getChannel(), new RandomAccessFile(path.toFile(), "rw").getChannel()); 69 | return true; 70 | } 71 | 72 | public boolean removeStore(int storeId) { 73 | if (storeId < 0 || storeId >= fileStores.length) { 74 | return false; 75 | } 76 | 77 | reset(); 78 | 79 | try { 80 | Files.deleteIfExists(root.resolve("main_file_cache.idx" + storeId)); 81 | return true; 82 | } catch (Exception ex) { 83 | ex.printStackTrace(); 84 | } 85 | return false; 86 | } 87 | 88 | public boolean defragment() { 89 | try { 90 | if (!isLoaded()) { 91 | return false; 92 | } 93 | 94 | File[] files = root.toFile().listFiles(); 95 | 96 | if (files == null) { 97 | return false; 98 | } 99 | 100 | final int storeCount = getStoreCount(); 101 | 102 | final Map> map = new LinkedHashMap<>(); 103 | final Map counts = new HashMap<>(); 104 | 105 | for (int store = 0; store < storeCount; store++) { 106 | 107 | RSFileStore fileStore = getStore(store); 108 | 109 | if (fileStore == null) { 110 | continue; 111 | } 112 | 113 | map.put(store, new ArrayList<>()); 114 | 115 | for (int file = 0; file < fileStore.getFileCount(); file++) { 116 | ByteBuffer buffer = fileStore.readFile(file); 117 | 118 | if (buffer == null) { 119 | buffer = ByteBuffer.wrap(new byte[0]); 120 | } 121 | 122 | if (buffer.capacity() > 0) { 123 | counts.put(store, file); 124 | } 125 | 126 | List data = map.get(store); 127 | data.add(buffer); 128 | } 129 | 130 | } 131 | 132 | reset(); 133 | 134 | Files.deleteIfExists(root.resolve("main_file_cache.dat")); 135 | 136 | for (int i = 0; i < fileStores.length; i++) { 137 | Files.deleteIfExists(root.resolve("main_file_cache.idx" + i)); 138 | } 139 | 140 | Files.createFile(root.resolve("main_file_cache.dat")); 141 | 142 | for (int i = 0; i < storeCount; i++) { 143 | Files.createFile(root.resolve("main_file_cache.idx" + i)); 144 | } 145 | 146 | load(); 147 | 148 | for (Map.Entry> entry : map.entrySet()) { 149 | 150 | final int fileStoreId = entry.getKey(); 151 | 152 | RSFileStore fileStore = getStore(fileStoreId); 153 | 154 | if (fileStore == null) { 155 | continue; 156 | } 157 | 158 | final int lastEntry = counts.getOrDefault(fileStoreId, 0); 159 | 160 | for (int file = 0; file < entry.getValue().size(); file++) { 161 | 162 | if (file > lastEntry) { 163 | break; 164 | } 165 | 166 | ByteBuffer data = entry.getValue().get(file); 167 | 168 | fileStore.writeFile(file, data == null ? new byte[0] : data.array()); 169 | } 170 | 171 | } 172 | 173 | return true; 174 | } catch (Exception ex) { 175 | ex.printStackTrace(); 176 | } 177 | return false; 178 | } 179 | 180 | public RSFileStore getStore(int storeId) { 181 | if (storeId < 0 || storeId >= fileStores.length) { 182 | return null; 183 | } 184 | return fileStores[storeId]; 185 | } 186 | 187 | public ByteBuffer readFile(int storeId, int fileId) { 188 | RSFileStore store = getStore(storeId); 189 | 190 | if (store == null) { 191 | return null; 192 | } 193 | 194 | return store.readFile(fileId); 195 | } 196 | 197 | public RSArchive getArchive(int fileId) throws IOException { 198 | RSFileStore store = getStore(RSFileStore.ARCHIVE_FILE_STORE); 199 | 200 | if (store == null) { 201 | return null; 202 | } 203 | 204 | return RSArchive.decode(store.readFile(fileId)); 205 | } 206 | 207 | public RSArchive getArchive(int storeId, int fileId) throws IOException { 208 | RSFileStore store = getStore(storeId); 209 | 210 | if (store == null) { 211 | return null; 212 | } 213 | 214 | return RSArchive.decode(store.readFile(fileId)); 215 | } 216 | 217 | public Path getRoot() { 218 | return root; 219 | } 220 | 221 | public void setRoot(Path root) { 222 | if (isLoaded()) { 223 | reset(); 224 | } 225 | this.root = root; 226 | } 227 | 228 | public int getStoreCount() { 229 | int count = 0; 230 | for (int i = 0; i < 255; i++) { 231 | Path indexPath = root.resolve("main_file_cache.idx" + i); 232 | if (Files.exists(indexPath)) { 233 | count++; 234 | } 235 | } 236 | 237 | return count; 238 | } 239 | 240 | public boolean isLoaded() { 241 | return loaded; 242 | } 243 | 244 | public void reset() { 245 | try { 246 | close(); 247 | loaded = false; 248 | Arrays.fill(fileStores, null); 249 | } catch (Exception ex) { 250 | ex.printStackTrace(); 251 | } 252 | } 253 | 254 | @Override 255 | public void close() throws IOException { 256 | for (final RSFileStore fileStore : fileStores) { 257 | if (fileStore == null) { 258 | continue; 259 | } 260 | 261 | fileStore.close(); 262 | } 263 | } 264 | 265 | } 266 | -------------------------------------------------------------------------------- /fs/src/main/java/scape/editor/fs/graphics/RSTexture.java: -------------------------------------------------------------------------------- 1 | package scape.editor.fs.graphics; 2 | 3 | import scape.editor.fs.RSArchive; 4 | import scape.editor.util.ByteBufferUtils; 5 | import scape.editor.util.HashUtils; 6 | 7 | import java.awt.image.BufferedImage; 8 | import java.awt.image.DataBufferInt; 9 | import java.io.IOException; 10 | import java.nio.ByteBuffer; 11 | 12 | public final class RSTexture { 13 | 14 | public byte palettePixels[]; 15 | 16 | public final int[] palette; 17 | 18 | public int width; 19 | public int height; 20 | public int drawOffsetX; 21 | public int drawOffsetY; 22 | public int resizeWidth; 23 | private int resizeHeight; 24 | 25 | private RSTexture(int[] palette) { 26 | this.palette = palette; 27 | } 28 | 29 | public static RSTexture decode(RSArchive archive, int hash, int id) throws IOException { 30 | ByteBuffer dataBuffer = archive.readFile(hash); 31 | ByteBuffer metaBuffer = archive.readFile("index.dat"); 32 | 33 | metaBuffer.position(dataBuffer.getShort() & 0xffff); 34 | 35 | final int resizeWidth = metaBuffer.getShort() & 0xffff; 36 | final int resizeHeight = metaBuffer.getShort() & 0xffff; 37 | final int colorLength = metaBuffer.get() & 0xff; 38 | 39 | final int[] palette = new int[colorLength]; 40 | 41 | final RSTexture indexedImage = new RSTexture(palette); 42 | 43 | indexedImage.resizeWidth = resizeWidth; 44 | indexedImage.resizeHeight = resizeHeight; 45 | 46 | for (int index = 0; index < colorLength - 1; index++) { 47 | indexedImage.palette[index + 1] = ByteBufferUtils.readU24Int(metaBuffer); 48 | } 49 | 50 | for (int i = 0; i < id; i++) { 51 | metaBuffer.position(metaBuffer.position() + 2); 52 | dataBuffer.position(dataBuffer.position() + metaBuffer.getShort() & 0xffff * metaBuffer.getShort() & 0xffff); 53 | metaBuffer.position(metaBuffer.position() + 1); 54 | } 55 | 56 | indexedImage.drawOffsetX = metaBuffer.get() & 0xff; 57 | indexedImage.drawOffsetY = metaBuffer.get() & 0xff; 58 | indexedImage.width = metaBuffer.getShort() & 0xffff; 59 | indexedImage.height = metaBuffer.getShort() & 0xffff; 60 | int type = metaBuffer.get() & 0xff; 61 | int pixels = indexedImage.width * indexedImage.height; 62 | indexedImage.palettePixels = new byte[pixels]; 63 | 64 | if (type == 0) { 65 | for (int index = 0; index < pixels; index++) { 66 | indexedImage.palettePixels[index] = dataBuffer.get(); 67 | } 68 | } else if (type == 1) { 69 | for (int x = 0; x < indexedImage.width; x++) { 70 | for (int y = 0; y < indexedImage.height; y++) { 71 | indexedImage.palettePixels[x + y * indexedImage.width] = dataBuffer.get(); 72 | } 73 | } 74 | } 75 | 76 | return indexedImage; 77 | } 78 | 79 | public static RSTexture decode(RSArchive archive, String name, int id) throws IOException { 80 | return decode(archive, HashUtils.hashName(name.contains(".dat") ? name : name + ".dat"), id); 81 | } 82 | 83 | public void downscale() { 84 | resizeWidth /= 2; 85 | resizeHeight /= 2; 86 | byte raster[] = new byte[resizeWidth * resizeHeight]; 87 | int sourceIndex = 0; 88 | for (int y = 0; y < height; y++) { 89 | for (int x = 0; x < width; x++) { 90 | raster[(x + drawOffsetX >> 1) + (y + drawOffsetY >> 1) * resizeWidth] = raster[sourceIndex++]; 91 | } 92 | } 93 | this.palettePixels = raster; 94 | width = resizeWidth; 95 | height = resizeHeight; 96 | drawOffsetX = 0; 97 | drawOffsetY = 0; 98 | } 99 | 100 | public void resize() { 101 | if (width == resizeWidth && height == resizeHeight) { 102 | return; 103 | } 104 | 105 | byte raster[] = new byte[resizeWidth * resizeHeight]; 106 | 107 | int i = 0; 108 | for (int y = 0; y < height; y++) { 109 | for (int x = 0; x < width; x++) { 110 | raster[x + drawOffsetX + (y + drawOffsetY) * resizeWidth] = raster[i++]; 111 | } 112 | } 113 | this.palettePixels = raster; 114 | width = resizeWidth; 115 | height = resizeHeight; 116 | drawOffsetX = 0; 117 | drawOffsetY = 0; 118 | } 119 | 120 | public void flipHorizontally() { 121 | byte raster[] = new byte[width * height]; 122 | int pixel = 0; 123 | for (int y = 0; y < height; y++) { 124 | for (int x = width - 1; x >= 0; x--) { 125 | raster[pixel++] = raster[x + y * width]; 126 | } 127 | } 128 | this.palettePixels = raster; 129 | drawOffsetX = resizeWidth - width - drawOffsetX; 130 | } 131 | 132 | public void flipVertically() { 133 | byte raster[] = new byte[width * height]; 134 | int pixel = 0; 135 | for (int y = height - 1; y >= 0; y--) { 136 | for (int x = 0; x < width; x++) { 137 | raster[pixel++] = raster[x + y * width]; 138 | } 139 | } 140 | this.palettePixels = raster; 141 | drawOffsetY = resizeHeight - height - drawOffsetY; 142 | } 143 | 144 | public void offsetColor(int redOffset, int greenOffset, int blueOffset) { 145 | for (int index = 0; index < palette.length; index++) { 146 | int red = palette[index] >> 16 & 0xff; 147 | red += redOffset; 148 | 149 | if (red < 0) { 150 | red = 0; 151 | } else if (red > 255) { 152 | red = 255; 153 | } 154 | 155 | int green = palette[index] >> 8 & 0xff; 156 | 157 | green += greenOffset; 158 | if (green < 0) { 159 | green = 0; 160 | } else if (green > 255) { 161 | green = 255; 162 | } 163 | 164 | int blue = palette[index] & 0xff; 165 | 166 | blue += blueOffset; 167 | if (blue < 0) { 168 | blue = 0; 169 | } else if (blue > 255) { 170 | blue = 255; 171 | } 172 | palette[index] = (red << 16) + (green << 8) + blue; 173 | } 174 | } 175 | 176 | private void draw(int i, int raster[], byte image[], int destStep, int destIndex, int width, int sourceIndex, int ai1[], int sourceStep) { 177 | int minX = -(width >> 2); 178 | width = -(width & 3); 179 | for (int y = -i; y < 0; y++) { 180 | for (int x = minX; x < 0; x++) { 181 | 182 | byte pixel = image[sourceIndex++]; 183 | 184 | if (pixel != 0) { 185 | raster[destIndex++] = ai1[pixel & 0xff]; 186 | } else { 187 | destIndex++; 188 | } 189 | pixel = image[sourceIndex++]; 190 | if (pixel != 0) { 191 | raster[destIndex++] = ai1[pixel & 0xff]; 192 | } else { 193 | destIndex++; 194 | } 195 | pixel = image[sourceIndex++]; 196 | if (pixel != 0) { 197 | raster[destIndex++] = ai1[pixel & 0xff]; 198 | } else { 199 | destIndex++; 200 | } 201 | pixel = image[sourceIndex++]; 202 | if (pixel != 0) { 203 | raster[destIndex++] = ai1[pixel & 0xff]; 204 | } else { 205 | destIndex++; 206 | } 207 | } 208 | for (int x = width; x < 0; x++) { 209 | byte pixel = image[sourceIndex++]; 210 | if (pixel != 0) { 211 | raster[destIndex++] = ai1[pixel & 0xff]; 212 | } else { 213 | destIndex++; 214 | } 215 | } 216 | destIndex += destStep; 217 | sourceIndex += sourceStep; 218 | } 219 | } 220 | 221 | public BufferedImage toBufferedImage() { 222 | 223 | BufferedImage image = new BufferedImage(this.width, this.height, BufferedImage.TYPE_BYTE_INDEXED); 224 | 225 | final int[] pixels = ((DataBufferInt) image.getRaster().getDataBuffer()).getData(); 226 | 227 | System.arraycopy(this.palette, 0, pixels, 0, this.palette.length); 228 | 229 | return image; 230 | } 231 | 232 | } 233 | -------------------------------------------------------------------------------- /fs/src/main/java/scape/editor/fs/graphics/draw/RSRaster.java: -------------------------------------------------------------------------------- 1 | package scape.editor.fs.graphics.draw; 2 | 3 | import java.awt.image.BufferedImage; 4 | import java.awt.image.DataBufferInt; 5 | 6 | public class RSRaster { 7 | 8 | public static int maxRight; 9 | public static int height; 10 | public static int[] raster; 11 | public static int width; 12 | private static int centreX; 13 | private static int centreY; 14 | private static int clipBottom; 15 | private static int clipLeft; 16 | private static int clipRight; 17 | private static int clipTop; 18 | 19 | public static void drawHorizontal(int x, int y, int length, int colour) { 20 | if (y < clipBottom || y >= clipTop) { 21 | return; 22 | } 23 | 24 | if (x < clipLeft) { 25 | length -= clipLeft - x; 26 | x = clipLeft; 27 | } 28 | 29 | if (x + length > clipRight) { 30 | length = clipRight - x; 31 | } 32 | 33 | int offset = x + y * width; 34 | for (int index = 0; index < length; index++) { 35 | raster[offset + index] = colour; 36 | } 37 | } 38 | 39 | public static void drawHorizontal(int x, int y, int length, int colour, int alpha) { 40 | if (y < clipBottom || y >= clipTop) { 41 | return; 42 | } 43 | 44 | if (x < clipLeft) { 45 | length -= clipLeft - x; 46 | x = clipLeft; 47 | } 48 | 49 | if (x + length > clipRight) { 50 | length = clipRight - x; 51 | } 52 | 53 | int invertedAlpha = 256 - alpha; 54 | int r = (colour >> 16 & 0xff) * alpha; 55 | int g = (colour >> 8 & 0xff) * alpha; 56 | int b = (colour & 0xff) * alpha; 57 | int index = x + y * width; 58 | 59 | for (int i = 0; i < length; i++) { 60 | int r2 = (raster[index] >> 16 & 0xff) * invertedAlpha; 61 | int g2 = (raster[index] >> 8 & 0xff) * invertedAlpha; 62 | int b2 = (raster[index] & 0xff) * invertedAlpha; 63 | raster[index++] = (r + r2 >> 8 << 16) + (g + g2 >> 8 << 8) + (b + b2 >> 8); 64 | } 65 | } 66 | 67 | public static void drawRectangle(int x, int y, int width, int height, int colour) { 68 | drawHorizontal(x, y, width, colour); 69 | drawHorizontal(x, y + height - 1, width, colour); 70 | drawVertical(x, y, height, colour); 71 | drawVertical(x + width - 1, y, height, colour); 72 | } 73 | 74 | public static void drawRectangle(int x, int y, int width, int height, int colour, int alpha) { 75 | drawHorizontal(x, y, width, colour, alpha); 76 | drawHorizontal(x, y + height - 1, width, colour, alpha); 77 | if (height >= 3) { 78 | drawVertical(x, y + 1, height - 2, colour, alpha); 79 | drawVertical(x + width - 1, y + 1, height - 2, colour, alpha); 80 | } 81 | } 82 | 83 | public static void drawVertical(int x, int y, int length, int colour) { 84 | if (x < clipLeft || x >= clipRight) { 85 | return; 86 | } 87 | 88 | if (y < clipBottom) { 89 | length -= clipBottom - y; 90 | y = clipBottom; 91 | } 92 | 93 | if (y + length > clipTop) { 94 | length = clipTop - y; 95 | } 96 | 97 | int offset = x + y * width; 98 | for (int index = 0; index < length; index++) { 99 | raster[offset + index * width] = colour; 100 | } 101 | } 102 | 103 | public static void drawVertical(int x, int y, int length, int colour, int alpha) { 104 | if (x < clipLeft || x >= clipRight) { 105 | return; 106 | } 107 | 108 | if (y < clipBottom) { 109 | length -= clipBottom - y; 110 | y = clipBottom; 111 | } 112 | 113 | if (y + length > clipTop) { 114 | length = clipTop - y; 115 | } 116 | 117 | int invertedAlpha = 256 - alpha; 118 | int r = (colour >> 16 & 0xff) * alpha; 119 | int g = (colour >> 8 & 0xff) * alpha; 120 | int b = (colour & 0xff) * alpha; 121 | int index = x + y * width; 122 | 123 | for (int i = 0; i < length; i++) { 124 | int r2 = (raster[index] >> 16 & 0xff) * invertedAlpha; 125 | int g2 = (raster[index] >> 8 & 0xff) * invertedAlpha; 126 | int b2 = (raster[index] & 0xff) * invertedAlpha; 127 | raster[index] = (r + r2 >> 8 << 16) + (g + g2 >> 8 << 8) + (b + b2 >> 8); 128 | index += width; 129 | } 130 | } 131 | 132 | public static void fillRectangle(int x, int y, int width, int height, int colour) { 133 | if (x < clipLeft) { 134 | width -= clipLeft - x; 135 | x = clipLeft; 136 | } 137 | 138 | if (y < clipBottom) { 139 | height -= clipBottom - y; 140 | y = clipBottom; 141 | } 142 | 143 | if (x + width > clipRight) { 144 | width = clipRight - x; 145 | } 146 | 147 | if (y + height > clipTop) { 148 | height = clipTop - y; 149 | } 150 | 151 | int dx = RSRaster.width - width; 152 | int pixel = x + y * RSRaster.width; 153 | 154 | for (int i2 = -height; i2 < 0; i2++) { 155 | for (int j2 = -width; j2 < 0; j2++) { 156 | raster[pixel++] = colour; 157 | } 158 | 159 | pixel += dx; 160 | } 161 | } 162 | 163 | public static void fillRectangle(int drawX, int drawY, int width, int height, int colour, int alpha) { 164 | if (drawX < clipLeft) { 165 | width -= clipLeft - drawX; 166 | drawX = clipLeft; 167 | } 168 | 169 | if (drawY < clipBottom) { 170 | height -= clipBottom - drawY; 171 | drawY = clipBottom; 172 | } 173 | 174 | if (drawX + width > clipRight) { 175 | width = clipRight - drawX; 176 | } 177 | 178 | if (drawY + height > clipTop) { 179 | height = clipTop - drawY; 180 | } 181 | 182 | int inverseAlpha = 256 - alpha; 183 | int r = (colour >> 16 & 0xff) * alpha; 184 | int g = (colour >> 8 & 0xff) * alpha; 185 | int b = (colour & 0xff) * alpha; 186 | int dx = RSRaster.width - width; 187 | int pixel = drawX + drawY * RSRaster.width; 188 | 189 | for (int x = 0; x < height; x++) { 190 | for (int y = -width; y < 0; y++) { 191 | int r2 = (raster[pixel] >> 16 & 0xff) * inverseAlpha; 192 | int g2 = (raster[pixel] >> 8 & 0xff) * inverseAlpha; 193 | int b2 = (raster[pixel] & 0xff) * inverseAlpha; 194 | raster[pixel++] = (r + r2 >> 8 << 16) + (g + g2 >> 8 << 8) + (b + b2 >> 8); 195 | } 196 | 197 | pixel += dx; 198 | } 199 | } 200 | 201 | public static int getCentreX() { 202 | return centreX; 203 | } 204 | 205 | public static int getCentreY() { 206 | return centreY; 207 | } 208 | 209 | public static int getClipBottom() { 210 | return clipBottom; 211 | } 212 | 213 | public static int getClipLeft() { 214 | return clipLeft; 215 | } 216 | 217 | public static int getClipRight() { 218 | return clipRight; 219 | } 220 | 221 | public static int getClipTop() { 222 | return clipTop; 223 | } 224 | 225 | public static void init(int height, int width, int[] pixels) { 226 | RSRaster.raster = pixels; 227 | RSRaster.width = width; 228 | RSRaster.height = height; 229 | setBounds(height, 0, width, 0); 230 | } 231 | 232 | public static void reset() { 233 | int count = width * height; 234 | for (int index = 0; index < count; index++) { 235 | raster[index] = 0; 236 | } 237 | } 238 | 239 | public static void setBounds(int clipTop, int clipLeft, int clipRight, int clipBottom) { 240 | if (clipLeft < 0) { 241 | clipLeft = 0; 242 | } 243 | 244 | if (clipBottom < 0) { 245 | clipBottom = 0; 246 | } 247 | 248 | if (clipRight > RSRaster.width) { 249 | clipRight = RSRaster.width; 250 | } 251 | 252 | if (clipTop > RSRaster.height) { 253 | clipTop = RSRaster.height; 254 | } 255 | 256 | RSRaster.clipLeft = clipLeft; 257 | RSRaster.clipBottom = clipBottom; 258 | RSRaster.clipRight = clipRight; 259 | RSRaster.clipTop = clipTop; 260 | 261 | maxRight = RSRaster.clipRight - 1; 262 | centreX = RSRaster.clipRight / 2; 263 | centreY = RSRaster.clipTop / 2; 264 | } 265 | 266 | public static void setCentreX(int centreX) { 267 | RSRaster.centreX = centreX; 268 | } 269 | 270 | public static void setCentreY(int centreY) { 271 | RSRaster.centreY = centreY; 272 | } 273 | 274 | public static void setClipBottom(int clipBottom) { 275 | RSRaster.clipBottom = clipBottom; 276 | } 277 | 278 | public static void setClipLeft(int clipLeft) { 279 | RSRaster.clipLeft = clipLeft; 280 | } 281 | 282 | public static void setClipRight(int clipRight) { 283 | RSRaster.clipRight = clipRight; 284 | } 285 | 286 | public static void setClipTop(int clipTop) { 287 | RSRaster.clipTop = clipTop; 288 | } 289 | 290 | public static void setDefaultBounds() { 291 | clipLeft = 0; 292 | clipBottom = 0; 293 | clipRight = width; 294 | clipTop = height; 295 | maxRight = clipRight - 1; 296 | centreX = clipRight / 2; 297 | } 298 | 299 | public static BufferedImage toBufferedImage() { 300 | BufferedImage bimage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); 301 | int[] data = ((DataBufferInt)(bimage.getData().getDataBuffer())).getData(); 302 | System.arraycopy(raster, 0, data, 0, raster.length); 303 | return bimage; 304 | } 305 | 306 | public static BufferedImage toBufferedImage(int width, int height, int[] raster) { 307 | BufferedImage bimage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); 308 | int[] data = ((DataBufferInt)(bimage.getData().getDataBuffer())).getData(); 309 | System.arraycopy(raster, 0, data, 0, raster.length); 310 | return bimage; 311 | } 312 | 313 | } -------------------------------------------------------------------------------- /fs/src/main/java/scape/editor/fs/graphics/RSSprite.java: -------------------------------------------------------------------------------- 1 | package scape.editor.fs.graphics; 2 | 3 | import scape.editor.fs.RSArchive; 4 | import scape.editor.fs.graphics.draw.RSRaster; 5 | import scape.editor.util.ByteBufferUtils; 6 | import scape.editor.util.HashUtils; 7 | 8 | import java.awt.image.BufferedImage; 9 | import java.awt.image.DataBufferInt; 10 | import java.io.IOException; 11 | import java.nio.ByteBuffer; 12 | 13 | public final class RSSprite { 14 | 15 | private int width; 16 | private int height; 17 | private int offsetX; 18 | private int offsetY; 19 | private int resizeWidth; 20 | private int resizeHeight; 21 | private int[] pixels; 22 | private int format; 23 | 24 | public RSSprite() { 25 | 26 | } 27 | 28 | public RSSprite(BufferedImage bimage) { 29 | this.pixels = ((DataBufferInt)bimage.getRaster().getDataBuffer()).getData(); 30 | this.width = bimage.getWidth(); 31 | this.height = bimage.getHeight(); 32 | this.resizeWidth = bimage.getWidth(); 33 | this.resizeHeight = bimage.getHeight(); 34 | } 35 | 36 | public RSSprite(int width, int height) { 37 | this.pixels = new int[width * height]; 38 | this.width = this.resizeWidth = width; 39 | this.height = this.resizeHeight = height; 40 | } 41 | 42 | public RSSprite(int resizeWidth, int resizeHeight, int horizontalOffset, int verticalOffset, int width, int height, int format, 43 | int[] pixels) { 44 | this.resizeWidth = resizeWidth; 45 | this.resizeHeight = resizeHeight; 46 | this.offsetX = horizontalOffset; 47 | this.offsetY = verticalOffset; 48 | this.width = width; 49 | this.height = height; 50 | this.format = format; 51 | this.pixels = pixels; 52 | } 53 | 54 | public static RSSprite decode(RSArchive archive, int hash, int id) throws IOException { 55 | ByteBuffer dataBuf = archive.readFile(hash); 56 | ByteBuffer metaBuf = archive.readFile("index.dat"); 57 | 58 | RSSprite sprite = new RSSprite(); 59 | 60 | // position of the current image archive within the archive 61 | metaBuf.position(dataBuf.getShort() & 0xFFFF); 62 | 63 | // the maximum width the images in this archive can scale to 64 | sprite.setResizeWidth(metaBuf.getShort() & 0xFFFF); 65 | 66 | // the maximum height the images in this archive can scale to 67 | sprite.setResizeHeight(metaBuf.getShort() & 0xFFFF); 68 | 69 | // the number of colors that are used in this image archive (limit is 256 if one of the rgb values is 0 else its 255) 70 | int colours = metaBuf.get() & 0xFF; 71 | 72 | // the array of colors that can only be used in this archive 73 | int[] palette = new int[colours]; 74 | 75 | for (int index = 0; index < colours - 1; index++) { 76 | int colour = ByteBufferUtils.readU24Int(metaBuf); 77 | // + 1 because index = 0 is for transparency, = 1 is a flag for opacity. (BufferedImage#OPAQUE) 78 | palette[index + 1] = colour == 0 ? 1 : colour; 79 | } 80 | 81 | for (int i = 0; i < id; i++) { 82 | 83 | if (metaBuf.position() + 7 >= metaBuf.capacity()) { 84 | return null; 85 | } 86 | 87 | // skip the current offsetX and offsetY 88 | metaBuf.position(metaBuf.position() + 2); 89 | 90 | // skip the current array of pixels 91 | dataBuf.position(dataBuf.position() + ((metaBuf.getShort() & 0xFFFF) * (metaBuf.getShort() & 0xFFFF))); 92 | 93 | // skip the current format 94 | metaBuf.position(metaBuf.position() + 1); 95 | } 96 | 97 | // offsets are used to reposition the sprite on an interface. 98 | sprite.setOffsetX(metaBuf.get() & 0xFF); 99 | sprite.setOffsetY(metaBuf.get() & 0xFF); 100 | 101 | // actual width of this sprite 102 | sprite.setWidth(metaBuf.getShort() & 0xFFFF); 103 | 104 | // actual height of this sprite 105 | sprite.setHeight(metaBuf.getShort() & 0xFFFF); 106 | 107 | // there are 2 ways the pixels can be written (0 or 1, 0 means the position is read horizontally, 1 means vertically) 108 | sprite.setFormat(metaBuf.get() & 0xFF); 109 | 110 | if (sprite.getFormat() != 0 && sprite.getFormat() != 1) { 111 | throw new IOException(String.format("Detected end of archive=%d id=%d or wrong format=%d", hash, id, sprite.getFormat())); 112 | } 113 | 114 | if (sprite.getWidth() > 765 || sprite.getHeight() > 765 || sprite.getWidth() <= 0 || sprite.getHeight() <= 0) { 115 | throw new IOException(String.format("Detected end of archive=%d id=%d", hash, id)); 116 | } 117 | 118 | int[] raster = new int[sprite.getWidth() * sprite.getHeight()]; 119 | 120 | if (sprite.getFormat() == 0) { // read horizontally 121 | for (int index = 0; index < raster.length; index++) { 122 | raster[index] = palette[dataBuf.get() & 0xFF]; 123 | } 124 | } else if (sprite.getFormat() == 1) { // read vertically 125 | for (int x = 0; x < sprite.getWidth(); x++) { 126 | for (int y = 0; y < sprite.getHeight(); y++) { 127 | raster[x + y * sprite.getWidth()] = palette[dataBuf.get() & 0xFF]; 128 | } 129 | } 130 | } 131 | sprite.setPixels(raster); 132 | return sprite; 133 | } 134 | 135 | public static RSSprite decode(RSArchive archive, String name, int id) throws IOException { 136 | return decode(archive, HashUtils.hashName(name.contains(".dat") ? name : name + ".dat"), id); 137 | } 138 | 139 | public void drawSprite(int x, int y) { 140 | x += offsetX; 141 | y += offsetY; 142 | int rasterClip = x + y * RSRaster.width; 143 | int imageClip = 0; 144 | int height = this.height; 145 | int width = this.width; 146 | int rasterOffset = RSRaster.width - width; 147 | int imageOffset = 0; 148 | 149 | if (y < RSRaster.getClipBottom()) { 150 | int dy = RSRaster.getClipBottom() - y; 151 | height -= dy; 152 | y = RSRaster.getClipBottom(); 153 | imageClip += dy * width; 154 | rasterClip += dy * RSRaster.width; 155 | } 156 | 157 | if (y + height > RSRaster.getClipTop()) { 158 | height -= y + height - RSRaster.getClipTop(); 159 | } 160 | 161 | if (x < RSRaster.getClipLeft()) { 162 | int dx = RSRaster.getClipLeft() - x; 163 | width -= dx; 164 | x = RSRaster.getClipLeft(); 165 | imageClip += dx; 166 | rasterClip += dx; 167 | imageOffset += dx; 168 | rasterOffset += dx; 169 | } 170 | 171 | if (x + width > RSRaster.getClipRight()) { 172 | int dx = x + width - RSRaster.getClipRight(); 173 | width -= dx; 174 | imageOffset += dx; 175 | rasterOffset += dx; 176 | } 177 | 178 | if (width > 0 && height > 0) { 179 | draw(RSRaster.raster, pixels, 0, imageClip, rasterClip, width, height, rasterOffset, imageOffset); 180 | } 181 | } 182 | 183 | private void draw(int raster[], int[] image, int colour, int sourceIndex, int destIndex, int width, int height, int destStep, 184 | int sourceStep) { 185 | int minX = -(width >> 2); 186 | width = -(width & 3); 187 | 188 | for (int y = -height; y < 0; y++) { 189 | for (int x = minX; x < 0; x++) { 190 | colour = image[sourceIndex++]; 191 | if (colour != 0) { 192 | raster[destIndex++] = colour; 193 | } else { 194 | destIndex++; 195 | } 196 | colour = image[sourceIndex++]; 197 | 198 | if (colour != 0) { 199 | raster[destIndex++] = colour; 200 | } else { 201 | destIndex++; 202 | } 203 | colour = image[sourceIndex++]; 204 | 205 | if (colour != 0) { 206 | raster[destIndex++] = colour; 207 | } else { 208 | destIndex++; 209 | } 210 | colour = image[sourceIndex++]; 211 | 212 | if (colour != 0) { 213 | raster[destIndex++] = colour; 214 | } else { 215 | destIndex++; 216 | } 217 | } 218 | 219 | for (int k2 = width; k2 < 0; k2++) { 220 | colour = image[sourceIndex++]; 221 | if (colour != 0) { 222 | raster[destIndex++] = colour; 223 | } else { 224 | destIndex++; 225 | } 226 | } 227 | 228 | destIndex += destStep; 229 | sourceIndex += sourceStep; 230 | } 231 | } 232 | 233 | public BufferedImage toBufferedImage() { 234 | 235 | BufferedImage image = new BufferedImage(this.width, this.height, BufferedImage.TYPE_INT_RGB); 236 | 237 | final int[] pixels = ((DataBufferInt) image.getRaster().getDataBuffer()).getData(); 238 | 239 | System.arraycopy(this.pixels, 0, pixels, 0, this.pixels.length); 240 | 241 | return image; 242 | } 243 | 244 | public int getWidth() { 245 | return width; 246 | } 247 | 248 | public void setWidth(int width) { 249 | this.width = width; 250 | } 251 | 252 | public int getHeight() { 253 | return height; 254 | } 255 | 256 | public void setHeight(int height) { 257 | this.height = height; 258 | } 259 | 260 | public int getOffsetX() { 261 | return offsetX; 262 | } 263 | 264 | public void setOffsetX(int offsetX) { 265 | this.offsetX = offsetX; 266 | } 267 | 268 | public int getOffsetY() { 269 | return offsetY; 270 | } 271 | 272 | public void setOffsetY(int offsetY) { 273 | this.offsetY = offsetY; 274 | } 275 | 276 | public int getResizeWidth() { 277 | return resizeWidth; 278 | } 279 | 280 | public void setResizeWidth(int resizeWidth) { 281 | this.resizeWidth = resizeWidth; 282 | } 283 | 284 | public int getResizeHeight() { 285 | return resizeHeight; 286 | } 287 | 288 | public void setResizeHeight(int resizeHeight) { 289 | this.resizeHeight = resizeHeight; 290 | } 291 | 292 | public int[] getPixels() { 293 | return pixels; 294 | } 295 | 296 | public void setPixels(int[] pixels) { 297 | this.pixels = pixels; 298 | } 299 | 300 | public int getFormat() { 301 | return format; 302 | } 303 | 304 | public void setFormat(int format) { 305 | this.format = format; 306 | } 307 | 308 | } 309 | -------------------------------------------------------------------------------- /fs/src/main/java/scape/editor/fs/RSArchive.java: -------------------------------------------------------------------------------- 1 | package scape.editor.fs; 2 | 3 | import scape.editor.util.ByteBufferUtils; 4 | import scape.editor.util.CompressionUtils; 5 | import scape.editor.util.HashUtils; 6 | 7 | import java.io.FileNotFoundException; 8 | import java.io.IOException; 9 | import java.nio.ByteBuffer; 10 | import java.util.Arrays; 11 | import java.util.LinkedHashMap; 12 | import java.util.Map; 13 | 14 | public final class RSArchive { 15 | 16 | public static final int TITLE_ARCHIVE = 1; 17 | public static final int CONFIG_ARCHIVE = 2; 18 | public static final int INTERFACE_ARCHIVE = 3; 19 | public static final int MEDIA_ARCHIVE = 4; 20 | public static final int VERSION_LIST_ARCHIVE = 5; 21 | public static final int TEXTURE_ARCHIVE = 6; 22 | public static final int WORDENC_ARCHIVE = 7; 23 | public static final int SOUND_ARCHIVE = 8; 24 | 25 | public static final class ArchiveEntry { 26 | 27 | private final int hash; 28 | private final int uncompressedSize; 29 | private final int compressedSize; 30 | private final byte[] data; 31 | 32 | public ArchiveEntry(int hash, int uncompressedSize, int compressedSize, byte[] data) { 33 | this.hash = hash; 34 | this.uncompressedSize = uncompressedSize; 35 | this.compressedSize = compressedSize; 36 | this.data = data; 37 | } 38 | 39 | public int getHash() { 40 | return hash; 41 | } 42 | 43 | public int getUncompressedSize() { 44 | return uncompressedSize; 45 | } 46 | 47 | public int getCompresseedSize() { 48 | return compressedSize; 49 | } 50 | 51 | public byte[] getData() { 52 | return data; 53 | } 54 | 55 | } 56 | 57 | private boolean extracted; 58 | 59 | private final Map entries = new LinkedHashMap<>(); 60 | 61 | public RSArchive() { 62 | 63 | } 64 | 65 | public RSArchive(ArchiveEntry[] entries) { 66 | Arrays.asList(entries).forEach(it -> this.entries.put(it.getHash(), it)); 67 | } 68 | 69 | public static RSArchive decode(ByteBuffer buffer) throws IOException { 70 | final int uncompressedLength = ByteBufferUtils.readU24Int(buffer); 71 | final int compressedLength = ByteBufferUtils.readU24Int(buffer); 72 | 73 | boolean extracted = false; 74 | 75 | if (uncompressedLength != compressedLength) { 76 | final byte[] compressed = new byte[compressedLength]; 77 | final byte[] decompressed = new byte[uncompressedLength]; 78 | buffer.get(compressed); 79 | CompressionUtils.debzip2(compressed, decompressed); 80 | buffer = ByteBuffer.wrap(decompressed); 81 | extracted = true; 82 | } 83 | 84 | final int entries = buffer.getShort() & 0xFFFF; 85 | 86 | final int[] hashes = new int[entries]; 87 | final int[] uncompressedSizes = new int[entries]; 88 | final int[] compressedSizes = new int[entries]; 89 | 90 | final ArchiveEntry[] archiveEntries = new ArchiveEntry[entries]; 91 | 92 | final ByteBuffer entryBuf = ByteBuffer.wrap(buffer.array()); 93 | entryBuf.position(buffer.position() + entries * 10); 94 | 95 | for (int i = 0; i < entries; i++) { 96 | 97 | hashes[i] = buffer.getInt(); 98 | uncompressedSizes[i] = ByteBufferUtils.readU24Int(buffer); 99 | compressedSizes[i] = ByteBufferUtils.readU24Int(buffer); 100 | 101 | final byte[] entryData = new byte[compressedSizes[i]]; 102 | entryBuf.get(entryData); 103 | 104 | archiveEntries[i] = new ArchiveEntry(hashes[i], uncompressedSizes[i], compressedSizes[i], entryData); 105 | } 106 | 107 | final RSArchive archive = new RSArchive(archiveEntries); 108 | archive.extracted = extracted; 109 | 110 | return archive; 111 | } 112 | 113 | public synchronized byte[] encode() throws IOException { 114 | int size = 2 + entries.size() * 10; 115 | 116 | for (ArchiveEntry file : entries.values()) { 117 | size += file.getCompresseedSize(); 118 | } 119 | 120 | ByteBuffer buffer; 121 | if (!extracted) { 122 | buffer = ByteBuffer.allocate(size + 6); 123 | ByteBufferUtils.write24Int(buffer, size); 124 | ByteBufferUtils.write24Int(buffer, size); 125 | } else { 126 | buffer = ByteBuffer.allocate(size); 127 | } 128 | 129 | buffer.putShort((short) entries.size()); 130 | 131 | for (ArchiveEntry entry : entries.values()) { 132 | buffer.putInt(entry.getHash()); 133 | ByteBufferUtils.write24Int(buffer, entry.getUncompressedSize()); 134 | ByteBufferUtils.write24Int(buffer, entry.getCompresseedSize()); 135 | } 136 | 137 | for (ArchiveEntry file : entries.values()) { 138 | buffer.put(file.getData()); 139 | } 140 | 141 | byte[] data; 142 | if (!extracted) { 143 | data = buffer.array(); 144 | } else { 145 | byte[] unzipped = buffer.array(); 146 | byte[] zipped = CompressionUtils.bzip2(unzipped); 147 | if (unzipped.length == zipped.length) { 148 | throw new RuntimeException("error zipped size matches original"); 149 | } 150 | buffer = ByteBuffer.allocate(zipped.length + 6); 151 | ByteBufferUtils.write24Int(buffer, unzipped.length); 152 | ByteBufferUtils.write24Int(buffer, zipped.length); 153 | buffer.put(zipped, 0, zipped.length); 154 | data = buffer.array(); 155 | } 156 | 157 | return data; 158 | } 159 | 160 | public ByteBuffer readFile(String name) throws IOException { 161 | return readFile(HashUtils.hashName(name)); 162 | } 163 | 164 | public ByteBuffer readFile(int hash) throws IOException { 165 | final ArchiveEntry entry = entries.get(hash); 166 | 167 | if (entry == null) { 168 | throw new FileNotFoundException(String.format("file=%d could not be found.", hash)); 169 | } 170 | 171 | if (!extracted) { 172 | byte[] decompressed = new byte[entry.getUncompressedSize()]; 173 | CompressionUtils.debzip2(entry.getData(), decompressed); 174 | return ByteBuffer.wrap(decompressed); 175 | } else { 176 | return ByteBuffer.wrap(entry.getData()); 177 | } 178 | } 179 | 180 | public boolean replaceFile(int oldHash, String newName, byte[] data) throws IOException { 181 | return replaceFile(oldHash, HashUtils.hashName(newName), data); 182 | } 183 | 184 | public boolean replaceFile(int oldHash, int newHash, byte[] data) throws IOException { 185 | if (!entries.containsKey(oldHash)) { 186 | return false; 187 | } 188 | 189 | ArchiveEntry entry; 190 | if (!extracted) { 191 | byte[] compressed = CompressionUtils.bzip2(data); 192 | entry = new RSArchive.ArchiveEntry(newHash, data.length, compressed.length, compressed); 193 | } else { 194 | entry = new RSArchive.ArchiveEntry(newHash, data.length, data.length, data); 195 | } 196 | 197 | entries.replace(oldHash, entry); 198 | return true; 199 | } 200 | 201 | public boolean writeFile(String name, byte[] data) throws IOException { 202 | return writeFile(HashUtils.hashName(name), data); 203 | } 204 | 205 | public boolean writeFile(int hash, byte[] data) throws IOException { 206 | if (entries.containsKey(hash)) { 207 | replaceFile(hash, hash, data); 208 | } 209 | 210 | ArchiveEntry entry; 211 | if (!extracted) { 212 | byte[] compressed = CompressionUtils.bzip2(data); 213 | entry = new RSArchive.ArchiveEntry(hash, data.length, compressed.length, compressed); 214 | } else { 215 | entry = new RSArchive.ArchiveEntry(hash, data.length, data.length, data); 216 | } 217 | 218 | entries.put(hash, entry); 219 | return true; 220 | } 221 | 222 | public boolean rename(int oldHash, String newName) { 223 | return rename(oldHash, HashUtils.hashName(newName)); 224 | } 225 | 226 | public boolean rename(int oldHash, int newHash) { 227 | final ArchiveEntry old = entries.get(oldHash); 228 | 229 | if (old == null) { 230 | return false; 231 | } 232 | 233 | entries.replace(oldHash, new ArchiveEntry(newHash, old.getUncompressedSize(), old.getCompresseedSize(), old.getData())); 234 | return true; 235 | } 236 | 237 | public ArchiveEntry getEntry(String name) throws FileNotFoundException { 238 | return getEntry(HashUtils.hashName(name)); 239 | } 240 | 241 | public ArchiveEntry getEntry(int hash) throws FileNotFoundException { 242 | if (entries.containsKey(hash)) { 243 | return entries.get(hash); 244 | } 245 | 246 | throw new FileNotFoundException(String.format("Could not find entry: %d.", hash)); 247 | } 248 | 249 | public ArchiveEntry getEntryAt(int index) throws IOException { 250 | if (index >= entries.size()) { 251 | throw new FileNotFoundException(String.format("File at index=%d could not be found.", index)); 252 | } 253 | 254 | int pos = 0; 255 | for (ArchiveEntry entry : entries.values()) { 256 | if (pos == index) { 257 | return entry; 258 | } 259 | pos++; 260 | } 261 | 262 | throw new FileNotFoundException(String.format("File at index=%d could not be found.", index)); 263 | } 264 | 265 | public int indexOf(String name) { 266 | return indexOf(HashUtils.hashName(name)); 267 | } 268 | 269 | public int indexOf(int hash) { 270 | int index = 0; 271 | for (ArchiveEntry entry : entries.values()) { 272 | if (entry.getHash() == hash) { 273 | return index; 274 | } 275 | index++; 276 | } 277 | 278 | return -1; 279 | } 280 | 281 | public boolean contains(String name) { 282 | return contains(HashUtils.hashName(name)); 283 | } 284 | 285 | public boolean contains(int hash) { 286 | return entries.containsKey(hash); 287 | } 288 | 289 | public boolean remove(String name) { 290 | return remove(HashUtils.hashName(name)); 291 | } 292 | 293 | public boolean remove(int hash) { 294 | if (entries.containsKey(hash)) { 295 | entries.remove(hash); 296 | return true; 297 | } 298 | return false; 299 | } 300 | 301 | public int getEntryCount() { 302 | return entries.size(); 303 | } 304 | 305 | public ArchiveEntry[] getEntries() { 306 | return entries.values().toArray(new ArchiveEntry[0]); 307 | } 308 | 309 | public boolean isExtracted() { 310 | return extracted; 311 | } 312 | 313 | } 314 | -------------------------------------------------------------------------------- /fs/src/main/java/scape/editor/fs/RSFileStore.java: -------------------------------------------------------------------------------- 1 | package scape.editor.fs; 2 | 3 | import scape.editor.util.ByteBufferUtils; 4 | 5 | import java.io.IOException; 6 | import java.nio.ByteBuffer; 7 | import java.nio.channels.FileChannel; 8 | import java.util.zip.CRC32; 9 | import java.util.zip.Checksum; 10 | 11 | public final class RSFileStore { 12 | 13 | private static final String[] crcFileNames = {"model_crc", "anim_crc", "midi_crc", "map_crc"}; 14 | private static final String[] versionFileNames = {"model_version", "anim_version", "midi_version", "map_version"}; 15 | 16 | private final Checksum checksum = new CRC32(); 17 | 18 | public static final int ARCHIVE_FILE_STORE = 0; 19 | public static final int MODEL_FILE_STORE = 1; 20 | public static final int ANIMATION_FILE_STORE = 2; 21 | public static final int MIDI_FILE_STORE = 3; 22 | public static final int MAP_FILE_STORE = 4; 23 | 24 | private static final int EXPANDED_HEADER_LENGTH = 10; 25 | private static final int HEADER_LENGTH = 8; 26 | 27 | private static final int EXPANDED_BLOCK_LENGTH = 510; 28 | private static final int BLOCK_LENGTH = 512; 29 | 30 | private static final int TOTAL_BLOCK_LENGTH = HEADER_LENGTH + BLOCK_LENGTH; 31 | private static final int META_BLOCK_LENGTH = 6; 32 | 33 | private static final ByteBuffer buffer = ByteBuffer.allocate(BLOCK_LENGTH + HEADER_LENGTH); 34 | 35 | private final int storeId; 36 | 37 | private final FileChannel dataChannel; 38 | 39 | private final FileChannel metaChannel; 40 | 41 | public RSFileStore(int storeId, FileChannel dataChannel, FileChannel metaChannel) { 42 | this.storeId = storeId; 43 | this.dataChannel = dataChannel; 44 | this.metaChannel = metaChannel; 45 | } 46 | 47 | public int calculateChecksum(RSArchive updateArchive, int fileId) throws IOException { 48 | // you can't calculate the checksum for archives like this they don't have an associated version and crc file in the version list archive 49 | if (storeId == 0) { 50 | return 0; 51 | } 52 | 53 | ByteBuffer versionBuf = updateArchive.readFile(versionFileNames[storeId - 1]); 54 | 55 | int versionCount = versionBuf.capacity() / Short.BYTES; 56 | 57 | int version = 0; 58 | 59 | if (fileId < versionCount) { 60 | versionBuf.position(fileId * Short.BYTES); 61 | 62 | version = versionBuf.getShort() & 0xFFFF; 63 | } 64 | 65 | // read the file 66 | ByteBuffer fileBuf = readFile(fileId); 67 | 68 | if (fileBuf == null) { 69 | return 0; 70 | } 71 | 72 | // file data first then version after 73 | ByteBuffer buf = ByteBuffer.allocate(fileBuf.capacity() + Short.BYTES); 74 | buf.put(fileBuf); 75 | buf.putShort((short) version); 76 | 77 | checksum.reset(); 78 | checksum.update(buf.array(), 0, buf.capacity()); 79 | 80 | return (int) checksum.getValue(); 81 | } 82 | 83 | public synchronized ByteBuffer readFile(int fileId) { 84 | try { 85 | 86 | if (fileId * META_BLOCK_LENGTH + META_BLOCK_LENGTH > metaChannel.size()) { 87 | return null; 88 | } 89 | 90 | buffer.position(0).limit(META_BLOCK_LENGTH); 91 | metaChannel.read(buffer, fileId * META_BLOCK_LENGTH); 92 | buffer.flip(); 93 | 94 | int size = ByteBufferUtils.readU24Int(buffer); 95 | int block = ByteBufferUtils.readU24Int(buffer); 96 | 97 | if (block <= 0 || (long) block > dataChannel.size() / 520L) { 98 | return null; 99 | } 100 | 101 | ByteBuffer fileBuffer = ByteBuffer.allocate(size); 102 | 103 | int remaining = size; 104 | int chunk = 0; 105 | int blockLength = fileId <= 0xFFFF ? BLOCK_LENGTH : EXPANDED_BLOCK_LENGTH; 106 | int headerLength = fileId <= 0xFFFF ? HEADER_LENGTH : EXPANDED_HEADER_LENGTH; 107 | 108 | while (remaining > 0) { 109 | if (block == 0) { 110 | return null; 111 | } 112 | 113 | int blockSize = remaining > blockLength ? blockLength : remaining; 114 | buffer.position(0).limit(blockSize + headerLength); 115 | dataChannel.read(buffer, block * TOTAL_BLOCK_LENGTH); 116 | buffer.flip(); 117 | 118 | int currentFile, currentChunk, nextBlock, currentIndex; 119 | 120 | if (fileId <= 65535) { 121 | currentFile = buffer.getShort() & 0xFFFF; 122 | currentChunk = buffer.getShort() & 0xFFFF; 123 | nextBlock = ByteBufferUtils.readU24Int(buffer); 124 | currentIndex = buffer.get() & 0xFF; 125 | } else { 126 | currentFile = buffer.getInt(); 127 | currentChunk = buffer.getShort() & 0xFFFF; 128 | nextBlock = ByteBufferUtils.readU24Int(buffer); 129 | currentIndex = buffer.get() & 0xFF; 130 | } 131 | 132 | if (fileId != currentFile || chunk != currentChunk || (storeId + 1) != currentIndex) { 133 | return null; 134 | } 135 | if (nextBlock < 0 || nextBlock > dataChannel.size() / TOTAL_BLOCK_LENGTH) { 136 | return null; 137 | } 138 | 139 | int rem = buffer.remaining(); 140 | 141 | for (int i = 0; i < rem; i++) { 142 | fileBuffer.put(buffer.get()); 143 | } 144 | 145 | remaining -= blockSize; 146 | block = nextBlock; 147 | chunk++; 148 | } 149 | fileBuffer.position(0); 150 | return fileBuffer; 151 | } catch (IOException _ex) { 152 | _ex.printStackTrace(); 153 | return null; 154 | } 155 | } 156 | 157 | public synchronized boolean writeFile(int id, byte[] data) { 158 | return writeFile(id, data, true) || writeFile(id, data, false); 159 | } 160 | 161 | private synchronized boolean writeFile(int fileId, byte[] data, boolean exists) { 162 | try { 163 | 164 | ByteBuffer dataBuf = ByteBuffer.wrap(data); 165 | 166 | int block; 167 | 168 | if (exists) { 169 | 170 | if (fileId * META_BLOCK_LENGTH + META_BLOCK_LENGTH > metaChannel.size()) { 171 | return false; 172 | } 173 | 174 | buffer.position(0).limit(META_BLOCK_LENGTH); 175 | metaChannel.read(buffer, fileId * META_BLOCK_LENGTH); 176 | buffer.flip(); 177 | 178 | // skip size 179 | buffer.position(3); 180 | 181 | block = ByteBufferUtils.readU24Int(buffer); 182 | 183 | if (block <= 0 || (long) block > dataChannel.size() / TOTAL_BLOCK_LENGTH) { 184 | return false; 185 | } 186 | 187 | } else { 188 | block = (int) ((dataChannel.size() + TOTAL_BLOCK_LENGTH - 1) / TOTAL_BLOCK_LENGTH); 189 | 190 | if (block == 0) { 191 | block = 1; 192 | } 193 | 194 | } 195 | 196 | buffer.position(0); 197 | ByteBufferUtils.write24Int(buffer, data.length); 198 | ByteBufferUtils.write24Int(buffer, block); 199 | buffer.flip(); 200 | 201 | metaChannel.write(buffer, fileId * META_BLOCK_LENGTH); 202 | 203 | int remaining = data.length; 204 | int chunk = 0; 205 | int blockLength = fileId <= 0xFFFF ? BLOCK_LENGTH : EXPANDED_BLOCK_LENGTH; 206 | int headerLength = fileId <= 0xFFFF ? HEADER_LENGTH : EXPANDED_HEADER_LENGTH; 207 | while (remaining > 0) { 208 | int nextBlock = 0; 209 | 210 | if (exists) { 211 | buffer.position(0).limit(headerLength); 212 | dataChannel.read(buffer, block * TOTAL_BLOCK_LENGTH); 213 | buffer.flip(); 214 | 215 | int currentFile, currentChunk, currentIndex; 216 | if (fileId <= 0xFFFF) { 217 | currentFile = buffer.getShort() & 0xFFFF; 218 | currentChunk = buffer.getShort() & 0xFFFF; 219 | nextBlock = ByteBufferUtils.readU24Int(buffer); 220 | currentIndex = buffer.get() & 0xFF; 221 | } else { 222 | currentFile = buffer.getInt(); 223 | currentChunk = buffer.getShort() & 0xFFFF; 224 | nextBlock = ByteBufferUtils.readU24Int(buffer); 225 | currentIndex = buffer.get() & 0xFF; 226 | } 227 | 228 | if (fileId != currentFile || chunk != currentChunk || (storeId + 1) != currentIndex) { 229 | return false; 230 | } 231 | 232 | if (nextBlock < 0 || nextBlock > dataChannel.size() / TOTAL_BLOCK_LENGTH) { 233 | return false; 234 | } 235 | 236 | } 237 | 238 | if (nextBlock == 0) { 239 | exists = false; 240 | nextBlock = (int) ((dataChannel.size() + TOTAL_BLOCK_LENGTH - 1) / TOTAL_BLOCK_LENGTH); 241 | 242 | if (nextBlock == 0) { 243 | nextBlock = 1; 244 | } 245 | 246 | if (nextBlock == block) { 247 | nextBlock++; 248 | } 249 | 250 | } 251 | 252 | if (remaining <= blockLength) { 253 | nextBlock = 0; 254 | } 255 | 256 | buffer.position(0).limit(TOTAL_BLOCK_LENGTH); 257 | 258 | if (fileId <= 0xFFFF) { 259 | buffer.putShort((short) fileId); 260 | buffer.putShort((short) chunk); 261 | ByteBufferUtils.write24Int(buffer, nextBlock); 262 | buffer.put((byte) (storeId + 1)); 263 | } else { 264 | buffer.putInt(fileId); 265 | buffer.putShort((short) chunk); 266 | ByteBufferUtils.write24Int(buffer, nextBlock); 267 | buffer.put((byte) (storeId + 1)); 268 | } 269 | 270 | int blockSize = remaining > blockLength ? blockLength : remaining; 271 | dataBuf.limit(dataBuf.position() + blockSize); 272 | buffer.put(dataBuf); 273 | buffer.flip(); 274 | 275 | dataChannel.write(buffer, block * TOTAL_BLOCK_LENGTH); 276 | remaining -= blockSize; 277 | block = nextBlock; 278 | chunk++; 279 | } 280 | 281 | return true; 282 | } catch (IOException ex) { 283 | return false; 284 | } 285 | } 286 | 287 | public void close() { 288 | try { 289 | dataChannel.close(); 290 | metaChannel.close(); 291 | } catch (IOException e) { 292 | e.printStackTrace(); 293 | } 294 | } 295 | 296 | public int getFileCount() { 297 | if (!metaChannel.isOpen()) { 298 | return 0; 299 | } 300 | 301 | try { 302 | return Math.toIntExact(metaChannel.size() / META_BLOCK_LENGTH); 303 | } catch (IOException e) { 304 | e.printStackTrace(); 305 | } 306 | return 0; 307 | } 308 | 309 | public int getStoreId() { 310 | return storeId; 311 | } 312 | } -------------------------------------------------------------------------------- /gui/src/main/java/scape/editor/fx/TupleCellFactory.java: -------------------------------------------------------------------------------- 1 | package scape.editor.fx; 2 | 3 | import com.google.common.primitives.Ints; 4 | import com.google.common.primitives.Shorts; 5 | import javafx.collections.FXCollections; 6 | import javafx.collections.ObservableList; 7 | import javafx.embed.swing.SwingFXUtils; 8 | import javafx.geometry.Pos; 9 | import javafx.scene.control.CheckBox; 10 | import javafx.scene.control.ComboBox; 11 | import javafx.scene.control.TableCell; 12 | import javafx.scene.control.TextField; 13 | import javafx.scene.image.ImageView; 14 | import org.imgscalr.Scalr; 15 | import scape.editor.fs.graphics.RSFont; 16 | import scape.editor.fs.graphics.RSSprite; 17 | import scape.editor.fx.component.NumericTextField; 18 | import scape.editor.gui.model.KeyModel; 19 | import scape.editor.gui.model.NamedValueModel; 20 | import scape.editor.gui.model.ValueModel; 21 | import scape.editor.gui.util.BufferedImageExtensionsKt; 22 | 23 | import java.awt.*; 24 | import java.awt.image.BufferedImage; 25 | import java.util.Arrays; 26 | 27 | public class TupleCellFactory extends TableCell { 28 | 29 | private void updateMap(ValueModel model, Object value) { 30 | KeyModel keyModel = model.getModel(); 31 | 32 | try { 33 | keyModel.getMap().put(model.getKey(), value); 34 | System.out.println(String.format("Updating key=%s value=%s", model.getKey(), value == null ? "null" : value.toString())); 35 | } catch(Exception ex) { 36 | ex.printStackTrace(); 37 | } 38 | } 39 | 40 | @Override 41 | protected void updateItem(ValueModel parent, boolean empty) { 42 | super.updateItem(parent, empty); 43 | 44 | if (parent != null) { 45 | ValueModel valueModel = (ValueModel) parent.getValue(); 46 | Object item = valueModel.getValue(); 47 | 48 | if (item != null) { 49 | if (item instanceof String) { 50 | String string = (String)item; 51 | setText(null); 52 | TextField tf = new TextField(string); 53 | tf.setAlignment(Pos.CENTER); 54 | tf.textProperty().addListener(((observable, oldValue, newValue) -> updateMap(valueModel, newValue))); 55 | setGraphic(tf); 56 | } else if (item instanceof Integer) { 57 | int value = (Integer)item; 58 | setText(null); 59 | 60 | NumericTextField nTf = new NumericTextField(); 61 | nTf.setAlignment(Pos.CENTER); 62 | nTf.setText(Integer.toString(value)); 63 | nTf.textProperty().addListener(((observable, oldValue, newValue) -> { 64 | try { 65 | if (!newValue.isEmpty()) { 66 | int intValue = Integer.parseInt(newValue); 67 | updateMap(valueModel, intValue); 68 | } 69 | } catch (Exception ex) { 70 | ex.printStackTrace(); 71 | } 72 | })); 73 | 74 | setGraphic(nTf); 75 | } else if (item instanceof Byte) { 76 | byte value = (Byte) item; 77 | NumericTextField nTf = new NumericTextField(); 78 | nTf.setAlignment(Pos.CENTER); 79 | nTf.setText(Integer.toString(value)); 80 | nTf.textProperty().addListener(((observable, oldValue, newValue) -> { 81 | try { 82 | if (!newValue.isEmpty()) { 83 | byte byteValue = Byte.parseByte(newValue); 84 | updateMap(valueModel, byteValue); 85 | } 86 | } catch (Exception ex) { 87 | ex.printStackTrace(); 88 | } 89 | })); 90 | setText(null); 91 | setGraphic(nTf); 92 | } else if (item instanceof Long) { 93 | long value = (Long)item; 94 | setText(null); 95 | 96 | NumericTextField nTf = new NumericTextField(); 97 | nTf.setAlignment(Pos.CENTER); 98 | nTf.setText(Long.toString(value)); 99 | nTf.textProperty().addListener(((observable, oldValue, newValue) -> { 100 | try { 101 | if (!newValue.isEmpty()) { 102 | long longValue = Long.parseLong(newValue); 103 | updateMap(valueModel, longValue); 104 | } 105 | } catch (Exception ex) { 106 | ex.printStackTrace(); 107 | } 108 | })); 109 | setGraphic(nTf); 110 | } else if (item instanceof Boolean) { 111 | CheckBox checkBox = new CheckBox(); 112 | checkBox.setSelected((boolean) item); 113 | checkBox.selectedProperty().addListener(((observable, oldValue, newValue) -> { 114 | boolean boolValue = newValue.booleanValue(); 115 | updateMap(valueModel, boolValue); 116 | })); 117 | setText(null); 118 | setGraphic(checkBox); 119 | } else if (item instanceof String[]) { 120 | ObservableList items = FXCollections.observableArrayList((String[]) item); 121 | ComboBox comboBox = new ComboBox<>(items); 122 | comboBox.setEditable(true); 123 | 124 | comboBox.getEditor().textProperty().addListener(((observable, oldValue, newValue) -> { 125 | final int selectedIndex = comboBox.getSelectionModel().getSelectedIndex(); 126 | KeyModel keyModel = valueModel.getModel(); 127 | String key = valueModel.getKey(); 128 | Object value = keyModel.getMap().get(key); 129 | 130 | try { 131 | if (value != null && value instanceof String[]) { 132 | String[] array = (String[]) value; 133 | if (selectedIndex >= 0 && selectedIndex < array.length) { 134 | array[selectedIndex] = newValue; 135 | items.set(selectedIndex, newValue); 136 | keyModel.getMap().put(key, array); 137 | System.out.println("Updated: " + Arrays.toString(array)); 138 | } 139 | } 140 | } catch (Exception ex) { 141 | ex.printStackTrace(); 142 | } 143 | })); 144 | 145 | setText(null); 146 | setGraphic(comboBox); 147 | } else if (item.getClass() == int[].class) { 148 | ObservableList items = FXCollections.observableArrayList(Ints.asList((int[]) item)); 149 | ComboBox comboBox = new ComboBox(items); 150 | comboBox.setEditable(true); 151 | 152 | comboBox.getEditor().textProperty().addListener(((observable, oldValue, newValue) -> { 153 | final int selectedIndex = comboBox.getSelectionModel().getSelectedIndex(); 154 | KeyModel keyModel = valueModel.getModel(); 155 | String key = valueModel.getKey(); 156 | Object value = keyModel.getMap().get(key); 157 | 158 | try { 159 | if (value != null && value.getClass() == int[].class) { 160 | int[] array = (int[]) value; 161 | int intValue = Integer.parseInt(newValue); 162 | 163 | if (selectedIndex >= 0 && selectedIndex < array.length) { 164 | array[selectedIndex] = intValue; 165 | items.set(selectedIndex, intValue); 166 | keyModel.getMap().put(key, array); 167 | System.out.println("Updated: " + Arrays.toString(array)); 168 | } 169 | 170 | } 171 | } catch (Exception ex) { 172 | ex.printStackTrace(); 173 | } 174 | })); 175 | 176 | setText(null); 177 | setGraphic(comboBox); 178 | } else if (item.getClass() == short[].class) { 179 | ObservableList items = FXCollections.observableArrayList(Shorts.asList((short[]) item)); 180 | ComboBox comboBox = new ComboBox(items); 181 | comboBox.setEditable(true); 182 | 183 | comboBox.getEditor().textProperty().addListener(((observable, oldValue, newValue) -> { 184 | final int selectedIndex = comboBox.getSelectionModel().getSelectedIndex(); 185 | KeyModel keyModel = valueModel.getModel(); 186 | String key = valueModel.getKey(); 187 | Object value = keyModel.getMap().get(key); 188 | 189 | try { 190 | if (value != null && value.getClass() == short[].class) { 191 | short[] array = (short[]) value; 192 | short shortValue = Short.parseShort(newValue); 193 | 194 | if (selectedIndex >= 0 && selectedIndex < array.length) { 195 | array[selectedIndex] = shortValue; 196 | items.set(selectedIndex, shortValue); 197 | keyModel.getMap().put(key, array); 198 | System.out.println("Updated: " + Arrays.toString(array)); 199 | } 200 | 201 | } 202 | } catch (Exception ex) { 203 | ex.printStackTrace(); 204 | } 205 | })); 206 | 207 | setText(null); 208 | setGraphic(comboBox); 209 | } else if (item instanceof RSSprite) { 210 | RSSprite sprite = (RSSprite) item; 211 | BufferedImage bimage = sprite.toBufferedImage(); 212 | ImageView imageView = new ImageView(); 213 | bimage = BufferedImageExtensionsKt.setColorTransparent(bimage, Color.BLACK); 214 | 215 | if (bimage.getWidth() > 64 || bimage.getHeight() > 64) { 216 | imageView.setImage(SwingFXUtils.toFXImage(Scalr.resize(bimage, 64), null)); 217 | } else { 218 | imageView.setImage(SwingFXUtils.toFXImage(bimage, null)); 219 | } 220 | 221 | setText(null); 222 | setGraphic(imageView); 223 | } else if (item instanceof RSFont) { 224 | RSFont font = (RSFont) item; 225 | BufferedImage bimage = font.toBufferedImage(); 226 | ImageView imageView = new ImageView(); 227 | bimage = BufferedImageExtensionsKt.setColorTransparent(bimage, Color.BLACK); 228 | 229 | if (bimage.getWidth() > 64 || bimage.getHeight() > 64) { 230 | imageView.setImage(SwingFXUtils.toFXImage(Scalr.resize(bimage, 64), null)); 231 | } else { 232 | imageView.setImage(SwingFXUtils.toFXImage(bimage, null)); 233 | } 234 | 235 | setText(null); 236 | setGraphic(imageView); 237 | } else { 238 | setText(item.getClass().getTypeName()); 239 | setGraphic(null); 240 | } 241 | } else { 242 | setText(null); 243 | setGraphic(null); 244 | } 245 | } else { 246 | setText(null); 247 | setGraphic(null); 248 | } 249 | } 250 | 251 | } 252 | -------------------------------------------------------------------------------- /fs/src/main/java/scape/editor/fs/graphics/RSWidget.java: -------------------------------------------------------------------------------- 1 | package scape.editor.fs.graphics; 2 | 3 | import scape.editor.fs.RSArchive; 4 | import scape.editor.util.ByteBufferUtils; 5 | import scape.editor.util.HashUtils; 6 | 7 | import java.io.IOException; 8 | import java.nio.ByteBuffer; 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | 12 | public class RSWidget { 13 | 14 | public static final int OPTION_CLOSE = 3; 15 | public static final int OPTION_CONTINUE = 6; 16 | public static final int OPTION_OK = 1; 17 | public static final int OPTION_RESET_SETTING = 5; 18 | public static final int OPTION_TOGGLE_SETTING = 4; 19 | public static final int OPTION_USABLE = 2; 20 | 21 | public static final int TYPE_CONTAINER = 0; 22 | public static final int TYPE_INVENTORY = 2; 23 | public static final int TYPE_ITEM_LIST = 7; 24 | public static final int TYPE_MODEL = 6; 25 | public static final int TYPE_MODEL_LIST = 1; 26 | public static final int TYPE_RECTANGLE = 3; 27 | public static final int TYPE_SPRITE = 5; 28 | public static final int TYPE_TEXT = 4; 29 | 30 | private static final Map spriteCache = new HashMap<>(); 31 | 32 | public RSWidget(int id) { 33 | this.id = id; 34 | } 35 | 36 | public static RSWidget[] decode(RSArchive interfaces, RSArchive graphics, RSFont[] fonts) throws IOException { 37 | ByteBuffer buffer = interfaces.readFile("data"); 38 | RSWidget[] widgets = new RSWidget[buffer.getShort() & 0xFFFF]; 39 | 40 | int parent = -1; 41 | 42 | while (buffer.position() < buffer.remaining()) { 43 | int id = buffer.getShort() & 0xFFFF; 44 | if (id == 65535) { 45 | parent = buffer.getShort() & 0xFFFF; 46 | id = buffer.getShort() & 0xFFFF; 47 | } 48 | 49 | RSWidget widget = new RSWidget(id); 50 | widget.parent = parent; 51 | widget.group = buffer.get() & 0xFF; 52 | widget.optionType = buffer.get() & 0xFF; 53 | widget.contentType = buffer.getShort() & 0xFFFF; 54 | widget.width = buffer.getShort() & 0xFFFF; 55 | widget.height = buffer.getShort() & 0xFFFF; 56 | widget.alpha = (byte)(buffer.get() & 0xFF); 57 | 58 | int hover = buffer.get() & 0xFF; 59 | widget.hoverId = (hover != 0) ? (hover - 1 << 8) | buffer.get() & 0xFF : -1; 60 | 61 | int operators = buffer.get() & 0xFF; 62 | if (operators > 0) { 63 | widget.scriptOperators = new int[operators]; 64 | widget.scriptDefaults = new int[operators]; 65 | 66 | for (int index = 0; index < operators; index++) { 67 | widget.scriptOperators[index] = buffer.get() & 0xFF; 68 | widget.scriptDefaults[index] = buffer.getShort() & 0xFFFF; 69 | } 70 | } 71 | 72 | int scripts = buffer.get() & 0xFF; 73 | if (scripts > 0) { 74 | widget.scripts = new int[scripts][]; 75 | 76 | for (int script = 0; script < scripts; script++) { 77 | int instructions = buffer.getShort() & 0xFFFF; 78 | widget.scripts[script] = new int[instructions]; 79 | 80 | for (int instruction = 0; instruction < instructions; instruction++) { 81 | widget.scripts[script][instruction] = buffer.getShort() & 0xFFFF; 82 | } 83 | } 84 | } 85 | 86 | if (widget.group == TYPE_CONTAINER) { 87 | widget.scrollLimit = buffer.getShort() & 0xFFFF; 88 | widget.hidden = (buffer.get() & 0xFF) == 1; 89 | 90 | int children = buffer.getShort() & 0xFFFF; 91 | widget.children = new int[children]; 92 | widget.childX = new int[children]; 93 | widget.childY = new int[children]; 94 | 95 | for (int index = 0; index < children; index++) { 96 | widget.children[index] = buffer.getShort() & 0xFFFF; 97 | widget.childX[index] = buffer.getShort(); 98 | widget.childY[index] = buffer.getShort(); 99 | } 100 | } 101 | 102 | if (widget.group == TYPE_MODEL_LIST) { 103 | buffer.getShort(); // if use, read unsigned 104 | buffer.get(); // if use, read unsigned 105 | } 106 | 107 | if (widget.group == TYPE_INVENTORY) { 108 | widget.inventoryIds = new int[widget.width * widget.height]; 109 | widget.inventoryAmounts = new int[widget.width * widget.height]; 110 | 111 | widget.swappableItems = (buffer.get() & 0xFF) == 1; 112 | widget.hasActions = (buffer.get() & 0xFF) == 1; 113 | widget.usableItems = (buffer.get() & 0xFF) == 1; 114 | widget.replaceItems = (buffer.get() & 0xFF) == 1; 115 | 116 | widget.spritePaddingX = buffer.get() & 0xFF; 117 | widget.spritePaddingY = buffer.get() & 0xFF; 118 | 119 | widget.spriteX = new int[20]; 120 | widget.spriteY = new int[20]; 121 | widget.sprites = new RSSprite[20]; 122 | 123 | for (int index = 0; index < 20; index++) { 124 | int exists = buffer.get() & 0xFF; 125 | if (exists == 1) { 126 | widget.spriteX[index] = buffer.getShort(); 127 | widget.spriteY[index] = buffer.getShort(); 128 | String name = ByteBufferUtils.getString(buffer); 129 | 130 | if (graphics != null && name.length() > 0) { 131 | int position = name.lastIndexOf(","); 132 | widget.sprites[index] = getSprite(graphics, 133 | name.substring(0, position), 134 | Integer.parseInt(name.substring(position + 1))); 135 | } 136 | } 137 | } 138 | 139 | widget.actions = new String[5]; 140 | for (int index = 0; index < 5; index++) { 141 | widget.actions[index] = ByteBufferUtils.getString(buffer); 142 | 143 | if (widget.actions[index].isEmpty()) { 144 | widget.actions[index] = null; 145 | } 146 | } 147 | } 148 | 149 | if (widget.group == TYPE_RECTANGLE) { 150 | widget.filled = (buffer.get() & 0xFF) == 1; 151 | } 152 | 153 | if (widget.group == TYPE_TEXT || widget.group == TYPE_MODEL_LIST) { 154 | widget.centeredText = (buffer.get() & 0xFF) == 1; 155 | int font = buffer.get() & 0xFF; 156 | 157 | if (fonts != null) { 158 | widget.font = fonts[font]; 159 | } 160 | 161 | widget.shadowedText = (buffer.get() & 0xFF) == 1; 162 | } 163 | 164 | if (widget.group == TYPE_TEXT) { 165 | widget.defaultText = ByteBufferUtils.getString(buffer); 166 | widget.secondaryText = ByteBufferUtils.getString(buffer); 167 | } 168 | 169 | if (widget.group == TYPE_MODEL_LIST || widget.group == TYPE_RECTANGLE 170 | || widget.group == TYPE_TEXT) { 171 | widget.defaultColour = buffer.getInt(); 172 | } 173 | 174 | if (widget.group == TYPE_RECTANGLE || widget.group == TYPE_TEXT) { 175 | widget.secondaryColour = buffer.getInt(); 176 | widget.defaultHoverColour = buffer.getInt(); 177 | widget.secondaryHoverColour = buffer.getInt(); 178 | } else if (widget.group == TYPE_SPRITE) { 179 | String name = ByteBufferUtils.getString(buffer); 180 | if (graphics != null && name.length() > 0) { 181 | int index = name.lastIndexOf(","); 182 | widget.defaultSprite = getSprite(graphics, name.substring(0, index), 183 | Integer.parseInt(name.substring(index + 1))); 184 | } 185 | 186 | name = ByteBufferUtils.getString(buffer); 187 | if (graphics != null && name.length() > 0) { 188 | int index = name.lastIndexOf(","); 189 | widget.secondarySprite = getSprite(graphics, name.substring(0, index), 190 | Integer.parseInt(name.substring(index + 1))); 191 | } 192 | } else if (widget.group == TYPE_MODEL) { 193 | int content = buffer.get() & 0xFF; 194 | if (content != 0) { 195 | widget.defaultMediaType = 1; 196 | widget.defaultMedia = (content - 1 << 8) + buffer.get() & 0xFF; 197 | } 198 | 199 | content = buffer.get() & 0xFF; 200 | if (content != 0) { 201 | widget.secondaryMediaType = 1; 202 | widget.secondaryMedia = (content - 1 << 8) + buffer.get() & 0xFF; 203 | } 204 | 205 | content = buffer.get() & 0xFF; 206 | widget.defaultAnimationId = (content != 0) ? (content - 1 << 8) + buffer.get() & 0xFF 207 | : -1; 208 | 209 | content = buffer.get() & 0xFF; 210 | widget.secondaryAnimationId = (content != 0) ? (content - 1 << 8) + buffer.get() & 0xFF 211 | : -1; 212 | 213 | widget.spriteScale = buffer.getShort() & 0xFFFF; 214 | widget.spritePitch = buffer.getShort() & 0xFFFF; 215 | widget.spriteRoll = buffer.getShort() & 0xFFFF; 216 | } else if (widget.group == TYPE_ITEM_LIST) { 217 | widget.inventoryIds = new int[widget.width * widget.height]; 218 | widget.inventoryAmounts = new int[widget.width * widget.height]; 219 | widget.centeredText = (buffer.get() & 0xFF) == 1; 220 | 221 | int font = buffer.get() & 0xFF; 222 | if (fonts != null) { 223 | widget.font = fonts[font]; 224 | } 225 | 226 | widget.shadowedText = (buffer.get() & 0xFF) == 1; 227 | widget.defaultColour = buffer.getInt(); 228 | widget.spritePaddingX = buffer.getShort(); 229 | widget.spritePaddingY = buffer.getShort(); 230 | widget.hasActions = (buffer.get() & 0xFF) == 1; 231 | widget.actions = new String[5]; 232 | 233 | for (int index = 0; index < 5; index++) { 234 | widget.actions[index] = ByteBufferUtils.getString(buffer); 235 | 236 | if (widget.actions[index].isEmpty()) { 237 | widget.actions[index] = null; 238 | } 239 | } 240 | } 241 | 242 | if (widget.optionType == OPTION_USABLE || widget.group == TYPE_INVENTORY) { 243 | widget.optionCircumfix = ByteBufferUtils.getString(buffer); 244 | widget.optionText = ByteBufferUtils.getString(buffer); 245 | widget.optionAttributes = buffer.getShort() & 0xFFFF; 246 | } 247 | 248 | if (widget.optionType == OPTION_OK || widget.optionType == OPTION_TOGGLE_SETTING 249 | || widget.optionType == OPTION_RESET_SETTING 250 | || widget.optionType == OPTION_CONTINUE) { 251 | widget.hover = ByteBufferUtils.getString(buffer); 252 | 253 | if (widget.hover.isEmpty()) { 254 | if (widget.optionType == OPTION_OK) { 255 | widget.hover = "Ok"; 256 | } else if (widget.optionType == OPTION_TOGGLE_SETTING) { 257 | widget.hover = "Select"; 258 | } else if (widget.optionType == OPTION_RESET_SETTING) { 259 | widget.hover = "Select"; 260 | } else if (widget.optionType == OPTION_CONTINUE) { 261 | widget.hover = "Continue"; 262 | } 263 | } 264 | } 265 | 266 | widgets[id] = widget; 267 | 268 | } 269 | 270 | spriteCache.clear(); 271 | 272 | return widgets; 273 | } 274 | 275 | private static RSSprite getSprite(RSArchive archive, String name, int id) { 276 | long key = (HashUtils.hashSpriteName(name) << 8) | id; 277 | RSSprite sprite = spriteCache.get(key); 278 | if (sprite != null) { 279 | return sprite; 280 | } 281 | 282 | try { 283 | sprite = RSSprite.decode(archive, name, id); 284 | } catch (Exception ex) { 285 | 286 | } 287 | 288 | spriteCache.put(key, sprite); 289 | 290 | return sprite; 291 | } 292 | 293 | public String[] actions; 294 | public byte alpha; 295 | public boolean centeredText; 296 | public int[] children; 297 | public int[] childX; 298 | public int[] childY; 299 | public int contentType; 300 | public int currentFrame; 301 | public int defaultAnimationId; 302 | public int defaultColour; 303 | public int defaultHoverColour; 304 | public int defaultMedia; 305 | public int defaultMediaType; 306 | public RSSprite defaultSprite; 307 | public String defaultText; 308 | public boolean filled; 309 | public RSFont font; 310 | public int group; 311 | public boolean hasActions; 312 | public int height; 313 | public boolean hidden; 314 | public int horizontalDrawOffset; 315 | public String hover; 316 | public int hoverId; 317 | public int id; 318 | public int[] inventoryAmounts; 319 | public int[] inventoryIds; 320 | public int lastFrameTime; 321 | public int optionAttributes; 322 | public String optionCircumfix; 323 | public String optionText; 324 | public int optionType; 325 | public int parent; 326 | public boolean replaceItems; 327 | public int[] scriptDefaults; 328 | public int[] scriptOperators; 329 | public int[][] scripts; 330 | public int scrollLimit; 331 | public int scrollPosition; 332 | public int secondaryAnimationId; 333 | public int secondaryColour; 334 | public int secondaryHoverColour; 335 | public int secondaryMedia; 336 | public int secondaryMediaType; 337 | public RSSprite secondarySprite; 338 | public String secondaryText; 339 | public boolean shadowedText; 340 | public int spritePaddingX; 341 | public int spritePaddingY; 342 | public int spritePitch; 343 | public int spriteRoll; 344 | public RSSprite[] sprites; 345 | public int spriteScale; 346 | public int[] spriteX; 347 | public int[] spriteY; 348 | public boolean swappableItems; 349 | public boolean usableItems; 350 | public int verticalDrawOffset; 351 | public int width; 352 | @Override 353 | public String toString() { 354 | return Integer.toString(id); 355 | } 356 | 357 | } -------------------------------------------------------------------------------- /fs/src/main/java/scape/editor/fs/graphics/RSFont.java: -------------------------------------------------------------------------------- 1 | package scape.editor.fs.graphics; 2 | 3 | import scape.editor.fs.RSArchive; 4 | import scape.editor.fs.graphics.draw.RSRaster; 5 | 6 | import java.io.IOException; 7 | import java.nio.ByteBuffer; 8 | import java.util.Random; 9 | 10 | public final class RSFont extends RSRaster { 11 | 12 | private int[] glyphHeights = new int[256]; 13 | private byte[][] glyphs = new byte[256][]; 14 | private int[] glyphSpacings = new int[256]; 15 | private int[] glyphWidths = new int[256]; 16 | private int[] horizontalOffsets = new int[256]; 17 | private int[] verticalOffsets = new int[256]; 18 | 19 | private Random random = new Random(); 20 | private boolean strikethrough; 21 | 22 | private int verticalSpace; 23 | 24 | private RSFont() { 25 | 26 | } 27 | 28 | public static RSFont decode(RSArchive archive, String name, boolean wideSpace) throws IOException { 29 | RSFont font = new RSFont(); 30 | ByteBuffer data = archive.readFile(name + ".dat"); 31 | ByteBuffer meta = archive.readFile("index.dat"); 32 | meta.position((data.getShort() & 0xFFFF) + 4); 33 | 34 | int position = meta.get() & 0xFF; 35 | 36 | if (position > 0) { 37 | meta.position(meta.position() + 3 * (position -1)); 38 | } 39 | 40 | for (int character = 0; character < 256; character++) { 41 | font.horizontalOffsets[character] = meta.get() & 0xFF; 42 | font.verticalOffsets[character] = meta.get() & 0xFF; 43 | int width = font.glyphWidths[character] = meta.getShort() & 0xFFFF; 44 | int height = font.glyphHeights[character] = meta.getShort() & 0xFFFF; 45 | int format = meta.get() & 0xFF; 46 | int pixels = width * height; 47 | font.glyphs[character] = new byte[pixels]; 48 | 49 | if (format == 0) { 50 | for (int pixel = 0; pixel < pixels; pixel++) { 51 | font.glyphs[character][pixel] = data.get(); 52 | } 53 | } else if (format == 1) { 54 | for (int x = 0; x < width; x++) { 55 | for (int y = 0; y < height; y++) { 56 | font.glyphs[character][x + y * width] = data.get(); 57 | } 58 | } 59 | } 60 | 61 | if (height > font.verticalSpace && character < 128) { 62 | font.verticalSpace = height; 63 | } 64 | 65 | font.horizontalOffsets[character] = 1; 66 | font.glyphSpacings[character] = width + 2; 67 | int filledCount = 0; 68 | 69 | for (int y = height / 7; y < height; y++) { 70 | filledCount += font.glyphs[character][y * width]; 71 | } 72 | 73 | if (filledCount <= height / 7) { 74 | font.glyphSpacings[character]--; 75 | font.horizontalOffsets[character] = 0; 76 | } 77 | filledCount = 0; 78 | 79 | for (int y = height / 7; y < height; y++) { 80 | filledCount += font.glyphs[character][width - 1 + y * width]; 81 | } 82 | 83 | if (filledCount <= height / 7) { 84 | font.glyphSpacings[character]--; 85 | } 86 | } 87 | 88 | font.glyphSpacings[' '] = wideSpace ? font.glyphSpacings['I'] : font.glyphSpacings['i']; 89 | return font; 90 | } 91 | 92 | public int getColouredTextWidth(String text) { 93 | if (text == null) { 94 | return 0; 95 | } 96 | 97 | int width = 0; 98 | for (int index = 0; index < text.length(); index++) { 99 | if (text.charAt(index) == '@' && index + 4 < text.length() && text.charAt(index + 4) == '@') { 100 | index += 4; 101 | } else { 102 | width += glyphSpacings[text.charAt(index)]; 103 | } 104 | } 105 | 106 | return width; 107 | } 108 | 109 | public int[] getGlyphHeights() { 110 | return glyphHeights; 111 | } 112 | 113 | public byte[][] getGlyphs() { 114 | return glyphs; 115 | } 116 | 117 | public int[] getGlyphSpacings() { 118 | return glyphSpacings; 119 | } 120 | 121 | public int[] getGlyphWidths() { 122 | return glyphWidths; 123 | } 124 | 125 | public int[] getHorizontalOffsets() { 126 | return horizontalOffsets; 127 | } 128 | 129 | public Random getRandom() { 130 | return random; 131 | } 132 | 133 | public int getTextWidth(String text) { 134 | if (text == null) { 135 | return 0; 136 | } 137 | 138 | int width = 0; 139 | for (int index = 0; index < text.length(); index++) { 140 | width += glyphSpacings[text.charAt(index)]; 141 | } 142 | 143 | return width; 144 | } 145 | 146 | public int[] getVerticalOffsets() { 147 | return verticalOffsets; 148 | } 149 | 150 | public int getVerticalSpace() { 151 | return verticalSpace; 152 | } 153 | 154 | public boolean isStrikethrough() { 155 | return strikethrough; 156 | } 157 | 158 | public void render(String text, int x, int y, int colour) { 159 | if (text == null) { 160 | return; 161 | } 162 | 163 | y -= verticalSpace; 164 | for (int index = 0; index < text.length(); index++) { 165 | char character = text.charAt(index); 166 | if (character != ' ') { 167 | if (character == 32 || character == 73 | character == 105) { 168 | 169 | } 170 | render(glyphs[character], x + horizontalOffsets[character], y + verticalOffsets[character], 171 | glyphWidths[character], glyphHeights[character], colour); 172 | } 173 | 174 | x += glyphSpacings[character]; 175 | } 176 | } 177 | 178 | public void renderCentre(int x, int y, String text, int colour) { 179 | render(text, x - getTextWidth(text) / 2, y, colour); 180 | } 181 | 182 | public void renderLeft(int x, int y, String text, int colour) { 183 | render(text, x - getTextWidth(text), y, colour); 184 | } 185 | 186 | public void renderRandom(String text, int x, int y, int colour, boolean shadow, int seed) { 187 | if (text == null) { 188 | return; 189 | } 190 | 191 | random.setSeed(seed); 192 | int alpha = 192 + (random.nextInt() & 0x1f); 193 | y -= verticalSpace; 194 | 195 | for (int index = 0; index < text.length(); index++) { 196 | if (text.charAt(index) == '@' && index + 4 < text.length() && text.charAt(index + 4) == '@') { 197 | int rgb = rgb(text.substring(index + 1, index + 4)); 198 | if (rgb != -1) { 199 | colour = rgb; 200 | } 201 | 202 | index += 4; 203 | } else { 204 | char character = text.charAt(index); 205 | if (character != ' ') { 206 | if (shadow) { 207 | renderRgba(glyphs[character], x + horizontalOffsets[character] + 1, y + 1 + verticalOffsets[character], 208 | glyphHeights[character], glyphWidths[character], 192, 0); 209 | } 210 | renderRgba(glyphs[character], x + horizontalOffsets[character], y + verticalOffsets[character], 211 | glyphHeights[character], glyphWidths[character], alpha, colour); 212 | } 213 | 214 | x += glyphSpacings[character]; 215 | if ((random.nextInt() & 3) == 0) { 216 | x++; 217 | } 218 | } 219 | } 220 | } 221 | 222 | public int rgb(String colour) { 223 | if (colour.equals("red")) { 224 | return 0xff0000; 225 | } else if (colour.equals("gre")) { 226 | return 65280; 227 | } else if (colour.equals("blu")) { 228 | return 255; 229 | } else if (colour.equals("yel")) { 230 | return 0xffff00; 231 | } else if (colour.equals("cya")) { 232 | return 65535; 233 | } else if (colour.equals("mag")) { 234 | return 0xff00ff; 235 | } else if (colour.equals("whi")) { 236 | return 0xffffff; 237 | } else if (colour.equals("bla")) { 238 | return 0; 239 | } else if (colour.equals("lre")) { 240 | return 0xff9040; 241 | } else if (colour.equals("dre")) { 242 | return 0x800000; 243 | } else if (colour.equals("dbl")) { 244 | return 128; 245 | } else if (colour.equals("or1")) { 246 | return 0xffb000; 247 | } else if (colour.equals("or2")) { 248 | return 0xff7000; 249 | } else if (colour.equals("or3")) { 250 | return 0xff3000; 251 | } else if (colour.equals("gr1")) { 252 | return 0xc0ff00; 253 | } else if (colour.equals("gr2")) { 254 | return 0x80ff00; 255 | } else if (colour.equals("gr3")) { 256 | return 0x40ff00; 257 | } else if (colour.equals("str")) { 258 | strikethrough = true; 259 | } else if (colour.equals("end")) { 260 | strikethrough = false; 261 | } 262 | 263 | return -1; 264 | } 265 | 266 | public void setGlyphHeights(int[] glyphHeights) { 267 | this.glyphHeights = glyphHeights; 268 | } 269 | 270 | public void setGlyphs(byte[][] glyphs) { 271 | this.glyphs = glyphs; 272 | } 273 | 274 | public void setGlyphSpacings(int[] glyphSpacings) { 275 | this.glyphSpacings = glyphSpacings; 276 | } 277 | 278 | public void setGlyphWidths(int[] glyphWidths) { 279 | this.glyphWidths = glyphWidths; 280 | } 281 | 282 | public void setHorizontalOffsets(int[] horizontalOffsets) { 283 | this.horizontalOffsets = horizontalOffsets; 284 | } 285 | 286 | public void setRandom(Random random) { 287 | this.random = random; 288 | } 289 | 290 | public void setStrikethrough(boolean strikethrough) { 291 | this.strikethrough = strikethrough; 292 | } 293 | 294 | public void setVerticalOffsets(int[] verticalOffsets) { 295 | this.verticalOffsets = verticalOffsets; 296 | } 297 | 298 | public void setVerticalSpace(int verticalSpace) { 299 | this.verticalSpace = verticalSpace; 300 | } 301 | 302 | public void shadow(int x, int y, String text, boolean shadow, int colour) { 303 | strikethrough = false; 304 | int width = x; 305 | if (text == null) { 306 | return; 307 | } 308 | y -= verticalSpace; 309 | 310 | for (int index = 0; index < text.length(); index++) { 311 | if (text.charAt(index) == '@' && index + 4 < text.length() && text.charAt(index + 4) == '@') { 312 | int rgb = rgb(text.substring(index + 1, index + 4)); 313 | if (rgb != -1) { 314 | colour = rgb; 315 | } 316 | 317 | index += 4; 318 | } else { 319 | char character = text.charAt(index); 320 | 321 | if (character != ' ') { 322 | if (shadow) { 323 | render(glyphs[character], x + horizontalOffsets[character] + 1, y + verticalOffsets[character] + 1, 324 | glyphWidths[character], glyphHeights[character], 0); 325 | } 326 | 327 | render(glyphs[character], x + horizontalOffsets[character], y + verticalOffsets[character], 328 | glyphWidths[character], glyphHeights[character], colour); 329 | } 330 | 331 | x += glyphSpacings[character]; 332 | } 333 | } 334 | 335 | if (strikethrough) { 336 | RSRaster.drawHorizontal(width, y + (int) (verticalSpace * 0.7D), x - width, 0x800000); 337 | } 338 | } 339 | 340 | public void shadowCentre(int x, int y, String text, boolean shadow, int colour) { 341 | shadow(x - getColouredTextWidth(text) / 2, y, text, shadow, colour); 342 | } 343 | 344 | public void shake(String text, int x, int y, int colour, int elapsed, int tick) { 345 | if (text == null) { 346 | return; 347 | } 348 | 349 | double amplitude = 7D - elapsed / 8D; 350 | if (amplitude < 0) { 351 | amplitude = 0; 352 | } 353 | x -= getTextWidth(text) / 2; 354 | y -= verticalSpace; 355 | 356 | for (int index = 0; index < text.length(); index++) { 357 | char character = text.charAt(index); 358 | if (character != ' ') { 359 | render(glyphs[character], x + horizontalOffsets[character], 360 | y + verticalOffsets[character] + (int) (Math.sin(index / 1.5D + tick) * amplitude), 361 | glyphWidths[character], glyphHeights[character], colour); 362 | } 363 | x += glyphSpacings[character]; 364 | } 365 | } 366 | 367 | public void wave(String text, int x, int y, int colour, int tick) { 368 | if (text == null) { 369 | return; 370 | } 371 | x -= getTextWidth(text) / 2; 372 | y -= verticalSpace; 373 | 374 | for (int index = 0; index < text.length(); index++) { 375 | char c = text.charAt(index); 376 | if (c != ' ') { 377 | render(glyphs[c], x + horizontalOffsets[c], 378 | y + verticalOffsets[c] + (int) (Math.sin(index / 2D + tick / 5D) * 5), glyphWidths[c], glyphHeights[c], 379 | colour); 380 | } 381 | x += glyphSpacings[c]; 382 | } 383 | } 384 | 385 | public void wave2(String text, int x, int y, int colour, int tick) { 386 | if (text == null) { 387 | return; 388 | } 389 | x -= getTextWidth(text) / 2; 390 | y -= verticalSpace; 391 | 392 | for (int index = 0; index < text.length(); index++) { 393 | char character = text.charAt(index); 394 | if (character != ' ') { 395 | render(glyphs[character], x + horizontalOffsets[character] + (int) (Math.sin(index / 5D + tick / 5D) * 5), y 396 | + verticalOffsets[character] + (int) (Math.sin(index / 3D + tick / 5D) * 5), glyphWidths[character], 397 | glyphHeights[character], colour); 398 | } 399 | 400 | x += glyphSpacings[character]; 401 | } 402 | } 403 | 404 | private void render(byte[] glyph, int x, int y, int width, int height, int colour) { 405 | int rasterIndex = x + y * RSRaster.width; 406 | int rasterClip = RSRaster.width - width; 407 | int glyphClip = 0; 408 | int glyphIndex = 0; 409 | 410 | if (y < RSRaster.getClipBottom()) { 411 | int dy = RSRaster.getClipBottom() - y; 412 | height -= dy; 413 | y = RSRaster.getClipBottom(); 414 | glyphIndex += dy * width; 415 | rasterIndex += dy * RSRaster.width; 416 | } 417 | 418 | if (y + height >= RSRaster.getClipTop()) { 419 | height -= y + height - RSRaster.getClipTop() + 1; 420 | } 421 | 422 | if (x < RSRaster.getClipLeft()) { 423 | int dx = RSRaster.getClipLeft() - x; 424 | width -= dx; 425 | x = RSRaster.getClipLeft(); 426 | glyphIndex += dx; 427 | rasterIndex += dx; 428 | glyphClip += dx; 429 | rasterClip += dx; 430 | } 431 | 432 | if (x + width >= RSRaster.getClipRight()) { 433 | int dx = x + width - RSRaster.getClipRight() + 1; 434 | width -= dx; 435 | glyphClip += dx; 436 | rasterClip += dx; 437 | } 438 | 439 | if (width > 0 && height > 0) { 440 | render(RSRaster.raster, glyph, colour, glyphIndex, rasterIndex, width, height, rasterClip, glyphClip); 441 | } 442 | } 443 | 444 | private void render(int[] raster, byte[] glyph, int colour, int glyphPosition, int rasterPosition, int width, int height, 445 | int rasterOffset, int glyphOffset) { 446 | int offsetX = -(width >> 2); 447 | width = -(width & 3); 448 | 449 | for (int y = -height; y < 0; y++) { 450 | for (int x = offsetX; x < 0; x++) { 451 | if (glyph[glyphPosition++] != 0) { 452 | raster[rasterPosition++] = colour; 453 | } else { 454 | rasterPosition++; 455 | } 456 | if (glyph[glyphPosition++] != 0) { 457 | raster[rasterPosition++] = colour; 458 | } else { 459 | rasterPosition++; 460 | } 461 | if (glyph[glyphPosition++] != 0) { 462 | raster[rasterPosition++] = colour; 463 | } else { 464 | rasterPosition++; 465 | } 466 | if (glyph[glyphPosition++] != 0) { 467 | raster[rasterPosition++] = colour; 468 | } else { 469 | rasterPosition++; 470 | } 471 | } 472 | 473 | for (int i = width; i < 0; i++) { 474 | if (glyph[glyphPosition++] != 0) { 475 | raster[rasterPosition++] = colour; 476 | } else { 477 | rasterPosition++; 478 | } 479 | } 480 | 481 | rasterPosition += rasterOffset; 482 | glyphPosition += glyphOffset; 483 | } 484 | } 485 | 486 | private void renderRgba(byte[] glyph, int x, int y, int height, int width, int alpha, int colour) { 487 | int rasterIndex = x + y * RSRaster.width; 488 | int rasterClip = RSRaster.width - width; 489 | int glyphClip = 0; 490 | int glyphIndex = 0; 491 | 492 | if (y < RSRaster.getClipBottom()) { 493 | int dy = RSRaster.getClipBottom() - y; 494 | height -= dy; 495 | y = RSRaster.getClipBottom(); 496 | glyphIndex += dy * width; 497 | rasterIndex += dy * RSRaster.width; 498 | } 499 | 500 | if (y + height >= RSRaster.getClipTop()) { 501 | height -= y + height - RSRaster.getClipTop() + 1; 502 | } 503 | 504 | if (x < RSRaster.getClipLeft()) { 505 | int dx = RSRaster.getClipLeft() - x; 506 | width -= dx; 507 | x = RSRaster.getClipLeft(); 508 | glyphIndex += dx; 509 | rasterIndex += dx; 510 | glyphClip += dx; 511 | rasterClip += dx; 512 | } 513 | 514 | if (x + width >= RSRaster.getClipRight()) { 515 | int dx = x + width - RSRaster.getClipRight() + 1; 516 | width -= dx; 517 | glyphClip += dx; 518 | rasterClip += dx; 519 | } 520 | 521 | if (width > 0 && height > 0) { 522 | renderRgba(glyph, height, rasterIndex, RSRaster.raster, glyphIndex, width, glyphClip, rasterClip, colour, alpha); 523 | } 524 | } 525 | 526 | private void renderRgba(byte[] glyph, int height, int rasterPosition, int[] raster, int glyphPosition, int width, 527 | int glyphOffset, int rasterOffset, int colour, int alpha) { 528 | colour = ((colour & 0xff00ff) * alpha & 0xff00ff00) + ((colour & 0xff00) * alpha & 0xff0000) >> 8; 529 | alpha = 256 - alpha; 530 | for (int y = -height; y < 0; y++) { 531 | for (int x = -width; x < 0; x++) { 532 | if (glyph[glyphPosition++] != 0) { 533 | int rgba = raster[rasterPosition]; 534 | raster[rasterPosition++] = (((rgba & 0xff00ff) * alpha & 0xff00ff00) + ((rgba & 0xff00) * alpha & 0xff0000) >> 8) 535 | + colour; 536 | } else { 537 | rasterPosition++; 538 | } 539 | } 540 | 541 | rasterPosition += rasterOffset; 542 | glyphPosition += glyphOffset; 543 | } 544 | } 545 | 546 | } --------------------------------------------------------------------------------