From 1a14bd7dbcce81fb0c0b1fc3edc68b6ff1b0a7c3 Mon Sep 17 00:00:00 2001 From: ceikry Date: Thu, 28 Oct 2021 20:00:23 -0500 Subject: [PATCH] NPC map stuff --- src/main/kotlin/ImgButton.kt | 54 ++++ src/main/kotlin/Rs2MapEditor.kt | 286 +++++++++++++++--- .../cache/definition/decoder/NPCDecoder.kt | 15 + .../definition/decoder/NPCSpawnDecoder.kt | 37 ++- src/main/kotlin/const/Image.kt | 9 + src/main/kotlin/const/Misc.kt | 6 + src/main/resources/add_dark.png | Bin 0 -> 356 bytes src/main/resources/add_hi.png | Bin 0 -> 337 bytes src/main/resources/dot_red.png | Bin 0 -> 114 bytes src/main/resources/dot_yellow.png | Bin 0 -> 115 bytes src/main/resources/trash_dark.png | Bin 0 -> 330 bytes src/main/resources/trash_hi.png | Bin 0 -> 323 bytes 12 files changed, 349 insertions(+), 58 deletions(-) create mode 100644 src/main/kotlin/ImgButton.kt create mode 100644 src/main/kotlin/const/Image.kt create mode 100644 src/main/kotlin/const/Misc.kt create mode 100644 src/main/resources/add_dark.png create mode 100644 src/main/resources/add_hi.png create mode 100644 src/main/resources/dot_red.png create mode 100644 src/main/resources/dot_yellow.png create mode 100644 src/main/resources/trash_dark.png create mode 100644 src/main/resources/trash_hi.png diff --git a/src/main/kotlin/ImgButton.kt b/src/main/kotlin/ImgButton.kt new file mode 100644 index 0000000..882a253 --- /dev/null +++ b/src/main/kotlin/ImgButton.kt @@ -0,0 +1,54 @@ +import java.awt.Image +import java.awt.event.MouseEvent +import java.awt.event.MouseListener +import javax.swing.ImageIcon +import javax.swing.JLabel + +open class ImgButton(enabledURL: String, disabledURL: String = enabledURL, val autoHandleMouse: Boolean = true) : JLabel() { + + private var hoverMethod: (MouseEvent) -> Unit = {} + private var mouseLeaveMethod: (MouseEvent) -> Unit = {} + private var onClickMethod: (MouseEvent) -> Unit = {} + + init { + isEnabled = false + icon = ImageIcon(javaClass.getResource(enabledURL)) + disabledIcon = ImageIcon(javaClass.getResource(disabledURL)) + addMouseListener(object : MouseListener { + override fun mouseClicked(p0: MouseEvent) { + onClickMethod.invoke(p0) + } + + override fun mousePressed(p0: MouseEvent?) {} + + override fun mouseReleased(p0: MouseEvent?) {} + + override fun mouseEntered(p0: MouseEvent) { + if(autoHandleMouse) isEnabled = true + hoverMethod.invoke(p0) + } + + override fun mouseExited(p0: MouseEvent) { + if(autoHandleMouse) isEnabled = false + mouseLeaveMethod.invoke(p0) + } + }) + } + + fun onClick(handler: (event: MouseEvent) -> Unit){ + onClickMethod = handler + } + + fun onMouseEnter(handler: (event: MouseEvent) -> Unit){ + hoverMethod = handler + } + + fun onMouseExit(handler: (event: MouseEvent) -> Unit){ + mouseLeaveMethod = handler + } + + fun scale(width: Int, height: Int){ + icon = ImageIcon((icon as ImageIcon).image.getScaledInstance(width,height, Image.SCALE_SMOOTH)) + disabledIcon = ImageIcon((disabledIcon as ImageIcon).image.getScaledInstance(width, height, Image.SCALE_SMOOTH)) + } +} \ No newline at end of file diff --git a/src/main/kotlin/Rs2MapEditor.kt b/src/main/kotlin/Rs2MapEditor.kt index b86a76b..c3a439c 100644 --- a/src/main/kotlin/Rs2MapEditor.kt +++ b/src/main/kotlin/Rs2MapEditor.kt @@ -3,21 +3,29 @@ import cacheops.cache.definition.data.MapTile import cacheops.cache.definition.data.UnderlayDefinition import cacheops.cache.definition.decoder.FloorUnderlayConfiguration import cacheops.cache.definition.decoder.MapTileParser +import cacheops.cache.definition.decoder.NPC +import cacheops.cache.definition.decoder.NPCSpawnParser import cacheops.cache.definition.encoder.MapTileWriter import com.displee.cache.CacheLibrary import com.formdev.flatlaf.FlatDarculaLaf +import const.Image.YELLOW_DOT import java.awt.* import java.awt.event.KeyEvent import java.awt.event.MouseAdapter import java.awt.event.MouseEvent +import java.lang.StringBuilder import javax.swing.* import javax.swing.border.* import kotlin.system.exitProcess object Rs2MapEditor { - val library = CacheLibrary.create("K:\\RSPSDev\\2-009Scape-578\\Server\\data\\cache") - var region = 12850 // Lumbridge + val library = CacheLibrary.create("/home/ceikry/IdeaProjects/rs09-remake/Server/data/cache/") + var region = 7474 // Lumbridge + var npcs = NPCSpawnParser.parseNPCSpawns(region) + val npcRows = ArrayList() + lateinit var mapPanel: MapPane + lateinit var npcPanel: NPCSpawnPanel @JvmStatic fun main(args: Array) { @@ -40,7 +48,7 @@ object Rs2MapEditor { // Stores the underlay ID for each point var colorPointMap: HashMap = hashMapOf() - val componentPointMap = hashMapOf() + val componentPointMap = hashMapOf() // Stores the underlay ID with the Underlay Definition lateinit var underlayMap: HashMap @@ -51,6 +59,8 @@ object Rs2MapEditor { var infoLabel = JLabel() var underlayInfo = JLabel("Right click a tile to get more information") var underlayLabel = JLabel() + val npcIdInput = JTextField() + var state = EditorState.NONE class MainFrame : JFrame("Praesecho Map Editor v0.3") { init { @@ -67,6 +77,8 @@ object Rs2MapEditor { maximumSize = Dimension(1567, 1100) defaultCloseOperation = EXIT_ON_CLOSE jMenuBar = MenuBar() + mapPanel = MapPane() + npcPanel = NPCSpawnPanel() add(SettingsPane(), BorderLayout.PAGE_START) add(ScrollPane(), BorderLayout.CENTER) add(ScrollPane2(), BorderLayout.EAST) @@ -80,38 +92,82 @@ object Rs2MapEditor { } } - class GridPane : JPanel() { + class MapPane : JPanel() { + val gbc = GridBagConstraints() + val mapHeight = 64 + val mapWidth = 64 init { layout = GridBagLayout() - val gbc = GridBagConstraints() - val height = 64 - val width = 64 gbc.fill = GridBagConstraints.NONE gbc.weightx = 1.0 - for (row in 0 until height) { - for (column in 0 until width) { + SwingUtilities.invokeLater { + for (row in 0 until mapHeight) { + for (column in 0 until mapWidth) { + gbc.gridx = column + gbc.gridy = row + val border: Border = + MatteBorder( + 1, + 1, + if (row == mapHeight) 1 else 0, + if (column == mapWidth) 1 else 0, + Color.GRAY + ) + val flippedY = (64 - row) - 1 + val point = Point(column, flippedY) + colorPointMap[point] = MapTileParser.definition.getTile(column, flippedY, 0).underlayId + val mapCell = MapCell() + mapCell.background = + underlayMap[MapTileParser.definition.getTile(column, flippedY, 0).underlayId]!!.getRGB() + mapCell.border = border + val npcsHere = npcs.filter { it.x == column && it.y == flippedY && it.plane == 0 }.toList() + if (npcsHere.isNotEmpty()) { + mapCell.layout = BorderLayout() + mapCell.add(JLabel(YELLOW_DOT)) + } + componentPointMap[point] = mapCell + add(mapCell, gbc) + } + } + } + } + + fun repaintMap() { + colorPointMap.clear() + componentPointMap.clear() + this.removeAll() + for (row in 0 until mapHeight) { + for (column in 0 until mapWidth) { gbc.gridx = column gbc.gridy = row - val border: Border = MatteBorder(1, 1, if (row == height) 1 else 0, if (column == width) 1 else 0, Color.GRAY) + val border: Border = + MatteBorder(1, 1, if (row == mapHeight) 1 else 0, if (column == mapWidth) 1 else 0, Color.GRAY) val flippedY = (64 - row) - 1 val point = Point(column, flippedY) colorPointMap[point] = MapTileParser.definition.getTile(column, flippedY, 0).underlayId - val cellPane = CellPane() - cellPane.background = underlayMap[MapTileParser.definition.getTile(column, flippedY, 0).underlayId]!!.getRGB() - cellPane.border = border - componentPointMap[point] = cellPane - add(cellPane, gbc) + val mapCell = MapCell() + mapCell.background = + underlayMap[MapTileParser.definition.getTile(column, flippedY, 0).underlayId]!!.getRGB() + mapCell.border = border + val npcsHere = npcs.filter { it.x == column && it.y == flippedY && it.plane == 0 }.toList() + if (npcsHere.isNotEmpty()) { + mapCell.layout = BorderLayout() + mapCell.add(JLabel(YELLOW_DOT)) + } + componentPointMap[point] = mapCell + add(mapCell, gbc) } } } } - class CellPane : JPanel() { + class MapCell : JPanel() { var defaultBackground: Color = background override fun getPreferredSize(): Dimension { return Dimension(cellX, cellY) } init { + //add(underlayLabel) addMouseListener(object : MouseAdapter() { override fun mouseEntered(e: MouseEvent) { @@ -124,7 +180,11 @@ object Rs2MapEditor { "Viewing Underlay: ${MapTileParser.definition.getTile(selectedPointX, selectedPointY, 0).underlayId} | " + "Selected Underlay: ${if (selectedUnderlayId == null) "No underlay selected!" else "$selectedUnderlayId"}" defaultBackground = background - background = Color.BLUE + if(state == EditorState.SET_UNDERLAY || state == EditorState.NONE) { + background = Color.BLUE + } else if (state == EditorState.ADD_NPC || state == EditorState.ADD_GROUNDITEM){ + background = Color.YELLOW + } } override fun mouseExited(e: MouseEvent) { @@ -133,7 +193,7 @@ object Rs2MapEditor { override fun mouseClicked(e: MouseEvent) { if (SwingUtilities.isLeftMouseButton(e)) { - if (selectedUnderlayId != null) { + if (selectedUnderlayId != null && state == EditorState.SET_UNDERLAY) { val originalTile = MapTileParser.definition.getTile(selectedPointX, selectedPointY, 0) val underlayID = selectedUnderlayId val point = Point(selectedPointX, selectedPointY) @@ -146,29 +206,65 @@ object Rs2MapEditor { // Update map tile cacheops.definitions with new tile information MapTileParser.definition.setTile(point.x, point.y, 0, MapTile(originalTile.height, originalTile.attrOpcode, originalTile.overlayId, originalTile.overlayPath, originalTile.overlayRotation, originalTile.settings, underlayID)) } + else if (npcIdInput.text.isNotEmpty() && state == EditorState.ADD_NPC){ + val id = npcIdInput.text.toIntOrNull() + if(id == null){ + state = EditorState.NONE + return + } + + val npc = NPC(id, selectedPointX, selectedPointY, 0) + npcs.add(npc) + componentPointMap.filter { it.key.x == selectedPointX && it.key.y == selectedPointY }.forEach {(_, cell) -> + var hasLabel = false + cell.components.forEach { if(it is JLabel) hasLabel = true } + cell.layout = BorderLayout() + if(!hasLabel) cell.add(JLabel(YELLOW_DOT)) + cell.repaint() + } + npcPanel.redrawRows() + } } else if (SwingUtilities.isRightMouseButton(e)) { val point = Point(selectedPointX, selectedPointY) val def = MapTileParser.definition.getTile(point.x, point.y, 0) - underlayInfo.text = - String.format("%s", - "

Tile [${point.x}, ${point.y}] Information


" + - "

" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "
UnderlayIDHeightSettings
${def.underlayId}${def.height}${def.settings}
") + val string = StringBuilder() + .append("

Tile [${point.x}, ${point.y}] Information


") + .append("

") + .append("") + .append("") + .append("") + .append("") + .append("") + .append("") + .append("") + .append("") + .append("") + .append("") + .append("") + .append("") + .append("") + .append("") + .append("") + .append("
UnderlayIDHeightSettings
${def.underlayId}${def.height}${def.settings}
") + .append("
") + .append("") + .append("") + .append("") + .append("") + .append("") + .append("") + .append("") + .append("") + val npcsHere = npcs.filter { println(it.definition.name + " ${it.x}:${it.y}:${it.plane}"); it.x == selectedPointX && it.y == selectedPointY && it.plane == 0 }.toList() + for(npc in npcsHere){ + string.append("") + string.append("") + string.append("") + string.append("") + } + println("${selectedPointX} : ${selectedPointY}") + string.append("
NPC IDName
${npc.id}${npc.definition.name}(${npc.definition.combat})

") + underlayInfo.text = String.format("%s", string.toString()) } } }) @@ -178,7 +274,7 @@ object Rs2MapEditor { class ScrollPane : JPanel() { init { // Scroll bar - val scrollFrame = JScrollPane(GridPane()) + val scrollFrame = JScrollPane(mapPanel) scrollFrame.maximumSize = Dimension(1300, 980) scrollFrame.preferredSize = Dimension(1300, 980) add(scrollFrame, BorderLayout.CENTER) @@ -201,17 +297,16 @@ object Rs2MapEditor { val underlayComponent: JComponent = FLUGridPanel() val panel3: JComponent = makeTextPanel("Howdy Ho! I'm an overlay panel, or soon will be that is!") - preferredSize = Dimension(230, 980) + preferredSize = Dimension(230, 4000) border = CompoundBorder(LineBorder(Color.DARK_GRAY), EmptyBorder(0, 0, 0, 0)) - - addTab("Information", informationComponent) setMnemonicAt(0, KeyEvent.VK_1) add("Underlays", underlayComponent) setMnemonicAt(1, KeyEvent.VK_2) addTab("Overlays", panel3) setMnemonicAt(2, KeyEvent.VK_3) + add("NPC Spawn", npcPanel) } } @@ -253,6 +348,96 @@ object Rs2MapEditor { } } + class NPCSpawnPanel : JPanel() { + val rowBorder = MatteBorder(1,1,1,1,Color.WHITE) + init { + layout = BoxLayout(this, BoxLayout.PAGE_AXIS) + + add(NPCSpawnerPanel()) + + npcs.forEach { + val row = NPCRow(it, this) + row.border = rowBorder + npcRows.add(row) + add(row) + } + } + + fun redrawRows() { + npcRows.forEach { + remove(it) + } + npcRows.clear() + npcs.forEach { + val row = NPCRow(it, this) + row.border = rowBorder + npcRows.add(row) + add(row) + } + repaint() + } + + class NPCSpawnerPanel : JPanel() { + init { + val addButton = ImgButton("add_hi.png","add_dark.png") + addButton.onClick { + state = EditorState.ADD_NPC + } + npcIdInput.minimumSize = Dimension(200,25) + npcIdInput.maximumSize = Dimension(200,25) + npcIdInput.preferredSize = Dimension(200,25) + minimumSize = Dimension(300, 60) + preferredSize = Dimension(300, 60) + maximumSize = Dimension(300, 60) + + layout = FlowLayout() + add(npcIdInput) + add(addButton) + border = MatteBorder(1,1,1,1, Color.GREEN) + } + } + + class NPCRow(val npc: NPC, val parent: JPanel) : JPanel(){ + init { + layout = FlowLayout() + add(JLabel("${npc.definition.name} (LVL ${npc.definition.combat}) [${npc.x} | ${npc.y} | ${npc.plane}]")) + val deleteButton = ImgButton("trash_hi.png","trash_dark.png") + add(deleteButton) + minimumSize = Dimension(300, 40) + preferredSize = Dimension(300, 40) + maximumSize = Dimension(300, 40) + deleteButton.onClick { + npcs.remove(npc) + parent.remove(this) + parent.repaint() + npcRows.remove(this) + if(npcs.filter { it.x == npc.x && it.y == npc.y }.isEmpty()){ + componentPointMap.filter { it.key.x == npc.x && it.key.y == npc.y }.forEach { (_,cell) -> + cell.components.forEach { c -> + if(c is JLabel) cell.remove(c) + } + cell.repaint() + } + } + } + + addMouseListener(object : MouseAdapter(){ + val border = MatteBorder(1,1,1,1,Color.YELLOW) + val defaultBorder = MatteBorder(1,1,1,1, Color.GRAY) + override fun mouseEntered(e: MouseEvent?) { + super.mouseEntered(e) + componentPointMap.filter { it.key.x == npc.x && it.key.y == npc.y }.forEach { it.value.border = border } + } + + override fun mouseExited(e: MouseEvent?) { + super.mouseExited(e) + componentPointMap.filter { it.key.x == npc.x && it.key.y == npc.y }.forEach{ it.value.border = defaultBorder } + } + }) + } + } + } + class UnderlaySquarePanel(color: Color, size: Int, val id: Any) : JPanel() { init { when (id) { @@ -291,7 +476,12 @@ object Rs2MapEditor { if (SwingUtilities.isLeftMouseButton(e)) { when (id) { is Int -> { - selectedUnderlayId = id + if(selectedUnderlayId == id){ + state = EditorState.NONE + } else { + state = EditorState.SET_UNDERLAY + selectedUnderlayId = id + } statusLabel.text = "Region: $region | " + "Local Coordinates: [$selectedPointX, $selectedPointY] | " + "Global Coordinates: [${MapTileParser.coordinateX(selectedPointX)}, ${MapTileParser.coordinateX(selectedPointY)}] | " + @@ -417,3 +607,13 @@ object Rs2MapEditor { } } } + +enum class EditorState{ + NONE, + SET_UNDERLAY, + SET_OVERLAY, + ADD_NPC, + ADD_GROUNDITEM, + DEL_NPC, + DEL_GROUNDITEM +} \ No newline at end of file diff --git a/src/main/kotlin/cacheops/cache/definition/decoder/NPCDecoder.kt b/src/main/kotlin/cacheops/cache/definition/decoder/NPCDecoder.kt index cba04ff..cc1708c 100644 --- a/src/main/kotlin/cacheops/cache/definition/decoder/NPCDecoder.kt +++ b/src/main/kotlin/cacheops/cache/definition/decoder/NPCDecoder.kt @@ -8,12 +8,27 @@ import const.Indices class NPCDecoder(cache: Cache, val member: Boolean) : DefinitionDecoder(cache, Indices.CONFIGURATION_NPCS) { + companion object { + val DEFINITIONS = HashMap() + } + override fun create() = NPCDefinition() override fun getFile(id: Int) = id and 0x7f override fun getArchive(id: Int) = id ushr 7 + override fun readData(id: Int): NPCDefinition? { + return super.readData(id) + } + + fun forId(id : Int): NPCDefinition { + if(DEFINITIONS[id] != null) return DEFINITIONS[id]!! + val def = readData(id) + DEFINITIONS[id] = def!! + return def + } + override fun NPCDefinition.read(opcode: Int, buffer: Reader) { when (opcode) { 1 -> { diff --git a/src/main/kotlin/cacheops/cache/definition/decoder/NPCSpawnDecoder.kt b/src/main/kotlin/cacheops/cache/definition/decoder/NPCSpawnDecoder.kt index d2f3738..2662a3e 100644 --- a/src/main/kotlin/cacheops/cache/definition/decoder/NPCSpawnDecoder.kt +++ b/src/main/kotlin/cacheops/cache/definition/decoder/NPCSpawnDecoder.kt @@ -1,42 +1,49 @@ package cacheops.cache.definition.decoder import Rs2MapEditor +import cacheops.cache.definition.data.NPCDefinition +import const.Image +import const.cache import ext.unsignedShort import java.nio.ByteBuffer +val decoder = NPCDecoder(cache, true) + class NPCSpawnDecoder { - fun decode(data: ByteArray) { + fun decode(data: ByteArray): ArrayList { val buffer = ByteBuffer.wrap(data) + val list = ArrayList() while (buffer.hasRemaining()) { val coords = buffer.unsignedShort() val lz = coords shr 14 - val ly = (coords shr 7) and 0x3f - val lx = coords and 0x3f + val lx = (coords shr 7) and 0x3f + val ly = coords and 0x3f val npcID = buffer.unsignedShort() - println("Npc: $npcID | Spawn: [$lx, $ly, $lz]") + list.add(NPC(npcID, lx, ly, lz)) } + return list } } object NPCSpawnParser { - fun parseNPCSpawns() { - // Meh 18000 is good enough for now and covers all of our regions - for (regionIDs in 0..18000) { - val x = (regionIDs shr 8) and 0xFF - val y = regionIDs and 0xFF - val npcSpawnData = Rs2MapEditor.library.data(5, "n${x}_${y}") + fun parseNPCSpawns(regionId: Int): ArrayList { + val x = (regionId shr 8) and 0xFF + val y = regionId and 0xFF + val npcSpawnData = Rs2MapEditor.library.data(5, "n${x}_${y}") - if (npcSpawnData != null && npcSpawnData.isNotEmpty()) { - println("NPC DATA FOR $regionIDs") - NPCSpawnDecoder().decode(npcSpawnData) - println("\n") - } + if (npcSpawnData != null && npcSpawnData.isNotEmpty()) { + return NPCSpawnDecoder().decode(npcSpawnData) } + return ArrayList() } +} + +class NPC(val id: Int, val x: Int, val y: Int, val plane: Int) { + val definition: NPCDefinition = decoder.forId(id) } \ No newline at end of file diff --git a/src/main/kotlin/const/Image.kt b/src/main/kotlin/const/Image.kt new file mode 100644 index 0000000..11edeb6 --- /dev/null +++ b/src/main/kotlin/const/Image.kt @@ -0,0 +1,9 @@ +package const + +import javax.imageio.ImageIO +import javax.swing.ImageIcon + +object Image { + val RED_DOT = ImageIcon(ImageIO.read(javaClass.getResource("/dot_red.png"))) + val YELLOW_DOT = ImageIcon(ImageIO.read(javaClass.getResource("/dot_yellow.png"))) +} \ No newline at end of file diff --git a/src/main/kotlin/const/Misc.kt b/src/main/kotlin/const/Misc.kt new file mode 100644 index 0000000..376b131 --- /dev/null +++ b/src/main/kotlin/const/Misc.kt @@ -0,0 +1,6 @@ +package const + +import cacheops.cache.Cache +import cacheops.cache.CacheDelegate + +val cache: Cache = CacheDelegate("/home/ceikry/IdeaProjects/rs09-remake/Server/data/cache/") diff --git a/src/main/resources/add_dark.png b/src/main/resources/add_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..f0222f9ac88894984a07c37043a1f99722bb0a5f GIT binary patch literal 356 zcmV-q0h|7bP)d!;lrB8`QfY9gLCH z=xfM<4NHy;LKuYZ8W0941YvZ8VB#PQ3&ZdLcZgvyn>1xbuzCmCAY_c;Dr^R|u8M!n0<(!S5Tu|aC-Z+{Rx%jpBqw6v z?$*Zt-7WRF4FgHw(u~cpzK%vr!(fJFB}b!RBQ<3d3=_v`7>I*z2+W{0$9lmS*&vu< zllwXm2H|oIA%l=z1~v@lnv9qbtlmL32;G|~t^yIskwGX1Ej`o<#~?%2pO}CmhHTK( zNj-!OT6mxy1EU*+;v$eB$S`bPgBd`S*Fb>{GYCDfkqttZ&xnioziiQC$G!gE7<+ki`V^8!Yg#VUR(14M8tP ju%&jG0c4d+#1?D-q=|NrsB zpY(=e5s`oY6O(`c7dw&4aPok~a|b3Ko}T~tOp?_HWZXK;IG7JHGB_v8G);7HVFen( N;OXk;vd$@?2>_27B-8)^ literal 0 HcmV?d00001 diff --git a/src/main/resources/dot_yellow.png b/src/main/resources/dot_yellow.png new file mode 100644 index 0000000000000000000000000000000000000000..7d8ef5f2084ba1c9a7c324295581e2c276f30b0a GIT binary patch literal 115 zcmeAS@N?(olHy`uVBq!ia0vp^EI`c4!3HFg*81KCQdXWWjv*Ddk`ox3>i@I+Z#H1= z`TU&!^8fYmlK<}SJ^f)m;}OP(+w&#=&9^+v5OkDdT4RBP;w=Rp4t55s>1svmeEeSl PO=0kK^>bP0l+XkKx1}bz literal 0 HcmV?d00001 diff --git a/src/main/resources/trash_dark.png b/src/main/resources/trash_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..38876c7c4d1e4473ba59acbc79d4fc79ac8912ed GIT binary patch literal 330 zcmV-Q0k!^#P)PfglA&YRdnOwILY9M#qssUjKuATyPr( zl0eq1r=|dBgXEC;p*~*!tBSKR4Fd^iD@%hhx(h&Z6E{ILf%q^rAXhcjl%g7j%^+A< zzzl)WAgr$;2UZgw8H6wh<|U9JAm@WHOd~Fg91Nf^0~-c&ji#&!+*P=u9vNe}3YS43 zn?Z`P(Z~iRBO9cyAW0pAC~*-yO~C?AMM{VygJ2Pak~m=!FjpZPgpOebfz-jW3`#lz zDFV3)-7suX5Au?Mnk=S4$gV;+3?0Md>@4&M=Qof-kb&5+mVy}E5~7L`n1RR`Bt~|* cL~OwZ0Eo(n3Q+{@z5oCK07*qoM6N<$f=~!~DgXcg literal 0 HcmV?d00001 diff --git a/src/main/resources/trash_hi.png b/src/main/resources/trash_hi.png new file mode 100644 index 0000000000000000000000000000000000000000..800b5211209dd45065a6dc53b3624f106b2c599b GIT binary patch literal 323 zcmV-J0lfZ+P)!n0<(!S5Tu|aC-Z+{Rx%jpBqw6v z?$*Zt-7WRF4FgHw(rl!rjA2+`Mo7~rlFbJD#U>b2@WWzu%0~-c&O-4)zhHLO)3|E1Ou|_*AQa~0H%x|#3$A&=$;WY%k7{QkJU0RZ=^ V{)-@BEF=H`002ovPDHLkV1kRVeOmwk literal 0 HcmV?d00001