this repo has no description
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

add asset build system

- Build: A new parallel, incremental build system for Projects where Asset
subclasses define a build method that will produce the ingame representation
of that asset. It can output multiple artifacts, all of which will be combined
into a mapfs archive and patched onto the papermario-dx ROM.
- Project now owns the asset directories.
- AssetsArchive: mapfs archive reader/writer with linked list structure that
chains multiple archives together, enabling layered asset resolution where
project archives override base game archives.
- Projects can be built into ".diorama" files for distribution.
- PlayButton: One-click build-and-launch workflow that compiles engine ROM,
builds project assets, patches the ROM, and launches in ares.
- AssetsDir: Separates owned (project) vs read-only (engine) asset directories
- Revamped the CLI to be more cargo-like and added docs for it.

+4053 -1066
+289
CLI.md
··· 1 + # Star Rod Command-Line Interface 2 + 3 + Star Rod provides a comprehensive CLI for automation, scripting, and headless builds. 4 + 5 + ## General Commands 6 + 7 + ### Help 8 + ```bash 9 + java -jar StarRod.jar help 10 + ``` 11 + Display all available commands with usage examples. 12 + 13 + ### Version 14 + ```bash 15 + java -jar StarRod.jar version 16 + ``` 17 + Show version information. 18 + 19 + --- 20 + 21 + ## Project Building 22 + 23 + ### Build Diorama Package 24 + ```bash 25 + java -jar StarRod.jar build 26 + ``` 27 + Build a complete Diorama distribution package. 28 + 29 + **Output:** `.starrod/build/<mod-id>.diorama` 30 + 31 + **What it does:** 32 + 1. Compiles all project assets (maps, sprites, textures, etc.) 33 + 2. Compresses binary data using Yay0 compression 34 + 3. Packages into AssetsArchive format 35 + 4. Includes project.kdl manifest 36 + 5. Generates target.json with engine SHA and configuration 37 + 6. Packages everything into a TAR.GZ archive 38 + 39 + **Use cases:** 40 + - Standard build command for development 41 + - Creating release packages for distribution 42 + - Automated build pipelines 43 + - Continuous integration 44 + 45 + ### Build AssetsArchive Only 46 + ```bash 47 + java -jar StarRod.jar archive 48 + ``` 49 + Build only the AssetsArchive (assets.bin) without packaging into Diorama. 50 + 51 + **Output:** `.starrod/build/assets.bin` 52 + 53 + **What it does:** 54 + 1. Compiles all project assets (maps, sprites, textures, etc.) 55 + 2. Compresses binary data using Yay0 compression 56 + 3. Writes to `.starrod/build/assets.bin` 57 + 58 + **Use cases:** 59 + - Testing asset compilation without creating a full distribution 60 + - Integration with custom build pipelines 61 + - Rapid iteration during development 62 + 63 + --- 64 + 65 + ## Diorama Management 66 + 67 + ### Apply Diorama to ROM 68 + ```bash 69 + java -jar StarRod.jar apply <pmdx-file> <rom-file> [output-rom] 70 + ``` 71 + 72 + Apply a Diorama package to a ROM file. 73 + 74 + **Arguments:** 75 + - `<pmdx-file>`: Path to the .diorama package 76 + - `<rom-file>`: Path to the base ROM (papermario.z64) 77 + - `[output-rom]`: Optional output path (defaults to modifying rom-file in-place) 78 + 79 + **Examples:** 80 + ```bash 81 + # Apply to a copy 82 + java -jar StarRod.jar apply mymod.diorama baserom.z64 modded.z64 83 + 84 + # Apply in-place (modifies baserom.z64) 85 + java -jar StarRod.jar apply mymod.diorama baserom.z64 86 + ``` 87 + 88 + **Diorama Package Format:** 89 + A Diorama package is a TAR.GZ archive containing: 90 + - `project.kdl` - Project manifest with mod metadata 91 + - `target.json` - Target configuration (engine SHA, ROM addresses) 92 + - `assets.bin` - AssetsArchive binary 93 + 94 + **target.json example:** 95 + ```json 96 + { 97 + "papermario-dx": { 98 + "engine_sha": "a1b2c3d4e5f6...", 99 + "assets_archive_ROM_START": "0x1E40000" 100 + } 101 + } 102 + ``` 103 + 104 + **What it does:** 105 + 1. Extracts assets.bin from the Diorama package 106 + 2. Copies the base ROM to the output location (if specified) 107 + 3. Validates that the ROM is a compatible papermario-dx build 108 + 4. Applies the mod using intelligent patching strategy (see below) 109 + 110 + **ROM Patching Strategy:** 111 + 112 + The patcher automatically detects if the mod already exists in the ROM (by project name): 113 + 114 + - **If mod exists and new version fits:** Replaces in-place for optimal ROM size 115 + - **If mod exists but new version is larger:** Removes old version, appends new one 116 + - **If mod doesn't exist:** Appends to the end of the chain 117 + 118 + This means you can repeatedly apply the same mod during development without the ROM growing unbounded. Each re-application replaces the previous version if possible. 119 + 120 + **Technical Details:** 121 + - Validates ROM has AssetsArchive chain (refuses vanilla PM64 ROMs) 122 + - Aligns data to 16-byte boundaries (N64 requirement) 123 + - Updates linked list to maintain chain integrity 124 + - Supports chaining multiple different mods 125 + 126 + ### Inspect AssetsArchive 127 + ```bash 128 + java -jar StarRod.jar inspect <archive-file> 129 + ``` 130 + 131 + Display the contents and metadata of an AssetsArchive file. 132 + 133 + **Example:** 134 + ```bash 135 + java -jar StarRod.jar inspect .starrod/build/assets.bin 136 + ``` 137 + 138 + **Output:** 139 + ``` 140 + AssetsArchive Information: 141 + Magic: MAPFS 142 + Project: MyMod 143 + File size: 245760 bytes 144 + 145 + Table of Contents: 146 + Name Offset Comp. Size Decomp. Size 147 + ---------------------------------------------------------------------------------------------------- 148 + kmr_20_shape.bin 184 12345 15000 149 + kmr_20_collision.bin 12529 8192 10240 150 + custom_texture.bin 20721 4096 4096 151 + END DATA - - - 152 + 153 + Total: 3 entries 154 + Compressed: 24633 bytes 155 + Decompressed: 29336 bytes 156 + Compression: 84.0% 157 + ``` 158 + 159 + **Use cases:** 160 + - Debugging archive contents 161 + - Verifying compression ratios 162 + - Checking which assets are included 163 + - Inspecting downloaded Diorama packages 164 + 165 + --- 166 + 167 + ## AssetsArchive Format 168 + 169 + The AssetsArchive format is based on Paper Mario's internal "mapfs" filesystem: 170 + 171 + ### Binary Structure 172 + ``` 173 + [Header: 32 bytes] 174 + - Magic: "MAPFS " (6 bytes) 175 + - Project name: (16 bytes, null-padded) 176 + - Reserved: (10 bytes) 177 + 178 + [Table of Contents: N × 76 bytes] 179 + For each entry: 180 + - Name: (64 bytes, null-terminated) 181 + - Data offset: (4 bytes, big-endian) 182 + - Compressed size: (4 bytes, big-endian) 183 + - Decompressed size: (4 bytes, big-endian) 184 + 185 + [Sentinel Entry: 76 bytes] 186 + - Name: "END DATA\0" 187 + - Next node offset: (4 bytes) - 0 = end of chain 188 + - Compressed size: 0 189 + - Decompressed size: 0 190 + 191 + [Data Section] 192 + - Raw or Yay0-compressed binary data 193 + ``` 194 + 195 + ### Compression 196 + - Files ≥64 bytes are compressed using Yay0 (LZSS variant) 197 + - Files <64 bytes stored uncompressed 198 + - Binary artifacts (BINARY, SHAPE, COLLISION types) are compressed 199 + - Headers and small files stored uncompressed 200 + 201 + ### ROM Integration 202 + - AssetsArchive uses a linked list in ROM 203 + - Multiple mods can chain together 204 + - Default ROM address: 0x1E40000 205 + - Each node can point to the next via sentinel entry 206 + 207 + --- 208 + 209 + ## Automation Examples 210 + 211 + ### CI/CD Build Pipeline 212 + ```bash 213 + #!/bin/bash 214 + # Build and release a mod 215 + 216 + set -e 217 + 218 + # Build the Diorama package 219 + java -jar StarRod.jar pmdx 220 + 221 + # Verify the archive 222 + java -jar StarRod.jar inspect .starrod/build/assets.bin 223 + 224 + # Upload to release 225 + MOD_ID=$(grep 'id =' project.kdl | cut -d'"' -f2) 226 + cp .starrod/build/${MOD_ID}.diorama releases/ 227 + ``` 228 + 229 + ### Rapid Asset Testing 230 + ```bash 231 + #!/bin/bash 232 + # Quick rebuild and inspect 233 + 234 + java -jar StarRod.jar archive 235 + java -jar StarRod.jar inspect .starrod/build/assets.bin 236 + ``` 237 + 238 + ### Iterative Development 239 + ```bash 240 + #!/bin/bash 241 + # Repeatedly apply your mod during development 242 + # The ROM won't grow each time - your mod gets replaced in-place 243 + 244 + while true; do 245 + # Make changes to your project... 246 + java -jar StarRod.jar pmdx 247 + java -jar StarRod.jar apply mymod.diorama papermario-dx.z64 test.z64 248 + # Test in emulator... 249 + done 250 + 251 + # Result: test.z64 stays roughly the same size because your mod 252 + # is replaced each iteration rather than appended 253 + ``` 254 + 255 + ### Multi-Mod ROM Creation 256 + ```bash 257 + #!/bin/bash 258 + # Apply multiple mods to a base ROM 259 + 260 + BASE_ROM="papermario.z64" 261 + OUTPUT_ROM="modded.z64" 262 + 263 + # Copy base 264 + cp "$BASE_ROM" "$OUTPUT_ROM" 265 + 266 + # Apply mods in sequence 267 + java -jar StarRod.jar apply mod1.diorama "$OUTPUT_ROM" 268 + java -jar StarRod.jar apply mod2.diorama "$OUTPUT_ROM" 269 + java -jar StarRod.jar apply mod3.diorama "$OUTPUT_ROM" 270 + 271 + echo "Multi-mod ROM created: $OUTPUT_ROM" 272 + ``` 273 + 274 + --- 275 + 276 + ## Exit Codes 277 + 278 + - `0`: Success 279 + - `1`: Error (build failed, file not found, etc.) 280 + 281 + --- 282 + 283 + ## Notes 284 + 285 + - All commands must be run from a valid project directory (except -HELP and -VERSION) 286 + - Paths can be relative or absolute 287 + - File extensions are important (.diorama, .z64, .bin) 288 + - Build output goes to `.starrod/build/` directory 289 + - ROM files are modified in-place unless an output path is specified
+1
build.gradle.kts
··· 88 88 implementation("commons-io:commons-io:2.16.1") 89 89 implementation("org.apache.commons:commons-text:1.12.0") 90 90 implementation("org.apache.commons:commons-lang3:3.14.0") 91 + implementation("org.apache.commons:commons-compress:1.26.0") 91 92 92 93 implementation("com.miglayout:miglayout-core:11.3") 93 94 implementation("com.miglayout:miglayout-swing:11.3")
+8
gradle/verification-metadata.xml
··· 549 549 <sha256 value="6e5bfc870822143f1ffbb2c6f08ec488772db314a5283302f4814b126b346939" origin="Generated by Gradle"/> 550 550 </artifact> 551 551 </component> 552 + <component group="org.apache.commons" name="commons-compress" version="1.26.0"> 553 + <artifact name="commons-compress-1.26.0.jar"> 554 + <sha256 value="051aceb8bbcc62d0f5b2b8ac72c53767f9c59bfbd050151e65bef6f51c8ed9c9" origin="Generated by Gradle"/> 555 + </artifact> 556 + <artifact name="commons-compress-1.26.0.pom"> 557 + <sha256 value="c1c9681f88d9421730c4da6176492bbaa54052a2326df9e40bc34f4c405227af" origin="Generated by Gradle"/> 558 + </artifact> 559 + </component> 552 560 <component group="org.apache.commons" name="commons-csv" version="1.11.0"> 553 561 <artifact name="commons-csv-1.11.0.jar"> 554 562 <sha256 value="b697fe3f94cfc4f7e2a87bddf78d15cd10d8c86cbe56ae9196a62d6edbf6b76d" origin="Generated by Gradle"/>
+13 -3
src/main/java/app/BuildOutputDialog.java
··· 39 39 40 40 private BuildEnvironment buildEnv; 41 41 private boolean buildComplete = false; 42 + private java.util.function.Consumer<BuildResult> onComplete; 42 43 43 44 public BuildOutputDialog(Frame parent) 44 45 { ··· 90 91 setLocationRelativeTo(parent); 91 92 } 92 93 94 + /** Sets a callback to be invoked when the build completes. */ 95 + public void setOnComplete(java.util.function.Consumer<BuildResult> onComplete) 96 + { 97 + this.onComplete = onComplete; 98 + } 99 + 93 100 /** 94 101 * Starts the build process asynchronously. 95 102 */ ··· 99 106 100 107 // Create build environment on background thread 101 108 Environment.getExecutor().submit(() -> { 102 - File projectDir = Environment.getProject().getDirectory(); 103 109 buildEnv = Environment.getProject().getEngine().getBuildEnvironment(); 104 110 105 111 try { 106 - buildEnv.configure(projectDir, getOutputListener()); 112 + buildEnv.configure(getOutputListener()); 107 113 } 108 114 catch (IOException | BuildException e) { 109 115 SwingUtilities.invokeLater(() -> { ··· 113 119 return; 114 120 } 115 121 116 - buildEnv.buildAsync(projectDir, getOutputListener()).thenAccept(result -> { 122 + buildEnv.buildAsync(getOutputListener()).thenAccept(result -> { 117 123 SwingUtilities.invokeLater(() -> handleBuildComplete(result)); 118 124 }).exceptionally(ex -> { 119 125 SwingUtilities.invokeLater(() -> handleBuildError(ex)); ··· 169 175 170 176 if (buildEnv != null) { 171 177 buildEnv = null; 178 + } 179 + 180 + if (onComplete != null) { 181 + onComplete.accept(result); 172 182 } 173 183 174 184 switch (result.getStatus()) {
+4 -4
src/main/java/app/Directories.java
··· 53 53 // Directories relative to the current project's engine (papermario-dx) 54 54 55 55 ENGINE_SRC (Root.ENGINE, "/src/"), 56 - ENGINE_SRC_WORLD (Root.ENGINE, ENGINE_SRC, "/world/"), 57 - ENGINE_SRC_STAGE (Root.ENGINE, ENGINE_SRC, "/battle/common/stage/"), 56 + ENGINE_SRC_WORLD (Root.ENGINE, ENGINE_SRC, "/world/"), 57 + ENGINE_SRC_STAGE (Root.ENGINE, ENGINE_SRC, "/battle/common/stage/"), 58 58 ENGINE_INCLUDE (Root.ENGINE, "/include/"), 59 - ENGINE_INCLUDE_MAPFS (Root.ENGINE, ENGINE_INCLUDE, "/mapfs/"), 60 - ENGINE_ASSETS_US (Root.ENGINE, "/assets/us/"); 59 + ENGINE_INCLUDE_MAPFS(Root.ENGINE, ENGINE_INCLUDE, "/mapfs/"), 60 + US_MAPFS (Root.ENGINE, "/assets/us/mapfs/"); 61 61 62 62 // @formatter:on 63 63 //=======================================================================================
+8 -19
src/main/java/app/Environment.java
··· 5 5 import java.awt.Image; 6 6 import java.awt.Toolkit; 7 7 import java.io.File; 8 - import java.io.FileInputStream; 9 8 import java.io.IOException; 10 9 import java.io.InputStreamReader; 11 10 import java.net.HttpURLConnection; ··· 16 15 import java.security.CodeSource; 17 16 import java.util.ArrayList; 18 17 import java.util.List; 19 - import java.util.Map; 20 - import java.util.Properties; 21 18 import java.util.concurrent.ExecutorService; 22 19 import java.util.concurrent.Executors; 23 20 import java.util.jar.Attributes; ··· 26 23 27 24 import javax.imageio.ImageIO; 28 25 import javax.swing.ImageIcon; 29 - import javax.swing.SwingUtilities; 30 26 import javax.swing.UIManager; 31 27 32 28 import org.apache.commons.io.FileExistsException; 33 29 import org.apache.commons.io.FileUtils; 34 30 import org.apache.commons.lang3.SystemUtils; 35 - import org.yaml.snakeyaml.Yaml; 36 31 37 32 import com.google.gson.JsonObject; 38 33 import com.google.gson.JsonParser; ··· 56 51 import game.ProjectDatabase; 57 52 import game.entity.EntityExtractor; 58 53 import game.map.editor.ui.dialogs.ChooseDialogResult; 59 - import game.map.editor.ui.dialogs.DirChooser; 60 54 import game.map.editor.ui.dialogs.OpenFileChooser; 61 55 import game.message.font.FontManager; 62 56 import util.Logger; ··· 104 98 105 99 private static final File usBaseRom = Directories.BASEROM.file("papermario.us.z64"); 106 100 private static ByteBuffer romBytes; 107 - 108 - public static List<File> assetDirectories; 109 101 110 102 private static boolean initialized = false; 111 103 ··· 537 529 538 530 LoadingBar.show("Loading project", Priority.MILESTONE, true); 539 531 540 - try { 541 - engine.splitAssets(); 542 - } catch (BuildException e) { 543 - Logger.logError("Failed to split assets: " + e.getMessage()); 544 - return false; 532 + // Only split assets if they don't already exist 533 + if (!Directories.US_MAPFS.toFile().exists()) { 534 + try { 535 + engine.splitAssets(); 536 + } catch (BuildException e) { 537 + Logger.logError("Failed to split assets: " + e.getMessage()); 538 + return false; 539 + } 545 540 } 546 - 547 - // Asset stack (TODO: move to Project?) 548 - assetDirectories = new ArrayList<>(); 549 - assetDirectories.add(project.getDirectory()); 550 - // ...dependencies... 551 - assetDirectories.add(Directories.ENGINE_ASSETS_US.toFile()); 552 541 553 542 if (!ensureDumpExtracted()) { 554 543 return false;
+212
src/main/java/app/PlayButton.kt
··· 1 + package app 2 + 3 + import assets.archive.AssetsArchiveRomPatcher 4 + import project.Build 5 + import project.engine.BuildResult 6 + import util.Logger 7 + import util.ui.ThemedIcon 8 + import java.awt.Color 9 + import java.awt.Dimension 10 + import java.io.File 11 + import java.io.IOException 12 + import java.nio.file.Files 13 + import java.nio.file.StandardCopyOption 14 + import java.util.concurrent.TimeUnit 15 + import javax.swing.JButton 16 + import javax.swing.JOptionPane 17 + import javax.swing.SwingUtilities 18 + import javax.swing.UIManager 19 + import kotlin.io.path.div 20 + 21 + /** 22 + * Button that builds the ROM and launches it in ares emulator. 23 + * Tracks three states: IDLE, BUILDING, RUNNING. 24 + */ 25 + class PlayButton : JButton() { 26 + private enum class State { 27 + IDLE, BUILDING, RUNNING 28 + } 29 + 30 + private var currentState = State.IDLE 31 + private var aresProcess: Process? = null 32 + 33 + init { 34 + icon = ThemedIcon.PLAY_24 35 + toolTipText = "Run" 36 + getAccessibleContext().accessibleName = "playButton" 37 + addActionListener { handleClick() } 38 + preferredSize = Dimension(48, 48) 39 + isFocusable = false 40 + } 41 + 42 + private fun handleClick() { 43 + when (currentState) { 44 + State.IDLE -> buildAndLaunch() 45 + State.BUILDING -> {} // Cancel not implemented yet 46 + State.RUNNING -> killAndRestart() 47 + } 48 + } 49 + 50 + private fun setState(newState: State) { 51 + currentState = newState 52 + SwingUtilities.invokeLater { 53 + when (currentState) { 54 + State.IDLE -> { 55 + icon = ThemedIcon.PLAY_24 56 + isEnabled = true 57 + background = UIManager.getColor("Button.background") 58 + isOpaque = false 59 + isContentAreaFilled = true 60 + toolTipText = "Run" 61 + } 62 + 63 + State.BUILDING -> { 64 + isEnabled = false 65 + toolTipText = "Building..." 66 + } 67 + 68 + State.RUNNING -> { 69 + isEnabled = true 70 + background = Color(87, 150, 92) 71 + foreground = Color.WHITE 72 + isOpaque = true 73 + isContentAreaFilled = true 74 + toolTipText = "Restart" 75 + } 76 + } 77 + } 78 + } 79 + 80 + private fun buildAndLaunch() { 81 + setState(State.BUILDING) 82 + 83 + // Phase 1: Build engine ROM with output dialog 84 + Logger.log("Building engine ROM...") 85 + val dialog = BuildOutputDialog(null) 86 + dialog.setOnComplete { engineResult -> 87 + if (!engineResult.isSuccess) { 88 + setState(State.IDLE) 89 + return@setOnComplete 90 + } 91 + 92 + dialog.dispose() 93 + 94 + // Continue with asset build and launch in background 95 + continueAfterEngineBuild(engineResult) 96 + } 97 + dialog.startBuild() 98 + } 99 + 100 + private fun continueAfterEngineBuild(engineResult: BuildResult) { 101 + Environment.getExecutor().submit { 102 + try { 103 + val project = Environment.getProject() 104 + val projectDir = project.directory 105 + 106 + // Phase 2: Build project assets into AssetsArchive 107 + Logger.log("Building project assets...") 108 + val assetBuild = Build(project) 109 + val assetsSuccess = assetBuild.executeAsync(generateArchive = true, generateDiorama = false).get() 110 + 111 + if (!assetsSuccess) { 112 + SwingUtilities.invokeLater { 113 + setState(State.IDLE) 114 + JOptionPane.showMessageDialog( 115 + this, 116 + "Asset build failed. Check the log for details.", 117 + "Build Failed", 118 + JOptionPane.ERROR_MESSAGE 119 + ) 120 + } 121 + return@submit 122 + } 123 + 124 + // Phase 3: Apply AssetsArchive to engine ROM 125 + Logger.log("Patching ROM with assets...") 126 + val engineRom = engineResult.outputRom.orElseThrow() 127 + val projectPath = projectDir.toPath() 128 + val archivePath = projectPath / ".starrod" / "build" / "assets.bin" 129 + val patchedRomPath = projectPath / ".starrod" / "build" / "${project.name}.z64" 130 + 131 + // Copy engine ROM to patched location 132 + Files.copy( 133 + engineRom.toPath(), 134 + patchedRomPath, 135 + StandardCopyOption.REPLACE_EXISTING 136 + ) 137 + 138 + // Apply AssetsArchive patch 139 + val patcher = AssetsArchiveRomPatcher(patchedRomPath) 140 + patcher.applyArchive(archivePath, engineResult.outputSyms.orElseThrow()) 141 + 142 + // Phase 4: Launch ares 143 + Logger.log("Launching ares...") 144 + SwingUtilities.invokeLater { launchAres(patchedRomPath.toFile()) } 145 + 146 + } catch (e: Exception) { 147 + SwingUtilities.invokeLater { 148 + setState(State.IDLE) 149 + StarRodMain.displayStackTrace(e) 150 + } 151 + } 152 + } 153 + } 154 + 155 + private fun launchAres(rom: File) { 156 + try { 157 + val pb = ProcessBuilder("ares", rom.absolutePath) 158 + pb.directory(Environment.getProjectDirectory()) 159 + 160 + aresProcess = pb.start() 161 + setState(State.RUNNING) 162 + 163 + // Monitor process exit 164 + Environment.getExecutor().submit { 165 + try { 166 + aresProcess!!.waitFor() 167 + SwingUtilities.invokeLater { 168 + if (currentState == State.RUNNING) { 169 + setState(State.IDLE) 170 + aresProcess = null 171 + } 172 + } 173 + } catch (e: InterruptedException) { 174 + // Killed intentionally 175 + } 176 + } 177 + 178 + } catch (e: IOException) { 179 + setState(State.IDLE) 180 + JOptionPane.showMessageDialog( 181 + this, 182 + "Could not start ares emulator:\n${e.message}", 183 + "Failed to Launch Ares", 184 + JOptionPane.ERROR_MESSAGE 185 + ) 186 + } 187 + } 188 + 189 + private fun killAndRestart() { 190 + aresProcess?.let { process -> 191 + if (process.isAlive) { 192 + process.destroyForcibly() 193 + try { 194 + process.waitFor(2, TimeUnit.SECONDS) 195 + } catch (e: InterruptedException) { 196 + Thread.currentThread().interrupt() 197 + } 198 + aresProcess = null 199 + } 200 + } 201 + buildAndLaunch() 202 + } 203 + 204 + /** Cleanup when application exits. */ 205 + fun cleanup() { 206 + aresProcess?.let { process -> 207 + if (process.isAlive) { 208 + process.destroyForcibly() 209 + } 210 + } 211 + } 212 + }
+268 -90
src/main/java/app/StarRodMain.java
··· 36 36 import assets.Asset; 37 37 import assets.AssetManager; 38 38 import assets.ExpectedAsset; 39 + import assets.ui.MapAsset; 39 40 import common.BaseEditor; 40 41 import game.globals.editor.GlobalsEditor; 41 42 import game.map.Map; ··· 49 50 import game.texture.editor.ImageEditor; 50 51 import game.worldmap.WorldMapEditor; 51 52 import net.miginfocom.swing.MigLayout; 53 + import project.build.BuildManager; 54 + import project.build.BuildStatusBar; 52 55 import tools.SwingInspectorKt; 53 56 import util.Logger; 54 57 ··· 59 62 private static final int MIN_WINDOW_WIDTH = MIN_PANE_WIDTH * 3 + 300; 60 63 private static final int MIN_WINDOW_HEIGHT = 600; 61 64 private static final int MIN_DOCK_HEIGHT = 140; 65 + 66 + private BuildManager buildManager; 67 + private PlayButton playButton; 62 68 63 69 public static void main(String[] args) throws InterruptedException 64 70 { ··· 73 79 boolean isCommandLine = args.length > 0 || GraphicsEnvironment.isHeadless(); 74 80 75 81 if (isCommandLine) { 82 + // Handle help command before initialization 83 + if (args.length > 0 && (args[0].equalsIgnoreCase("-HELP") || args[0].equalsIgnoreCase("-H") || args[0].equalsIgnoreCase("--HELP"))) { 84 + printHelp(); 85 + return; 86 + } 87 + 76 88 Environment.initialize(true); 77 89 runCommandLine(args); 78 90 Environment.exit(); ··· 176 188 }); 177 189 buttons.add(themesMenuButton); 178 190 179 - // Build Project button 180 - JButton buildProjectButton = new JButton("Build Project"); 181 - trySetIcon(buildProjectButton, ExpectedAsset.ICON_GOLD); 182 - SwingUtils.setFontSize(buildProjectButton, 12); 183 - buildProjectButton.getAccessibleContext().setAccessibleName("buildProjectButton"); 184 - buildProjectButton.addActionListener((e) -> { 185 - action_buildProject(); 186 - }); 187 - buttons.add(buildProjectButton); 188 - 189 191 // Open directories buttons 190 192 JButton openConfigDirButton = new JButton("Open Config Dir"); 191 193 trySetIcon(openConfigDirButton, ExpectedAsset.ICON_SILVER); ··· 222 224 .choose(); 223 225 224 226 if (choice == JOptionPane.OK_OPTION) { 227 + // Stop build manager before exiting 228 + if (buildManager != null) { 229 + buildManager.stop(); 230 + } 231 + // Cleanup play button 232 + if (playButton != null) { 233 + playButton.cleanup(); 234 + } 225 235 dispose(); 226 236 Environment.exit(); 227 237 } ··· 244 254 buttonsPanel.add(themesMenuButton, "growx"); 245 255 buttonsPanel.add(openConfigDirButton, "growx"); 246 256 buttonsPanel.add(openProjectDirButton, "growx"); 247 - buttonsPanel.add(buildProjectButton, "growx, gaptop 8"); 248 257 249 258 leftPane.add(buttonsPanel, "growx"); 250 259 leftPane.setMinimumSize(new Dimension(MIN_PANE_WIDTH, 0)); ··· 256 265 middlePane.add(new JLabel("Middle Pane"), "center"); 257 266 middlePane.setMinimumSize(new Dimension(MIN_PANE_WIDTH, 0)); 258 267 259 - // Right pane - placeholder for now 268 + // Right pane - play button 260 269 Pane rightPane = new Pane(); 261 270 rightPane.setLayout(new MigLayout("fill, ins 8")); 262 271 rightPane.getAccessibleContext().setAccessibleName("rightPane"); 263 - rightPane.add(new JLabel("Right Pane"), "center"); 272 + 273 + playButton = new PlayButton(); 274 + rightPane.add(playButton, "pos 0 0, w 48!, h 48!"); 275 + 264 276 rightPane.setMinimumSize(new Dimension(MIN_PANE_WIDTH, 0)); 265 277 266 278 // Dock (bottom panel in middle column) ··· 291 303 // Status bar 292 304 Bar statusBar = new Bar(); 293 305 306 + // Build status bar 307 + BuildStatusBar buildStatusBar = new BuildStatusBar(); 308 + statusBar.setBuildStatusBar(buildStatusBar); 309 + 310 + // Build manager - start background build and file watching 311 + buildManager = new BuildManager(Environment.getProject()); 312 + buildManager.addListener(buildStatusBar); 313 + buildManager.start(); 314 + 294 315 // Layout 295 316 setLayout(new MigLayout("fill, ins 4, gap 4, wrap")); 296 317 add(mainHorizontalSplit, "grow, push"); ··· 396 417 }); 397 418 } 398 419 399 - private void action_buildProject() 400 - { 401 - BuildOutputDialog dialog = new BuildOutputDialog(this); 402 - dialog.startBuild(); 403 - } 404 - 405 420 private void action_openDir(File dir) 406 421 { 407 422 if (dir.exists()) { ··· 476 491 } 477 492 } 478 493 494 + private static void printHelp() 495 + { 496 + System.out.println("Star Rod - Paper Mario Modding Toolkit"); 497 + System.out.println("Usage: java -jar StarRod.jar <command> [args...]"); 498 + System.out.println(); 499 + System.out.println("Commands:"); 500 + System.out.println(" help Show this help message"); 501 + System.out.println(" version Show version information"); 502 + System.out.println(); 503 + System.out.println("Project Building:"); 504 + System.out.println(" build Build complete Diorama distribution package"); 505 + System.out.println(" archive Build AssetsArchive only (assets.bin)"); 506 + System.out.println(); 507 + System.out.println("Package Management:"); 508 + System.out.println(" apply <diorama> <rom> [out] Apply Diorama package to ROM file"); 509 + System.out.println(" inspect <archive> Show contents of AssetsArchive file"); 510 + System.out.println(); 511 + System.out.println("Examples:"); 512 + System.out.println(" java -jar StarRod.jar build"); 513 + System.out.println(" java -jar StarRod.jar apply mymod.diorama papermario.z64 modded.z64"); 514 + System.out.println(" java -jar StarRod.jar inspect .starrod/build/assets.bin"); 515 + } 516 + 517 + private static void applyDioramaToRom(String dioramaFile, String romFile, String outputRom) throws Exception 518 + { 519 + Logger.log("Applying Diorama to ROM..."); 520 + Logger.log(" Diorama: " + dioramaFile); 521 + Logger.log(" ROM: " + romFile); 522 + Logger.log(" Output: " + outputRom); 523 + 524 + java.nio.file.Path dioramaPath = java.nio.file.Paths.get(dioramaFile); 525 + java.nio.file.Path romPath = java.nio.file.Paths.get(romFile); 526 + java.nio.file.Path outputPath = java.nio.file.Paths.get(outputRom); 527 + 528 + if (!java.nio.file.Files.exists(dioramaPath)) { 529 + throw new java.io.IOException("Diorama file not found: " + dioramaFile); 530 + } 531 + 532 + if (!java.nio.file.Files.exists(romPath)) { 533 + throw new java.io.IOException("ROM file not found: " + romFile); 534 + } 535 + 536 + // Copy ROM to output location if different 537 + if (!romPath.equals(outputPath)) { 538 + java.nio.file.Files.copy(romPath, outputPath, java.nio.file.StandardCopyOption.REPLACE_EXISTING); 539 + } 540 + 541 + // Apply Diorama 542 + assets.archive.AssetsArchiveRomPatcher patcher = new assets.archive.AssetsArchiveRomPatcher(outputPath); 543 + patcher.applyDiorama(dioramaPath); 544 + 545 + Logger.log("Diorama applied successfully!"); 546 + Logger.log("Output ROM: " + outputPath); 547 + } 548 + 549 + private static void inspectArchive(String archiveFile) throws Exception 550 + { 551 + Logger.log("Inspecting AssetsArchive: " + archiveFile); 552 + 553 + java.nio.file.Path archivePath = java.nio.file.Paths.get(archiveFile); 554 + 555 + if (!java.nio.file.Files.exists(archivePath)) { 556 + throw new java.io.IOException("Archive file not found: " + archiveFile); 557 + } 558 + 559 + byte[] data = java.nio.file.Files.readAllBytes(archivePath); 560 + 561 + // Parse header 562 + java.nio.ByteBuffer buffer = java.nio.ByteBuffer.wrap(data).order(java.nio.ByteOrder.BIG_ENDIAN); 563 + 564 + // Magic (6 bytes) 565 + byte[] magicBytes = new byte[6]; 566 + buffer.get(magicBytes); 567 + String magic = new String(magicBytes, java.nio.charset.StandardCharsets.US_ASCII); 568 + 569 + // Project name (16 bytes) 570 + byte[] nameBytes = new byte[16]; 571 + buffer.get(nameBytes); 572 + String projectName = new String(nameBytes, java.nio.charset.StandardCharsets.US_ASCII).trim().replaceAll("\0", ""); 573 + 574 + // Skip reserved (10 bytes) 575 + buffer.position(32); 576 + 577 + System.out.println(); 578 + System.out.println("AssetsArchive Information:"); 579 + System.out.println(" Magic: " + magic); 580 + System.out.println(" Project: " + projectName); 581 + System.out.println(" File size: " + data.length + " bytes"); 582 + System.out.println(); 583 + System.out.println("Table of Contents:"); 584 + System.out.println(" " + String.format("%-60s %12s %12s %12s", "Name", "Offset", "Comp. Size", "Decomp. Size")); 585 + System.out.println(" " + "-".repeat(100)); 586 + 587 + int entryCount = 0; 588 + long totalCompressed = 0; 589 + long totalDecompressed = 0; 590 + 591 + // Read TOC entries 592 + while (buffer.remaining() >= 76) { 593 + // Name (64 bytes) 594 + byte[] entryNameBytes = new byte[64]; 595 + buffer.get(entryNameBytes); 596 + String entryName = new String(entryNameBytes, java.nio.charset.StandardCharsets.US_ASCII).trim().replaceAll("\0", ""); 597 + 598 + // Offset, compressed size, decompressed size (4 bytes each) 599 + int offset = buffer.getInt(); 600 + int compressedSize = buffer.getInt(); 601 + int decompressedSize = buffer.getInt(); 602 + 603 + // Check for sentinel 604 + if (entryName.startsWith("END DATA")) { 605 + System.out.println(" " + String.format("%-60s %12s %12s %12s", entryName, "-", "-", "-")); 606 + if (offset != 0) { 607 + System.out.println(" (Next node at: 0x" + Integer.toHexString(offset) + ")"); 608 + } 609 + break; 610 + } 611 + 612 + System.out.println(" " + String.format("%-60s %12d %12d %12d", entryName, offset, compressedSize, decompressedSize)); 613 + 614 + entryCount++; 615 + totalCompressed += compressedSize; 616 + totalDecompressed += decompressedSize; 617 + } 618 + 619 + System.out.println(" " + "-".repeat(100)); 620 + System.out.println(" Total: " + entryCount + " entries"); 621 + System.out.println(" Compressed: " + totalCompressed + " bytes"); 622 + System.out.println(" Decompressed: " + totalDecompressed + " bytes"); 623 + 624 + if (totalDecompressed > 0) { 625 + double ratio = (double) totalCompressed / totalDecompressed * 100.0; 626 + System.out.println(" Compression: " + String.format("%.1f%%", ratio)); 627 + } 628 + 629 + System.out.println(); 630 + } 631 + 479 632 private static void runCommandLine(String[] args) 480 633 { 481 634 for (int i = 0; i < args.length; i++) { 482 - switch (args[i].toUpperCase()) { 483 - case "-VERSION": 635 + String cmd = args[i].toLowerCase(); 636 + 637 + switch (cmd) { 638 + case "help": 639 + case "-h": 640 + case "--help": 641 + // Backward compatibility 642 + case "-help": 643 + printHelp(); 644 + break; 645 + 646 + case "version": 647 + // Backward compatibility 648 + case "-version": 484 649 System.out.println("VERSION=" + Environment.getVersionString()); 485 650 break; 486 651 487 - case "-COMPILESHAPE": 488 - case "-COMPILEHIT": 489 - case "-GENERATESCRIPT": 490 - case "-COMPILEMAP": 491 - if (args.length > i + 1) { 492 - String mapName = args[i + 1]; 493 - Asset mapAsset = AssetManager.getMap(mapName); 652 + case "build": 653 + case "diorama": // Alias 654 + // Backward compatibility 655 + case "pmdx": 656 + case "-buildproject": 657 + case "-buildpmdx": 658 + try { 659 + Logger.log("Building Diorama package..."); 660 + project.Build build = new project.Build(Environment.getProject()); 661 + boolean success = build.executeAsync(true, true).get(); 662 + if (!success) { 663 + Logger.logError("Diorama build failed"); 664 + Environment.exit(1); 665 + } 666 + Logger.log("Diorama built successfully"); 667 + } 668 + catch (Exception e) { 669 + Logger.logError("Diorama build failed: " + e.getMessage()); 670 + Logger.printStackTrace(e); 671 + Environment.exit(1); 672 + } 673 + break; 494 674 495 - if (mapAsset == null) { 496 - Logger.logfError("Cannot find map '%s'!", mapName); 497 - break; 675 + case "archive": 676 + // Backward compatibility 677 + case "-buildarchive": 678 + try { 679 + Logger.log("Building AssetsArchive..."); 680 + project.Build build = new project.Build(Environment.getProject()); 681 + boolean success = build.executeAsync(true, false).get(); 682 + if (!success) { 683 + Logger.logError("AssetsArchive build failed"); 684 + Environment.exit(1); 498 685 } 686 + Logger.log("AssetsArchive built successfully: .starrod/build/assets.bin"); 687 + } 688 + catch (Exception e) { 689 + Logger.logError("AssetsArchive build failed: " + e.getMessage()); 690 + Logger.printStackTrace(e); 691 + Environment.exit(1); 692 + } 693 + break; 499 694 500 - Map map = Map.loadMap(mapAsset.getFile()); 695 + case "apply": 696 + // Backward compatibility 697 + case "-applypmdx": 698 + if (args.length > i + 2) { 699 + String dioramaFile = args[i + 1]; 700 + String romFile = args[i + 2]; 701 + String outputRom = args.length > i + 3 ? args[i + 3] : romFile; 702 + 501 703 try { 502 - if (args[i].equalsIgnoreCase("-CompileMap")) { 503 - new GeometryCompiler(map); 504 - new CollisionCompiler(map); 505 - } 506 - else if (args[i].equalsIgnoreCase("-CompileShape")) { 507 - new GeometryCompiler(map); 508 - } 509 - else if (args[i].equalsIgnoreCase("-CompileHit")) { 510 - new CollisionCompiler(map); 511 - } 512 - else if (args[i].equalsIgnoreCase("-GenerateScript")) { 513 - new ScriptGenerator(map); 514 - } 515 - else { 516 - throw new IllegalStateException(); 517 - } 704 + applyDioramaToRom(dioramaFile, romFile, outputRom); 705 + } 706 + catch (assets.archive.InvalidRomException e) { 707 + Logger.logError("Invalid ROM: " + e.getMessage()); 708 + Logger.logError(""); 709 + Logger.logError("To create a valid papermario-dx ROM:"); 710 + Logger.logError(" 1. Clone papermario-dx: git clone https://github.com/bates64/papermario-dx.git"); 711 + Logger.logError(" 2. Build the ROM: cd papermario-dx && ./configure && ninja"); 712 + Logger.logError(" 3. Use the built ROM (ver/current/papermario.z64) with 'apply'"); 713 + Environment.exit(1); 518 714 } 519 - catch (BuildException | IOException | InvalidInputException e) { 715 + catch (Exception e) { 716 + Logger.logError("Failed to apply Diorama: " + e.getMessage()); 520 717 Logger.printStackTrace(e); 718 + Environment.exit(1); 521 719 } 522 720 523 - i++; 721 + i += (args.length > i + 3) ? 3 : 2; 524 722 } 525 - else 526 - Logger.logfError("%s expects a mapName argument!", args[i]); 723 + else { 724 + Logger.logError("'apply' requires arguments: <diorama-file> <rom-file> [output-rom]"); 725 + Environment.exit(1); 726 + } 527 727 break; 528 728 529 - case "-COMPILEMAPS": 530 - try { 531 - File buildDir = AssetManager.getMapBuildDir(); 532 - for (Asset ah : AssetManager.getMapSources()) { 533 - // get existing compiled binaries 534 - String mapName = Map.deriveName(ah.getFile()); 535 - File binShape = new File(buildDir, mapName + "_shape.bin"); 536 - File binHit = new File(buildDir, mapName + "_hit.bin"); 729 + case "inspect": 730 + // Backward compatibility 731 + case "-inspectarchive": 732 + if (args.length > i + 1) { 733 + String archiveFile = args[i + 1]; 537 734 538 - // check if the map source is newer 539 - boolean buildShape = !binShape.exists() || binShape.lastModified() < ah.lastModified(); 540 - boolean buildHit = !binHit.exists() || binHit.lastModified() < ah.lastModified(); 541 - 542 - if (!buildShape && !buildHit) { 543 - continue; 544 - } 545 - 546 - try { 547 - Map map = Map.loadMap(ah.getFile()); 548 - if (buildShape) { 549 - new GeometryCompiler(map); 550 - } 551 - if (buildHit) { 552 - new CollisionCompiler(map); 553 - } 554 - } 555 - catch (IOException | BuildException e) { 556 - Logger.printStackTrace(e); 557 - } 735 + try { 736 + inspectArchive(archiveFile); 558 737 } 559 - } 560 - catch (IOException e) { 561 - Logger.printStackTrace(e); 562 - } 563 - break; 738 + catch (Exception e) { 739 + Logger.logError("Failed to inspect archive: " + e.getMessage()); 740 + Logger.printStackTrace(e); 741 + Environment.exit(1); 742 + } 564 743 565 - case "-BUILDPROJECT": 566 - try { 567 - Environment.getProject().build(); 744 + i++; 568 745 } 569 - catch (Exception e) { 570 - Logger.logError("Build failed: " + e.getMessage()); 746 + else { 747 + Logger.logError("'inspect' requires argument: <archive-file>"); 748 + Environment.exit(1); 571 749 } 572 750 break; 573 751
+16
src/main/java/app/bar/Bar.java
··· 3 3 import javax.swing.JPanel; 4 4 5 5 import net.miginfocom.swing.MigLayout; 6 + import project.build.BuildStatusBar; 6 7 7 8 /** A horizontal bar component for displaying status information. */ 8 9 public class Bar extends JPanel 9 10 { 10 11 private final GitBranch gitBranch; 12 + private BuildStatusBar buildStatusBar; 11 13 12 14 public Bar() 13 15 { ··· 16 18 17 19 gitBranch = new GitBranch(); 18 20 add(gitBranch, "alignx left"); 21 + 22 + // Build status bar will be added later via setBuildStatusBar 23 + } 24 + 25 + public void setBuildStatusBar(BuildStatusBar buildStatusBar) 26 + { 27 + if (this.buildStatusBar != null) { 28 + remove(this.buildStatusBar); 29 + } 30 + 31 + this.buildStatusBar = buildStatusBar; 32 + add(buildStatusBar, "alignx right"); 33 + revalidate(); 34 + repaint(); 19 35 } 20 36 21 37 public void dispose()
+19
src/main/java/app/pane/explorer/AssetItem.java
··· 1 1 package app.pane.explorer; 2 2 3 + import java.awt.AlphaComposite; 4 + import java.awt.Graphics; 5 + import java.awt.Graphics2D; 3 6 import java.awt.Image; 4 7 import java.awt.datatransfer.Transferable; 5 8 ··· 12 15 class AssetItem extends Item 13 16 { 14 17 final Asset asset; 18 + private final boolean owned; 15 19 16 20 AssetItem(Tab explorer, Asset asset) 17 21 { 18 22 super(explorer, asset.getName(), ThemedIcon.PACKAGE_24, false); 19 23 this.asset = asset; 24 + this.owned = asset.isOwned(); 20 25 21 26 new SwingWorker<Image, Void>() { 22 27 @Override ··· 44 49 String desc = asset.getAssetDescription(); 45 50 if (desc != null && !desc.isEmpty()) 46 51 setToolTipText(desc); 52 + } 53 + 54 + @Override 55 + protected void paintComponent(Graphics g) 56 + { 57 + if (!owned) { 58 + Graphics2D g2 = (Graphics2D) g.create(); 59 + g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.7f)); 60 + super.paintComponent(g2); 61 + g2.dispose(); 62 + } 63 + else { 64 + super.paintComponent(g); 65 + } 47 66 } 48 67 49 68 @Override
+15 -15
src/main/java/app/pane/explorer/Item.java
··· 56 56 boolean dropTarget; 57 57 private JTextField renameField; 58 58 59 - private static final JPopupMenu contextMenu = buildContextMenu(); 60 59 private static Item popupItem; 61 60 62 61 Item(Tab explorer, String name, Icon defaultIcon, boolean checkerboard) ··· 141 140 142 141 explorer.select(this); 143 142 popupItem = this; 144 - contextMenu.show(this, e.getX(), e.getY()); 143 + 144 + JPopupMenu menu = buildContextMenu(asset); 145 + menu.show(this, e.getX(), e.getY()); 145 146 } 146 147 147 - private static JPopupMenu buildContextMenu() 148 + private static JPopupMenu buildContextMenu(Asset asset) 148 149 { 149 150 var menu = new JPopupMenu(); 151 + boolean owned = asset.isOwned(); 150 152 151 - var renameItem = new JMenuItem("Rename"); 153 + var renameItem = new JMenuItem(owned ? "Rename..." : "Create copy..."); 152 154 renameItem.addActionListener(e -> { 153 155 if (popupItem != null) 154 156 popupItem.onRename(); ··· 156 158 menu.add(renameItem); 157 159 158 160 var deleteItem = new JMenuItem("Delete"); 161 + deleteItem.setEnabled(owned); 162 + if (!owned) { 163 + deleteItem.setToolTipText("This asset is not owned by your project"); 164 + } 159 165 deleteItem.addActionListener(e -> { 160 166 if (popupItem != null) 161 167 popupItem.onDelete(); ··· 236 242 .setTitle("Rename Failed") 237 243 .setMessage("Could not rename " + currentName + " to " + newName + ".") 238 244 .show(); 245 + } else { 246 + explorer.refresh(); 239 247 } 240 248 }; 241 249 ··· 269 277 if (asset == null) 270 278 return; 271 279 272 - String assetName = asset.getName(); 273 - int result = SwingUtils.getConfirmDialog() 274 - .setTitle("Delete") 275 - .setMessage("Delete " + assetName + "?") 276 - .setOptionsType(JOptionPane.YES_NO_OPTION) 277 - .choose(); 278 - 279 - if (result != JOptionPane.YES_OPTION) 280 - return; 281 - 282 280 if (!asset.delete()) { 283 281 SwingUtils.getErrorDialog() 284 282 .setTitle("Delete Failed") 285 - .setMessage("Could not delete " + assetName + ".") 283 + .setMessage("Could not delete " + asset.getName() + ".") 286 284 .show(); 285 + } else { 286 + explorer.refresh(); 287 287 } 288 288 } 289 289
+35 -5
src/main/java/app/pane/explorer/Tab.java
··· 25 25 import java.util.List; 26 26 27 27 import javax.swing.Icon; 28 + import javax.swing.JButton; 28 29 import javax.swing.JLabel; 29 30 import javax.swing.JPanel; 30 31 import javax.swing.JScrollPane; ··· 51 52 52 53 private JPanel topBar; 53 54 private JPanel breadcrumbsPanel; 55 + private JButton filterButton; 54 56 private SearchField searchField; 55 57 private JPanel resultsPanel; 56 58 private JScrollPane scrollPane; 59 + 60 + private boolean showOnlyOwned = false; 57 61 58 62 private WatchService watchService; 59 63 private Thread watchThread; ··· 67 71 // topBar (36) + border top (7) + 2 rows (160) + 1 vgap (1) + border bottom (12) = 216 68 72 setPreferredSize(new Dimension(400, 216)); 69 73 70 - topBar = new JPanel(new MigLayout("h 36!, ins 5, fill, gap 8", "[grow][]", "[fill]")); 74 + topBar = new JPanel(new MigLayout("h 36!, ins 5, fill, gap 8", "[grow][][]", "[fill]")); 71 75 72 76 breadcrumbsPanel = new JPanel(new MigLayout("ins 5 14 0 14, gap 0", "", "[center]")); 73 77 topBar.add(breadcrumbsPanel); 78 + 79 + filterButton = new JButton(ThemedIcon.EYE); 80 + filterButton.setFocusable(false); 81 + filterButton.putClientProperty("JButton.buttonType", "borderless"); 82 + filterButton.addActionListener(e -> { 83 + showOnlyOwned = !showOnlyOwned; 84 + updateFilterButton(); 85 + refresh(); 86 + }); 87 + updateFilterButton(); 88 + topBar.add(filterButton, "w 26!"); 74 89 75 90 searchField = new SearchField(); 76 91 topBar.add(searchField, "w 200!"); ··· 148 163 { 149 164 breadcrumbsPanel.removeAll(); 150 165 151 - String projectId = Environment.getProject().getManifest().getId(); 152 - breadcrumbsPanel.add(createBreadcrumbLabel(projectId, ""), "aligny baseline"); 166 + breadcrumbsPanel.add(createBreadcrumbLabel("assets", ""), "aligny baseline"); 153 167 154 168 if (!currentPath.isEmpty()) { 155 169 String[] parts = currentPath.split("/"); ··· 253 267 254 268 // --- Results --- 255 269 256 - private void refresh() 270 + private void updateFilterButton() 271 + { 272 + if (showOnlyOwned) { 273 + filterButton.setIcon(ThemedIcon.EYE_OFF); 274 + filterButton.setToolTipText("Show assets from dependencies"); 275 + } else { 276 + filterButton.setIcon(ThemedIcon.EYE); 277 + filterButton.setToolTipText("Hide assets from dependencies"); 278 + } 279 + } 280 + 281 + void refresh() 257 282 { 258 283 resultsPanel.removeAll(); 259 284 ··· 262 287 for (String subdirName : listing.subdirectories()) 263 288 resultsPanel.add(new DirectoryItem(this, subdirName, currentPath + subdirName + "/")); 264 289 265 - for (Asset asset : listing.files()) 290 + for (Asset asset : listing.files()) { 291 + // Filter based on ownership if toggle is enabled 292 + if (showOnlyOwned && !asset.isOwned()) 293 + continue; 294 + 266 295 resultsPanel.add(new AssetItem(this, asset)); 296 + } 267 297 268 298 resultsPanel.revalidate(); 269 299 resultsPanel.repaint();
+68 -14
src/main/java/assets/Asset.kt
··· 1 1 package assets 2 2 3 + import app.Environment 3 4 import org.apache.commons.io.FileUtils 5 + import project.build.BuildCtx 6 + import project.build.BuildResult 4 7 import java.awt.Image 5 8 import java.awt.RenderingHints 6 9 import java.awt.datatransfer.DataFlavor ··· 10 13 import java.nio.file.Path 11 14 import kotlin.io.path.* 12 15 13 - /** An asset on disk, but not yet loaded because it may be expensive to load. */ 16 + /** 17 + * An asset on disk, but not yet loaded because it may be expensive to load. 18 + * Has Copy-on-Write semantics: if an unowned asset is modified, it is copied to the project so it can be owned. 19 + */ 14 20 open class Asset internal constructor( 15 - val root: Path, 21 + var assetsDir: AssetsDir, 16 22 var relativePath: Path, 17 23 ) { 18 24 init { ··· 21 27 } 22 28 } 23 29 24 - internal constructor(root: Path, relativePath: String) : this(root, Path(relativePath)) 25 - internal constructor(root: File, relativePath: String) : this(root.toPath(), relativePath) 26 - internal constructor(other: Asset) : this(other.root, other.relativePath) 30 + /** Root directory containing this asset. */ 31 + val root: Path 32 + get() = assetsDir.path 33 + 34 + /** Whether this asset is owned by the project. */ 35 + val isOwned: Boolean 36 + get() = assetsDir.isOwned 27 37 28 38 /** Full path on disk. May be a directory. */ 29 39 var path: Path ··· 33 43 relativePath = root.relativize(value) 34 44 } 35 45 36 - val name: String get() = relativePath.nameWithoutExtension 37 - val extension: String get() = relativePath.extension 46 + val name: String get() = relativePath.nameWithoutAssetExtension 47 + val extension: String get() = relativePath.assetExtension 38 48 val isDirectory: Boolean get() = path.isDirectory() 39 49 40 50 private var cachedThumbnail: Image? = null ··· 44 54 45 55 fun exists(): Boolean = path.exists() 46 56 47 - fun lastModified(): Long = path.getLastModifiedTime().toMillis() 48 - 49 57 open fun getAssetDescription(): String? = null 50 58 51 - /** Deletes this asset from disk. */ 52 - open fun delete(): Boolean = FileUtils.deleteQuietly(path.toFile()) 59 + /** 60 + * Copies this asset to the project directory if it's not already owned. 61 + * Updates this asset in-place to point to the project directory. 62 + */ 63 + private fun ensureOwned() { 64 + if (isOwned) return 53 65 54 - /** Renames this asset within its current directory. */ 66 + // Get the project's owned assets directory 67 + val project = Environment.getProject() 68 + val projectAssetsDir = project.ownedAssetsDir 69 + val projectPath = projectAssetsDir.path / relativePath 70 + 71 + // Copy the file/directory to the project if it doesn't already exist 72 + if (!projectPath.exists()) { 73 + if (isDirectory) { 74 + FileUtils.copyDirectory(path.toFile(), projectPath.toFile()) 75 + } else { 76 + projectPath.parent?.createDirectories() 77 + path.copyTo(projectPath, overwrite = false) 78 + } 79 + } 80 + 81 + // Update this asset to point to the project directory 82 + assetsDir = projectAssetsDir 83 + } 84 + 85 + /** Deletes this asset from disk. Only works for owned assets. */ 86 + open fun delete(): Boolean { 87 + if (!isOwned) { 88 + return false 89 + } 90 + return FileUtils.deleteQuietly(path.toFile()) 91 + } 92 + 93 + /** Renames this asset within its current directory. CoW: copies to project first if needed. */ 55 94 open fun rename(name: String): Boolean { 95 + ensureOwned() 96 + 56 97 // Preserve file extension 57 - val newPath = path.parent?.resolve(name + extension) ?: return false 98 + val newPath = path.parent?.resolve("$name.$extension") ?: return false 58 99 if (newPath.exists()) 59 100 return false 60 101 ··· 65 106 } 66 107 67 108 // TODO: take a Path 68 - /** Moves this asset to a different directory. */ 109 + /** Moves this asset to a different directory. CoW: copies to project first if needed. */ 69 110 open fun move(targetDir: File): Boolean { 111 + ensureOwned() 112 + 70 113 val targetPath = targetDir.toPath().resolve(path.name) 71 114 if (targetPath.exists()) 72 115 return false ··· 76 119 true 77 120 }.getOrDefault(false) 78 121 } 122 + 123 + /** 124 + * Generate a header file for this asset if needed. 125 + * Called before build() for all assets. 126 + * @param headerPath Path where the header should be written 127 + * @return true if a header was generated, false if this asset doesn't need a header 128 + */ 129 + open suspend fun writeHeader(headerPath: Path): Boolean = false 130 + 131 + /** Build this asset. Override in subclasses that require compilation. */ 132 + open suspend fun build(ctx: BuildCtx): BuildResult = BuildResult.NoOp 79 133 80 134 /** Whether to paint a checkerboard behind the thumbnail for transparency. */ 81 135 open fun thumbnailHasCheckerboard(): Boolean = true
+108 -52
src/main/java/assets/AssetExtractor.java
··· 2 2 3 3 import java.io.File; 4 4 import java.io.IOException; 5 - import java.util.HashMap; 6 5 7 6 import org.apache.commons.io.FileUtils; 8 - import org.apache.commons.io.FilenameUtils; 9 7 10 - import app.Directories; 11 8 import app.Environment; 12 - import app.LoadingBar; 13 - import app.Resource; 14 - import app.Resource.ResourceType; 15 9 import app.StarRodMain; 16 - import app.input.IOUtils; 17 10 import game.map.Map; 18 11 import game.map.compiler.CollisionDecompiler; 19 12 import game.map.compiler.GeometryDecompiler; 20 13 import game.map.marker.Marker; 21 14 import util.Logger; 15 + import util.Priority; 22 16 23 17 public class AssetExtractor 24 18 { ··· 34 28 private final String name; 35 29 private String desc; 36 30 37 - private String shapeName; 38 - private String hitName; 31 + private String shapePath; 32 + private String hitPath; 39 33 private String texName; 40 34 private String bgName; 41 35 ··· 46 40 private boolean hasHitOverride; 47 41 private boolean hasTexOverride; 48 42 49 - public MapTemplate(String[] mapDef) 43 + private String shapeOverrideName; 44 + private String hitOverrideName; 45 + 46 + /** 47 + * Creates a MapTemplate from CSV definition. 48 + * @param mapfsRoot The mapfs root directory 49 + * @param mapDef CSV tokens: [mapName, shapePath, hitPath, texPath, bgPath, desc] 50 + * @param isStage true if this is a stage, false if map 51 + */ 52 + public MapTemplate(File mapfsRoot, String[] mapDef, boolean isStage) 50 53 { 51 54 this.name = mapDef[0]; 55 + this.shapePath = mapDef[1]; 56 + this.hitPath = mapDef[2]; 57 + String texPath = mapDef[3]; 58 + String bgPath = mapDef[4]; 59 + this.desc = mapDef[5]; 60 + 52 61 String areaName = name.substring(0, 3); 62 + String extension = isStage ? ".stage" : ".map"; 53 63 54 - shapeName = mapDef[1]; 55 - hasShapeOverride = !shapeName.equals(name + "_shape"); 64 + // Resolve shape file and detect override 65 + String expectedShapePath = "areas/" + areaName + "/" + name + extension + "/shape.bin"; 66 + hasShapeOverride = !shapePath.equals(expectedShapePath); 67 + shapeFile = new File(mapfsRoot, shapePath); 56 68 57 - hitName = mapDef[2]; 58 - hasHitOverride = !hitName.equals(name + "_hit"); 69 + if (hasShapeOverride) { 70 + // Extract override name from path: "areas/tik/tik_18.stage/shape.bin" -> "tik_18" 71 + String[] parts = shapePath.split("/"); 72 + if (parts.length >= 3) { 73 + String dirName = parts[2]; // "tik_18.stage" or "tik_18.map" 74 + shapeOverrideName = dirName.replaceAll("\\.(map|stage)$", ""); 75 + } 76 + } 59 77 60 - texName = mapDef[3]; 61 - if (texName.equalsIgnoreCase("none")) 78 + // Resolve hit file and detect override 79 + String expectedHitPath = "areas/" + areaName + "/" + name + extension + "/hit.bin"; 80 + hasHitOverride = !hitPath.equals(expectedHitPath); 81 + hitFile = new File(mapfsRoot, hitPath); 82 + 83 + if (hasHitOverride) { 84 + String[] parts = hitPath.split("/"); 85 + if (parts.length >= 3) { 86 + String dirName = parts[2]; 87 + hitOverrideName = dirName.replaceAll("\\.(map|stage)$", ""); 88 + } 89 + } 90 + 91 + // Resolve texture name 92 + if (texPath.equals("none")) { 62 93 texName = ""; 63 - hasTexOverride = !texName.equals(areaName + "_tex"); 94 + hasTexOverride = false; 95 + } 96 + else { 97 + // Extract texture name from path: "areas/kmr/kmr.tex" -> "kmr" 98 + String[] parts = texPath.split("/"); 99 + if (parts.length >= 2) { 100 + String texDirName = parts[2]; // "kmr.tex" 101 + texName = texDirName.replace(".tex", ""); 102 + } 103 + else { 104 + texName = ""; 105 + } 64 106 65 - bgName = mapDef[4]; 66 - if (bgName.equalsIgnoreCase("none")) 107 + String expectedTexName = areaName; 108 + hasTexOverride = !texName.equals(expectedTexName); 109 + } 110 + 111 + // Resolve background name 112 + if (bgPath.equals("none")) { 67 113 bgName = ""; 114 + } 115 + else { 116 + // Extract bg name from path: "backgrounds/kmr.bg.png" -> "kmr" 117 + String[] parts = bgPath.split("/"); 118 + if (parts.length >= 2) { 119 + String bgFileName = parts[1]; // "kmr.bg.png" 120 + bgName = bgFileName.replace(".bg.png", ""); 121 + } 122 + else { 123 + bgName = ""; 124 + } 125 + } 126 + } 68 127 69 - desc = mapDef[5]; 128 + /** 129 + * Returns the directory path for this map (e.g., "areas/kmr/kmr_00.map"). 130 + */ 131 + public String getMapDirPath() 132 + { 133 + // Extract from shapePath: "areas/kmr/kmr_00.map/shape.bin" -> "areas/kmr/kmr_00.map" 134 + int lastSlash = shapePath.lastIndexOf('/'); 135 + if (lastSlash != -1) { 136 + return shapePath.substring(0, lastSlash); 137 + } 138 + return ""; 70 139 } 71 140 } 72 141 73 142 public static void extractAll() throws IOException 74 143 { 75 144 // only extract in base asset dir 76 - int numDirs = Environment.assetDirectories.size(); 77 - File assetDir = Environment.assetDirectories.get(numDirs - 1); 145 + File assetDir = AssetManager.getBaseAssetDir(); 78 146 79 147 File sentinel = new File(assetDir, ".star_rod_extracted"); 80 148 if (!sentinel.exists()) { 81 - LoadingBar.show("Extracting Assets"); 82 - Logger.log("Extracting assets in " + assetDir.getName()); 83 - 84 - HashMap<String, File> assetFiles = new HashMap<>(); 149 + Logger.log("Processing engine assets...", Priority.MILESTONE); 85 150 86 - // find all relevant asset files 87 - File subdir = AssetSubdir.MAP_GEOM.get(assetDir); 88 - for (File assetFile : IOUtils.getFilesWithExtension(subdir, ".bin", true)) { 89 - String name = FilenameUtils.getBaseName(assetFile.getName()); 90 - assetFiles.put(name, assetFile); 91 - } 151 + File mapfsDir = AssetSubdir.MAPFS.get(assetDir); 92 152 93 153 // extract maps 94 - for (String mapInfo : Resource.getText(ResourceType.Extract, "maps.csv")) { 154 + for (String mapInfo : app.Resource.getText(app.Resource.ResourceType.Extract, "maps.csv")) { 95 155 String[] tokens = mapInfo.trim().split("\\s*,\\s*"); 96 - MapTemplate template = new MapTemplate(tokens); 97 - 98 - template.shapeFile = assetFiles.get(template.shapeName); 99 - template.hitFile = assetFiles.get(template.hitName); 156 + MapTemplate template = new MapTemplate(mapfsDir, tokens, false); 100 157 101 158 Logger.log("Generating map source: " + template.name); 102 159 Map map = generateMap(template); 103 160 try { 104 - map.saveMapWithoutHeader(new File(subdir, map.getName() + Directories.EXT_MAP)); 161 + File outputDir = new File(mapfsDir, template.getMapDirPath()); 162 + File outputFile = new File(outputDir, "map.xml"); 163 + map.saveMapWithoutHeader(outputFile); 105 164 } 106 165 catch (Exception e) { 107 166 StarRodMain.displayStackTrace(e); ··· 109 168 } 110 169 111 170 // extract stages 112 - for (String stageInfo : Resource.getText(ResourceType.Extract, "stages.csv")) { 171 + for (String stageInfo : app.Resource.getText(app.Resource.ResourceType.Extract, "stages.csv")) { 113 172 String[] tokens = stageInfo.trim().split("\\s*,\\s*"); 114 - MapTemplate template = new MapTemplate(tokens); 115 - 116 - template.shapeFile = assetFiles.get(template.shapeName); 117 - template.hitFile = assetFiles.get(template.hitName); 173 + MapTemplate template = new MapTemplate(mapfsDir, tokens, true); 118 174 119 175 Logger.log("Generating stage source: " + template.name); 120 176 Map map = generateMap(template); ··· 124 180 } 125 181 126 182 try { 127 - map.saveMapWithoutHeader(new File(subdir, map.getName() + Directories.EXT_MAP)); 183 + File outputDir = new File(mapfsDir, template.getMapDirPath()); 184 + File outputFile = new File(outputDir, "map.xml"); 185 + map.saveMapWithoutHeader(outputFile); 128 186 } 129 187 catch (Exception e) { 130 188 StarRodMain.displayStackTrace(e); ··· 145 203 146 204 map.desc = cfg.desc; 147 205 206 + // Set shape override if needed 148 207 if (cfg.hasShapeOverride) { 149 208 map.scripts.overrideShape.set(true); 150 - String override = cfg.shapeName; 151 - if (override.endsWith("_shape")) 152 - override = override.substring(0, override.length() - "_shape".length()); 153 - map.scripts.shapeOverrideName.set(override); 209 + map.scripts.shapeOverrideName.set(cfg.shapeOverrideName); 154 210 } 155 211 212 + // Set hit override if needed 156 213 if (cfg.hasHitOverride) { 157 214 map.scripts.overrideHit.set(true); 158 - String override = cfg.shapeName; 159 - if (override.endsWith("_hit")) 160 - override = override.substring(0, override.length() - "_hit".length()); 161 - map.scripts.hitOverrideName.set(override); 215 + map.scripts.hitOverrideName.set(cfg.hitOverrideName); 162 216 } 163 217 218 + // Set texture override if needed 164 219 map.scripts.overrideTex.set(cfg.hasTexOverride); 165 220 221 + // Decompile geometry and collision 166 222 if (cfg.shapeFile != null && cfg.shapeFile.exists()) { 167 223 new GeometryDecompiler(map, cfg.shapeFile); 168 224 }
+80 -56
src/main/java/assets/AssetManager.java
··· 17 17 import java.util.function.Predicate; 18 18 import java.util.stream.Collectors; 19 19 20 + import assets.ui.BackgroundAsset; 21 + import assets.ui.MapAsset; 22 + import assets.ui.TexturesAsset; 20 23 import org.apache.commons.io.FileUtils; 21 24 22 25 import app.Environment; ··· 27 30 { 28 31 public static File getTopLevelAssetDir() 29 32 { 30 - return Environment.assetDirectories.get(0); 33 + return Environment.getProject().getOwnedAssetsDir().getPath().toFile(); 31 34 } 32 35 33 36 public static File getBaseAssetDir() 34 37 { 35 - int numDirs = Environment.assetDirectories.size(); 36 - return Environment.assetDirectories.get(numDirs - 1); 38 + return Environment.getProject().getEngineAssetsDir().getPath().toFile(); 37 39 } 38 40 39 41 public static Asset get(AssetSubdir subdir, String path) 40 42 { 41 - for (File assetDir : Environment.assetDirectories) { 42 - Asset ah = AssetRegistry.getInstance().create(assetDir.toPath(), java.nio.file.Path.of(subdir + path)); 43 + for (AssetsDir assetsDir : Environment.getProject().getAssetDirectories()) { 44 + Asset ah = AssetRegistry.getInstance().create(assetsDir, java.nio.file.Path.of(subdir + path)); 43 45 44 46 if (ah.exists()) 45 47 return ah; 46 48 } 47 - return AssetRegistry.getInstance().create(AssetManager.getTopLevelAssetDir().toPath(), java.nio.file.Path.of(subdir + path)); 49 + return AssetRegistry.getInstance().create(java.nio.file.Path.of(subdir + path)); 48 50 } 49 51 50 52 public static Asset getTopLevel(Asset source) 51 53 { 52 - return AssetRegistry.getInstance().create(getTopLevelAssetDir().toPath(), source.getRelativePath()); 54 + return AssetRegistry.getInstance().create(source.getRelativePath()); 53 55 } 54 56 55 57 public static Asset getBase(AssetSubdir subdir, String path) 56 58 { 57 - return AssetRegistry.getInstance().create(getBaseAssetDir().toPath(), java.nio.file.Path.of(subdir + path)); 59 + return AssetRegistry.getInstance().create(Environment.getProject().getEngineAssetsDir(), java.nio.file.Path.of(subdir + path)); 58 60 } 59 61 60 62 /** ··· 63 65 */ 64 66 public static void deleteAll(Asset asset) 65 67 { 66 - for (File assetDir : Environment.assetDirectories) { 67 - File f = new File(assetDir, asset.getRelativePath().toString()); 68 + for (AssetsDir assetsDir : Environment.getProject().getAssetDirectories()) { 69 + File f = new File(assetsDir.getPath().toFile(), asset.getRelativePath().toString()); 68 70 if (f.exists()) 69 71 FileUtils.deleteQuietly(f); 70 72 } 71 73 } 72 74 73 - 74 - public static Asset getTextureArchive(String texName) 75 + public static TexturesAsset getTextureArchive(String texName) 75 76 { 76 - return get(AssetSubdir.MAP_TEX, texName + EXT_NEW_TEX); 77 - } 78 - 79 - public static File getTexBuildDir() 80 - { 81 - return AssetSubdir.MAP_TEX.getModDir(); 77 + Asset asset = get(AssetSubdir.MAPFS, texName); 78 + if (!asset.exists()) { 79 + texName = texName.replace("_tex", ""); 80 + asset = get(AssetSubdir.MAPFS, "areas/" + texName + "/" + texName + ".tex"); 81 + } 82 + return (TexturesAsset) asset; 82 83 } 83 84 84 - public static Asset getMap(String mapName) 85 + public static MapAsset getMap(String mapName) 85 86 { 86 - return get(AssetSubdir.MAP_GEOM, mapName + EXT_MAP); 87 + Asset asset = get(AssetSubdir.MAPFS, mapName); 88 + if (!asset.exists()) { 89 + String areaName = asset.getName().substring(0, 3); 90 + asset = get(AssetSubdir.MAPFS, "areas/" + areaName + "/" + mapName + ".map"); 91 + } 92 + return (MapAsset) asset; 87 93 } 88 94 89 95 public static File getSaveMapFile(String mapName) 90 96 { 91 - return new File(getMapBuildDir(), mapName + EXT_MAP); 97 + Asset map = getMap(mapName); 98 + return map.exists() ? map.getPath().toFile() : null; 92 99 } 93 100 94 101 public static File getMapBuildDir() 95 102 { 96 - return AssetSubdir.MAP_GEOM.getModDir(); 97 - } 98 - 99 - public static Asset getBackground(String bgName) 100 - { 101 - return get(AssetSubdir.MAP_BG, bgName + EXT_PNG); 103 + // Maps are now in areas subdirectories 104 + return AssetSubdir.MAPFS.getModDir(); 102 105 } 103 106 104 - public static File getBackgroundBuildDir() 107 + public static BackgroundAsset getBackground(String bgName) 105 108 { 106 - return AssetSubdir.MAP_BG.getModDir(); 109 + Asset asset = get(AssetSubdir.MAPFS, bgName); 110 + if (!asset.exists()) { 111 + bgName = bgName.replace("_bg", ""); 112 + asset = get(AssetSubdir.MAPFS, "backgrounds/" + bgName + ".bg" + EXT_PNG); 113 + } 114 + return (BackgroundAsset) asset; 107 115 } 108 116 109 117 public static Asset getNpcSprite(String spriteName) ··· 136 144 return getAssetMap(AssetSubdir.PLR_SPRITE_PAL, EXT_PNG); 137 145 } 138 146 139 - public static Collection<Asset> getMapSources() throws IOException 140 - { 141 - return getAssets(AssetSubdir.MAP_GEOM, EXT_MAP, (p) -> { 142 - // skip crash and backup files 143 - String filename = p.getFileName().toString(); 144 - return !(filename.endsWith(MAP_CRASH_SUFFIX) || filename.endsWith(MAP_BACKUP_SUFFIX)); 145 - }); 146 - } 147 - 148 - public static Collection<Asset> getBackgrounds() throws IOException 147 + /** 148 + * Gets all map sources in the new structure (mapfs/areas/**\/*.map/map.xml). 149 + * @return Collection of MapAsset instances 150 + */ 151 + public static Collection<assets.ui.MapAsset> getMapSources() throws IOException 149 152 { 150 - return getAssets(AssetSubdir.MAP_BG, EXT_PNG); 153 + return AssetRegistry.getInstance().getAll(MapAsset.class); 151 154 } 152 155 153 - public static Collection<Asset> getLegacyTextureArchives() throws IOException 156 + /** 157 + * Gets all background images in the new structure (mapfs/backgrounds/*.bg.png). 158 + * @return Collection of background image assets 159 + */ 160 + public static Collection<BackgroundAsset> getBackgrounds() throws IOException 154 161 { 155 - return getAssets(AssetSubdir.MAP_TEX, EXT_OLD_TEX); 162 + return AssetRegistry.getInstance().getAll(BackgroundAsset.class); 156 163 } 157 164 158 - public static Collection<Asset> getTextureArchives() throws IOException 165 + /** 166 + * Gets all texture archives in the new structure (mapfs/areas/**\/*.tex/). 167 + * @return Collection of texture directory assets 168 + */ 169 + public static Collection<TexturesAsset> getTextureArchives() throws IOException 159 170 { 160 - return getAssets(AssetSubdir.MAP_TEX, EXT_NEW_TEX); 171 + return AssetRegistry.getInstance().getAll(TexturesAsset.class); 161 172 } 162 173 163 174 public static Collection<Asset> getMessages() throws IOException ··· 210 221 { 211 222 Map<String, Asset> assetMap = new HashMap<>(); 212 223 213 - for (File stackDir : Environment.assetDirectories) { 214 - Path assetDir = dir.get(stackDir).toPath(); 224 + for (AssetsDir assetsDir : Environment.getProject().getAssetDirectories()) { 225 + Path stackDir = assetsDir.getPath(); 226 + Path assetDir = dir.get(stackDir.toFile()).toPath(); 215 227 216 228 if (!subdir.isEmpty()) 217 229 assetDir = assetDir.resolve(subdir); ··· 229 241 continue; 230 242 231 243 String relPath = dir + subdir + filename; 232 - Asset ah = AssetRegistry.getInstance().create(stackDir.toPath(), java.nio.file.Path.of(relPath)); 244 + Asset ah = AssetRegistry.getInstance().create(assetsDir, java.nio.file.Path.of(relPath)); 233 245 234 246 // only add first occurance down the asset stack traversal 235 247 assetMap.putIfAbsent(filename, ah); ··· 251 263 * Lists all files and subdirectories at a relative path across the asset stack. 252 264 * Files are returned as Assets (first occurrence in the stack wins). 253 265 * Subdirectories are returned as names (union across all stack levels). 254 - * @param relativePath Relative path from asset root, e.g. "" for root, "mapfs/", "mapfs/geom/" 266 + * @param relativePath Relative path from asset root 255 267 */ 256 268 public static DirectoryListing listDirectory(String relativePath) 257 269 { ··· 261 273 TreeSet<String> ignoredPaths = new TreeSet<>(); 262 274 ignoredPaths.add(project.engine.Engine.PROJECT_ENGINE_PATH); 263 275 264 - for (File stackDir : Environment.assetDirectories) { 265 - Path dir = stackDir.toPath().resolve(relativePath); 276 + for (AssetsDir assetsDir : Environment.getProject().getAssetDirectories()) { 277 + Path stackDir = assetsDir.getPath(); 278 + Path dir = stackDir.resolve(relativePath); 266 279 267 280 if (!Files.exists(dir) || !Files.isDirectory(dir)) 268 281 continue; ··· 278 291 continue; 279 292 280 293 if (Files.isDirectory(entry)) { 281 - subdirSet.add(name); 294 + // Check if directory has a registered extension (e.g., "foo.map" directory) 295 + Path dirAsPath = java.nio.file.Path.of(relPath); 296 + String dirExtension = assets.AssetRegistryKt.getAssetExtension(dirAsPath); 297 + if (!dirExtension.isEmpty()) { 298 + // Directory has a registered extension, treat it as an asset 299 + Asset asset = AssetRegistry.getInstance().create(assetsDir, dirAsPath); 300 + fileMap.putIfAbsent(name, asset); 301 + } else { 302 + // Normal subdirectory 303 + subdirSet.add(name); 304 + } 282 305 } else { 283 - Asset asset = AssetRegistry.getInstance().create(stackDir.toPath(), java.nio.file.Path.of(relPath)); 306 + Asset asset = AssetRegistry.getInstance().create(assetsDir, java.nio.file.Path.of(relPath)); 284 307 fileMap.putIfAbsent(name, asset); 285 308 } 286 309 } ··· 305 328 public static List<File> getStackDirsForPath(String relativePath) 306 329 { 307 330 List<File> dirs = new ArrayList<>(); 308 - for (File stackDir : Environment.assetDirectories) { 309 - File dir = new File(stackDir, relativePath); 331 + for (AssetsDir assetsDir : Environment.getProject().getAssetDirectories()) { 332 + File dir = new File(assetsDir.getPath().toFile(), relativePath); 310 333 if (dir.isDirectory()) 311 334 dirs.add(dir); 312 335 } ··· 318 341 // use TreeMap to keep assets sorted 319 342 TreeMap<String, Asset> assetMap = new TreeMap<>(); 320 343 321 - for (File assetDir : Environment.assetDirectories) { 344 + for (AssetsDir assetsDir : Environment.getProject().getAssetDirectories()) { 345 + File assetDir = assetsDir.getPath().toFile(); 322 346 File iconDir = AssetSubdir.ICON.get(assetDir); 323 347 if (!iconDir.exists()) 324 348 continue; ··· 334 358 continue; 335 359 } 336 360 337 - Asset ah = AssetRegistry.getInstance().create(assetDir.toPath(), java.nio.file.Path.of(AssetSubdir.ICON + relativeString)); 361 + Asset ah = AssetRegistry.getInstance().create(assetsDir, java.nio.file.Path.of(AssetSubdir.ICON + relativeString)); 338 362 if (!assetMap.containsKey(relativeString)) { 339 363 assetMap.put(relativeString, ah); 340 364 }
+109 -21
src/main/java/assets/AssetRegistry.kt
··· 1 1 package assets 2 2 3 + import app.Environment 3 4 import assets.ui.BackgroundAsset 4 5 import assets.ui.MapAsset 5 6 import assets.ui.TexturesAsset 6 - import java.io.IOException 7 - import java.nio.file.DirectoryStream 8 - import java.nio.file.Files 7 + import util.Logger 9 8 import java.nio.file.Path 10 9 import kotlin.io.path.* 11 10 12 11 /** 12 + * Gets the asset extension for a path by checking registered extensions. 13 + * For "foo.bg.png", returns "bg.png" if "bg.png" is registered. 14 + * For "bar.map", returns "map" if "map" is registered. 15 + * If no registered extension matches, returns an empty string. 16 + */ 17 + val Path.assetExtension: String 18 + get() { 19 + val filename = fileName?.toString() ?: return "" 20 + val parts = filename.split('.') 21 + 22 + if (parts.size < 2) return "" // No extension 23 + 24 + // Try progressively longer extensions, starting from the longest 25 + // e.g., for "foo.bg.png" try: "bg.png", then "png" 26 + for (i in 1 until parts.size) { 27 + val ext = parts.subList(i, parts.size).joinToString(".") 28 + if (AssetRegistry.instance.byExtension.containsKey(ext)) { 29 + return ext 30 + } 31 + } 32 + 33 + return "" 34 + } 35 + 36 + /** 37 + * Gets the filename without the asset extension. 38 + * For "foo.bg.png", returns "foo". 39 + * For "bar.map", returns "bar". 40 + */ 41 + val Path.nameWithoutAssetExtension: String 42 + get() { 43 + val ext = assetExtension 44 + return if (ext.isNotEmpty()) { 45 + val filename = fileName?.toString() ?: "" 46 + filename.removeSuffix(".$ext") 47 + } else { 48 + nameWithoutExtension 49 + } 50 + } 51 + 52 + /** 13 53 * Registry for asset types. Maps file extensions to factory functions that create typed Asset instances. 14 54 * 15 55 * Built-in types are registered at startup via init(). 16 56 * Addon types can be registered dynamically via register() with addon = true. 17 57 * 18 - * When an unregistered extension is encountered, create() returns a plain Asset instance. 58 + * All assets are created in the project's owned asset directory. 19 59 */ 20 60 class AssetRegistry { 21 61 internal data class Registration( 22 62 val extension: String, 23 - val factory: (Path, Path) -> Asset, 63 + val factory: (AssetsDir, Path) -> Asset, 24 64 val addon: Boolean, 25 65 ) 26 66 ··· 29 69 /** 30 70 * Registers an asset type for a file extension. 31 71 * @param extension File extension without leading dot (e.g., "xml", "png") 32 - * @param factory Constructor reference that takes (root: Path, relativePath: Path) -> Asset 72 + * @param factory Constructor reference that takes (AssetsDir, relativePath: Path) -> Asset 33 73 * @param addon Whether this is an addon type (used for hot reload) 34 74 */ 35 - fun register(extension: String, factory: (Path, Path) -> Asset, addon: Boolean = false) { 75 + fun register(extension: String, factory: (AssetsDir, Path) -> Asset, addon: Boolean = false) { 36 76 byExtension[extension] = Registration(extension, factory, addon) 37 77 } 38 78 ··· 45 85 } 46 86 47 87 /** 48 - * Creates an Asset for the given path. 88 + * Creates an Asset for the given AssetsDir and relative path. 49 89 * Returns a typed subclass if the extension is registered, otherwise a plain Asset. 90 + * Use this for discovering existing assets across the asset stack. 50 91 */ 51 - fun create(root: Path, relativePath: Path): Asset { 52 - val ext = relativePath.extension 92 + fun create(assetsDir: AssetsDir, relativePath: Path): Asset { 93 + val ext = relativePath.assetExtension 53 94 val reg = byExtension[ext] 54 - return reg?.factory?.invoke(root, relativePath) ?: Asset(root, relativePath) 95 + return reg?.factory?.invoke(assetsDir, relativePath) ?: Asset(assetsDir, relativePath) 55 96 } 56 97 57 98 /** 58 - * Returns all assets of a specific type by scanning asset directories. 59 - * @param T The asset type to filter by 60 - * @param root Root directory to scan 61 - * @return List of assets matching the type 99 + * Creates an Asset for the given relative path. 100 + * Always creates assets in the project's owned asset directory. 101 + * Returns a typed subclass if the extension is registered, otherwise a plain Asset. 102 + * Use this when creating new assets that should be owned by the project. 103 + */ 104 + fun create(relativePath: Path): Asset { 105 + val assetsDir = Environment.getProject().ownedAssetsDir 106 + return create(assetsDir, relativePath) 107 + } 108 + 109 + /** 110 + * Recursively collects all assets of a specific type across all asset directories. 111 + * Skips searching inside directories that have a registered extension. 62 112 */ 63 - inline fun <reified T : Asset> getAllOfType(root: Path): List<T> { 64 - // TODO: Implement 65 - return emptyList() 113 + fun <T : Asset> getAll(clazz: Class<T>): List<T> { 114 + val results = mutableListOf<T>() 115 + 116 + fun collectRecursive(assetsDir: AssetsDir, directory: Path) { 117 + directory.listDirectoryEntries().forEach { entry -> 118 + if (entry.isDirectory()) { 119 + // Check if this directory has a registered extension 120 + val ext = entry.assetExtension 121 + if (ext.isNotEmpty()) { 122 + // This directory is itself an asset - check if it matches our type 123 + val asset = create(assetsDir, assetsDir.path.relativize(entry)) 124 + if (clazz.isInstance(asset)) { 125 + @Suppress("UNCHECKED_CAST") 126 + results.add(asset as T) 127 + } 128 + // Don't recurse into asset directories 129 + } else { 130 + // Regular directory - recurse into it 131 + collectRecursive(assetsDir, entry) 132 + } 133 + } else if (entry.isRegularFile()) { 134 + // Check if this file has a registered extension 135 + val ext = entry.assetExtension 136 + if (ext.isNotEmpty()) { 137 + val asset = create(assetsDir, assetsDir.path.relativize(entry)) 138 + if (clazz.isInstance(asset)) { 139 + @Suppress("UNCHECKED_CAST") 140 + results.add(asset as T) 141 + } 142 + } 143 + } 144 + } 145 + } 146 + 147 + for (assetsDir in Environment.getProject().assetDirectories) { 148 + val root = assetsDir.path 149 + if (!root.exists() || !root.isDirectory()) continue 150 + collectRecursive(assetsDir, root) 151 + } 152 + return results 66 153 } 67 154 68 155 companion object { ··· 79 166 */ 80 167 @JvmStatic 81 168 fun init() { 82 - instance.register("xml", ::MapAsset) 83 - instance.register("png", ::BackgroundAsset) 84 - instance.register("json", ::TexturesAsset) 169 + instance.register("map", ::MapAsset) 170 + instance.register("stage", ::MapAsset) 171 + instance.register("bg.png", ::BackgroundAsset) 172 + instance.register("tex", ::TexturesAsset) 85 173 } 86 174 } 87 175 }
+8 -12
src/main/java/assets/AssetSubdir.java
··· 5 5 public enum AssetSubdir 6 6 { 7 7 // @formatter:off 8 - MAPFS ( "mapfs/"), 9 - MAP_GEOM (MAPFS, "/geom/"), 10 - MAP_TEX (MAPFS, "/tex/"), 11 - MAP_BG (MAPFS, "/bg/"), 12 - 13 - ENTITY ( "entity/"), 8 + MAPFS ( ""), 9 + ENTITY ( "../entity/"), 14 10 15 - MSG ( "msg/"), 11 + MSG ( "../msg/"), 16 12 17 - SPRITE ( "sprite/"), 13 + SPRITE ( "../sprite/"), 18 14 PLR_SPRITE (SPRITE, "/player/"), 19 15 PLR_SPRITE_IMG (PLR_SPRITE, "/rasters/"), 20 16 PLR_SPRITE_PAL (PLR_SPRITE, "/palettes/"), 21 17 NPC_SPRITE (SPRITE, "/npc/"), 22 18 23 - CHARSET ( "charset/"), 19 + CHARSET ( "../charset/"), 24 20 STANDARD_CHARS (CHARSET, "/standard/"), 25 21 STANDARD_CHARS_PAL (STANDARD_CHARS, "/palette/"), 26 22 TITLE_CHARS (CHARSET, "/title/"), 27 23 SUBTITLE_CHARS (CHARSET, "/subtitle/"), 28 24 29 - UI ( "ui/"), 25 + UI ( "../ui/"), 30 26 UI_MSG (UI, "/msg/"), 31 27 UI_PAUSE (UI, "/pause/"), 32 28 33 - PAUSE ( "pause/"), 29 + PAUSE ( "../pause/"), 34 30 35 - ICON ( "icon/"); 31 + ICON ( "../icon/"); 36 32 // @formatter:on 37 33 38 34 private final String path;
+33
src/main/java/assets/AssetsDir.kt
··· 1 + package assets 2 + 3 + import project.Project as ProjectClass 4 + import project.engine.Engine 5 + import java.nio.file.Path 6 + 7 + /** 8 + * Represents a directory containing assets, with a reference to its owner (Project or Engine). 9 + * Assets in a Project directory are owned by the project (modifiable). 10 + * Assets in an Engine directory are owned by the engine (read-only, CoW on modification). 11 + */ 12 + sealed class AssetsDir( 13 + val path: Path 14 + ) { 15 + /** Assets directory belonging to a project (owned, modifiable). */ 16 + data class Project( 17 + val project: ProjectClass, 18 + private val _path: Path 19 + ) : AssetsDir(_path) { 20 + override val isOwned: Boolean get() = true 21 + } 22 + 23 + /** Assets directory belonging to the engine (not owned, read-only). */ 24 + data class Engine( 25 + val engine: project.engine.Engine, 26 + private val _path: Path 27 + ) : AssetsDir(_path) { 28 + override val isOwned: Boolean get() = false 29 + } 30 + 31 + /** Whether this asset is owned by the project (true) or comes from the engine (false). */ 32 + abstract val isOwned: Boolean 33 + }
+1 -1
src/main/java/assets/ExpectedAsset.java
··· 21 21 ICON_X (AssetSubdir.ICON, "battle/XBandage.png", true), 22 22 CIRCLE_SHADOW (AssetSubdir.ENTITY, "shadow/circle.png", false), 23 23 SQUARE_SHADOW (AssetSubdir.ENTITY, "shadow/square.png", false), 24 - KMR_BG (AssetSubdir.MAP_BG, "kmr_bg.png", true), 24 + KMR_BG (AssetSubdir.MAPFS, "backgrounds/kmr.bg.png", true), 25 25 WORLD_MAP_BG (AssetSubdir.PAUSE, "world_map.png", true), 26 26 CRASH_GUY (AssetSubdir.NPC_SPRITE, "ShyGuy/rasters/Raster1A.png", true); 27 27 // @formatter:on
+133
src/main/java/assets/archive/AssetsArchiveBuilder.kt
··· 1 + package assets.archive 2 + 3 + import java.nio.ByteBuffer 4 + import java.nio.ByteOrder 5 + 6 + /** 7 + * Builds an AssetsArchive binary in the Paper Mario mapfs format. 8 + * 9 + * Binary format: 10 + * - Header (32 bytes): Magic "MAPFS " + project name (16 bytes) + reserved (10 bytes) 11 + * - Table of Contents: 76-byte entries (name + offset + compressedSize + decompressedSize) 12 + * - Sentinel Entry: "END DATA\0" + next-node offset (0 = end of chain) 13 + * - Asset Data: Compressed (Yay0) or raw binary data 14 + */ 15 + class AssetsArchiveBuilder(private val projectName: String) { 16 + private val entries = mutableListOf<AssetsArchiveEntry>() 17 + 18 + /** 19 + * Adds an entry to the archive. 20 + * @param name Entry name (max 63 chars) 21 + * @param data Raw data 22 + * @param compress Whether to apply Yay0 compression 23 + */ 24 + fun addEntry(name: String, data: ByteArray, compress: Boolean = true) { 25 + val finalData: ByteArray 26 + val decompressedSize: Int 27 + val isCompressed: Boolean 28 + 29 + if (compress) { 30 + val compressed = AssetsArchiveCompressor.compress(data) 31 + finalData = compressed 32 + decompressedSize = data.size 33 + isCompressed = compressed !== data // Check if compression actually occurred 34 + } else { 35 + finalData = data 36 + decompressedSize = data.size 37 + isCompressed = false 38 + } 39 + 40 + entries.add( 41 + AssetsArchiveEntry( 42 + name = name, 43 + data = finalData, 44 + compressed = isCompressed, 45 + decompressedSize = decompressedSize 46 + ) 47 + ) 48 + } 49 + 50 + /** 51 + * Builds the complete binary archive. 52 + * @return Binary data in mapfs format 53 + */ 54 + fun build(): ByteArray { 55 + val headerSize = 32 56 + val entrySize = 76 57 + val sentinelSize = 76 58 + val tocSize = (entries.size * entrySize) + sentinelSize 59 + 60 + val dataSize = entries.sumOf { it.compressedSize } 61 + val totalSize = headerSize + tocSize + dataSize 62 + 63 + val buffer = ByteBuffer.allocate(totalSize).order(ByteOrder.BIG_ENDIAN) 64 + 65 + writeHeader(buffer) 66 + writeToc(buffer) 67 + writeData(buffer) 68 + 69 + return buffer.array() 70 + } 71 + 72 + private fun writeHeader(buffer: ByteBuffer) { 73 + // Magic (6 bytes): "MAPFS " 74 + buffer.put("MAPFS ".toByteArray(Charsets.US_ASCII)) 75 + 76 + // Project name (16 bytes, null-terminated) 77 + val projectNameBytes = projectName.take(15).toByteArray(Charsets.US_ASCII) 78 + buffer.put(projectNameBytes) 79 + buffer.put(ByteArray(16 - projectNameBytes.size)) // Null padding 80 + 81 + // Reserved (10 bytes) 82 + buffer.put(ByteArray(10)) 83 + } 84 + 85 + private fun writeToc(buffer: ByteBuffer) { 86 + val headerSize = 32 87 + val entrySize = 76 88 + val sentinelSize = 76 89 + val tocSize = (entries.size * entrySize) + sentinelSize 90 + 91 + var dataOffset = headerSize + tocSize 92 + 93 + // Write entries 94 + for (entry in entries) { 95 + // Name (64 bytes, null-terminated) 96 + val nameBytes = entry.name.toByteArray(Charsets.US_ASCII) 97 + buffer.put(nameBytes) 98 + buffer.put(ByteArray(64 - nameBytes.size)) // Null padding 99 + 100 + // Offset (4 bytes) 101 + buffer.putInt(dataOffset) 102 + 103 + // Compressed size (4 bytes) 104 + buffer.putInt(entry.compressedSize) 105 + 106 + // Decompressed size (4 bytes) 107 + buffer.putInt(entry.decompressedSize) 108 + 109 + dataOffset += entry.compressedSize 110 + } 111 + 112 + // Write sentinel entry 113 + val sentinelName = "END DATA" 114 + val sentinelBytes = sentinelName.toByteArray(Charsets.US_ASCII) 115 + buffer.put(sentinelBytes) 116 + buffer.put(ByteArray(64 - sentinelBytes.size)) // Null padding 117 + 118 + // Next-node offset (4 bytes): 0 = end of chain 119 + buffer.putInt(0) 120 + 121 + // Compressed size (4 bytes): 0 122 + buffer.putInt(0) 123 + 124 + // Decompressed size (4 bytes): 0 125 + buffer.putInt(0) 126 + } 127 + 128 + private fun writeData(buffer: ByteBuffer) { 129 + for (entry in entries) { 130 + buffer.put(entry.data) 131 + } 132 + } 133 + }
+35
src/main/java/assets/archive/AssetsArchiveCompressor.kt
··· 1 + package assets.archive 2 + 3 + import game.yay0.Yay0Helper 4 + import project.build.ArtifactType 5 + import util.Logger 6 + 7 + /** Handles Yay0 compression for AssetsArchive entries. */ 8 + object AssetsArchiveCompressor { 9 + /** 10 + * Compresses data using Yay0 encoding. 11 + * Falls back to uncompressed if compression fails or data is too small. 12 + */ 13 + fun compress(data: ByteArray): ByteArray { 14 + if (data.size < 64) { 15 + // Not worth compressing small files 16 + return data 17 + } 18 + 19 + return try { 20 + Yay0Helper.encode(data) 21 + } catch (e: Exception) { 22 + Logger.logWarning("Compression failed, storing uncompressed: ${e.message}") 23 + data 24 + } 25 + } 26 + 27 + /** 28 + * Determines whether an artifact type should be compressed. 29 + */ 30 + fun shouldCompress(type: ArtifactType): Boolean = type in setOf( 31 + ArtifactType.BINARY, 32 + ArtifactType.SHAPE, 33 + ArtifactType.COLLISION 34 + ) 35 + }
+38
src/main/java/assets/archive/AssetsArchiveEntry.kt
··· 1 + package assets.archive 2 + 3 + /** Entry in an AssetsArchive. */ 4 + data class AssetsArchiveEntry( 5 + val name: String, 6 + val data: ByteArray, 7 + val compressed: Boolean, 8 + val decompressedSize: Int 9 + ) { 10 + val compressedSize: Int get() = data.size 11 + 12 + init { 13 + require(name.length in 1..63) { "Entry name must be 1-63 characters" } 14 + require(name.isNotEmpty()) { "Entry name cannot be empty" } 15 + } 16 + 17 + override fun equals(other: Any?): Boolean { 18 + if (this === other) return true 19 + if (javaClass != other?.javaClass) return false 20 + 21 + other as AssetsArchiveEntry 22 + 23 + if (name != other.name) return false 24 + if (!data.contentEquals(other.data)) return false 25 + if (compressed != other.compressed) return false 26 + if (decompressedSize != other.decompressedSize) return false 27 + 28 + return true 29 + } 30 + 31 + override fun hashCode(): Int { 32 + var result = name.hashCode() 33 + result = 31 * result + data.contentHashCode() 34 + result = 31 * result + compressed.hashCode() 35 + result = 31 * result + decompressedSize 36 + return result 37 + } 38 + }
+21
src/main/java/assets/archive/AssetsArchiveNode.kt
··· 1 + package assets.archive 2 + 3 + /** Represents a node in the AssetsArchive linked list in ROM. */ 4 + data class AssetsArchiveNode( 5 + val romAddress: Int, 6 + val entries: List<AssetsArchiveTocEntry>, 7 + val nextNodeAddress: Int 8 + ) { 9 + /** Total size of this node in ROM (header + TOC + data). */ 10 + val totalSize: Int 11 + get() { 12 + val headerSize = 32 13 + val tocSize = (entries.size + 1) * 76 // +1 for sentinel 14 + val dataSize = entries.filter { !it.isSentinel }.sumOf { it.compressedSize } 15 + return headerSize + tocSize + dataSize 16 + } 17 + 18 + /** Whether this is the last node in the chain. */ 19 + val isLastNode: Boolean 20 + get() = nextNodeAddress == 0 21 + }
+80
src/main/java/assets/archive/AssetsArchiveRomParser.kt
··· 1 + package assets.archive 2 + 3 + import java.nio.ByteBuffer 4 + import java.nio.ByteOrder 5 + 6 + /** 7 + * Parses AssetsArchive linked list from ROM. 8 + * 9 + * The ROM contains a linked list of AssetsArchive nodes starting at a known address. 10 + * Each node has a sentinel entry with a "next" pointer (0 = end of chain). 11 + */ 12 + class AssetsArchiveRomParser(private val romData: ByteArray) { 13 + /** 14 + * Parses the entire linked list chain starting at the given address. 15 + * @param startAddress ROM offset to first node 16 + * @return List of nodes in the chain 17 + */ 18 + fun parseChain(startAddress: Int): List<AssetsArchiveNode> { 19 + val nodes = mutableListOf<AssetsArchiveNode>() 20 + var currentAddress = startAddress 21 + 22 + while (currentAddress != 0 && currentAddress < romData.size) { 23 + val node = parseNode(currentAddress) 24 + nodes.add(node) 25 + 26 + if (node.isLastNode) 27 + break 28 + 29 + currentAddress = node.nextNodeAddress 30 + } 31 + 32 + return nodes 33 + } 34 + 35 + /** 36 + * Parses a single AssetsArchive node at the given address. 37 + */ 38 + private fun parseNode(address: Int): AssetsArchiveNode { 39 + if (address + 32 > romData.size) 40 + throw IllegalArgumentException("Invalid node address: $address") 41 + 42 + val buffer = ByteBuffer.wrap(romData, address, romData.size - address) 43 + .order(ByteOrder.BIG_ENDIAN) 44 + 45 + // Skip header (32 bytes) 46 + buffer.position(address + 32) 47 + 48 + // Parse TOC entries until we hit the sentinel 49 + val entries = mutableListOf<AssetsArchiveTocEntry>() 50 + var nextNodeAddress = 0 51 + 52 + while (true) { 53 + if (buffer.position() + 76 > romData.size) 54 + throw IllegalArgumentException("Incomplete TOC entry at ${buffer.position()}") 55 + 56 + // Read name (64 bytes) 57 + val nameBytes = ByteArray(64) 58 + buffer.get(nameBytes) 59 + val name = nameBytes.takeWhile { it != 0.toByte() } 60 + .toByteArray() 61 + .toString(Charsets.US_ASCII) 62 + 63 + // Read offset, compressed size, decompressed size (4 bytes each) 64 + val offset = buffer.int 65 + val compressedSize = buffer.int 66 + val decompressedSize = buffer.int 67 + 68 + val entry = AssetsArchiveTocEntry(name, offset, compressedSize, decompressedSize) 69 + entries.add(entry) 70 + 71 + // Check if this is the sentinel entry 72 + if (entry.isSentinel) { 73 + nextNodeAddress = offset // Sentinel reuses offset field for next pointer 74 + break 75 + } 76 + } 77 + 78 + return AssetsArchiveNode(address, entries, nextNodeAddress) 79 + } 80 + }
+347
src/main/java/assets/archive/AssetsArchiveRomPatcher.kt
··· 1 + package assets.archive 2 + 3 + import org.apache.commons.compress.archivers.tar.TarArchiveInputStream 4 + import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream 5 + import util.Logger 6 + import java.nio.ByteBuffer 7 + import java.nio.ByteOrder 8 + import java.nio.file.Files 9 + import java.nio.file.Path 10 + import kotlin.io.path.* 11 + 12 + /** 13 + * Applies AssetsArchive patches to ROM files. 14 + * 15 + * Supports two patching strategies: 16 + * 1. Replace existing mod - If the mod is already in the chain and fits 17 + * 2. Append new node - Extends ROM with new data 18 + */ 19 + class AssetsArchiveRomPatcher(private val romPath: Path) { 20 + /** 21 + * Applies AssetsArchive from a Diorama archive. 22 + * Extracts the ROM address from target.json in the Diorama package. 23 + * @param dioramaArchive Path to .diorama TAR file 24 + */ 25 + fun applyDiorama(dioramaArchive: Path) { 26 + val archiveBin = extractArchiveFromDiorama(dioramaArchive) 27 + val romStart = extractRomStartFromDiorama(dioramaArchive) 28 + try { 29 + applyArchive(archiveBin, romStart) 30 + } finally { 31 + // Clean up temp file 32 + Files.deleteIfExists(archiveBin) 33 + } 34 + } 35 + 36 + /** 37 + * Applies AssetsArchive binary directly to ROM. 38 + * @param archivePath Path to assets.bin 39 + * @param symsFile Linker symbols file containing mapfs_ROM_START 40 + * @throws InvalidRomException if ROM is not a valid papermario-dx ROM 41 + */ 42 + fun applyArchive(archivePath: Path, symsFile: java.io.File) { 43 + val romStart = readMapfsRomStart(symsFile) 44 + Logger.log("Read mapfs_ROM_START = 0x${romStart.toString(16)} from ${symsFile.name}") 45 + applyArchive(archivePath, romStart) 46 + } 47 + 48 + /** 49 + * Applies AssetsArchive binary directly to ROM. 50 + * @param archivePath Path to assets.bin 51 + * @param romStart ROM address where AssetsArchive chain begins 52 + * @throws InvalidRomException if ROM is not a valid papermario-dx ROM 53 + */ 54 + fun applyArchive(archivePath: Path, romStart: Int) { 55 + Logger.log("Patching ROM...") 56 + Logger.log("Validating ROM at address 0x${romStart.toString(16)}...") 57 + 58 + val romData = romPath.readBytes() 59 + val archiveData = archivePath.readBytes() 60 + 61 + // Validate ROM by parsing existing linked list 62 + val parser = AssetsArchiveRomParser(romData) 63 + val existingNodes = try { 64 + val nodes = parser.parseChain(romStart) 65 + if (nodes.isEmpty()) { 66 + throw InvalidRomException( 67 + "ROM does not contain a valid AssetsArchive chain at address 0x${romStart.toString(16)}. " + 68 + "This ROM is not a compatible papermario-dx build. " + 69 + "Please ensure you are using a ROM built from papermario-dx, not the original Paper Mario ROM." 70 + ) 71 + } 72 + nodes 73 + } catch (e: InvalidRomException) { 74 + throw e 75 + } catch (e: Exception) { 76 + throw InvalidRomException( 77 + "Failed to parse AssetsArchive chain at address 0x${romStart.toString(16)}: ${e.message}. " + 78 + "This ROM is not a compatible papermario-dx build. " + 79 + "Please ensure you are using a ROM built from papermario-dx, not the original Paper Mario ROM.", 80 + e 81 + ) 82 + } 83 + 84 + Logger.log("Found valid AssetsArchive chain with ${existingNodes.size} node(s)") 85 + 86 + // Parse project name from incoming archive 87 + val incomingProjectName = parseProjectName(archiveData) 88 + Logger.log("Applying mod: $incomingProjectName") 89 + 90 + // Find if this mod already exists in the chain 91 + val existingNodeIndex = existingNodes.indexOfFirst { node -> 92 + val nodeName = parseProjectNameFromRom(romData, node.romAddress) 93 + nodeName == incomingProjectName 94 + } 95 + 96 + val patchedRom = if (existingNodeIndex >= 0) { 97 + val existingNode = existingNodes[existingNodeIndex] 98 + Logger.log("Mod already exists at 0x${existingNode.romAddress.toString(16)}") 99 + 100 + // Check if new archive fits in existing space 101 + if (archiveData.size <= existingNode.totalSize) { 102 + Logger.log("Replacing in-place (fits in existing space)") 103 + replaceNode(romData, existingNode, archiveData, existingNodes, existingNodeIndex) 104 + } else { 105 + Logger.log("New version too large (${archiveData.size} > ${existingNode.totalSize}), appending new node") 106 + removeNodeAndAppend(romData, existingNodes, existingNodeIndex, archiveData, romStart) 107 + } 108 + } else { 109 + Logger.log("Mod not found in chain, appending new node") 110 + appendNewNode(romData, existingNodes, archiveData, romStart) 111 + } 112 + 113 + // Write patched ROM 114 + romPath.writeBytes(patchedRom) 115 + Logger.log("ROM patched successfully (${patchedRom.size} bytes)") 116 + } 117 + 118 + /** 119 + * Parses the project name from an AssetsArchive binary. 120 + */ 121 + private fun parseProjectName(archiveData: ByteArray): String { 122 + if (archiveData.size < 32) return "unknown" 123 + 124 + val buffer = ByteBuffer.wrap(archiveData, 6, 16).order(ByteOrder.BIG_ENDIAN) 125 + val nameBytes = ByteArray(16) 126 + buffer.get(nameBytes) 127 + return nameBytes.takeWhile { it != 0.toByte() } 128 + .toByteArray() 129 + .toString(Charsets.US_ASCII) 130 + .trim() 131 + } 132 + 133 + /** 134 + * Parses the project name from an AssetsArchive node in ROM. 135 + */ 136 + private fun parseProjectNameFromRom(romData: ByteArray, address: Int): String { 137 + if (address + 32 > romData.size) return "unknown" 138 + 139 + val buffer = ByteBuffer.wrap(romData, address + 6, 16).order(ByteOrder.BIG_ENDIAN) 140 + val nameBytes = ByteArray(16) 141 + buffer.get(nameBytes) 142 + return nameBytes.takeWhile { it != 0.toByte() } 143 + .toByteArray() 144 + .toString(Charsets.US_ASCII) 145 + .trim() 146 + } 147 + 148 + /** 149 + * Replaces an existing node in-place with new data. 150 + */ 151 + private fun replaceNode( 152 + rom: ByteArray, 153 + existingNode: AssetsArchiveNode, 154 + newArchiveData: ByteArray, 155 + allNodes: List<AssetsArchiveNode>, 156 + nodeIndex: Int 157 + ): ByteArray { 158 + val newRom = rom.copyOf() 159 + 160 + // Write new archive data at existing location 161 + System.arraycopy(newArchiveData, 0, newRom, existingNode.romAddress, newArchiveData.size) 162 + 163 + // Zero out any remaining space 164 + val remainingSpace = existingNode.totalSize - newArchiveData.size 165 + if (remainingSpace > 0) { 166 + val zeroStart = existingNode.romAddress + newArchiveData.size 167 + for (i in 0 until remainingSpace) { 168 + newRom[zeroStart + i] = 0 169 + } 170 + } 171 + 172 + // If there's a next node, preserve the link 173 + if (nodeIndex < allNodes.size - 1) { 174 + val nextNode = allNodes[nodeIndex + 1] 175 + updateSentinelPointer(newRom, existingNode.romAddress, newArchiveData, nextNode.romAddress) 176 + } 177 + 178 + return newRom 179 + } 180 + 181 + /** 182 + * Updates the sentinel pointer in an archive to point to the next node. 183 + */ 184 + private fun updateSentinelPointer(rom: ByteArray, archiveAddress: Int, archiveData: ByteArray, nextAddress: Int) { 185 + // Parse the archive to find the sentinel position 186 + val buffer = ByteBuffer.wrap(archiveData).order(ByteOrder.BIG_ENDIAN) 187 + buffer.position(32) // Skip header 188 + 189 + // Count TOC entries to find sentinel 190 + var entryCount = 0 191 + while (buffer.remaining() >= 76) { 192 + val nameBytes = ByteArray(64) 193 + buffer.get(nameBytes) 194 + val name = nameBytes.takeWhile { it != 0.toByte() }.toByteArray().toString(Charsets.US_ASCII) 195 + 196 + if (name.startsWith("END DATA")) { 197 + // Found sentinel, update its offset field (next 4 bytes) 198 + val sentinelOffsetPos = archiveAddress + 32 + (entryCount * 76) + 64 199 + updatePointer(rom, sentinelOffsetPos, nextAddress) 200 + break 201 + } 202 + 203 + buffer.position(buffer.position() + 12) // Skip offset, compSize, decompSize 204 + entryCount++ 205 + } 206 + } 207 + 208 + /** 209 + * Removes a node from the chain and appends a new one. 210 + */ 211 + private fun removeNodeAndAppend( 212 + rom: ByteArray, 213 + existingNodes: List<AssetsArchiveNode>, 214 + removeIndex: Int, 215 + newArchiveData: ByteArray, 216 + firstNodeAddress: Int 217 + ): ByteArray { 218 + val nodeToRemove = existingNodes[removeIndex] 219 + 220 + // Update chain to skip removed node 221 + val newRom = rom.copyOf() 222 + 223 + if (removeIndex == 0 && existingNodes.size == 1) { 224 + // Only node - will be replaced by append 225 + } else if (removeIndex == 0) { 226 + // First node - update firstNodeAddress to point to second node 227 + val secondNode = existingNodes[1] 228 + updatePointer(newRom, firstNodeAddress, secondNode.romAddress) 229 + } else { 230 + // Middle or last node - update previous node's sentinel 231 + val prevNode = existingNodes[removeIndex - 1] 232 + val nextAddress = if (removeIndex < existingNodes.size - 1) { 233 + existingNodes[removeIndex + 1].romAddress 234 + } else { 235 + 0 // End of chain 236 + } 237 + 238 + val headerSize = 32 239 + val sentinelTocPos = prevNode.romAddress + headerSize + (prevNode.entries.size - 1) * 76 240 + val nextPointerPos = sentinelTocPos + 64 241 + updatePointer(newRom, nextPointerPos, nextAddress) 242 + } 243 + 244 + // Now append the new node 245 + val remainingNodes = existingNodes.filterIndexed { index, _ -> index != removeIndex } 246 + return appendNewNode(newRom, if (remainingNodes.isEmpty()) existingNodes.take(1) else remainingNodes, newArchiveData, firstNodeAddress) 247 + } 248 + 249 + /** 250 + * Appends a new AssetsArchive node to the end of ROM. 251 + * Updates the linked list to point to the new node. 252 + */ 253 + private fun appendNewNode( 254 + rom: ByteArray, 255 + existingNodes: List<AssetsArchiveNode>, 256 + archiveData: ByteArray, 257 + firstNodeAddress: Int 258 + ): ByteArray { 259 + require(existingNodes.isNotEmpty()) { "Cannot append to empty chain" } 260 + 261 + // Calculate append address (align to 16-byte boundary for N64) 262 + val rawAppendAddress = rom.size 263 + val appendAddress = (rawAppendAddress + 15) and 0xFFFFFFF0.toInt() 264 + 265 + // Create new ROM with archive appended 266 + val newSize = appendAddress + archiveData.size 267 + val newRom = ByteArray(newSize) 268 + 269 + // Copy original ROM 270 + System.arraycopy(rom, 0, newRom, 0, rom.size) 271 + 272 + // Fill alignment padding with zeros 273 + for (i in rom.size until appendAddress) { 274 + newRom[i] = 0 275 + } 276 + 277 + // Append archive data 278 + System.arraycopy(archiveData, 0, newRom, appendAddress, archiveData.size) 279 + 280 + // Update last node's sentinel to point to new node 281 + val lastNode = existingNodes.last() 282 + Logger.log("Linking new node to existing chain (last node at 0x${lastNode.romAddress.toString(16)})") 283 + 284 + // Find sentinel entry position in last node 285 + val headerSize = 32 286 + val sentinelTocPos = lastNode.romAddress + headerSize + (lastNode.entries.size - 1) * 76 287 + 288 + // Sentinel's offset field (at +64) contains the next-node pointer 289 + val nextPointerPos = sentinelTocPos + 64 290 + updatePointer(newRom, nextPointerPos, appendAddress) 291 + 292 + return newRom 293 + } 294 + 295 + /** 296 + * Updates a 4-byte big-endian pointer in ROM. 297 + */ 298 + private fun updatePointer(rom: ByteArray, address: Int, value: Int) { 299 + val buffer = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN) 300 + buffer.putInt(value) 301 + System.arraycopy(buffer.array(), 0, rom, address, 4) 302 + } 303 + 304 + /** 305 + * Extracts assets.bin from a Diorama TAR archive. 306 + * @return Path to temporary extracted file 307 + */ 308 + private fun extractArchiveFromDiorama(dioramaPath: Path): Path { 309 + TarArchiveInputStream( 310 + GzipCompressorInputStream(dioramaPath.inputStream()) 311 + ).use { tar -> 312 + var entry = tar.nextTarEntry 313 + while (entry != null) { 314 + if (entry.name == "assets.bin") { 315 + val tempPath = Files.createTempFile("assets", ".bin") 316 + tempPath.outputStream().use { output -> 317 + tar.copyTo(output) 318 + } 319 + return tempPath 320 + } 321 + entry = tar.nextTarEntry 322 + } 323 + } 324 + throw IllegalArgumentException("Diorama archive missing assets.bin: $dioramaPath") 325 + } 326 + 327 + /** 328 + * Extracts ROM start address from target.json in a Diorama TAR archive. 329 + * @return mapfs_ROM_START address 330 + */ 331 + private fun extractRomStartFromDiorama(dioramaPath: Path): Int { 332 + TarArchiveInputStream( 333 + GzipCompressorInputStream(dioramaPath.inputStream()) 334 + ).use { tar -> 335 + var entry = tar.nextTarEntry 336 + while (entry != null) { 337 + if (entry.name == "target.json") { 338 + val json = tar.readBytes().toString(Charsets.UTF_8) 339 + val config = kotlinx.serialization.json.Json.decodeFromString<DioramaConfig.TargetJson>(json) 340 + return config.engine.assets_archive_ROM_START 341 + } 342 + entry = tar.nextTarEntry 343 + } 344 + } 345 + throw IllegalArgumentException("Diorama archive missing target.json: $dioramaPath") 346 + } 347 + }
+12
src/main/java/assets/archive/AssetsArchiveTocEntry.kt
··· 1 + package assets.archive 2 + 3 + /** Table of Contents entry in an AssetsArchive. */ 4 + data class AssetsArchiveTocEntry( 5 + val name: String, 6 + val offset: Int, 7 + val compressedSize: Int, 8 + val decompressedSize: Int 9 + ) { 10 + val isSentinel: Boolean 11 + get() = name.startsWith("END DATA") 12 + }
+73
src/main/java/assets/archive/DioramaArchiver.kt
··· 1 + package assets.archive 2 + 3 + import org.apache.commons.compress.archivers.tar.TarArchiveEntry 4 + import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream 5 + import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream 6 + import util.Logger 7 + import java.nio.file.Path 8 + import kotlin.io.path.* 9 + 10 + /** 11 + * Creates Diorama (TAR.GZ) distribution archives. 12 + * 13 + * Archive format: 14 + * - project.kdl - Project manifest 15 + * - target.json - Target configuration (engine SHA, ROM addresses) 16 + * - assets.bin - AssetsArchive binary 17 + */ 18 + class DioramaArchiver( 19 + private val projectManifest: Path, 20 + private val config: DioramaConfig, 21 + private val archiveBin: Path 22 + ) { 23 + /** 24 + * Creates a Diorama TAR.GZ archive at the specified path. 25 + */ 26 + fun createArchive(outputPath: Path) { 27 + Logger.log("Creating Diorama archive: ${outputPath.fileName}") 28 + 29 + outputPath.parent?.createDirectories() 30 + 31 + TarArchiveOutputStream( 32 + GzipCompressorOutputStream(outputPath.outputStream()) 33 + ).use { tar -> 34 + // Set long file name mode for compatibility 35 + tar.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX) 36 + 37 + // Add project manifest 38 + addFileEntry(tar, "project.kdl", projectManifest) 39 + 40 + // Add target.json 41 + addTextEntry(tar, "target.json", config.toJson()) 42 + 43 + // Add assets.bin 44 + addFileEntry(tar, "assets.bin", archiveBin) 45 + } 46 + 47 + Logger.log("Diorama archive created: ${outputPath.fileSize()} bytes") 48 + } 49 + 50 + /** 51 + * Adds a text entry to the TAR archive. 52 + */ 53 + private fun addTextEntry(tar: TarArchiveOutputStream, name: String, content: String) { 54 + val bytes = content.toByteArray(Charsets.UTF_8) 55 + val entry = TarArchiveEntry(name) 56 + entry.size = bytes.size.toLong() 57 + tar.putArchiveEntry(entry) 58 + tar.write(bytes) 59 + tar.closeArchiveEntry() 60 + } 61 + 62 + /** 63 + * Adds a file entry to the TAR archive. 64 + */ 65 + private fun addFileEntry(tar: TarArchiveOutputStream, name: String, file: Path) { 66 + val entry = TarArchiveEntry(file.toFile(), name) 67 + tar.putArchiveEntry(entry) 68 + file.inputStream().use { input -> 69 + input.copyTo(tar) 70 + } 71 + tar.closeArchiveEntry() 72 + } 73 + }
+40
src/main/java/assets/archive/DioramaConfig.kt
··· 1 + package assets.archive 2 + 3 + import kotlinx.serialization.Serializable 4 + import kotlinx.serialization.encodeToString 5 + import kotlinx.serialization.json.Json 6 + 7 + /** Target configuration for Diorama package. */ 8 + data class DioramaConfig( 9 + val assetsArchiveRomStart: Int, 10 + val engineSha: String 11 + ) { 12 + companion object { 13 + /** Standard ROM address for AssetsArchive in papermario-dx builds. */ 14 + const val DEFAULT_ROM_START = 0x1E40000 15 + } 16 + /** 17 + * Generates target.json content. 18 + */ 19 + fun toJson(): String { 20 + val json = Json { prettyPrint = false } 21 + val target = TargetJson( 22 + engine = PapermarioDxConfig( 23 + sha = engineSha, 24 + assets_archive_ROM_START = assetsArchiveRomStart 25 + ) 26 + ) 27 + return json.encodeToString(target) 28 + } 29 + 30 + @Serializable 31 + internal data class TargetJson( 32 + val engine: PapermarioDxConfig 33 + ) 34 + 35 + @Serializable 36 + internal data class PapermarioDxConfig( 37 + val sha: String, 38 + val assets_archive_ROM_START: Int 39 + ) 40 + }
+6
src/main/java/assets/archive/InvalidRomException.kt
··· 1 + package assets.archive 2 + 3 + /** 4 + * Exception thrown when attempting to patch a ROM that is not a valid papermario-dx build. 5 + */ 6 + class InvalidRomException(message: String, cause: Throwable? = null) : Exception(message, cause)
+36
src/main/java/assets/archive/SymbolReader.kt
··· 1 + package assets.archive 2 + 3 + import java.io.File 4 + 5 + /** 6 + * Reads a linker symbols file (syms.ld) and extracts symbol addresses. 7 + */ 8 + fun readSymbolAddress(symsFile: File, symbolName: String): Int? { 9 + if (!symsFile.exists()) { 10 + return null 11 + } 12 + 13 + // syms.ld format: 14 + // symbolName = 0x12345678; /* optional comment */ 15 + val pattern = Regex("""^\s*$symbolName\s*=\s*0x([0-9A-Fa-f]+)\s*;""") 16 + 17 + symsFile.useLines { lines -> 18 + for (line in lines) { 19 + val match = pattern.find(line) 20 + if (match != null) { 21 + val hexValue = match.groupValues[1] 22 + return hexValue.toIntOrNull(16) 23 + } 24 + } 25 + } 26 + 27 + return null 28 + } 29 + 30 + /** 31 + * Reads the mapfs_ROM_START symbol from a linker symbols file. 32 + */ 33 + fun readMapfsRomStart(symsFile: File): Int { 34 + return readSymbolAddress(symsFile, "mapfs_ROM_START") 35 + ?: throw IllegalStateException("mapfs_ROM_START symbol not found in ${symsFile.absolutePath}") 36 + }
+2 -4
src/main/java/assets/ui/BackgroundAsset.kt
··· 1 1 package assets.ui 2 2 3 3 import assets.Asset 4 + import assets.AssetsDir 4 5 import java.awt.Image 5 6 import java.awt.image.BufferedImage 6 7 import java.io.IOException 7 8 import java.nio.file.Path 8 9 import javax.imageio.ImageIO 9 10 10 - class BackgroundAsset(root: Path, relativePath: Path) : Asset(root, relativePath) { 11 + class BackgroundAsset(assetsDir: AssetsDir, relativePath: Path) : Asset(assetsDir, relativePath) { 11 12 @JvmField 12 13 val bimg: BufferedImage? 13 - 14 - /** Convenience constructor for Java interop. */ 15 - constructor(asset: Asset) : this(asset.root, asset.relativePath) 16 14 17 15 init { 18 16 bimg = try {
+40 -8
src/main/java/assets/ui/MapAsset.kt
··· 4 4 import app.Environment 5 5 import assets.Asset 6 6 import assets.AssetManager 7 + import assets.AssetsDir 8 + import game.map.Map 9 + import game.map.compiler.CollisionCompiler 10 + import game.map.compiler.GeometryCompiler 7 11 import game.map.editor.MapEditor 12 + import project.build.ArtifactType 13 + import project.build.BuildArtifact 14 + import project.build.BuildCtx 15 + import project.build.BuildResult 8 16 import util.Logger 9 17 import util.Priority 10 18 import java.awt.Image ··· 17 25 import java.util.regex.Pattern 18 26 import javax.imageio.ImageIO 19 27 20 - class MapAsset(root: Path, relativePath: Path) : Asset(root, relativePath) { 28 + class MapAsset(assetsDir: AssetsDir, relativePath: Path) : Asset(assetsDir, relativePath) { 21 29 @JvmField 22 30 var desc: String = "" 23 31 24 - /** Convenience constructor for Java interop. */ 25 - constructor(asset: Asset) : this(asset.root, asset.relativePath) 32 + /** The map.xml file inside the .map directory. */ 33 + val xmlFile: File get() = path.resolve("map.xml").toFile() 26 34 27 35 init { 28 36 // Read Map tag quickly without parsing whole XML file 29 37 try { 30 - BufferedReader(FileReader(getFile())).use { reader -> 38 + BufferedReader(FileReader(xmlFile)).use { reader -> 31 39 var line: String? 32 40 while (true) { 33 41 line = reader.readLine() ?: break // Encountered final line without finding Map tag ··· 50 58 51 59 override fun getAssetDescription(): String = desc 52 60 61 + override suspend fun build(ctx: BuildCtx): BuildResult { 62 + return try { 63 + // Load the map from XML 64 + val map = Map.loadMap(xmlFile) 65 + 66 + // Compile geometry 67 + val shape = ctx.artifact(this, "_shape") 68 + GeometryCompiler(map, shape) 69 + 70 + // Compile collision 71 + val hit = ctx.artifact(this, "_hit") 72 + CollisionCompiler(map, hit) 73 + 74 + BuildResult.Success( 75 + artifacts = listOf( 76 + BuildArtifact(shape, ArtifactType.SHAPE), 77 + BuildArtifact(hit, ArtifactType.COLLISION) 78 + ) 79 + ) 80 + } catch (e: Exception) { 81 + BuildResult.Failed(e) 82 + } 83 + } 84 + 53 85 override fun delete(): Boolean { 54 86 val thumbFile = File("$PROJ_THUMBNAIL${relativePath}.png") 55 87 if (thumbFile.exists()) ··· 101 133 var editor: MapEditor? = null 102 134 103 135 try { 104 - for (asset in AssetManager.getMapSources()) { 105 - val thumbFile = File("$PROJ_THUMBNAIL${asset.relativePath}.png") 136 + for (mapAsset in AssetManager.getMapSources()) { 137 + val thumbFile = File("$PROJ_THUMBNAIL${mapAsset.relativePath}.png") 106 138 if (thumbFile.exists()) 107 139 continue 108 - Logger.log("Capturing thumbnail for $asset...", Priority.MILESTONE) 140 + Logger.log("Capturing thumbnail for $mapAsset...", Priority.MILESTONE) 109 141 if (editor == null) 110 142 editor = MapEditor(false) 111 143 editor.generateThumbnail( 112 - asset.getFile(), 144 + mapAsset.xmlFile, 113 145 thumbFile, 114 146 Asset.THUMBNAIL_WIDTH * 2, 115 147 Asset.THUMBNAIL_HEIGHT * 2
+3 -4
src/main/java/assets/ui/SelectBackgroundDialog.java
··· 25 25 26 26 import app.Environment; 27 27 import app.SwingUtils; 28 - import assets.Asset; 29 28 import assets.AssetManager; 30 29 import net.miginfocom.swing.MigLayout; 31 30 import util.Logger; ··· 62 61 private DialogResult result = DialogResult.NONE; 63 62 private BackgroundAsset selectedObject; 64 63 65 - private SelectBackgroundDialog(Collection<Asset> assets, String initialSelection) 64 + private SelectBackgroundDialog(Collection<BackgroundAsset> assets, String initialSelection) 66 65 { 67 66 super(null, java.awt.Dialog.ModalityType.TOOLKIT_MODAL); 68 67 setDefaultCloseOperation(DISPOSE_ON_CLOSE); 69 68 70 69 DefaultListModel<BackgroundAsset> listModel = new DefaultListModel<>(); 71 70 listModel.addElement(null); 72 - for (Asset ah : assets) { 71 + for (BackgroundAsset ah : assets) { 73 72 // ignore .alt background 74 73 if (ah.getRelativePath().toString().endsWith("_bg.png")) { 75 - listModel.addElement(new BackgroundAsset(ah)); 74 + listModel.addElement(ah); 76 75 } 77 76 } 78 77
+5 -5
src/main/java/assets/ui/SelectMapDialog.java
··· 62 62 63 63 switch (chooser.result) { 64 64 case DUPLICATE: 65 - newMapFile = promptCopyMap(selectedFile.getFile()); 65 + newMapFile = promptCopyMap(selectedFile.getXmlFile()); 66 66 if (newMapFile == null) { 67 67 chooser.result = SelectMapResult.CANCEL; 68 68 } ··· 74 74 } 75 75 return newMapFile; 76 76 case OPEN: 77 - return selectedFile.getFile(); 77 + return selectedFile.getXmlFile(); 78 78 case CANCEL: 79 79 break; 80 80 } ··· 204 204 205 205 private SelectMapResult result = SelectMapResult.CANCEL; 206 206 207 - private SelectMapDialog(Collection<Asset> assets) 207 + private SelectMapDialog(Collection<MapAsset> assets) 208 208 { 209 209 super(null, java.awt.Dialog.ModalityType.TOOLKIT_MODAL); 210 210 setDefaultCloseOperation(DISPOSE_ON_CLOSE); 211 211 212 212 DefaultListModel<MapAsset> listModel = new DefaultListModel<>(); 213 - for (Asset ah : assets) { 214 - listModel.addElement(new MapAsset(ah)); 213 + for (MapAsset mapAsset : assets) { 214 + listModel.addElement(mapAsset); 215 215 } 216 216 217 217 list = new JList<>();
+2 -16
src/main/java/assets/ui/SelectTexDialog.java
··· 6 6 import java.io.File; 7 7 import java.io.IOException; 8 8 import java.util.Collection; 9 - import java.util.List; 10 - import java.util.concurrent.CompletableFuture; 11 - import java.util.stream.Collectors; 12 9 13 10 import javax.swing.BorderFactory; 14 11 import javax.swing.DefaultListModel; ··· 28 25 29 26 import app.Environment; 30 27 import app.SwingUtils; 31 - import assets.Asset; 32 28 import assets.AssetManager; 33 29 import net.miginfocom.swing.MigLayout; 34 30 import util.Logger; ··· 70 66 private DialogResult result = DialogResult.NONE; 71 67 private TexturesAsset selectedObject; 72 68 73 - private SelectTexDialog(Collection<Asset> assets, String initialSelection) 69 + private SelectTexDialog(Collection<TexturesAsset> assets, String initialSelection) 74 70 { 75 71 super(null, java.awt.Dialog.ModalityType.TOOLKIT_MODAL); 76 72 setDefaultCloseOperation(DISPOSE_ON_CLOSE); 77 73 78 - // load texture archive previews in parallel 79 - List<CompletableFuture<TexturesAsset>> futures = assets.stream() 80 - .map(ah -> CompletableFuture.supplyAsync(() -> new TexturesAsset(ah), Environment.getExecutor())) 81 - .collect(Collectors.toList()); 82 - 83 - // wait for all tasks to complete and collect results 84 - List<TexturesAsset> textureAssets = futures.stream() 85 - .map(CompletableFuture::join) 86 - .collect(Collectors.toList()); 87 - 88 74 DefaultListModel<TexturesAsset> listModel = new DefaultListModel<>(); 89 - textureAssets.forEach(listModel::addElement); 75 + assets.forEach(listModel::addElement); 90 76 91 77 JList<TexturesAsset> list = new JList<>(); 92 78 list.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
+17 -55
src/main/java/assets/ui/TexturesAsset.kt
··· 1 1 package assets.ui 2 2 3 3 import assets.Asset 4 + import assets.AssetsDir 4 5 import assets.AssetSubdir 5 6 import org.apache.commons.io.FileUtils 6 7 import org.apache.commons.io.FilenameUtils ··· 14 15 import java.nio.file.Path 15 16 import javax.imageio.ImageIO 16 17 17 - class TexturesAsset(root: Path, relativePath: Path) : Asset(root, relativePath) { 18 - private val textures = mutableListOf<BufferedImage>() 19 - 20 - /** Convenience constructor for Java interop. */ 21 - constructor(asset: Asset) : this(asset.root, asset.relativePath) 22 - 23 - init { 24 - val dirName = "${FilenameUtils.getBaseName(name)}/" 25 - val dir = File(root.toFile(), "${AssetSubdir.MAP_TEX}$dirName") 26 - 18 + /** 19 + * Textures are directories shaped like this: 20 + * name.tex/ 21 + * textures.json 22 + * some_texture.png 23 + * another_texture.png 24 + */ 25 + class TexturesAsset(assetsDir: AssetsDir, relativePath: Path) : Asset(assetsDir, relativePath) { 26 + fun loadTextures(): List<BufferedImage> { 27 + val textures = mutableListOf<BufferedImage>() 27 28 try { 28 29 val images = mutableListOf<File>() 29 30 30 - Files.newDirectoryStream(dir.toPath(), "*.png").use { stream -> 31 + Files.newDirectoryStream(path, "*.png").use { stream -> 31 32 for (file in stream) { 32 33 if (Files.isRegularFile(file)) 33 34 images.add(file.toFile()) 34 35 } 35 36 } 36 37 37 - images.shuffle() 38 - 39 38 for (image in images) { 40 39 val img = readImage(image) 41 40 if (img != null) 42 41 textures.add(img) 43 42 } 44 43 } catch (e: IOException) { 45 - Logger.logError("IOException while gathering previews from $dirName") 46 - } 47 - } 48 - 49 - fun getPreview(index: Int): BufferedImage? = 50 - if (index < textures.size) textures[index] else null 51 - 52 - private fun getCompanionDir(): File? { 53 - val dirName = "${FilenameUtils.getBaseName(name)}/" 54 - val dir = File(root.toFile(), "${AssetSubdir.MAP_TEX}$dirName") 55 - return if (dir.isDirectory) dir else null 56 - } 57 - 58 - override fun delete(): Boolean { 59 - val dir = getCompanionDir() 60 - if (dir != null) 61 - FileUtils.deleteQuietly(dir) 62 - return super.delete() 63 - } 64 - 65 - override fun rename(name: String): Boolean { 66 - val dir = getCompanionDir() 67 - if (dir != null) { 68 - try { 69 - val newDirName = FilenameUtils.getBaseName(name) 70 - val newDir = File(dir.parentFile, newDirName) 71 - Files.move(dir.toPath(), newDir.toPath()) 72 - } catch (e: IOException) { 73 - return false 74 - } 44 + Logger.logError("IOException loading textures from $relativePath: ${e.message}") 75 45 } 76 - return super.rename(name) 46 + return textures 77 47 } 78 48 79 - override fun move(targetDir: File): Boolean { 80 - val dir = getCompanionDir() 81 - if (dir != null) { 82 - try { 83 - FileUtils.moveDirectory(dir, File(targetDir, dir.name)) 84 - } catch (e: IOException) { 85 - return false 86 - } 87 - } 88 - return super.move(targetDir) 89 - } 49 + @Deprecated("use explorer") 50 + fun getPreview(index: Int): BufferedImage? = null 90 51 91 52 override fun thumbnailHasCheckerboard(): Boolean = false 92 53 93 54 override fun loadThumbnail(): Image? { 55 + val textures = loadTextures() 94 56 if (textures.isEmpty()) 95 57 return null 96 58
+2 -2
src/main/java/game/map/Map.java
··· 695 695 long t1 = System.nanoTime(); 696 696 double sec = (t1 - t0) / 1e9; 697 697 if (sec > 0.5) 698 - Logger.logf("Loaded %s in %.02f seconds", f.getName(), sec); 698 + Logger.logf("Loaded %s in %.02f seconds", f.toString(), sec); 699 699 else 700 - Logger.logf("Loaded %s in %.02f ms", f.getName(), sec * 1e3); 700 + Logger.logf("Loaded %s in %.02f ms", f.toString(), sec * 1e3); 701 701 702 702 return map; 703 703 }
+33 -11
src/main/java/game/map/compiler/CollisionCompiler.java
··· 4 4 import java.io.IOException; 5 5 import java.io.PrintWriter; 6 6 import java.io.RandomAccessFile; 7 + import java.nio.file.Path; 7 8 import java.util.ArrayList; 8 9 import java.util.HashMap; 9 10 import java.util.Objects; ··· 27 28 { 28 29 public CollisionCompiler(Map map) throws IOException 29 30 { 30 - File build_dec = new File(AssetManager.getMapBuildDir(), map.getName() + "_hit.bin"); 31 + this(map, new File(AssetManager.getMapBuildDir(), map.getName() + "_hit.bin"), 32 + Directories.ENGINE_INCLUDE_MAPFS.file(map.getName() + "_hit.h")); 33 + } 34 + 35 + public CollisionCompiler(Map map, Path outputPath) throws IOException 36 + { 37 + this(map, outputPath.toFile(), null); 38 + } 39 + 40 + public CollisionCompiler(Map map, File outputFile) throws IOException 41 + { 42 + this(map, outputFile, null); 43 + } 31 44 32 - Logger.log("Compiling map collision to " + build_dec.getPath()); 45 + public CollisionCompiler(Map map, Path outputPath, Path headerPath) throws IOException 46 + { 47 + this(map, outputPath.toFile(), headerPath != null ? headerPath.toFile() : null); 48 + } 49 + 50 + public CollisionCompiler(Map map, File outputFile, File headerFile) throws IOException 51 + { 52 + Logger.log("Compiling map collision to " + outputFile.getPath()); 53 + File build_dec = outputFile; 33 54 34 55 if (build_dec.exists()) 35 56 build_dec.delete(); ··· 44 65 raf.writeInt(zoneHeaderOffset); 45 66 raf.close(); 46 67 47 - File headerFile = Directories.ENGINE_INCLUDE_MAPFS.file(map.getName() + "_hit.h"); 48 - try (PrintWriter pw = IOUtils.getBufferedPrintWriter(headerFile)) { 49 - for (Collider c : map.colliderTree.getList()) { 50 - pw.printf("#define %-23s 0x%X%n", "COLLIDER_" + c.getName(), c.getNode().getTreeIndex()); 51 - } 52 - pw.println(); 53 - for (Zone z : map.zoneTree.getList()) { 54 - pw.printf("#define %-23s 0x%X%n", "ZONE_" + z.getName(), z.getNode().getTreeIndex()); 68 + if (headerFile != null) { 69 + try (PrintWriter pw = IOUtils.getBufferedPrintWriter(headerFile)) { 70 + for (Collider c : map.colliderTree.getList()) { 71 + pw.printf("#define %-23s 0x%X%n", "COLLIDER_" + c.getName(), c.getNode().getTreeIndex()); 72 + } 73 + pw.println(); 74 + for (Zone z : map.zoneTree.getList()) { 75 + pw.printf("#define %-23s 0x%X%n", "ZONE_" + z.getName(), z.getNode().getTreeIndex()); 76 + } 77 + pw.println(); 55 78 } 56 - pw.println(); 57 79 } 58 80 } 59 81
+12 -2
src/main/java/game/map/compiler/GeometryCompiler.java
··· 4 4 import java.io.IOException; 5 5 import java.io.PrintWriter; 6 6 import java.io.RandomAccessFile; 7 + import java.nio.file.Path; 7 8 import java.util.ArrayList; 8 9 import java.util.Collections; 9 10 import java.util.HashMap; ··· 74 75 75 76 public GeometryCompiler(Map map) throws IOException 76 77 { 77 - File build_dec = new File(AssetManager.getMapBuildDir(), map.getName() + "_shape.bin"); 78 + this(map, new File(AssetManager.getMapBuildDir(), map.getName() + "_shape.bin")); 79 + } 80 + 81 + public GeometryCompiler(Map map, Path outputPath) throws IOException 82 + { 83 + this(map, outputPath.toFile()); 84 + } 78 85 79 - Logger.log("Compiling map geometry to " + build_dec.getPath()); 86 + public GeometryCompiler(Map map, File outputFile) throws IOException 87 + { 88 + Logger.log("Compiling map geometry to " + outputFile.getPath()); 89 + File build_dec = outputFile; 80 90 81 91 if (build_dec.exists()) 82 92 build_dec.delete();
+12 -11
src/main/java/game/map/editor/MapEditor.java
··· 52 52 import app.config.Options.Scope; 53 53 import assets.Asset; 54 54 import assets.AssetManager; 55 + import assets.ui.MapAsset; 55 56 import assets.ui.SelectMapDialog; 56 57 import assets.ui.SelectTexDialog; 57 58 import common.FrameLimiter; ··· 859 860 continue; 860 861 } 861 862 862 - Asset ah = AssetManager.getMap(mapName); 863 + MapAsset ah = AssetManager.getMap(mapName); 863 864 if (ah.exists()) { 864 865 recentMaps.add(mapName); 865 866 } ··· 963 964 try { 964 965 File backupFile = AssetManager.getSaveMapFile(baseMap.getName() + Directories.MAP_BACKUP_SUFFIX); 965 966 966 - if (!backupFile.exists() || backupFile.lastModified() <= baseMap.lastModified) 967 + if (backupFile == null || !backupFile.exists() || backupFile.lastModified() <= baseMap.lastModified) 967 968 return baseMap; 968 969 969 970 Map backupMap = Map.loadMap(backupFile); ··· 1000 1001 if (editorConfig != null) { 1001 1002 String lastMapName = editorConfig.getString(Options.RecentMap0); 1002 1003 if (lastMapName != null && !lastMapName.isBlank()) { 1003 - Asset ah = AssetManager.getMap(lastMapName); 1004 + MapAsset ah = AssetManager.getMap(lastMapName); 1004 1005 if (ah.exists()) { 1005 1006 options = new String[] { "Browse Maps", "Reopen " + lastMapName }; 1006 - lastMap = ah.getFile(); 1007 + lastMap = ah.getXmlFile(); 1007 1008 } 1008 1009 } 1009 1010 } ··· 3173 3174 3174 3175 case CHOSE_MAP: 3175 3176 changeMapState = ChangeMapState.LOADING_MAP; 3176 - destMapFile = AssetManager.getMap(destMapName).getFile(); 3177 + destMapFile = AssetManager.getMap(destMapName).getXmlFile(); 3177 3178 if (!destMapFile.exists()) { 3178 3179 changeMapState = ChangeMapState.LOADING_FAILED; 3179 3180 break; ··· 3993 3994 Logger.logError("Override name is missing or empty."); 3994 3995 return; 3995 3996 } 3996 - Asset ah = AssetManager.getMap(overrideName); 3997 + MapAsset ah = AssetManager.getMap(overrideName); 3997 3998 if (!ah.exists()) { 3998 3999 Logger.logError("Couldn't find map: " + overrideName + Directories.EXT_MAP); 3999 4000 return; 4000 4001 } 4001 - shapeOverride = Map.loadMap(ah.getFile()); 4002 + shapeOverride = Map.loadMap(ah.getXmlFile()); 4002 4003 TextureManager.assignModelTextures(shapeOverride); 4003 - Logger.log("Loaded override geometry: " + shapeOverride.getName()); 4004 + Logger.log("Loaded override geometry: " + ah.getName()); 4004 4005 } 4005 4006 } 4006 4007 ··· 4014 4015 Logger.logError("Override name is missing or empty."); 4015 4016 return; 4016 4017 } 4017 - Asset ah = AssetManager.getMap(overrideName); 4018 + MapAsset ah = AssetManager.getMap(overrideName); 4018 4019 if (!ah.exists()) { 4019 4020 Logger.logError("Couldn't find map: " + overrideName + Directories.EXT_MAP); 4020 4021 return; 4021 4022 } 4022 - hitOverride = Map.loadMap(ah.getFile()); 4023 - Logger.log("Loaded override collision: " + hitOverride.getName()); 4023 + hitOverride = Map.loadMap(ah.getXmlFile()); 4024 + Logger.log("Loaded override collision: " + ah.getName()); 4024 4025 } 4025 4026 } 4026 4027
+3 -3
src/main/java/game/map/editor/render/TextureManager.java
··· 137 137 TextureArchive ta; 138 138 139 139 try { 140 - Asset ah = AssetManager.getTextureArchive(texArchiveName); 141 - if (!ah.exists()) 140 + var ah = AssetManager.getTextureArchive(texArchiveName); 141 + if (ah == null || !ah.exists()) 142 142 return false; 143 143 if (FilenameUtils.getExtension(ah.getName()).equals(Directories.EXT_OLD_TEX)) { 144 144 ta = TextureArchive.loadLegacy(ah.getFile()); 145 145 } 146 146 else { 147 - ta = TextureArchive.load(ah.getFile()); 147 + ta = TextureArchive.load(ah); 148 148 } 149 149 } 150 150 catch (IOException e) {
+3 -2
src/main/java/game/map/editor/ui/SwingGUI.java
··· 55 55 import app.SwingUtils.OpenDialogCounter; 56 56 import assets.Asset; 57 57 import assets.AssetManager; 58 + import assets.ui.MapAsset; 58 59 import assets.ui.SelectBackgroundDialog; 59 60 import assets.ui.SelectMapDialog; 60 61 import assets.ui.SelectTexDialog; ··· 1303 1304 private void prompt_OpenMap(String mapName) 1304 1305 { 1305 1306 if (!editor.map.modified || promptForSave()) { 1306 - Asset ah = AssetManager.getMap(mapName); 1307 + MapAsset ah = AssetManager.getMap(mapName); 1307 1308 if (ah.exists()) { 1308 1309 editor.doNextFrame(() -> { 1309 - editor.action_OpenMap(ah.getFile()); 1310 + editor.action_OpenMap(ah.getXmlFile()); 1310 1311 }); 1311 1312 } 1312 1313 }
+8 -4
src/main/java/game/map/impex/ObjExporter.java
··· 34 34 35 35 public void writeModels(Iterable<Model> models, String texName) 36 36 { 37 - File textureFile = AssetManager.get(AssetSubdir.MAP_TEX, texName + "/" + texName + ".mtl").getFile(); 38 - if (textureFile.exists()) { 39 - pw.println("mtllib " + texName); 40 - pw.println(""); 37 + // Look for MTL file in texture archive directory 38 + assets.Asset texArchive = AssetManager.getTextureArchive(texName); 39 + if (texArchive != null) { 40 + File textureFile = new File(texArchive.getFile(), texName + ".mtl"); 41 + if (textureFile.exists()) { 42 + pw.println("mtllib " + texName); 43 + pw.println(""); 44 + } 41 45 } 42 46 43 47 for (Model mdl : models) {
+1 -1
src/main/java/game/map/scripts/extract/Extractor.java
··· 89 89 { 90 90 Logger.log("Extracting data from " + mapName, Priority.IMPORTANT); 91 91 92 - File mapFile = AssetManager.getMap(mapName).getFile(); 92 + File mapFile = AssetManager.getMap(mapName).getXmlFile(); 93 93 if (!mapFile.exists()) { 94 94 throw new StarRodException("Couldn't find map file for " + mapName); 95 95 }
+1 -1
src/main/java/game/map/scripts/extract/StageExtractor.java
··· 71 71 if (suffix == null) 72 72 suffix = ""; 73 73 74 - File mapFile = AssetManager.getMap(baseName).getFile(); 74 + File mapFile = AssetManager.getMap(baseName).getXmlFile(); 75 75 if (!mapFile.exists()) { 76 76 Logger.logWarning("Couldn't find map file for stage: " + baseName); 77 77 return;
+7 -15
src/main/java/game/texture/Texture.java
··· 7 7 import java.util.LinkedList; 8 8 import java.util.List; 9 9 10 - import org.apache.commons.io.FilenameUtils; 11 - 12 10 import app.input.InputFileException; 13 - import assets.Asset; 14 - import assets.AssetManager; 15 - import assets.AssetSubdir; 16 11 import game.texture.TextureArchive.JsonTexture; 17 12 18 13 public class Texture ··· 248 243 249 244 public static Texture parseTexture(File source, JsonTexture json) throws IOException 250 245 { 251 - File dir = source.getParentFile(); 252 - String texName = FilenameUtils.getBaseName(source.getName()); 253 - //File subdir = new File(dir, FilenameUtils.getBaseName(source.getName())); 246 + File dir = source; 254 247 255 248 Texture tx = new Texture(json.name); 256 249 tx.hWrap = new int[2]; ··· 309 302 throw new InputFileException(source, "(%s) Texture cannot have both mipmaps and aux.", json.name); 310 303 } 311 304 312 - Asset mainAsset = AssetManager.get(AssetSubdir.MAP_TEX, texName + "/" + tx.name + ".png"); 313 - 314 - tx.main = Tile.load(mainAsset.getFile(), imgFormat); 305 + File mainFile = new File(dir, tx.name + ".png"); 306 + tx.main = Tile.load(mainFile, imgFormat); 315 307 316 308 if (tx.hasAux) { 317 - Asset auxAsset = AssetManager.get(AssetSubdir.MAP_TEX, texName + "/" + tx.name + "_AUX.png"); 318 - tx.aux = Tile.load(auxAsset.getFile(), auxFormat); 309 + File auxFile = new File(dir, tx.name + "_AUX.png"); 310 + tx.aux = Tile.load(auxFile, auxFormat); 319 311 } 320 312 321 313 if (tx.hasMipmaps) { ··· 329 321 int mmWidth = tx.main.width / divisor; 330 322 331 323 String mmName = tx.name + "_MM" + (tx.mipmapList.size() + 1); 332 - Asset mmAsset = AssetManager.get(AssetSubdir.MAP_TEX, texName + "/" + mmName + ".png"); 333 - Tile mipmap = Tile.load(mmAsset.getFile(), imgFormat); 324 + File mmFile = new File(dir, mmName + ".png"); 325 + Tile mipmap = Tile.load(mmFile, imgFormat); 334 326 335 327 if (mipmap.height != mmHeight) 336 328 throw new InputFileException(source, "%s has incorrect height: %s instead of %s", mmName, mipmap.height, mmHeight);
+6 -4
src/main/java/game/texture/TextureArchive.java
··· 7 7 import java.util.LinkedList; 8 8 import java.util.List; 9 9 10 + import assets.Asset; 11 + import assets.ui.TexturesAsset; 10 12 import org.apache.commons.io.FilenameUtils; 11 13 12 14 import com.google.gson.Gson; ··· 45 47 boolean variant; 46 48 } 47 49 48 - public static TextureArchive load(File texFile) throws IOException 50 + public static TextureArchive load(TexturesAsset asset) throws IOException 49 51 { 50 - String texName = FilenameUtils.getBaseName(texFile.getName()); 52 + String texName = asset.getName(); 51 53 TextureArchive ta = new TextureArchive(texName); 52 54 53 55 Gson gson = new Gson(); 54 - JsonReader jsonReader = new JsonReader(new FileReader(texFile)); 56 + JsonReader jsonReader = new JsonReader(new FileReader(asset.getPath().resolve("textures.json").toFile())); 55 57 JsonTexture[] jsonTextures = gson.fromJson(jsonReader, JsonTexture[].class); 56 58 57 59 for (JsonTexture tex : jsonTextures) { 58 - ta.textureList.add(Texture.parseTexture(texFile, tex)); 60 + ta.textureList.add(Texture.parseTexture(asset.getFile(), tex)); 59 61 } 60 62 61 63 return ta;
+371
src/main/java/project/Build.kt
··· 1 + package project 2 + 3 + import assets.Asset 4 + import assets.AssetManager 5 + import assets.AssetRegistry 6 + import assets.archive.AssetsArchiveBuilder 7 + import assets.archive.AssetsArchiveCompressor 8 + import assets.archive.DioramaArchiver 9 + import assets.archive.DioramaConfig 10 + import kotlinx.coroutines.* 11 + import kotlinx.coroutines.channels.Channel 12 + import kotlinx.coroutines.future.future 13 + import project.build.* 14 + import util.Logger 15 + import java.nio.file.Path 16 + import java.util.concurrent.CompletableFuture 17 + import kotlin.io.path.* 18 + 19 + /** 20 + * Represents a single build operation. 21 + * Create via Project.createBuild(), execute, then dispose. 22 + */ 23 + class Build(private val project: Project) { 24 + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) 25 + 26 + /** 27 + * Progress callback for asset building. 28 + */ 29 + interface ProgressCallback { 30 + fun onBuildStarted(totalAssets: Int) 31 + fun onAssetBuilt(asset: Asset, successCount: Int, totalAssets: Int) 32 + fun onAssetFailed(asset: Asset, error: Exception) 33 + fun onBuildComplete(successCount: Int, errorCount: Int) 34 + } 35 + 36 + /** 37 + * Executes the build asynchronously. 38 + * @param progressCallback Progress updates during asset building 39 + * @param generateArchive Whether to generate AssetsArchive after building 40 + * @param generateDiorama Whether to package as Diorama (requires generateArchive=true) 41 + * @return true if the build succeeded, false if there were errors 42 + */ 43 + suspend fun execute( 44 + progressCallback: ProgressCallback? = null, 45 + generateArchive: Boolean = false, 46 + generateDiorama: Boolean = false 47 + ): Boolean { 48 + return withContext(Dispatchers.IO) { 49 + // Phase 1: Build all assets 50 + val success = buildAllAssets(progressCallback) 51 + if (!success || !generateArchive) return@withContext success 52 + 53 + // Phase 2: Generate AssetsArchive 54 + val artifacts = collectArtifacts() 55 + val archivePath = generateAssetsArchive(artifacts) 56 + 57 + // Phase 3: Package Diorama (optional) 58 + if (generateDiorama) { 59 + packageDiorama(archivePath) 60 + } 61 + 62 + true 63 + } 64 + } 65 + 66 + /** 67 + * Executes the build asynchronously (Java-friendly version). 68 + * @param generateArchive Whether to generate AssetsArchive after building 69 + * @param generateDiorama Whether to package as Diorama (requires generateArchive=true) 70 + * @return true if the build succeeded, false if there were errors 71 + */ 72 + fun executeAsync( 73 + generateArchive: Boolean = false, 74 + generateDiorama: Boolean = false 75 + ): CompletableFuture<Boolean> { 76 + return scope.future { 77 + execute( 78 + progressCallback = null, 79 + generateArchive = generateArchive, 80 + generateDiorama = generateDiorama 81 + ) 82 + } 83 + } 84 + 85 + /** 86 + * Cancels the build operation. 87 + */ 88 + fun cancel() { 89 + scope.cancel() 90 + } 91 + 92 + /** 93 + * Discovers all assets in the project by recursively walking the asset directory. 94 + * Only scans owned (project) assets, NOT engine assets. 95 + * 96 + * Note: Engine assets are read-only and never built. If a user modifies an engine asset, 97 + * the Asset's Copy-on-Write semantics will automatically copy it to the project directory, 98 + * where it will be discovered and built on the next scan. 99 + */ 100 + private fun discoverAssets(): List<Asset> { 101 + // getTopLevelAssetDir() returns the project's owned asset directory (index 0 in the stack) 102 + // This excludes engine assets which are read-only and at the base of the stack 103 + val assetRoot = AssetManager.getTopLevelAssetDir().toPath() 104 + if (!assetRoot.exists() || !assetRoot.isDirectory()) 105 + return emptyList() 106 + 107 + val assets = mutableListOf<Asset>() 108 + assetRoot.walk() 109 + .filter { it.isRegularFile() } 110 + .forEach { file -> 111 + val relativePath = assetRoot.relativize(file) 112 + val asset = AssetRegistry.instance.create(relativePath) 113 + assets.add(asset) 114 + } 115 + 116 + return assets 117 + } 118 + 119 + /** 120 + * Builds all assets in parallel, with header generation first. 121 + * @return true if the build succeeded, false if there were errors 122 + */ 123 + private suspend fun buildAllAssets(progressCallback: ProgressCallback?): Boolean { 124 + val buildDir = project.directory.toPath() / ".starrod" / "build" 125 + buildDir.createDirectories() 126 + 127 + val headersDir = buildDir / "headers" 128 + headersDir.createDirectories() 129 + 130 + val stateFile = project.directory.toPath() / ".starrod" / "build-state" / "state.json" 131 + val engineSha = getEngineSha() 132 + 133 + // Load or create build state 134 + var buildState = BuildState.load(stateFile, engineSha) 135 + if (buildState == null) { 136 + Logger.log("Build state invalidated or missing, rebuilding all assets") 137 + buildState = BuildState.create(engineSha) 138 + } 139 + 140 + // Discover all assets 141 + val allAssets = discoverAssets() 142 + Logger.log("Discovered ${allAssets.size} assets") 143 + 144 + // Filter to only assets that need rebuilding 145 + val assetsToRebuild = allAssets.filter { buildState.needsRebuild(it) } 146 + Logger.log("${assetsToRebuild.size} assets need rebuilding") 147 + 148 + if (assetsToRebuild.isEmpty()) { 149 + progressCallback?.onBuildComplete(0, 0) 150 + return true 151 + } 152 + 153 + progressCallback?.onBuildStarted(assetsToRebuild.size) 154 + 155 + // Generate headers first (in parallel) 156 + Logger.log("Generating headers...") 157 + val headerDispatcher = Dispatchers.IO.limitedParallelism(32) 158 + coroutineScope { 159 + assetsToRebuild.forEach { asset -> 160 + launch(headerDispatcher) { 161 + val headerPath = headersDir / "${asset.name}.hpp" 162 + try { 163 + asset.writeHeader(headerPath) 164 + } catch (e: Exception) { 165 + Logger.logError("Failed to generate header for ${asset.name}: ${e.message}") 166 + } 167 + } 168 + } 169 + } 170 + 171 + // Build context for compilation phase 172 + val ctx = BuildCtx( 173 + buildDir = buildDir, 174 + project = project, 175 + engineSha = engineSha, 176 + buildStateVersion = BuildState.CURRENT_VERSION, 177 + headersDir = headersDir 178 + ) 179 + 180 + // Build all assets in parallel 181 + val (successCount, errorCount, _) = buildAssetsInParallel( 182 + assetsToRebuild, 183 + ctx, 184 + buildState, 185 + progressCallback, 186 + 0, 187 + assetsToRebuild.size 188 + ) 189 + 190 + // Save build state (even on partial failure) 191 + buildState.save(stateFile) 192 + 193 + progressCallback?.onBuildComplete(successCount, errorCount) 194 + 195 + if (errorCount > 0) { 196 + Logger.logError("Build completed with $errorCount error(s)") 197 + return false 198 + } else { 199 + Logger.log("Build completed successfully: $successCount assets built") 200 + return true 201 + } 202 + } 203 + 204 + /** 205 + * Builds a list of assets in parallel with controlled concurrency. 206 + * Returns (successCount, errorCount, artifacts). 207 + */ 208 + private suspend fun buildAssetsInParallel( 209 + assets: List<Asset>, 210 + ctx: BuildCtx, 211 + buildState: BuildState, 212 + progressCallback: ProgressCallback?, 213 + currentSuccessCount: Int, 214 + totalAssets: Int 215 + ): Triple<Int, Int, List<BuildArtifact>> { 216 + val dispatcher = Dispatchers.IO.limitedParallelism(32) 217 + val errorChannel = Channel<Pair<Asset, Exception>>(Channel.UNLIMITED) 218 + val artifactChannel = Channel<BuildArtifact>(Channel.UNLIMITED) 219 + 220 + var successCount = currentSuccessCount 221 + 222 + // Launch parallel build jobs 223 + val jobs = assets.map { asset -> 224 + CoroutineScope(dispatcher + SupervisorJob()).launch { 225 + try { 226 + when (val result = asset.build(ctx)) { 227 + is BuildResult.NoOp -> { 228 + // Asset doesn't need building, but mark as visited 229 + buildState.markBuilt(asset) 230 + } 231 + is BuildResult.Success -> { 232 + buildState.markBuilt(asset) 233 + result.artifacts.forEach { artifactChannel.send(it) } 234 + synchronized(this@Build) { 235 + successCount++ 236 + progressCallback?.onAssetBuilt(asset, successCount, totalAssets) 237 + } 238 + } 239 + is BuildResult.Failed -> { 240 + errorChannel.send(asset to result.error) 241 + progressCallback?.onAssetFailed(asset, result.error) 242 + } 243 + } 244 + } catch (e: Exception) { 245 + if (e is CancellationException) throw e 246 + errorChannel.send(asset to e) 247 + progressCallback?.onAssetFailed(asset, e) 248 + } 249 + } 250 + } 251 + 252 + // Wait for all jobs to complete 253 + jobs.forEach { it.join() } 254 + 255 + // Collect errors 256 + errorChannel.close() 257 + val errors = mutableListOf<Pair<Asset, Exception>>() 258 + for (error in errorChannel) { 259 + errors.add(error) 260 + Logger.logError("Failed to build ${error.first.relativePath}: ${error.second.message}") 261 + } 262 + 263 + // Collect artifacts 264 + artifactChannel.close() 265 + val artifacts = mutableListOf<BuildArtifact>() 266 + for (artifact in artifactChannel) { 267 + artifacts.add(artifact) 268 + } 269 + 270 + return Triple(successCount - currentSuccessCount, errors.size, artifacts) 271 + } 272 + 273 + /** 274 + * Gets the current engine git SHA for cache invalidation. 275 + * Returns "unknown" if not in a git repository. 276 + */ 277 + private fun getEngineSha(): String { 278 + return try { 279 + val process = ProcessBuilder("git", "rev-parse", "HEAD") 280 + .directory(project.directory) 281 + .redirectErrorStream(true) 282 + .start() 283 + 284 + val sha = process.inputStream.bufferedReader().readText().trim() 285 + process.waitFor() 286 + 287 + if (process.exitValue() == 0) sha else "unknown" 288 + } catch (e: Exception) { 289 + "unknown" 290 + } 291 + } 292 + 293 + /** 294 + * Collects all built artifacts from the build directory. 295 + */ 296 + private fun collectArtifacts(): List<BuildArtifact> { 297 + val buildDir = project.directory.toPath() / ".starrod" / "build" 298 + if (!buildDir.exists() || !buildDir.isDirectory()) 299 + return emptyList() 300 + 301 + val artifacts = mutableListOf<BuildArtifact>() 302 + 303 + // Collect binary, shape, and collision artifacts 304 + buildDir.walk() 305 + .filter { it.isRegularFile() } 306 + .filter { !it.startsWith(buildDir / "headers") } // Exclude headers 307 + .filter { it.extension in setOf("bin", "shape", "collision") } 308 + .forEach { file -> 309 + val type = when (file.extension) { 310 + "shape" -> ArtifactType.SHAPE 311 + "collision" -> ArtifactType.COLLISION 312 + else -> ArtifactType.BINARY 313 + } 314 + artifacts.add(BuildArtifact(file, type)) 315 + } 316 + 317 + return artifacts 318 + } 319 + 320 + /** 321 + * Generates an AssetsArchive binary from built artifacts. 322 + * @return Path to the generated assets.bin 323 + */ 324 + private suspend fun generateAssetsArchive(artifacts: List<BuildArtifact>): Path { 325 + return withContext(Dispatchers.IO) { 326 + Logger.log("Generating AssetsArchive from ${artifacts.size} artifacts...") 327 + 328 + val builder = AssetsArchiveBuilder(project.manifest.name) 329 + 330 + // Add each artifact to the archive 331 + for (artifact in artifacts) { 332 + val name = artifact.path.fileName.toString() 333 + val data = artifact.path.readBytes() 334 + val compress = AssetsArchiveCompressor.shouldCompress(artifact.type) 335 + 336 + builder.addEntry(name, data, compress) 337 + } 338 + 339 + // Build the archive 340 + val archiveBin = builder.build() 341 + val outputPath = project.directory.toPath() / ".starrod" / "build" / "assets.bin" 342 + outputPath.writeBytes(archiveBin) 343 + 344 + Logger.log("Generated AssetsArchive: ${archiveBin.size} bytes") 345 + outputPath 346 + } 347 + } 348 + 349 + /** 350 + * Packages the AssetsArchive into a Diorama distribution file. 351 + */ 352 + private suspend fun packageDiorama(archivePath: Path) { 353 + return withContext(Dispatchers.IO) { 354 + Logger.log("Packaging Diorama...") 355 + 356 + val projectManifest = project.directory.toPath() / Manifest.FILENAME 357 + val engineSha = getEngineSha() 358 + val config = DioramaConfig( 359 + assetsArchiveRomStart = DioramaConfig.DEFAULT_ROM_START, 360 + engineSha = engineSha 361 + ) 362 + val modId = project.manifest.id ?: "unknown" 363 + val outputPath = project.directory.toPath() / ".starrod" / "build" / "$modId.diorama" 364 + 365 + val archiver = DioramaArchiver(projectManifest, config, archivePath) 366 + archiver.createArchive(outputPath) 367 + 368 + Logger.log("Diorama engine SHA: $engineSha") 369 + } 370 + } 371 + }
-70
src/main/java/project/Project.java
··· 1 - package project; 2 - 3 - import static app.Directories.DATABASE_TEMPLATES; 4 - 5 - import java.io.File; 6 - import java.io.IOException; 7 - 8 - import org.apache.commons.io.FileUtils; 9 - 10 - import dev.kdl.parse.KdlParseException; 11 - import project.engine.BuildException; 12 - import project.engine.Engine; 13 - 14 - /** 15 - * A fully-loaded project with an initialized engine. 16 - * Extends {@link ProjectListing} so it can be used anywhere a listing is expected. 17 - */ 18 - public class Project extends ProjectListing 19 - { 20 - private final Engine engine; 21 - 22 - /** Loads a project from a directory, initializing the engine. */ 23 - public Project(File path) throws IOException, KdlParseException 24 - { 25 - super(path); 26 - try { 27 - this.engine = Engine.forProject(this); 28 - } 29 - catch (BuildException e) { 30 - throw new IOException("Failed to initialize engine: " + e.getMessage(), e); 31 - } 32 - } 33 - 34 - public Engine getEngine() 35 - { 36 - return engine; 37 - } 38 - 39 - /** Creates a new project from a template. */ 40 - public static ProjectListing create(File path, String template, String id, String name) throws IOException, KdlParseException 41 - { 42 - if (!path.exists()) 43 - path.mkdirs(); 44 - if (!path.isDirectory()) 45 - throw new IllegalArgumentException("Project path must be a directory: " + path); 46 - 47 - // Copy entire template directory here 48 - File templateDir = DATABASE_TEMPLATES.file(template); 49 - if (!templateDir.exists()) 50 - throw new IllegalArgumentException("Missing template: " + templateDir.getPath()); 51 - FileUtils.copyDirectory(templateDir, path); 52 - 53 - // Substitute placeholders in project.kdl 54 - File manifestFile = new File(path, Manifest.FILENAME); 55 - if (manifestFile.exists()) { 56 - String content = FileUtils.readFileToString(manifestFile, "UTF-8"); 57 - content = content.replace("$PROJECT_ID", '"' + id + '"'); 58 - content = content.replace("$PROJECT_NAME", '"' + name.replace('"', '\\') + '"'); 59 - content = content.replace("$PROJECT_DESCRIPTION", '"' + "An amazing mod of Paper Mario" + '"'); // TODO: ui 60 - FileUtils.writeStringToFile(manifestFile, content, "UTF-8"); 61 - } 62 - 63 - return new ProjectListing(path); 64 - } 65 - 66 - public void build() 67 - { 68 - // TODO 69 - } 70 - }
+83
src/main/java/project/Project.kt
··· 1 + package project 2 + 3 + import app.Directories 4 + import assets.AssetsDir 5 + import dev.kdl.parse.KdlParseException 6 + import org.apache.commons.io.FileUtils 7 + import project.engine.BuildException 8 + import project.engine.Engine 9 + import java.io.File 10 + import java.io.IOException 11 + import kotlin.io.path.div 12 + 13 + /** 14 + * A fully-loaded project with an initialized engine. 15 + * Extends [ProjectListing] so it can be used anywhere a listing is expected. 16 + */ 17 + class Project @Throws(IOException::class, KdlParseException::class) constructor(path: File) : ProjectListing(path) { 18 + val engine: Engine = try { 19 + Engine.forProject(this) 20 + } catch (e: BuildException) { 21 + throw IOException("Failed to initialize engine: ${e.message}", e) 22 + } 23 + 24 + /** 25 + * Asset directory stack for the project. 26 + * 27 + * Assets are searched from index 0 to last, with first match winning. 28 + * Modifications always happen to the owned directory (index 0) via CoW. 29 + */ 30 + val assetDirectories: List<AssetsDir> 31 + get() = buildList { 32 + add(AssetsDir.Project(this@Project, directory.toPath() / "assets")) 33 + // Future: add(AssetsDir.Project(dependency, dependencyPath)) 34 + add(AssetsDir.Engine(engine, Directories.US_MAPFS.toFile().toPath())) // mapfs subdir 35 + } 36 + 37 + /** The owned (project) assets directory. */ 38 + val ownedAssetsDir: AssetsDir.Project 39 + get() = assetDirectories[0] as AssetsDir.Project 40 + 41 + /** The engine assets directory. */ 42 + val engineAssetsDir: AssetsDir.Engine 43 + get() = assetDirectories.last() as AssetsDir.Engine 44 + 45 + fun build() { 46 + // TODO 47 + } 48 + 49 + companion object { 50 + /** Creates a new project from a template. */ 51 + @JvmStatic 52 + @Throws(IOException::class, KdlParseException::class) 53 + fun create(path: File, template: String, id: String, name: String): ProjectListing { 54 + if (!path.exists()) 55 + path.mkdirs() 56 + if (!path.isDirectory) 57 + throw IllegalArgumentException("Project path must be a directory: $path") 58 + 59 + // Copy entire template directory here 60 + val templateDir = Directories.DATABASE_TEMPLATES.file(template) 61 + if (!templateDir.exists()) 62 + throw IllegalArgumentException("Missing template: ${templateDir.path}") 63 + FileUtils.copyDirectory(templateDir, path) 64 + 65 + // Substitute placeholders in project.kdl 66 + val manifestFile = File(path, Manifest.FILENAME) 67 + if (manifestFile.exists()) { 68 + var content = FileUtils.readFileToString(manifestFile, "UTF-8") 69 + content = content.replace("\$PROJECT_ID", "\"$id\"") 70 + content = content.replace("\$PROJECT_NAME", "\"${name.replace("\"", "\\\\")}\"") 71 + content = content.replace("\$PROJECT_DESCRIPTION", "\"An amazing mod of Paper Mario\"") // TODO: ui 72 + FileUtils.writeStringToFile(manifestFile, content, "UTF-8") 73 + } 74 + 75 + // Create assets directory 76 + val assetsDir = File(path, "assets") 77 + if (!assetsDir.exists()) 78 + assetsDir.mkdirs() 79 + 80 + return ProjectListing(path) 81 + } 82 + } 83 + }
+34
src/main/java/project/build/BuildCtx.kt
··· 1 + package project.build 2 + 3 + import assets.Asset 4 + import project.Project 5 + import java.nio.file.Path 6 + import kotlin.io.path.div 7 + import kotlin.io.path.pathString 8 + 9 + /** 10 + * Immutable context passed to asset build methods. 11 + */ 12 + data class BuildCtx( 13 + /** Output directory for build artifacts. */ 14 + val buildDir: Path, 15 + 16 + /** Current project being built. */ 17 + val project: Project, 18 + 19 + /** Engine SHA for cache invalidation. */ 20 + val engineSha: String, 21 + 22 + /** Build state format version for migration. */ 23 + val buildStateVersion: Int, 24 + 25 + /** Directory containing generated headers. */ 26 + val headersDir: Path 27 + ) { 28 + /** Returns the path to the typical build artifact for the given asset with an optional suffix. */ 29 + fun artifact(asset: Asset, suffix: String = ""): Path { 30 + val dir = buildDir / asset.relativePath.parent 31 + dir.toFile().mkdirs() 32 + return dir / "${asset.name}$suffix" 33 + } 34 + }
+184
src/main/java/project/build/BuildManager.kt
··· 1 + package project.build 2 + 3 + import assets.Asset 4 + import kotlinx.coroutines.* 5 + import project.Build 6 + import project.Project 7 + import util.Logger 8 + 9 + /** 10 + * Manages the build lifecycle for a project. 11 + * Coordinates asset building, file watching, and progress reporting. 12 + */ 13 + class BuildManager(private val project: Project) : Build.ProgressCallback { 14 + private val scope = CoroutineScope(Dispatchers.IO.limitedParallelism(32) + SupervisorJob()) 15 + private var currentBuild: Build? = null 16 + private var fileWatcher: FileWatcher? = null 17 + private val listeners = mutableListOf<BuildProgressListener>() 18 + 19 + @Volatile 20 + var isBuilding: Boolean = false 21 + private set 22 + 23 + @Volatile 24 + var buildProgress: BuildProgress = BuildProgress.idle() 25 + private set 26 + 27 + /** 28 + * Starts the build manager. 29 + * Begins an initial background build and starts file watching. 30 + */ 31 + fun start() { 32 + Logger.log("Starting build manager for project: ${project.name}") 33 + 34 + // Start initial build 35 + rebuild(forceRebuild = false) 36 + 37 + // Start file watcher 38 + fileWatcher = FileWatcher(project) { changedFiles -> 39 + Logger.log("Detected ${changedFiles.size} file changes, triggering rebuild") 40 + rebuild(forceRebuild = false) 41 + } 42 + fileWatcher?.start() 43 + } 44 + 45 + /** 46 + * Stops the build manager. 47 + * Cancels any running builds and stops file watching. 48 + */ 49 + fun stop() { 50 + Logger.log("Stopping build manager") 51 + 52 + // Stop file watcher 53 + fileWatcher?.stop() 54 + fileWatcher = null 55 + 56 + // Cancel any running builds 57 + currentBuild?.cancel() 58 + currentBuild = null 59 + 60 + // Cancel all coroutines 61 + scope.cancel() 62 + } 63 + 64 + /** 65 + * Triggers a rebuild. 66 + * If a build is already running, this does nothing. 67 + */ 68 + fun rebuild(forceRebuild: Boolean) { 69 + if (isBuilding) { 70 + Logger.log("Build already in progress, skipping rebuild request") 71 + return 72 + } 73 + 74 + scope.launch { 75 + runBuild(forceRebuild) 76 + } 77 + } 78 + 79 + /** 80 + * Adds a progress listener. 81 + */ 82 + fun addListener(listener: BuildProgressListener) { 83 + synchronized(listeners) { 84 + listeners.add(listener) 85 + } 86 + } 87 + 88 + /** 89 + * Removes a progress listener. 90 + */ 91 + fun removeListener(listener: BuildProgressListener) { 92 + synchronized(listeners) { 93 + listeners.remove(listener) 94 + } 95 + } 96 + 97 + /** 98 + * Runs a build asynchronously. 99 + */ 100 + private suspend fun runBuild(forceRebuild: Boolean) { 101 + isBuilding = true 102 + 103 + try { 104 + val build = Build(project) 105 + currentBuild = build 106 + 107 + // TODO: Support forceRebuild by deleting build state file 108 + 109 + build.execute(progressCallback = this) 110 + 111 + currentBuild = null 112 + } catch (e: CancellationException) { 113 + Logger.log("Build cancelled") 114 + throw e 115 + } catch (e: Exception) { 116 + Logger.logError("Build failed: ${e.message}") 117 + notifyListeners { onBuildComplete(0, 1) } 118 + } finally { 119 + isBuilding = false 120 + } 121 + } 122 + 123 + /** 124 + * Notifies all listeners with a callback. 125 + */ 126 + private fun notifyListeners(callback: BuildProgressListener.() -> Unit) { 127 + synchronized(listeners) { 128 + listeners.forEach { it.callback() } 129 + } 130 + } 131 + 132 + // Build.ProgressCallback implementation 133 + 134 + override fun onBuildStarted(totalAssets: Int) { 135 + buildProgress = BuildProgress( 136 + totalAssets = totalAssets, 137 + builtAssets = 0, 138 + failedAssets = 0, 139 + isComplete = false 140 + ) 141 + notifyListeners { onBuildStarted(totalAssets) } 142 + } 143 + 144 + override fun onAssetBuilt(asset: Asset, successCount: Int, totalAssets: Int) { 145 + buildProgress = buildProgress.copy( 146 + builtAssets = successCount, 147 + totalAssets = totalAssets 148 + ) 149 + notifyListeners { onAssetBuilt(asset, successCount, totalAssets) } 150 + } 151 + 152 + override fun onAssetFailed(asset: Asset, error: Exception) { 153 + buildProgress = buildProgress.copy( 154 + failedAssets = buildProgress.failedAssets + 1 155 + ) 156 + notifyListeners { onAssetFailed(asset, error) } 157 + } 158 + 159 + override fun onBuildComplete(successCount: Int, errorCount: Int) { 160 + buildProgress = buildProgress.copy( 161 + builtAssets = successCount, 162 + failedAssets = errorCount, 163 + isComplete = true 164 + ) 165 + notifyListeners { onBuildComplete(successCount, errorCount) } 166 + } 167 + } 168 + 169 + /** 170 + * Represents the current build progress. 171 + */ 172 + data class BuildProgress( 173 + val totalAssets: Int, 174 + val builtAssets: Int, 175 + val failedAssets: Int, 176 + val isComplete: Boolean 177 + ) { 178 + val percentage: Int 179 + get() = if (totalAssets > 0) (builtAssets * 100 / totalAssets) else 0 180 + 181 + companion object { 182 + fun idle(): BuildProgress = BuildProgress(0, 0, 0, true) 183 + } 184 + }
+21
src/main/java/project/build/BuildProgressListener.kt
··· 1 + package project.build 2 + 3 + import assets.Asset 4 + 5 + /** 6 + * Observer interface for build progress events. 7 + * Implement this to react to build state changes. 8 + */ 9 + interface BuildProgressListener { 10 + /** Called when a build starts. */ 11 + fun onBuildStarted(totalAssets: Int) 12 + 13 + /** Called when an asset is successfully built. */ 14 + fun onAssetBuilt(asset: Asset, successCount: Int, totalAssets: Int) 15 + 16 + /** Called when an asset fails to build. */ 17 + fun onAssetFailed(asset: Asset, error: Exception) 18 + 19 + /** Called when the build completes (with or without errors). */ 20 + fun onBuildComplete(successCount: Int, errorCount: Int) 21 + }
+36
src/main/java/project/build/BuildResult.kt
··· 1 + package project.build 2 + 3 + import java.nio.file.Path 4 + 5 + /** 6 + * Result of building an asset. 7 + */ 8 + sealed class BuildResult { 9 + /** Asset doesn't need building. */ 10 + object NoOp : BuildResult() 11 + 12 + /** Asset built successfully. */ 13 + data class Success( 14 + val artifacts: List<BuildArtifact> = emptyList() 15 + ) : BuildResult() 16 + 17 + /** Asset build failed. */ 18 + data class Failed(val error: Exception) : BuildResult() 19 + } 20 + 21 + /** 22 + * A file produced by building an asset. 23 + */ 24 + data class BuildArtifact( 25 + val path: Path, 26 + val type: ArtifactType 27 + ) 28 + 29 + enum class ArtifactType { 30 + HEADER, 31 + OBJECT, 32 + BINARY, 33 + SHAPE, 34 + COLLISION, 35 + OTHER 36 + }
+159
src/main/java/project/build/BuildState.kt
··· 1 + package project.build 2 + 3 + import assets.Asset 4 + import kotlinx.serialization.Serializable 5 + import kotlinx.serialization.encodeToString 6 + import kotlinx.serialization.json.Json 7 + import java.nio.file.Path 8 + import kotlin.io.path.* 9 + 10 + /** 11 + * Tracks which assets have been built and their modification times. 12 + * Also tracks asset class modification times to invalidate when build logic changes. 13 + * Persisted to `.starrod/build-state/state.json`. 14 + */ 15 + @Serializable 16 + data class BuildState( 17 + val engineSha: String, 18 + val version: Int, 19 + val assetTimestamps: MutableMap<String, Long> = mutableMapOf(), 20 + val assetClassTimestamps: MutableMap<String, Long> = mutableMapOf() 21 + ) { 22 + /** 23 + * Check if an asset needs rebuilding based on its modification time. 24 + * Also checks if the asset's class file has changed (build logic changed). 25 + */ 26 + fun needsRebuild(asset: Asset): Boolean { 27 + // Check if asset class changed (build logic changed) 28 + val className = asset.javaClass.name 29 + val lastClassTime = assetClassTimestamps[className] 30 + val currentClassTime = computeClassModificationTime(asset.javaClass) 31 + if (lastClassTime == null || currentClassTime > lastClassTime) { 32 + return true 33 + } 34 + 35 + // Check if asset file changed 36 + val relativePath = asset.relativePath.pathString 37 + val lastBuilt = assetTimestamps[relativePath] ?: return true 38 + val currentModTime = computeModificationTime(asset) 39 + return currentModTime > lastBuilt 40 + } 41 + 42 + /** 43 + * Mark an asset as successfully built. 44 + * Records both the asset file modification time and the asset class modification time. 45 + */ 46 + fun markBuilt(asset: Asset) { 47 + val relativePath = asset.relativePath.pathString 48 + assetTimestamps[relativePath] = computeModificationTime(asset) 49 + 50 + val className = asset.javaClass.name 51 + assetClassTimestamps[className] = computeClassModificationTime(asset.javaClass) 52 + } 53 + 54 + /** 55 + * Compute modification time for an asset. 56 + * For directories, recursively finds the maximum modification time of all children. 57 + */ 58 + private fun computeModificationTime(asset: Asset): Long { 59 + val path = asset.path 60 + if (!path.exists()) 61 + return 0L 62 + 63 + return if (path.isDirectory()) { 64 + computeDirectoryModTime(path) 65 + } else { 66 + path.getLastModifiedTime().toMillis() 67 + } 68 + } 69 + 70 + /** 71 + * Recursively compute the maximum modification time in a directory tree. 72 + */ 73 + private fun computeDirectoryModTime(dir: Path): Long { 74 + if (!dir.isDirectory()) 75 + return 0L 76 + 77 + return dir.walk() 78 + .filter { it.isRegularFile() } 79 + .maxOfOrNull { it.getLastModifiedTime().toMillis() } 80 + ?: 0L 81 + } 82 + 83 + /** 84 + * Compute modification time of a class file. 85 + * Returns 0 if the class file cannot be found (development mode, etc). 86 + */ 87 + private fun computeClassModificationTime(clazz: Class<*>): Long { 88 + return try { 89 + // Get the .class file location 90 + val classFileName = clazz.simpleName + ".class" 91 + val resource = clazz.getResource(classFileName) ?: return 0L 92 + 93 + // Convert URL to Path if it's a file:// URL 94 + val path = when (resource.protocol) { 95 + "file" -> resource.toURI().toPath() 96 + "jar" -> { 97 + // For JAR files, get the JAR file's modification time 98 + val jarPath = resource.path.substringBefore("!") 99 + if (jarPath.startsWith("file:")) { 100 + java.net.URI.create(jarPath).toPath() 101 + } else null 102 + } 103 + else -> null 104 + } 105 + 106 + path?.getLastModifiedTime()?.toMillis() ?: 0L 107 + } catch (e: Exception) { 108 + // If we can't determine class file time, return 0 (never invalidate) 109 + 0L 110 + } 111 + } 112 + 113 + /** 114 + * Save build state to disk. 115 + */ 116 + fun save(stateFile: Path) { 117 + stateFile.parent?.createDirectories() 118 + val json = Json { prettyPrint = true } 119 + stateFile.writeText(json.encodeToString(this)) 120 + } 121 + 122 + companion object { 123 + const val CURRENT_VERSION = 1 124 + 125 + private val json = Json { 126 + prettyPrint = true 127 + ignoreUnknownKeys = true 128 + } 129 + 130 + /** 131 + * Load build state from disk, or create a fresh state if the file doesn't exist. 132 + * Returns null if the state is invalid (wrong engine SHA or version). 133 + */ 134 + fun load(stateFile: Path, currentEngineSha: String): BuildState? { 135 + if (!stateFile.exists()) 136 + return null 137 + 138 + return try { 139 + val state = json.decodeFromString<BuildState>(stateFile.readText()) 140 + // Invalidate if engine changed or version mismatch 141 + if (state.engineSha != currentEngineSha || state.version != CURRENT_VERSION) { 142 + null 143 + } else { 144 + state 145 + } 146 + } catch (e: Exception) { 147 + // Corrupted or incompatible state file 148 + null 149 + } 150 + } 151 + 152 + /** 153 + * Create a fresh build state. 154 + */ 155 + fun create(engineSha: String): BuildState { 156 + return BuildState(engineSha, CURRENT_VERSION) 157 + } 158 + } 159 + }
+87
src/main/java/project/build/BuildStatusBar.kt
··· 1 + package project.build 2 + 3 + import assets.Asset 4 + import util.Logger 5 + import java.awt.Color 6 + import java.awt.Cursor 7 + import java.awt.FlowLayout 8 + import java.awt.event.MouseAdapter 9 + import java.awt.event.MouseEvent 10 + import javax.swing.JLabel 11 + import javax.swing.JPanel 12 + import javax.swing.SwingUtilities 13 + 14 + /** 15 + * Status bar UI component that shows build progress. 16 + * Implements BuildProgressListener to receive build events. 17 + */ 18 + class BuildStatusBar : JPanel(), BuildProgressListener { 19 + private val statusLabel = JLabel("Idle") 20 + private val errors = mutableListOf<Pair<Asset, Exception>>() 21 + 22 + init { 23 + layout = FlowLayout(FlowLayout.LEFT, 10, 5) 24 + add(statusLabel) 25 + 26 + // Make clickable to show errors 27 + cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) 28 + addMouseListener(object : MouseAdapter() { 29 + override fun mouseClicked(e: MouseEvent) { 30 + if (errors.isNotEmpty()) { 31 + showErrorDialog() 32 + } 33 + } 34 + }) 35 + 36 + updateStatus("Idle", Color.GRAY) 37 + } 38 + 39 + override fun onBuildStarted(totalAssets: Int) { 40 + SwingUtilities.invokeLater { 41 + errors.clear() 42 + updateStatus("Building... 0/$totalAssets assets (0%)", Color.BLUE) 43 + } 44 + } 45 + 46 + override fun onAssetBuilt(asset: Asset, successCount: Int, totalAssets: Int) { 47 + SwingUtilities.invokeLater { 48 + val percentage = if (totalAssets > 0) (successCount * 100 / totalAssets) else 0 49 + updateStatus("Building... $successCount/$totalAssets assets ($percentage%)", Color.BLUE) 50 + } 51 + } 52 + 53 + override fun onAssetFailed(asset: Asset, error: Exception) { 54 + SwingUtilities.invokeLater { 55 + errors.add(asset to error) 56 + } 57 + } 58 + 59 + override fun onBuildComplete(successCount: Int, errorCount: Int) { 60 + SwingUtilities.invokeLater { 61 + if (errorCount > 0) { 62 + updateStatus( 63 + "Build complete with $errorCount error(s) - Click to view", 64 + Color.RED 65 + ) 66 + } else if (successCount > 0) { 67 + updateStatus("Build complete: $successCount assets built", Color(0, 128, 0)) 68 + } else { 69 + updateStatus("Build complete: No changes", Color(0, 128, 0)) 70 + } 71 + } 72 + } 73 + 74 + private fun updateStatus(text: String, color: Color) { 75 + statusLabel.text = text 76 + statusLabel.foreground = color 77 + } 78 + 79 + private fun showErrorDialog() { 80 + // For now, just log errors to console 81 + // TODO: Create a proper error dialog UI 82 + Logger.logError("Build errors:") 83 + errors.forEach { (asset, error) -> 84 + Logger.logError(" ${asset.relativePath}: ${error.message}") 85 + } 86 + } 87 + }
+186
src/main/java/project/build/FileWatcher.kt
··· 1 + package project.build 2 + 3 + import assets.AssetManager 4 + import kotlinx.coroutines.* 5 + import project.Project 6 + import util.Logger 7 + import java.nio.file.* 8 + import kotlin.io.path.exists 9 + import kotlin.io.path.isDirectory 10 + import kotlin.io.path.walk 11 + 12 + /** 13 + * Watches the project asset directory for file changes and triggers rebuilds. 14 + * Uses Java WatchService with recursive directory registration and debouncing. 15 + */ 16 + class FileWatcher( 17 + private val project: Project, 18 + private val onChange: (changedFiles: Set<Path>) -> Unit 19 + ) { 20 + private var watchService: WatchService? = null 21 + private var watchJob: Job? = null 22 + private val watchKeys = mutableListOf<WatchKey>() 23 + 24 + /** 25 + * Starts watching for file changes. 26 + */ 27 + fun start() { 28 + try { 29 + watchService = FileSystems.getDefault().newWatchService() 30 + registerDirectories() 31 + startWatchJob() 32 + Logger.log("File watcher started") 33 + } catch (e: Exception) { 34 + Logger.logError("Failed to start file watcher: ${e.message}") 35 + } 36 + } 37 + 38 + /** 39 + * Stops watching for file changes. 40 + */ 41 + fun stop() { 42 + Logger.log("Stopping file watcher") 43 + 44 + // Cancel watch job 45 + watchJob?.cancel() 46 + watchJob = null 47 + 48 + // Cancel all watch keys 49 + watchKeys.forEach { it.cancel() } 50 + watchKeys.clear() 51 + 52 + // Close watch service 53 + watchService?.close() 54 + watchService = null 55 + } 56 + 57 + /** 58 + * Registers all directories recursively for watching. 59 + * Only watches owned (project) assets, NOT engine assets. 60 + * 61 + * Note: Engine assets are read-only and changes to them are not watched. 62 + * If a user modifies an engine asset through the UI, the Asset's Copy-on-Write 63 + * semantics will automatically copy it to the project directory, triggering a 64 + * file system event that will be caught by this watcher. 65 + */ 66 + private fun registerDirectories() { 67 + // getTopLevelAssetDir() returns the project's owned asset directory (index 0 in the stack) 68 + // This excludes engine assets which are read-only and at the base of the stack 69 + val assetRoot = AssetManager.getTopLevelAssetDir().toPath() 70 + if (!assetRoot.exists() || !assetRoot.isDirectory()) 71 + return 72 + 73 + // Register root directory 74 + registerDirectory(assetRoot) 75 + 76 + // Register all subdirectories recursively (within owned assets only) 77 + assetRoot.walk() 78 + .filter { it.isDirectory() } 79 + .forEach { registerDirectory(it) } 80 + } 81 + 82 + /** 83 + * Registers a single directory for watching. 84 + */ 85 + private fun registerDirectory(dir: Path) { 86 + try { 87 + val key = dir.register( 88 + watchService, 89 + StandardWatchEventKinds.ENTRY_CREATE, 90 + StandardWatchEventKinds.ENTRY_DELETE, 91 + StandardWatchEventKinds.ENTRY_MODIFY 92 + ) 93 + watchKeys.add(key) 94 + } catch (e: Exception) { 95 + Logger.logError("Failed to register directory for watching: ${dir}: ${e.message}") 96 + } 97 + } 98 + 99 + /** 100 + * Starts the coroutine that polls for file changes. 101 + */ 102 + private fun startWatchJob() { 103 + watchJob = CoroutineScope(Dispatchers.IO).launch { 104 + val service = watchService ?: return@launch 105 + 106 + while (isActive) { 107 + try { 108 + // Wait for events 109 + val key = service.take() 110 + val watchedDir = key.watchable() as? Path 111 + 112 + // Collect all changed files 113 + val changedFiles = mutableSetOf<Path>() 114 + for (event in key.pollEvents()) { 115 + val context = event.context() as? Path ?: continue 116 + 117 + // Build full path 118 + val fullPath = watchedDir?.resolve(context) 119 + 120 + // If a directory was created, register it for watching 121 + if (event.kind() == StandardWatchEventKinds.ENTRY_CREATE && 122 + fullPath != null && fullPath.isDirectory()) { 123 + Logger.log("New directory created, registering for watching: $fullPath") 124 + registerDirectory(fullPath) 125 + // Register subdirectories recursively 126 + fullPath.walk() 127 + .filter { it.isDirectory() && it != fullPath } 128 + .forEach { registerDirectory(it) } 129 + } 130 + 131 + if (fullPath != null) { 132 + changedFiles.add(fullPath) 133 + } 134 + } 135 + key.reset() 136 + 137 + // Debounce: wait 500ms and collect more events 138 + delay(500) 139 + 140 + var extraKey: WatchKey? 141 + while (service.poll().also { extraKey = it } != null) { 142 + extraKey?.let { k -> 143 + val extraWatchedDir = k.watchable() as? Path 144 + for (event in k.pollEvents()) { 145 + val context = event.context() as? Path ?: continue 146 + 147 + // Build full path 148 + val fullPath = extraWatchedDir?.resolve(context) 149 + 150 + // If a directory was created, register it for watching 151 + if (event.kind() == StandardWatchEventKinds.ENTRY_CREATE && 152 + fullPath != null && fullPath.isDirectory()) { 153 + Logger.log("New directory created, registering for watching: $fullPath") 154 + registerDirectory(fullPath) 155 + // Register subdirectories recursively 156 + fullPath.walk() 157 + .filter { it.isDirectory() && it != fullPath } 158 + .forEach { registerDirectory(it) } 159 + } 160 + 161 + if (fullPath != null) { 162 + changedFiles.add(fullPath) 163 + } 164 + } 165 + k.reset() 166 + } 167 + } 168 + 169 + // Trigger callback if we have changes 170 + if (changedFiles.isNotEmpty()) { 171 + Logger.log("File watcher detected changes: ${changedFiles.map { it.fileName }}") 172 + onChange(changedFiles) 173 + } 174 + } catch (e: InterruptedException) { 175 + break 176 + } catch (e: CancellationException) { 177 + break 178 + } catch (e: Exception) { 179 + if (isActive) { 180 + Logger.logError("File watcher error: ${e.message}") 181 + } 182 + } 183 + } 184 + } 185 + } 186 + }
+13 -12
src/main/java/project/engine/BuildEnvironment.java
··· 72 72 String getName(); 73 73 74 74 /** 75 - * Runs configure (./configure). 76 - * @param projectDir The project directory to build in 75 + * Returns the engine directory this build environment operates on. 76 + */ 77 + File getEngineDir(); 78 + 79 + /** 80 + * Runs configure (./configure) in the engine directory. 77 81 * @param listener Callback for real-time build output 78 82 * @return The result of the configure operation 79 83 * @throws BuildException If the build environment is not properly set up 80 84 * @throws IOException If an I/O error occurs 81 85 */ 82 - BuildResult configure(File projectDir, BuildOutputListener listener) throws BuildException, IOException; 86 + BuildResult configure(BuildOutputListener listener) throws BuildException, IOException; 83 87 84 88 /** 85 - * Builds the project (ninja). 86 - * @param projectDir The project directory to build in 89 + * Builds the engine (ninja) in the engine directory. 87 90 * @param listener Callback for real-time build output 88 91 * @return The result of the build operation 89 92 * @throws BuildException If the build environment is not properly set up 90 93 * @throws IOException If an I/O error occurs 91 94 */ 92 - BuildResult build(File projectDir, BuildOutputListener listener) throws BuildException, IOException; 95 + BuildResult build(BuildOutputListener listener) throws BuildException, IOException; 93 96 94 97 /** 95 - * Cleans the build directory (./configure --clean). 96 - * @param projectDir The project directory to clean 98 + * Cleans the build directory (./configure --clean) in the engine directory. 97 99 * @param listener Callback for real-time build output 98 100 * @return The result of the clean operation 99 101 * @throws BuildException If the build environment is not properly set up 100 102 * @throws IOException If an I/O error occurs 101 103 */ 102 - BuildResult clean(File projectDir, BuildOutputListener listener) throws BuildException, IOException; 104 + BuildResult clean(BuildOutputListener listener) throws BuildException, IOException; 103 105 104 106 /** 105 - * Builds the project asynchronously. 106 - * @param projectDir The project directory to build in 107 + * Builds the engine asynchronously. 107 108 * @param listener Callback for real-time build output 108 109 * @return A CompletableFuture that completes with the build result 109 110 */ 110 - CompletableFuture<BuildResult> buildAsync(File projectDir, BuildOutputListener listener); 111 + CompletableFuture<BuildResult> buildAsync(BuildOutputListener listener); 111 112 112 113 /** 113 114 * Cancels any running build operation.
+10
src/main/java/project/engine/BuildResult.java
··· 71 71 return Optional.ofNullable(outputRom); 72 72 } 73 73 74 + public Optional<File> getOutputSyms() 75 + { 76 + if (outputRom == null) 77 + return Optional.empty(); 78 + 79 + // syms.ld is in the same directory as the ROM 80 + File syms = new File(outputRom.getParentFile(), "syms.ld"); 81 + return syms.exists() ? Optional.of(syms) : Optional.empty(); 82 + } 83 + 74 84 public Optional<String> getErrorMessage() 75 85 { 76 86 return Optional.ofNullable(errorMessage);
+18 -7
src/main/java/project/engine/Engine.java
··· 52 52 */ 53 53 public static Engine forProject(Project project) throws BuildException, IOException 54 54 { 55 - BuildEnvironment buildEnv = createBuildEnvironment(); 56 55 String ref = project.getManifest().getEngineRef(); 57 56 58 57 // Check for submodule first 59 58 File submoduleDir = new File(project.getDirectory(), PROJECT_ENGINE_PATH); 60 - if (isGitRepo(submoduleDir)) 59 + if (isGitRepo(submoduleDir)) { 60 + BuildEnvironment buildEnv = createBuildEnvironment(submoduleDir); 61 61 return new Engine(submoduleDir, ref, true, buildEnv); 62 + } 62 63 63 64 // Worktree-based engine 64 - File engineBase = buildEnv.getEngineBaseDir(); 65 + // Create a temporary BuildEnvironment to get the base directory 66 + BuildEnvironment tempEnv = createBuildEnvironmentWithoutDir(); 67 + File engineBase = tempEnv.getEngineBaseDir(); 65 68 66 69 // Use ref as directory name so projects with the same ref share the worktree 67 70 // Prefix with wt- to prevent collision between foo and foo/bar 68 71 File worktreeDir = new File(engineBase, "worktrees/wt-" + ref); 69 72 73 + BuildEnvironment buildEnv = createBuildEnvironment(worktreeDir); 70 74 return new Engine(worktreeDir, ref, false, buildEnv); 71 75 } 72 76 73 - private static BuildEnvironment createBuildEnvironment() throws BuildException 77 + private static BuildEnvironment createBuildEnvironment(File engineDir) throws BuildException 78 + { 79 + if (Environment.isWindows()) 80 + return new WslNixOsEnvironment(engineDir); 81 + return new NixEnvironment(engineDir); 82 + } 83 + 84 + private static BuildEnvironment createBuildEnvironmentWithoutDir() throws BuildException 74 85 { 75 86 if (Environment.isWindows()) 76 - return new WslNixOsEnvironment(); 77 - return new NixEnvironment(); 87 + return new WslNixOsEnvironment(null); 88 + return new NixEnvironment(null); 78 89 } 79 90 80 91 private File getBareRepoDir() ··· 130 141 public void splitAssets() throws BuildException, IOException 131 142 { 132 143 Logger.log("Splitting assets from ROM...", Priority.MILESTONE); 133 - BuildResult result = buildEnv.configure(directory, BuildOutputListener.toLogger()); 144 + BuildResult result = buildEnv.configure(BuildOutputListener.toLogger()); 134 145 if (!result.isSuccess()) 135 146 throw new BuildException("Failed to split assets: " + result.getErrorMessage()); 136 147 }
+28 -12
src/main/java/project/engine/NixEnvironment.java
··· 16 16 private static final String ROM_PATH = "ver/us/build/papermario.z64"; 17 17 18 18 private final ProcessRunner runner = new ProcessRunner(); 19 + private final File engineDir; 20 + 21 + /** 22 + * Creates a new Nix environment. 23 + * @param engineDir The engine directory to build in (null if only using for getEngineBaseDir) 24 + */ 25 + public NixEnvironment(File engineDir) 26 + { 27 + this.engineDir = engineDir; 28 + } 19 29 20 30 // --- Engine storage and git operations --- 21 31 ··· 112 122 } 113 123 114 124 @Override 115 - public BuildResult configure(File projectDir, BuildOutputListener listener) throws BuildException, IOException 125 + public File getEngineDir() 116 126 { 117 - validateEnvironment(projectDir); 118 - return runNixCommand(projectDir, "./configure", listener); 127 + return engineDir; 119 128 } 120 129 121 130 @Override 122 - public BuildResult build(File projectDir, BuildOutputListener listener) throws BuildException, IOException 131 + public BuildResult configure(BuildOutputListener listener) throws BuildException, IOException 123 132 { 124 - validateEnvironment(projectDir); 125 - ProcessRunner.ProcessResult result = runNixCommandRaw(projectDir, "NINJA_STATUS='" + BuildOutputDialog.NINJA_STATUS + "' ninja", listener); 133 + validateEnvironment(engineDir); 134 + return runNixCommand(engineDir, "./configure", listener); 135 + } 136 + 137 + @Override 138 + public BuildResult build(BuildOutputListener listener) throws BuildException, IOException 139 + { 140 + validateEnvironment(engineDir); 141 + ProcessRunner.ProcessResult result = runNixCommandRaw(engineDir, "NINJA_STATUS='" + BuildOutputDialog.NINJA_STATUS + "' ninja", listener); 126 142 127 143 if (result.wasCancelled()) { 128 144 return BuildResult.cancelled(result.getDuration()); 129 145 } 130 146 131 - File rom = new File(projectDir, ROM_PATH); 147 + File rom = new File(engineDir, ROM_PATH); 132 148 if (result.isSuccess() && rom.exists()) { 133 149 return BuildResult.success(result.getExitCode(), result.getDuration(), rom); 134 150 } ··· 139 155 } 140 156 141 157 @Override 142 - public BuildResult clean(File projectDir, BuildOutputListener listener) throws BuildException, IOException 158 + public BuildResult clean(BuildOutputListener listener) throws BuildException, IOException 143 159 { 144 - validateEnvironment(projectDir); 145 - return runNixCommand(projectDir, "./configure --clean", listener); 160 + validateEnvironment(engineDir); 161 + return runNixCommand(engineDir, "./configure --clean", listener); 146 162 } 147 163 148 164 @Override 149 - public CompletableFuture<BuildResult> buildAsync(File projectDir, BuildOutputListener listener) 165 + public CompletableFuture<BuildResult> buildAsync(BuildOutputListener listener) 150 166 { 151 167 return CompletableFuture.supplyAsync(() -> { 152 168 try { 153 - return build(projectDir, listener); 169 + return build(listener); 154 170 } 155 171 catch (BuildException | IOException e) { 156 172 return BuildResult.failure(-1, java.time.Duration.ZERO, e.getMessage());
+22 -10
src/main/java/project/engine/WslNixOsEnvironment.java
··· 32 32 33 33 private final ProcessRunner runner = new ProcessRunner(); 34 34 private boolean shutdownRegistered = false; 35 + private final File engineDir; 35 36 36 37 private static final int MIN_WINDOWS_BUILD = 19041; // Windows 10 version 2004 37 38 38 - public WslNixOsEnvironment() throws BuildException 39 + /** 40 + * Creates a new WSL NixOS environment. 41 + * @param engineDir The engine directory to build in (null if only using for getEngineBaseDir) 42 + */ 43 + public WslNixOsEnvironment(File engineDir) throws BuildException 39 44 { 45 + this.engineDir = engineDir; 40 46 validateSystemRequirements(); 41 47 validateWslSupport(); 42 48 registerShutdownHook(); ··· 158 164 } 159 165 160 166 @Override 161 - public BuildResult configure(File projectDir, BuildOutputListener listener) throws BuildException, IOException 167 + public File getEngineDir() 162 168 { 163 - return runWslCommand(projectDir, "./configure", listener); 169 + return engineDir; 164 170 } 165 171 166 172 @Override 167 - public BuildResult build(File projectDir, BuildOutputListener listener) throws BuildException, IOException 173 + public BuildResult configure(BuildOutputListener listener) throws BuildException, IOException 168 174 { 169 - ProcessRunner.ProcessResult result = runWslCommandRaw(projectDir, "NINJA_STATUS='" + BuildOutputDialog.NINJA_STATUS + "' ./configure && ninja", listener); 175 + return runWslCommand(engineDir, "./configure", listener); 176 + } 177 + 178 + @Override 179 + public BuildResult build(BuildOutputListener listener) throws BuildException, IOException 180 + { 181 + ProcessRunner.ProcessResult result = runWslCommandRaw(engineDir, "NINJA_STATUS='" + BuildOutputDialog.NINJA_STATUS + "' ./configure && ninja", listener); 170 182 171 183 if (result.wasCancelled()) { 172 184 return BuildResult.cancelled(result.getDuration()); 173 185 } 174 186 175 - File rom = new File(projectDir, ROM_PATH); 187 + File rom = new File(engineDir, ROM_PATH); 176 188 if (result.isSuccess() && rom.exists()) { 177 189 return BuildResult.success(result.getExitCode(), result.getDuration(), rom); 178 190 } ··· 183 195 } 184 196 185 197 @Override 186 - public BuildResult clean(File projectDir, BuildOutputListener listener) throws BuildException, IOException 198 + public BuildResult clean(BuildOutputListener listener) throws BuildException, IOException 187 199 { 188 - return runWslCommand(projectDir, "./configure --clean", listener); 200 + return runWslCommand(engineDir, "./configure --clean", listener); 189 201 } 190 202 191 203 @Override 192 - public CompletableFuture<BuildResult> buildAsync(File projectDir, BuildOutputListener listener) 204 + public CompletableFuture<BuildResult> buildAsync(BuildOutputListener listener) 193 205 { 194 206 return CompletableFuture.supplyAsync(() -> { 195 207 try { 196 - return build(projectDir, listener); 208 + return build(listener); 197 209 } 198 210 catch (BuildException | IOException e) { 199 211 return BuildResult.failure(-1, java.time.Duration.ZERO, e.getMessage());
+14 -3
src/main/java/util/ui/ThemedIcon.java
··· 9 9 10 10 import app.Resource; 11 11 import app.Resource.ResourceType; 12 + import org.jetbrains.annotations.NotNull; 12 13 import util.Logger; 13 14 14 15 public abstract class ThemedIcon ··· 24 25 Logger.logError(e.getMessage()); 25 26 return null; 26 27 } 28 + } 29 + 30 + @NotNull 31 + private static FlatSVGIcon getIcon15(String name) 32 + { 33 + FlatSVGIcon icon = getIcon(name).derive(15, 15); 34 + assert icon != null; 35 + return icon; 27 36 } 28 37 29 38 public static final FlatSVGIcon VISIBILITY_OFF_24 = getIcon("visibility_off_24"); ··· 84 93 public static final FlatSVGIcon PACKAGE_24 = getIcon("package"); 85 94 public static final FlatSVGIcon PACKAGE_16 = PACKAGE_24.derive(16, 16); 86 95 87 - public static final FlatSVGIcon GIT_BRANCH = getIcon("git_branch"); 88 - 89 - public static final FlatSVGIcon SEARCH = getIcon("search"); 96 + /* Add new icons here, no need to use _24/_16 suffixes */ 97 + public static final FlatSVGIcon GIT_BRANCH = getIcon15("git_branch"); 98 + public static final FlatSVGIcon SEARCH = getIcon15("search"); 99 + public static final FlatSVGIcon EYE = getIcon15("eye"); 100 + public static final FlatSVGIcon EYE_OFF = getIcon15("eye_off"); 90 101 }
+422 -422
src/main/resources/extract/maps.csv
··· 1 - kmr_00,kmr_00_shape,kmr_00_hit,kmr_tex,kmr_bg,Forest Clearing 2 - kmr_02,kmr_02_shape,kmr_02_hit,kmr_tex,kmr_bg,Goomba Village 3 - kmr_03,kmr_03_shape,kmr_03_hit,kmr_tex,kmr_bg,Bottom of the Cliff 4 - kmr_04,kmr_04_shape,kmr_04_hit,kmr_tex,kmr_bg,Jr. Troopa's Playground 5 - kmr_05,kmr_05_shape,kmr_05_hit,kmr_tex,kmr_bg,Behind the Village 6 - kmr_06,kmr_06_shape,kmr_06_hit,kmr_tex,kmr_bg,Goomba Road 2 7 - kmr_07,kmr_07_shape,kmr_07_hit,kmr_tex,kmr_bg,Goomba Road 3 8 - kmr_09,kmr_09_shape,kmr_09_hit,kmr_tex,kmr_bg,Goomba Road 1 9 - kmr_10,kmr_10_shape,kmr_10_hit,kmr_tex,kmr_bg,Toad Town Entrance 10 - kmr_11,kmr_11_shape,kmr_11_hit,kmr_tex,kmr_bg,Goomba King's Castle 11 - kmr_12,kmr_12_shape,kmr_12_hit,kmr_tex,kmr_bg,Goomba Road 4 12 - kmr_20,kmr_20_shape,kmr_20_hit,kmr_tex,kmr_bg,Mario's House 13 - kmr_21,kmr_21_shape,kmr_21_hit,kmr_tex,none,Title Screen 14 - kmr_22,kmr_22_shape,kmr_22_hit,kmr_tex,none,Chapter Start 15 - kmr_23,kmr_23_shape,kmr_23_hit,kmr_tex,none,Chapter End 16 - kmr_24,kmr_24_shape,kmr_24_hit,kmr_tex,none,Save and Continue 17 - kmr_30,kmr_30_shape,kmr_30_hit,kmr_tex,none,Mario's House (Ending) 18 - machi,machi_shape,machi_hit,mac_tex,nok_bg,Debug Warp Zone 19 - mac_00,mac_00_shape,mac_00_hit,mac_tex,nok_bg,Gate District 20 - mac_01,mac_01_shape,mac_01_hit,mac_tex,nok_bg,Plaza District 21 - mac_02,mac_02_shape,mac_02_hit,mac_tex,nok_bg,Southern District 22 - mac_03,mac_03_shape,mac_03_hit,mac_tex,nok_bg,Station District 23 - mac_04,mac_04_shape,mac_04_hit,mac_tex,nok_bg,Residental District 24 - mac_05,mac_05_shape,mac_05_hit,mac_tex,nok_bg,Port District 25 - mac_06,mac_06_shape,mac_06_hit,mac_tex,nok_bg,Riding the Whale 26 - tik_01,tik_01_shape,tik_01_hit,tik_tex,none,Warp Zone 1 (B1) 27 - tik_02,tik_02_shape,tik_02_hit,tik_tex,none,Blooper Boss 1 (B1) 28 - tik_03,tik_03_shape,tik_03_hit,tik_tex,none,Short Elevator Room (B1) 29 - tik_04,tik_04_shape,tik_04_hit,tik_tex,none,Scales Room (B2) 30 - tik_05,tik_05_shape,tik_05_hit,tik_tex,none,Spring Room (B2) 31 - tik_06,tik_06_shape,tik_06_hit,tik_tex,none,Sewer Entrance (B1) 32 - tik_07,tik_07_shape,tik_07_hit,tik_tex,none,Elevator Attic Room (B2) 33 - tik_08,tik_08_shape,tik_08_hit,tik_tex,none,Second Level Entry (B2) 34 - tik_09,tik_09_shape,tik_09_hit,tik_tex,none,Warp Zone 2 (B2) 35 - tik_10,tik_10_shape,tik_10_hit,tik_tex,none,Block Puzzle Room (B2) 36 - tik_12,tik_12_shape,tik_12_hit,tik_tex,none,Metal Block Room (B3) 37 - tik_14,tik_14_shape,tik_14_hit,tik_tex,none,Rip Cheato Antechamber (B3) 38 - tik_15,tik_15_shape,tik_15_hit,tik_tex,none,Rip Cheato's Home (B3) 39 - tik_17,tik_17_shape,tik_17_hit,tik_tex,none,Frozen Room (B3) 40 - tik_18,tik_18_shape,tik_18_hit,tik_tex,none,Hall to Blooper 1 (B1) 41 - tik_19,tik_19_shape,tik_19_hit,tik_tex,none,Under the Toad Town Pond 42 - tik_20,tik_20_shape,tik_20_hit,tik_tex,none,Room with Spikes (B2) 43 - tik_21,tik_21_shape,tik_21_hit,tik_tex,none,Hidden Blocks Room (B2) 44 - tik_22,tik_22_shape,tik_22_hit,tik_tex,none,Path to Shiver City (B2) 45 - tik_23,tik_23_shape,tik_23_hit,tik_tex,none,Windy Path (B3) 46 - tik_24,tik_18_shape,tik_18_hit,tik_tex,none,Hall to Ultra Boots (B3) 47 - tik_25,tik_25_shape,tik_25_hit,tik_tex,none,Ultra Boots Room (B3) 48 - kgr_01,kgr_01_shape,kgr_01_hit,kgr_tex,none,Whale Mouth 49 - kgr_02,kgr_02_shape,kgr_02_hit,kgr_tex,none,Whale Stomach 50 - kkj_00,kkj_00_shape,kkj_00_hit,kkj_tex,nok_bg,Intro Entry Hall (1F) 51 - kkj_01,kkj_01_shape,kkj_01_hit,kkj_tex,nok_bg,Intro Upper Hall (2F) 52 - kkj_02,kkj_02_shape,kkj_02_hit,kkj_tex,nok_bg,Intro Stairs Hallway (3F) 53 - kkj_03,kkj_03_shape,kkj_03_hit,kkj_tex,nok_bg,Intro Window Hallway (4F) 54 - kkj_10,kkj_10_shape,kkj_10_hit,kkj_tex,none,Entry Hall (1F) 55 - kkj_11,kkj_11_shape,kkj_11_hit,kkj_tex,none,Upper Hall (2F) 56 - kkj_12,kkj_12_shape,kkj_12_hit,kkj_tex,none,Stairs Hallway (3F) 57 - kkj_13,kkj_13_shape,kkj_13_hit,kkj_tex,kpa_bg,Window Hallway (4F) 58 - kkj_14,kkj_14_shape,kkj_14_hit,kkj_tex,kpa_bg,Peach's Room (2F) 59 - kkj_15,kkj_15_shape,kkj_15_hit,kkj_tex,none,Passage Outlet (2F) 60 - kkj_16,kkj_16_shape,kkj_16_hit,kkj_tex,none,Library (2F) 61 - kkj_17,kkj_17_shape,kkj_17_hit,kkj_tex,none,Storeroom (2F) 62 - kkj_18,kkj_18_shape,kkj_18_hit,kkj_tex,kpa_bg,Dining Room (2F) 63 - kkj_19,kkj_19_shape,kkj_19_hit,kkj_tex,none,Kitchen (1F) 64 - kkj_20,kkj_20_shape,kkj_20_hit,kkj_tex,none,Guest Room (1F) 65 - kkj_21,kkj_21_shape,kkj_21_hit,kkj_tex,none,Inactive Quiz-Off (1F) 66 - kkj_22,kkj_22_shape,kkj_22_hit,kkj_tex,kpa_bg,Double Staircase (4F) 67 - kkj_23,kkj_23_shape,kkj_23_hit,kkj_tex,kpa_bg,Rooftop (5F) 68 - kkj_24,kkj_24_shape,kkj_24_hit,kkj_tex,kpa_bg,Tower Staircase (5F) 69 - kkj_25,kkj_25_shape,kkj_25_hit,kkj_tex,kpa_bg,Final Boss Arena (6F) 70 - kkj_26,kkj_26_shape,kkj_26_hit,kkj_tex,kpa_bg,Balcony (2F) 71 - kkj_27,kkj_27_shape,kkj_27_hit,kkj_tex,none,Secret Passage (2F) 72 - kkj_28,kkj_28_shape,kkj_28_hit,kkj_tex,none,Darkened Quiz-Off (1F) 73 - kkj_29,kkj_29_shape,kkj_29_hit,kkj_tex,none,Quiz-Off Room (1F) 74 - hos_00,hos_00_shape,hos_00_hit,hos_tex,nok_bg,Shooting Star Path 75 - hos_01,hos_01_shape,hos_01_hit,hos_tex,hos_bg,Shooting Star Summit 76 - hos_02,hos_02_shape,hos_02_hit,hos_tex,hos_bg,Star Way 77 - hos_03,hos_03_shape,hos_03_hit,hos_tex,hos_bg,Star Haven 78 - hos_04,hos_04_shape,hos_04_hit,hos_tex,hos_bg,Outside the Sanctuary 79 - hos_05,hos_05_shape,hos_05_hit,hos_tex,hos_bg,Star Sanctuary 80 - hos_06,hos_06_shape,hos_06_hit,hos_tex,hos_bg,Merluvlee's House 81 - hos_10,hos_10_shape,hos_10_hit,hos_tex,hos_bg,Ending Descent Scene 82 - hos_20,hos_20_shape,hos_20_hit,hos_tex,hos_bg,Riding Star Ship Scene 83 - nok_01,nok_01_shape,nok_01_hit,nok_tex,nok_bg,Koopa Village 1 84 - nok_02,nok_02_shape,nok_02_hit,nok_tex,nok_bg,Koopa Village 2 85 - nok_03,nok_03_shape,nok_03_hit,nok_tex,nok_bg,Behind Koopa Village 86 - nok_04,nok_04_shape,nok_04_hit,nok_tex,nok_bg,Fuzzy Forest 87 - nok_11,nok_11_shape,nok_11_hit,nok_tex,nok_bg,Pleasant Path Entry 88 - nok_12,nok_12_shape,nok_12_hit,nok_tex,nok_bg,Pleasant Path Bridge 89 - nok_13,nok_13_shape,nok_13_hit,nok_tex,nok_bg,Pleasant Crossroads 90 - nok_14,nok_14_shape,nok_14_hit,nok_tex,nok_bg,Path to Fortress 1 91 - nok_15,nok_15_shape,nok_15_hit,nok_tex,nok_bg,Path to Fortress 2 92 - trd_00,trd_00_shape,trd_00_hit,trd_tex,nok_bg,Fortress Exterior 93 - trd_01,trd_01_shape,trd_01_hit,trd_tex,none,Left Tower 94 - trd_02,trd_02_shape,trd_02_hit,trd_tex,none,Left Stairway 95 - trd_03,trd_03_shape,trd_03_hit,trd_tex,none,Central Hall 96 - trd_04,trd_04_shape,trd_04_hit,trd_tex,none,Right Stairway 97 - trd_05,trd_05_shape,trd_05_hit,trd_tex,none,Right Tower 98 - trd_06,trd_06_shape,trd_06_hit,trd_tex,none,Jail 99 - trd_07,trd_07_shape,trd_07_hit,trd_tex,none,Dungeon Trap 100 - trd_08,trd_08_shape,trd_08_hit,trd_tex,none,Dungeon Fire Room 101 - trd_09,trd_09_shape,trd_09_hit,trd_tex,nok_bg,Battlement 102 - trd_10,trd_10_shape,trd_10_hit,trd_tex,none,Boss Battle Room 103 - iwa_00,iwa_00_shape,iwa_00_hit,iwa_tex,iwa_bg,Mt Rugged 1 104 - iwa_01,iwa_01_shape,iwa_01_hit,iwa_tex,iwa_bg,Mt Rugged 2 105 - iwa_02,iwa_02_shape,iwa_02_hit,iwa_tex,iwa_bg,Mt Rugged 3 106 - iwa_03,iwa_03_shape,iwa_03_hit,iwa_tex,iwa_bg,Mt Rugged 4 107 - iwa_04,iwa_04_shape,iwa_04_hit,iwa_tex,iwa_bg,Suspension Bridge 108 - iwa_10,iwa_10_shape,iwa_10_hit,iwa_tex,iwa_bg,Train Station 109 - iwa_11,iwa_11_shape,iwa_11_hit,iwa_tex,iwa_bg,Train Ride Scene 110 - dro_01,dro_01_shape,dro_01_hit,dro_tex,sbk_bg,Outpost 1 111 - dro_02,dro_02_shape,dro_02_hit,dro_tex,sbk_bg,Outpost 2 112 - sbk_00,sbk_00_shape,sbk_00_hit,sbk_tex,sbk_bg,N3W3 113 - sbk_01,sbk_01_shape,sbk_01_hit,sbk_tex,sbk_bg,N3W2 114 - sbk_02,sbk_02_shape,sbk_02_hit,sbk_tex,sbk_bg,N3W1 Ruins Entrance 115 - sbk_03,sbk_03_shape,sbk_03_hit,sbk_tex,sbk_bg,N3 116 - sbk_04,sbk_04_shape,sbk_04_hit,sbk_tex,sbk_bg,N3E1 117 - sbk_05,sbk_05_shape,sbk_05_hit,sbk_tex,sbk_bg,N3E2 Pokey Army 118 - sbk_06,sbk_06_shape,sbk_06_hit,sbk_tex,sbk_bg,N3E3 119 - sbk_10,sbk_10_shape,sbk_10_hit,sbk_tex,sbk_bg,N2W3 120 - sbk_11,sbk_11_shape,sbk_11_hit,sbk_tex,sbk_bg,N2W2 121 - sbk_12,sbk_12_shape,sbk_12_hit,sbk_tex,sbk_bg,N2W1 122 - sbk_13,sbk_13_shape,sbk_13_hit,sbk_tex,sbk_bg,N2 123 - sbk_14,sbk_14_shape,sbk_14_hit,sbk_tex,sbk_bg,N2E1 (Tweester A) 124 - sbk_15,sbk_15_shape,sbk_15_hit,sbk_tex,sbk_bg,N2E2 125 - sbk_16,sbk_16_shape,sbk_16_hit,sbk_tex,sbk_bg,N2E3 126 - sbk_20,sbk_20_shape,sbk_20_hit,sbk_tex,sbk_bg,N1W3 Special Block 127 - sbk_21,sbk_21_shape,sbk_21_hit,sbk_tex,sbk_bg,N1W2 128 - sbk_22,sbk_22_shape,sbk_22_hit,sbk_tex,sbk_bg,N1W1 129 - sbk_23,sbk_23_shape,sbk_23_hit,sbk_tex,sbk_bg,N1 (Tweester B) 130 - sbk_24,sbk_24_shape,sbk_24_hit,sbk_tex,sbk_bg,N1E1 Palm Trio 131 - sbk_25,sbk_25_shape,sbk_25_hit,sbk_tex,sbk_bg,N1E2 132 - sbk_26,sbk_26_shape,sbk_26_hit,sbk_tex,sbk_bg,N1E3 133 - sbk_30,sbk_30_shape,sbk_30_hit,sbk_tex,sbk_bg,W3 Kolorado's Camp 134 - sbk_31,sbk_31_shape,sbk_31_hit,sbk_tex,sbk_bg,W2 135 - sbk_32,sbk_32_shape,sbk_32_hit,sbk_tex,sbk_bg,W1 136 - sbk_33,sbk_33_shape,sbk_33_hit,sbk_tex,sbk_bg,Center (Tweester C) 137 - sbk_34,sbk_34_shape,sbk_34_hit,sbk_tex,sbk_bg,E1 Nomadimouse 138 - sbk_35,sbk_35_shape,sbk_35_hit,sbk_tex,sbk_bg,E2 139 - sbk_36,sbk_36_shape,sbk_36_hit,sbk_tex,sbk_bg,E3 Outside Outpost 140 - sbk_40,sbk_40_shape,sbk_40_hit,sbk_tex,sbk_bg,S1W3 141 - sbk_41,sbk_41_shape,sbk_41_hit,sbk_tex,sbk_bg,S1W2 (Tweester D) 142 - sbk_42,sbk_42_shape,sbk_42_hit,sbk_tex,sbk_bg,S1W1 143 - sbk_43,sbk_43_shape,sbk_43_hit,sbk_tex,sbk_bg,S1 144 - sbk_44,sbk_44_shape,sbk_44_hit,sbk_tex,sbk_bg,S1E1 145 - sbk_45,sbk_45_shape,sbk_45_hit,sbk_tex,sbk_bg,S1E2 Small Bluffs 146 - sbk_46,sbk_46_shape,sbk_46_hit,sbk_tex,sbk_bg,S1E3 North of Oasis 147 - sbk_50,sbk_50_shape,sbk_50_hit,sbk_tex,sbk_bg,S2W3 148 - sbk_51,sbk_51_shape,sbk_51_hit,sbk_tex,sbk_bg,S2W2 149 - sbk_52,sbk_52_shape,sbk_52_hit,sbk_tex,sbk_bg,S2W1 150 - sbk_53,sbk_53_shape,sbk_53_hit,sbk_tex,sbk_bg,S2 151 - sbk_54,sbk_54_shape,sbk_54_hit,sbk_tex,sbk_bg,S2E1 Blue Cactus 152 - sbk_55,sbk_55_shape,sbk_55_hit,sbk_tex,sbk_bg,S2E2 West of Oasis 153 - sbk_56,sbk_56_shape,sbk_56_hit,sbk_tex,sbk_bg,S2E3 Oasis 154 - sbk_60,sbk_60_shape,sbk_60_hit,sbk_tex,sbk_bg,S3W3 155 - sbk_61,sbk_61_shape,sbk_61_hit,sbk_tex,sbk_bg,S3W2 Hidden AttackFX 156 - sbk_62,sbk_62_shape,sbk_62_hit,sbk_tex,sbk_bg,S3W1 157 - sbk_63,sbk_63_shape,sbk_63_hit,sbk_tex,sbk_bg,S3 158 - sbk_64,sbk_64_shape,sbk_64_hit,sbk_tex,sbk_bg,S3E1 159 - sbk_65,sbk_65_shape,sbk_65_hit,sbk_tex,sbk_bg,S3E2 160 - sbk_66,sbk_66_shape,sbk_66_hit,sbk_tex,sbk_bg,S3E3 South of Oasis 161 - sbk_99,sbk_99_shape,sbk_99_hit,sbk_tex,sbk_bg,Entrance 162 - isk_01,isk_01_shape,isk_01_hit,isk_tex,sbk3_bg,Entrance 163 - isk_02,isk_02_shape,isk_02_hit,isk_tex,sbk3_bg,Sarcophagus Hall 1 164 - isk_03,isk_03_shape,isk_03_hit,isk_tex,none,Sand Drainage Room 1 165 - isk_04,isk_04_shape,isk_04_hit,isk_tex,sbk3_bg,Descending Stairs 1 166 - isk_05,isk_05_shape,isk_05_hit,isk_tex,none,Pyramid Stone Room 167 - isk_06,isk_06_shape,isk_06_hit,isk_tex,none,Sand Drainage Room 2 168 - isk_07,isk_07_shape,isk_07_hit,isk_tex,none,Sarcophagus Hall 2 169 - isk_08,isk_08_shape,isk_08_hit,isk_tex,none,Descending Stairs 2 170 - isk_09,isk_09_shape,isk_09_hit,isk_tex,none,Super Hammer Room 171 - isk_10,isk_10_shape,isk_10_hit,isk_tex,none,Vertical Shaft 172 - isk_11,isk_11_shape,isk_11_hit,isk_tex,none,Stone Puzzle Room 173 - isk_12,isk_12_shape,isk_12_hit,isk_tex,none,Sand Drainage Room 3 174 - isk_13,isk_13_shape,isk_13_hit,isk_tex,none,Lunar Stone Room 175 - isk_14,isk_14_shape,isk_14_hit,isk_tex,none,Diamond Stone Room 176 - isk_16,isk_16_shape,isk_16_hit,isk_tex,none,Tutankoopa Room 177 - isk_18,isk_18_shape,isk_18_hit,isk_tex,none,Deep Tunnel 178 - isk_19,isk_19_shape,isk_19_hit,isk_tex,none,Boss Antechamber 179 - mim_01,mim_01_shape,mim_01_hit,mim_tex,obk_bg,Flower Sounds 180 - mim_02,mim_02_shape,mim_02_hit,mim_tex,obk_bg,Stump Eyes 181 - mim_03,mim_03_shape,mim_03_hit,mim_tex,obk_bg,Flowers (Oaklie) 182 - mim_04,mim_04_shape,mim_04_hit,mim_tex,obk_bg,Tree Face (Bub-ulb) 183 - mim_05,mim_05_shape,mim_05_hit,mim_tex,obk_bg,Mushrooms (Path Splits) 184 - mim_06,mim_06_shape,mim_06_hit,mim_tex,obk_bg,Flowers Vanish 185 - mim_07,mim_07_shape,mim_07_hit,mim_tex,obk_bg,Laughing Rock 186 - mim_08,mim_08_shape,mim_08_hit,mim_tex,obk_bg,Bee Hive (HP Plus) 187 - mim_09,mim_09_shape,mim_09_hit,mim_tex,obk_bg,Flowers Appear (FP Plus) 188 - mim_10,mim_10_shape,mim_10_hit,mim_tex,nok_bg,Exit to Toad Town 189 - mim_11,mim_11_shape,mim_11_hit,mim_tex,obk_bg,Outside Boo's Mansion 190 - mim_12,mim_12_shape,mim_12_hit,mim_tex,arn_bg,Exit to Gusty Gulch 191 - obk_01,obk_01_shape,obk_01_hit,obk_tex,none,Foyer 192 - obk_02,obk_02_shape,obk_02_hit,obk_tex,obk_bg,Basement Stairs 193 - obk_03,obk_03_shape,obk_03_hit,obk_tex,none,Basement 194 - obk_04,obk_04_shape,obk_04_hit,obk_tex,none,Super Boots Room 195 - obk_05,obk_05_shape,obk_05_hit,obk_tex,obk_bg,Pot Room 196 - obk_06,obk_06_shape,obk_06_hit,obk_tex,none,Library 197 - obk_07,obk_07_shape,obk_07_hit,obk_tex,obk_bg,Record Player Room 198 - obk_08,obk_08_shape,obk_08_hit,obk_tex,obk_bg,Record Room 199 - obk_09,obk_09_shape,obk_09_hit,obk_tex,none,Lady Bow's Room 200 - arn_02,arn_02_shape,arn_02_hit,arn_tex,arn_bg,Wasteland Ascent 1 201 - arn_03,arn_03_shape,arn_03_hit,arn_tex,arn_bg,Ghost Town 1 202 - arn_04,arn_04_shape,arn_04_hit,arn_tex,arn_bg,Wasteland Ascent 2 203 - arn_05,arn_05_shape,arn_05_hit,arn_tex,arn_bg,Ghost Town 2 204 - arn_07,arn_07_shape,arn_07_hit,arn_tex,arn_bg,Windmill Exterior 205 - arn_08,arn_08_shape,arn_08_hit,arn_tex,none,Windmill Interior 206 - arn_09,arn_09_shape,arn_09_hit,arn_tex,none,Windmill Tunnel Entry 207 - arn_10,arn_10_shape,arn_10_hit,arn_tex,none,Tunnel 1 208 - arn_11,arn_11_shape,arn_11_hit,arn_tex,none,Tubba's Heart Chamber 209 - arn_12,arn_12_shape,arn_12_hit,arn_tex,none,Tunnel 2 210 - arn_13,arn_13_shape,arn_13_hit,arn_tex,none,Tunnel 3 211 - arn_20,arn_20_shape,arn_20_hit,arn_tex,none,Tubba's Manor Exterior 212 - dgb_00,arn_20_shape,arn_20_hit,arn_tex,arn_bg,Escape Scene 213 - dgb_01,dgb_01_shape,dgb_01_hit,dgb_tex,none,Great Hall 214 - dgb_02,dgb_02_shape,dgb_02_hit,dgb_tex,none,West Hall (1F) 215 - dgb_03,dgb_03_shape,dgb_03_hit,dgb_tex,none,Table/Clock Room (1/2F) 216 - dgb_04,dgb_04_shape,dgb_04_hit,dgb_tex,none,Stairs to Basement 217 - dgb_05,dgb_05_shape,dgb_05_hit,dgb_tex,none,Stairs Above Basement 218 - dgb_06,dgb_06_shape,dgb_06_hit,dgb_tex,none,Basement 219 - dgb_07,dgb_07_shape,dgb_07_hit,dgb_tex,none,Study (1F) 220 - dgb_08,dgb_08_shape,dgb_08_hit,dgb_tex,none,East Hall (1/2F) 221 - dgb_09,dgb_09_shape,dgb_09_hit,dgb_tex,none,West Hall (2F) 222 - dgb_10,dgb_10_shape,dgb_10_hit,dgb_tex,none,Sealed Room (2F) 223 - dgb_11,dgb_11_shape,dgb_11_hit,dgb_tex,none,Covered Tables Room (1F) 224 - dgb_12,dgb_12_shape,dgb_12_hit,dgb_tex,none,Spike Trap Room (2F) 225 - dgb_13,dgb_13_shape,dgb_13_hit,dgb_tex,none,Hidden Bedroom (2F) 226 - dgb_14,dgb_14_shape,dgb_14_hit,dgb_tex,none,Stairs to Third Floor 227 - dgb_15,dgb_15_shape,dgb_15_hit,dgb_tex,none,West Hall (3F) 228 - dgb_16,dgb_16_shape,dgb_16_hit,dgb_tex,none,Sleeping Clubbas Room (3F) 229 - dgb_17,dgb_17_shape,dgb_17_hit,dgb_tex,none,Save Room (3F) 230 - dgb_18,dgb_18_shape,dgb_18_hit,dgb_tex,none,Master Bedroom (3F) 231 - omo_01,omo_01_shape,omo_01_hit,omo_tex,omo_bg,BLU Large Playroom 232 - omo_02,omo_02_shape,omo_02_hit,omo_tex,omo_bg,RED Boss Barricade 233 - omo_03,omo_03_shape,omo_03_hit,omo_tex,omo_bg,BLU Station 234 - omo_04,omo_04_shape,omo_04_hit,omo_tex,omo_bg,BLU Block City 235 - omo_05,omo_05_shape,omo_05_hit,omo_tex,omo_bg,PNK Gourmet Guy Crossing 236 - omo_06,omo_06_shape,omo_06_hit,omo_tex,omo_bg,PNK Station 237 - omo_07,omo_07_shape,omo_07_hit,omo_tex,omo_bg,PNK Playhouse 238 - omo_08,omo_08_shape,omo_08_hit,omo_tex,omo_bg,GRN Station 239 - omo_09,omo_09_shape,omo_09_hit,omo_tex,omo_bg,GRN Treadmills/Slot Machine 240 - omo_10,omo_10_shape,omo_10_hit,omo_tex,omo_bg,RED Station 241 - omo_11,omo_11_shape,omo_11_hit,omo_tex,omo_bg,RED Moving Platforms 242 - omo_12,omo_12_shape,omo_12_hit,omo_tex,none,RED Lantern Ghost 243 - omo_13,omo_13_shape,omo_13_hit,omo_tex,omo_bg,BLU Anti-Guy Hall 244 - omo_14,omo_14_shape,omo_14_hit,omo_tex,none,RED Boss Antechamber 245 - omo_15,omo_15_shape,omo_15_hit,omo_tex,omo_bg,RED General Guy Room 246 - omo_16,omo_16_shape,omo_16_hit,omo_tex,omo_bg,Riding the Train 247 - omo_17,omo_17_shape,omo_17_hit,omo_tex,omo_bg,PNK Tracks Hallway 248 - jan_00,jan_00_shape,jan_00_hit,jan_tex,yos_bg,Whale Cove 249 - jan_01,jan_01_shape,jan_01_hit,jan_tex,yos_bg,Beach 250 - jan_02,jan_02_shape,jan_02_hit,jan_tex,yos_bg,Village Cove 251 - jan_03,jan_03_shape,jan_03_hit,jan_tex,yos_bg,Village Buildings 252 - jan_04,jan_04_shape,jan_04_hit,jan_tex,yos_bg,Sushi Tree 253 - jan_05,jan_05_shape,jan_05_hit,jan_tex,yos_bg,SE Jungle (Quake Hammer) 254 - jan_06,jan_06_shape,jan_06_hit,jan_tex,jan_bg,NE Jungle (Raven Statue) 255 - jan_07,jan_07_shape,jan_07_hit,jan_tex,yos_bg,Small Jungle Ledge 256 - jan_08,jan_08_shape,jan_08_hit,jan_tex,yos_bg,SW Jungle (Super Block) 257 - jan_09,jan_09_shape,jan_09_hit,jan_tex,yos_bg,NW Jungle (Large Ledge) 258 - jan_10,jan_10_shape,jan_10_hit,jan_tex,yos_bg,Western Dead End 259 - jan_11,jan_11_shape,jan_11_hit,jan_tex,jan_bg,Root Cavern 260 - jan_12,jan_12_shape,jan_12_hit,jan_tex,jan_bg,Deep Jungle 1 261 - jan_13,jan_13_shape,jan_13_hit,jan_tex,jan_bg,Deep Jungle 2 (Block Puzzle) 262 - jan_14,jan_14_shape,jan_14_hit,jan_tex,jan_bg,Deep Jungle 3 263 - jan_15,jan_15_shape,jan_15_hit,jan_tex,jan_bg,Deep Jungle 4 (Ambush) 264 - jan_16,jan_16_shape,jan_16_hit,jan_tex,jan_bg,Base of Great Tree 265 - jan_17,jan_17_shape,jan_17_hit,jan_tex,jan_bg,Lower Great Tree Interior 266 - jan_18,jan_18_shape,jan_18_hit,jan_tex,yos_bg,Great Tree Vine Ascent 267 - jan_19,jan_19_shape,jan_19_hit,jan_tex,jan_bg,Upper Great Tree Interior 268 - jan_22,jan_22_shape,jan_22_hit,jan_tex,jan_bg,Path to the Volcano 269 - jan_23,jan_23_shape,jan_23_hit,jan_tex,yos_bg,Great Treetop Roost 270 - kzn_01,kzn_01_shape,kzn_01_hit,kzn_tex,none,Volcano Entrance 271 - kzn_02,kzn_02_shape,kzn_02_hit,kzn_tex,none,First Lava Lake 272 - kzn_03,kzn_03_shape,kzn_03_hit,kzn_tex,none,Central Cavern 273 - kzn_04,kzn_04_shape,kzn_04_hit,kzn_tex,none,Fire Bar Bridge 274 - kzn_05,kzn_05_shape,kzn_05_hit,kzn_tex,none,Descent Toward Ultra Hammer 275 - kzn_06,kzn_06_shape,kzn_06_hit,kzn_tex,none,Flowing Lava Puzzle 276 - kzn_07,kzn_07_shape,kzn_07_hit,kzn_tex,none,Ultra Hammer Room 277 - kzn_08,kzn_08_shape,kzn_08_hit,kzn_tex,none,Dizzy Stomp Room 278 - kzn_09,kzn_09_shape,kzn_09_hit,kzn_tex,none,Zipline Cavern 279 - kzn_10,kzn_10_shape,kzn_10_hit,kzn_tex,none,Descent Toward Boss 280 - kzn_11,kzn_11_shape,kzn_11_hit,kzn_tex,none,Second Lava Lake 281 - kzn_17,kzn_17_shape,kzn_17_hit,kzn_tex,none,Spike Roller Trap 282 - kzn_18,kzn_18_shape,kzn_18_hit,kzn_tex,none,Boss Antechamber 283 - kzn_19,kzn_19_shape,kzn_19_hit,kzn_tex,none,Boss Room 284 - kzn_20,kzn_20_shape,kzn_20_hit,kzn_tex,none,Rising Lava 1 285 - kzn_22,kzn_22_shape,kzn_22_hit,kzn_tex,none,Rising Lava 2 286 - kzn_23,kzn_23_shape,kzn_23_hit,kzn_tex,yos_bg,Volcano Escape 287 - flo_00,flo_00_shape,flo_00_hit,flo_tex,fla_bg,Center 288 - flo_03,flo_03_shape,flo_03_hit,flo_tex,fla_bg,(East) Petunia's Field 289 - flo_07,flo_07_shape,flo_07_hit,flo_tex,fla_bg,(SW) Posie and Crystal Tree 290 - flo_08,flo_08_shape,flo_08_hit,flo_tex,fla_bg,(SE) Briar Platforming 291 - flo_09,flo_09_shape,flo_09_hit,flo_tex,fla_bg,(East) Triple Tree Path 292 - flo_10,flo_10_shape,flo_10_hit,flo_tex,fla_bg,(SE) Lily's Fountain 293 - flo_11,flo_11_shape,flo_11_hit,flo_tex,fla_bg,(West) Maze 294 - flo_12,flo_12_shape,flo_12_hit,flo_tex,fla_bg,(West) Rosie's Trellis 295 - flo_13,flo_13_shape,flo_13_hit,flo_tex,fla_bg,(NW) Lakilester 296 - flo_14,flo_14_shape,flo_14_hit,flo_tex,fla_bg,(NW) Bubble Flower 297 - flo_15,flo_15_shape,flo_15_hit,flo_tex,fla_bg,(NW) Sun Tower 298 - flo_16,flo_16_shape,flo_16_hit,flo_tex,fla_bg,(NE) Elevators 299 - flo_17,flo_17_shape,flo_17_hit,flo_tex,fla_bg,(NE) Fallen Logs 300 - flo_18,flo_18_shape,flo_18_hit,flo_tex,fla_bg,(NE) Puff Puff Machine 301 - flo_19,flo_19_shape,flo_19_hit,flo_tex,sra_bg,Cloudy Climb 302 - flo_21,flo_21_shape,flo_21_hit,flo_tex,sra_bg,Huff N Puff Room 303 - flo_22,flo_22_shape,flo_22_hit,flo_tex,fla_bg,(East) Old Well 304 - flo_23,flo_23_shape,flo_23_hit,flo_tex,fla_bg,(West) Path to Maze 305 - flo_24,flo_24_shape,flo_24_hit,flo_tex,fla_bg,(SE) Water Level Room 306 - flo_25,flo_25_shape,flo_25_hit,flo_tex,fla_bg,(SW) Path to Crystal Tree 307 - sam_01,sam_01_shape,sam_01_hit,sam_tex,yki_bg,Shiver City Mayor Area 308 - sam_02,sam_02_shape,sam_02_hit,sam_tex,yki_bg,Shiver City Center 309 - sam_03,sam_03_shape,sam_03_hit,sam_tex,yki_bg,Road to Shiver Snowfield 310 - sam_04,sam_04_shape,sam_04_hit,sam_tex,yki_bg,Shiver Snowfield 311 - sam_05,sam_05_shape,sam_05_hit,sam_tex,sam_bg,Path to Starborn Valley 312 - sam_06,sam_06_shape,sam_06_hit,sam_tex,sam_bg,Starborn Valley 313 - sam_07,sam_07_shape,sam_07_hit,sam_tex,yki_bg,Shiver Mountain Passage 314 - sam_08,sam_08_shape,sam_08_hit,sam_tex,yki_bg,Shiver Mountain Hills 315 - sam_09,sam_09_shape,sam_09_hit,sam_tex,yki_bg,Shiver Mountain Tunnel 316 - sam_10,sam_10_shape,sam_10_hit,sam_tex,yki_bg,Shiver Mountain Peaks 317 - sam_11,sam_11_shape,sam_11_hit,sam_tex,yki_bg,Shiver City Pond Area 318 - sam_12,sam_12_shape,sam_12_hit,sam_tex,yki_bg,Merlar's Sanctuary 319 - pra_01,pra_01_shape,pra_01_hit,pra_tex,yki_bg,Entrance 320 - pra_02,pra_02_shape,pra_02_hit,pra_tex,none,Entry Hall 321 - pra_03,pra_03_shape,pra_03_hit,pra_tex,none,Save Room 322 - pra_04,pra_04_shape,pra_04_hit,pra_tex,none,Reflected Save Room 323 - pra_05,pra_05_shape,pra_05_hit,pra_tex,none,Blue Key Room 324 - pra_06,pra_05_shape,pra_05_hit,pra_tex,none,Shooting Star Room 325 - pra_09,pra_09_shape,pra_09_hit,pra_tex,none,Red Key Hall 326 - pra_10,pra_10_shape,pra_10_hit,pra_tex,none,P-Down D-Up Hall 327 - pra_11,pra_11_shape,pra_11_hit,pra_tex,none,Red Key Room 328 - pra_12,pra_05_shape,pra_05_hit,pra_tex,none,P-Down D-Up Room 329 - pra_13,pra_13_shape,pra_13_hit,pra_tex,none,Blue Mirror Hall 1 330 - pra_14,pra_14_shape,pra_14_hit,pra_tex,none,Blue Mirror Hall 2 331 - pra_15,pra_15_shape,pra_15_hit,pra_tex,yki_bg,Star Piece Cave 332 - pra_16,pra_16_shape,pra_16_hit,pra_tex,none,Red Mirror Hall 333 - pra_18,pra_18_shape,pra_18_hit,pra_tex,none,Bridge Mirror Hall 334 - pra_19,pra_19_shape,pra_19_hit,pra_tex,none,Reflection Mimic Room 335 - pra_20,pra_20_shape,pra_20_hit,pra_tex,none,Mirrored Door Room 336 - pra_21,pra_21_shape,pra_21_hit,pra_tex,none,Huge Statue Room 337 - pra_22,pra_22_shape,pra_22_hit,pra_tex,none,Small Statue Room 338 - pra_27,pra_05_shape,pra_05_hit,pra_tex,none,Palace Key Room 339 - pra_28,pra_05_shape,pra_05_hit,pra_tex,none,P-Up D-Down Room 340 - pra_29,pra_29_shape,pra_29_hit,pra_tex,none,Hidden Bridge Room 341 - pra_31,pra_31_shape,pra_31_hit,pra_tex,none,Dino Puzzle Room 342 - pra_32,pra_32_shape,pra_32_hit,pra_tex,sam_bg,Crystal Summit 343 - pra_33,pra_33_shape,pra_33_hit,pra_tex,none,Turnstyle Room 344 - pra_34,pra_34_shape,pra_34_hit,pra_tex,none,Mirror Hole Room 345 - pra_35,pra_35_shape,pra_35_hit,pra_tex,none,Triple Dip Room 346 - pra_36,pra_10_shape,pra_10_hit,pra_tex,none,Palace Key Hall 347 - pra_37,pra_10_shape,pra_10_hit,pra_tex,none,P-Up D-Down Hall 348 - pra_38,pra_10_shape,pra_10_hit,pra_tex,none,Blue Key Hall 349 - pra_39,pra_10_shape,pra_10_hit,pra_tex,none,Shooting Star Hall 350 - pra_40,pra_40_shape,pra_40_hit,pra_tex,none,Boss Antechamber 351 - kpa_01,kpa_01_shape,kpa_01_hit,kpa_tex,none,Dark Cave 1 352 - kpa_03,kpa_03_shape,kpa_03_hit,kpa_tex,none,Dark Cave 2 353 - kpa_04,kpa_04_shape,kpa_04_hit,kpa_tex,none,Cave Exit 354 - kpa_08,kpa_08_shape,kpa_08_hit,kpa_tex,none,Castle Key Timing Puzzle 355 - kpa_09,kpa_09_shape,kpa_09_hit,kpa_tex,none,Ultra Shroom Timing Puzzle 356 - kpa_10,kpa_10_shape,kpa_10_hit,kpa_tex,none,Outside Lower Jail (No Lava) 357 - kpa_11,kpa_11_shape,kpa_11_hit,kpa_tex,none,Outside Lower Jail (Lava) 358 - kpa_12,kpa_12_shape,kpa_12_hit,kpa_tex,none,Lava Channel 1 359 - kpa_13,kpa_13_shape,kpa_13_hit,kpa_tex,none,Lava Channel 2 360 - kpa_14,kpa_14_shape,kpa_14_hit,kpa_tex,none,Lava Channel 3 361 - kpa_15,kpa_15_shape,kpa_15_hit,kpa_tex,none,Lava Key Room 362 - kpa_16,kpa_16_shape,kpa_16_hit,kpa_tex,none,Lava Control Room 363 - kpa_17,kpa_17_shape,kpa_17_hit,kpa_tex,none,Lower Jail 364 - kpa_32,kpa_32_shape,kpa_32_hit,kpa_tex,kpa_bg,Lower Grand Hall 365 - kpa_33,kpa_33_shape,kpa_33_hit,kpa_tex,kpa_bg,Upper Grand Hall 366 - kpa_40,kpa_40_shape,kpa_40_hit,kpa_tex,none,Maze Guide Room 367 - kpa_41,kpa_41_shape,kpa_41_hit,kpa_tex,none,Maze Room 368 - kpa_50,kpa_50_shape,kpa_50_hit,kpa_tex,none,Hall to Guard Door 1 369 - kpa_51,kpa_50_shape,kpa_50_hit,kpa_tex,none,Hall to Water Puzzle 370 - kpa_52,kpa_52_shape,kpa_52_hit,kpa_tex,none,Split Level Hall 371 - kpa_53,kpa_50_shape,kpa_50_hit,kpa_tex,none,Fake Peach Hallway 372 - kpa_60,kpa_60_shape,kpa_60_hit,kpa_tex,kpa_bg,Ship Enter/Exit Scenes 373 - kpa_61,kpa_61_shape,kpa_61_hit,kpa_tex,kpa_bg,Battlement 374 - kpa_62,kpa_62_shape,kpa_62_hit,kpa_tex,kpa_bg,Front Door Exterior 375 - kpa_63,kpa_63_shape,kpa_63_hit,kpa_tex,none,Hanger 376 - kpa_70,kpa_70_shape,kpa_70_hit,kpa_tex,none,Entry Lava Hall 377 - kpa_81,kpa_80_shape,kpa_80_hit,kpa_tex,none,Guard Door 1 378 - kpa_82,kpa_80_shape,kpa_80_hit,kpa_tex,none,Guard Door 2 379 - kpa_83,kpa_80_shape,kpa_80_hit,kpa_tex,none,Guard Door 3 380 - kpa_90,kpa_90_shape,kpa_90_hit,kpa_tex,none,Stairs to East Upper Jail 381 - kpa_91,kpa_91_shape,kpa_91_hit,kpa_tex,none,East Upper Jail 382 - kpa_94,kpa_94_shape,kpa_94_hit,kpa_tex,none,Stairs to West Upper Jail 383 - kpa_95,kpa_95_shape,kpa_95_hit,kpa_tex,none,West Upper Jail 384 - kpa_96,kpa_96_shape,kpa_96_hit,kpa_tex,none,Item Shop 385 - kpa_100,kpa_117_shape,kpa_117_hit,kpa_tex,none,Castle Key Room 386 - kpa_101,kpa_119_shape,kpa_119_hit,kpa_tex,none,Ultra Shroom Room 387 - kpa_102,kpa_102_shape,kpa_102_hit,kpa_tex,none,Blue Fire Bridge 388 - kpa_111,kpa_111_shape,kpa_111_hit,kpa_tex,none,Room with Hidden Door 1 389 - kpa_112,kpa_112_shape,kpa_112_hit,kpa_tex,none,Hidden Passage 1 390 - kpa_113,kpa_113_shape,kpa_113_hit,kpa_tex,none,Room with Hidden Door 2 391 - kpa_114,kpa_112_shape,kpa_112_hit,kpa_tex,none,Hidden Passage 2 392 - kpa_115,kpa_115_shape,kpa_115_hit,kpa_tex,none,Room with Hidden Door 3 393 - kpa_116,kpa_116_shape,kpa_116_hit,kpa_tex,none,Dead End Passage 394 - kpa_117,kpa_117_shape,kpa_117_hit,kpa_tex,none,Dead End Room 395 - kpa_118,kpa_118_shape,kpa_118_hit,kpa_tex,none,Hidden Passage 3 396 - kpa_119,kpa_119_shape,kpa_119_hit,kpa_tex,none,Hidden Key Room 397 - kpa_121,kpa_121_shape,kpa_121_hit,kpa_tex,none,Exit to Peach's Castle 398 - kpa_130,kpa_130_shape,kpa_130_hit,kpa_tex,none,Bill Blaster Hall 399 - kpa_133,kpa_133_shape,kpa_133_hit,kpa_tex,none,Left Water Puzzle 400 - kpa_134,kpa_134_shape,kpa_134_hit,kpa_tex,none,Right Water Puzzle 401 - kpa_80,kpa_80_shape,kpa_80_hit,kpa_tex,none,Guard Door Geometry 402 - osr_00,osr_00_shape,osr_00_hit,osr_tex,nok_bg,Intro Castle Grounds 403 - osr_01,osr_01_shape,osr_01_hit,osr_tex,nok_bg,Ruined Castle Grounds 404 - osr_02,osr_02_shape,osr_02_hit,osr_tex,kpa_bg,Hijacked Castle Entrance 405 - osr_03,osr_03_shape,osr_03_hit,osr_tex,kpa_bg,Outside Hijacked Castle 406 - osr_04,osr_03_shape,osr_03_hit,osr_tex,nok_bg,Castle Hijacking Scene 407 - end_00,end_00_shape,end_00_hit,end_tex,none,Parade (Day) 408 - end_01,end_01_shape,end_01_hit,end_tex,none,Parade (Night) 409 - mgm_00,mgm_00_shape,mgm_00_hit,mgm_tex,none,Playroom Lobby 410 - mgm_01,mgm_01_shape,mgm_01_hit,mgm_tex,none,Jump Attack Minigame 411 - mgm_02,mgm_02_shape,mgm_02_hit,mgm_tex,none,Smash Attack Minigame 412 - mgm_03,mgm_03_shape,mgm_03_hit,mgm_tex,none,Large Debug Room 413 - gv_01,gv_01_shape,gv_01_hit,gv__tex,none,Game Over Screen 414 - tst_01,tst_01_shape,tst_01_hit,tst_tex,nok_bg,Jump Width Test 415 - tst_02,tst_02_shape,tst_02_hit,tst_tex,nok_bg,Jump Height Test 416 - tst_03,tst_03_shape,tst_03_hit,tst_tex,nok_bg,Entity Test 417 - tst_04,tst_04_shape,tst_04_hit,tst_tex,nok_bg,Moving Platforms Test 418 - tst_10,tst_10_shape,tst_10_hit,tst_tex,nok_bg,Entry and Camera Test 419 - tst_11,tst_11_shape,tst_11_hit,tst_tex,nok_bg,Reflection Test 420 - tst_12,tst_12_shape,tst_12_hit,tst_tex,nok_bg,Flower Fields Test 421 - tst_13,tst_13_shape,tst_13_hit,tst_tex,nok_bg,Partners and Shockwave 422 - tst_20,tst_20_shape,tst_20_hit,tst_tex,nok_bg,Pipes Gallery 1 + kmr_00,areas/kmr/kmr_00.map/shape.bin,areas/kmr/kmr_00.map/hit.bin,areas/kmr/kmr.tex,backgrounds/kmr.bg.png,Forest Clearing 2 + kmr_02,areas/kmr/kmr_02.map/shape.bin,areas/kmr/kmr_02.map/hit.bin,areas/kmr/kmr.tex,backgrounds/kmr.bg.png,Goomba Village 3 + kmr_03,areas/kmr/kmr_03.map/shape.bin,areas/kmr/kmr_03.map/hit.bin,areas/kmr/kmr.tex,backgrounds/kmr.bg.png,Bottom of the Cliff 4 + kmr_04,areas/kmr/kmr_04.map/shape.bin,areas/kmr/kmr_04.map/hit.bin,areas/kmr/kmr.tex,backgrounds/kmr.bg.png,Jr. Troopa's Playground 5 + kmr_05,areas/kmr/kmr_05.map/shape.bin,areas/kmr/kmr_05.map/hit.bin,areas/kmr/kmr.tex,backgrounds/kmr.bg.png,Behind the Village 6 + kmr_06,areas/kmr/kmr_06.map/shape.bin,areas/kmr/kmr_06.map/hit.bin,areas/kmr/kmr.tex,backgrounds/kmr.bg.png,Goomba Road 2 7 + kmr_07,areas/kmr/kmr_07.map/shape.bin,areas/kmr/kmr_07.map/hit.bin,areas/kmr/kmr.tex,backgrounds/kmr.bg.png,Goomba Road 3 8 + kmr_09,areas/kmr/kmr_09.map/shape.bin,areas/kmr/kmr_09.map/hit.bin,areas/kmr/kmr.tex,backgrounds/kmr.bg.png,Goomba Road 1 9 + kmr_10,areas/kmr/kmr_10.map/shape.bin,areas/kmr/kmr_10.map/hit.bin,areas/kmr/kmr.tex,backgrounds/kmr.bg.png,Toad Town Entrance 10 + kmr_11,areas/kmr/kmr_11.map/shape.bin,areas/kmr/kmr_11.map/hit.bin,areas/kmr/kmr.tex,backgrounds/kmr.bg.png,Goomba King's Castle 11 + kmr_12,areas/kmr/kmr_12.map/shape.bin,areas/kmr/kmr_12.map/hit.bin,areas/kmr/kmr.tex,backgrounds/kmr.bg.png,Goomba Road 4 12 + kmr_20,areas/kmr/kmr_20.map/shape.bin,areas/kmr/kmr_20.map/hit.bin,areas/kmr/kmr.tex,backgrounds/kmr.bg.png,Mario's House 13 + kmr_21,areas/kmr/kmr_21.map/shape.bin,areas/kmr/kmr_21.map/hit.bin,areas/kmr/kmr.tex,none,Title Screen 14 + kmr_22,areas/kmr/kmr_22.map/shape.bin,areas/kmr/kmr_22.map/hit.bin,areas/kmr/kmr.tex,none,Chapter Start 15 + kmr_23,areas/kmr/kmr_23.map/shape.bin,areas/kmr/kmr_23.map/hit.bin,areas/kmr/kmr.tex,none,Chapter End 16 + kmr_24,areas/kmr/kmr_24.map/shape.bin,areas/kmr/kmr_24.map/hit.bin,areas/kmr/kmr.tex,none,Save and Continue 17 + kmr_30,areas/kmr/kmr_30.map/shape.bin,areas/kmr/kmr_30.map/hit.bin,areas/kmr/kmr.tex,none,Mario's House (Ending) 18 + machi,areas/mac/machi.map/shape.bin,areas/mac/machi.map/hit.bin,areas/mac/mac.tex,backgrounds/nok.bg.png,Debug Warp Zone 19 + mac_00,areas/mac/mac_00.map/shape.bin,areas/mac/mac_00.map/hit.bin,areas/mac/mac.tex,backgrounds/nok.bg.png,Gate District 20 + mac_01,areas/mac/mac_01.map/shape.bin,areas/mac/mac_01.map/hit.bin,areas/mac/mac.tex,backgrounds/nok.bg.png,Plaza District 21 + mac_02,areas/mac/mac_02.map/shape.bin,areas/mac/mac_02.map/hit.bin,areas/mac/mac.tex,backgrounds/nok.bg.png,Southern District 22 + mac_03,areas/mac/mac_03.map/shape.bin,areas/mac/mac_03.map/hit.bin,areas/mac/mac.tex,backgrounds/nok.bg.png,Station District 23 + mac_04,areas/mac/mac_04.map/shape.bin,areas/mac/mac_04.map/hit.bin,areas/mac/mac.tex,backgrounds/nok.bg.png,Residental District 24 + mac_05,areas/mac/mac_05.map/shape.bin,areas/mac/mac_05.map/hit.bin,areas/mac/mac.tex,backgrounds/nok.bg.png,Port District 25 + mac_06,areas/mac/mac_06.map/shape.bin,areas/mac/mac_06.map/hit.bin,areas/mac/mac.tex,backgrounds/nok.bg.png,Riding the Whale 26 + tik_01,areas/tik/tik_01.map/shape.bin,areas/tik/tik_01.map/hit.bin,areas/tik/tik.tex,none,Warp Zone 1 (B1) 27 + tik_02,areas/tik/tik_02.map/shape.bin,areas/tik/tik_02.map/hit.bin,areas/tik/tik.tex,none,Blooper Boss 1 (B1) 28 + tik_03,areas/tik/tik_03.map/shape.bin,areas/tik/tik_03.map/hit.bin,areas/tik/tik.tex,none,Short Elevator Room (B1) 29 + tik_04,areas/tik/tik_04.map/shape.bin,areas/tik/tik_04.map/hit.bin,areas/tik/tik.tex,none,Scales Room (B2) 30 + tik_05,areas/tik/tik_05.map/shape.bin,areas/tik/tik_05.map/hit.bin,areas/tik/tik.tex,none,Spring Room (B2) 31 + tik_06,areas/tik/tik_06.map/shape.bin,areas/tik/tik_06.map/hit.bin,areas/tik/tik.tex,none,Sewer Entrance (B1) 32 + tik_07,areas/tik/tik_07.map/shape.bin,areas/tik/tik_07.map/hit.bin,areas/tik/tik.tex,none,Elevator Attic Room (B2) 33 + tik_08,areas/tik/tik_08.map/shape.bin,areas/tik/tik_08.map/hit.bin,areas/tik/tik.tex,none,Second Level Entry (B2) 34 + tik_09,areas/tik/tik_09.map/shape.bin,areas/tik/tik_09.map/hit.bin,areas/tik/tik.tex,none,Warp Zone 2 (B2) 35 + tik_10,areas/tik/tik_10.map/shape.bin,areas/tik/tik_10.map/hit.bin,areas/tik/tik.tex,none,Block Puzzle Room (B2) 36 + tik_12,areas/tik/tik_12.map/shape.bin,areas/tik/tik_12.map/hit.bin,areas/tik/tik.tex,none,Metal Block Room (B3) 37 + tik_14,areas/tik/tik_14.map/shape.bin,areas/tik/tik_14.map/hit.bin,areas/tik/tik.tex,none,Rip Cheato Antechamber (B3) 38 + tik_15,areas/tik/tik_15.map/shape.bin,areas/tik/tik_15.map/hit.bin,areas/tik/tik.tex,none,Rip Cheato's Home (B3) 39 + tik_17,areas/tik/tik_17.map/shape.bin,areas/tik/tik_17.map/hit.bin,areas/tik/tik.tex,none,Frozen Room (B3) 40 + tik_18,areas/tik/tik_18.map/shape.bin,areas/tik/tik_18.map/hit.bin,areas/tik/tik.tex,none,Hall to Blooper 1 (B1) 41 + tik_19,areas/tik/tik_19.map/shape.bin,areas/tik/tik_19.map/hit.bin,areas/tik/tik.tex,none,Under the Toad Town Pond 42 + tik_20,areas/tik/tik_20.map/shape.bin,areas/tik/tik_20.map/hit.bin,areas/tik/tik.tex,none,Room with Spikes (B2) 43 + tik_21,areas/tik/tik_21.map/shape.bin,areas/tik/tik_21.map/hit.bin,areas/tik/tik.tex,none,Hidden Blocks Room (B2) 44 + tik_22,areas/tik/tik_22.map/shape.bin,areas/tik/tik_22.map/hit.bin,areas/tik/tik.tex,none,Path to Shiver City (B2) 45 + tik_23,areas/tik/tik_23.map/shape.bin,areas/tik/tik_23.map/hit.bin,areas/tik/tik.tex,none,Windy Path (B3) 46 + tik_24,areas/tik/tik_18.map/shape.bin,areas/tik/tik_18.map/hit.bin,areas/tik/tik.tex,none,Hall to Ultra Boots (B3) 47 + tik_25,areas/tik/tik_25.map/shape.bin,areas/tik/tik_25.map/hit.bin,areas/tik/tik.tex,none,Ultra Boots Room (B3) 48 + kgr_01,areas/kgr/kgr_01.map/shape.bin,areas/kgr/kgr_01.map/hit.bin,areas/kgr/kgr.tex,none,Whale Mouth 49 + kgr_02,areas/kgr/kgr_02.map/shape.bin,areas/kgr/kgr_02.map/hit.bin,areas/kgr/kgr.tex,none,Whale Stomach 50 + kkj_00,areas/kkj/kkj_00.map/shape.bin,areas/kkj/kkj_00.map/hit.bin,areas/kkj/kkj.tex,backgrounds/nok.bg.png,Intro Entry Hall (1F) 51 + kkj_01,areas/kkj/kkj_01.map/shape.bin,areas/kkj/kkj_01.map/hit.bin,areas/kkj/kkj.tex,backgrounds/nok.bg.png,Intro Upper Hall (2F) 52 + kkj_02,areas/kkj/kkj_02.map/shape.bin,areas/kkj/kkj_02.map/hit.bin,areas/kkj/kkj.tex,backgrounds/nok.bg.png,Intro Stairs Hallway (3F) 53 + kkj_03,areas/kkj/kkj_03.map/shape.bin,areas/kkj/kkj_03.map/hit.bin,areas/kkj/kkj.tex,backgrounds/nok.bg.png,Intro Window Hallway (4F) 54 + kkj_10,areas/kkj/kkj_10.map/shape.bin,areas/kkj/kkj_10.map/hit.bin,areas/kkj/kkj.tex,none,Entry Hall (1F) 55 + kkj_11,areas/kkj/kkj_11.map/shape.bin,areas/kkj/kkj_11.map/hit.bin,areas/kkj/kkj.tex,none,Upper Hall (2F) 56 + kkj_12,areas/kkj/kkj_12.map/shape.bin,areas/kkj/kkj_12.map/hit.bin,areas/kkj/kkj.tex,none,Stairs Hallway (3F) 57 + kkj_13,areas/kkj/kkj_13.map/shape.bin,areas/kkj/kkj_13.map/hit.bin,areas/kkj/kkj.tex,backgrounds/kpa.bg.png,Window Hallway (4F) 58 + kkj_14,areas/kkj/kkj_14.map/shape.bin,areas/kkj/kkj_14.map/hit.bin,areas/kkj/kkj.tex,backgrounds/kpa.bg.png,Peach's Room (2F) 59 + kkj_15,areas/kkj/kkj_15.map/shape.bin,areas/kkj/kkj_15.map/hit.bin,areas/kkj/kkj.tex,none,Passage Outlet (2F) 60 + kkj_16,areas/kkj/kkj_16.map/shape.bin,areas/kkj/kkj_16.map/hit.bin,areas/kkj/kkj.tex,none,Library (2F) 61 + kkj_17,areas/kkj/kkj_17.map/shape.bin,areas/kkj/kkj_17.map/hit.bin,areas/kkj/kkj.tex,none,Storeroom (2F) 62 + kkj_18,areas/kkj/kkj_18.map/shape.bin,areas/kkj/kkj_18.map/hit.bin,areas/kkj/kkj.tex,backgrounds/kpa.bg.png,Dining Room (2F) 63 + kkj_19,areas/kkj/kkj_19.map/shape.bin,areas/kkj/kkj_19.map/hit.bin,areas/kkj/kkj.tex,none,Kitchen (1F) 64 + kkj_20,areas/kkj/kkj_20.map/shape.bin,areas/kkj/kkj_20.map/hit.bin,areas/kkj/kkj.tex,none,Guest Room (1F) 65 + kkj_21,areas/kkj/kkj_21.map/shape.bin,areas/kkj/kkj_21.map/hit.bin,areas/kkj/kkj.tex,none,Inactive Quiz-Off (1F) 66 + kkj_22,areas/kkj/kkj_22.map/shape.bin,areas/kkj/kkj_22.map/hit.bin,areas/kkj/kkj.tex,backgrounds/kpa.bg.png,Double Staircase (4F) 67 + kkj_23,areas/kkj/kkj_23.map/shape.bin,areas/kkj/kkj_23.map/hit.bin,areas/kkj/kkj.tex,backgrounds/kpa.bg.png,Rooftop (5F) 68 + kkj_24,areas/kkj/kkj_24.map/shape.bin,areas/kkj/kkj_24.map/hit.bin,areas/kkj/kkj.tex,backgrounds/kpa.bg.png,Tower Staircase (5F) 69 + kkj_25,areas/kkj/kkj_25.map/shape.bin,areas/kkj/kkj_25.map/hit.bin,areas/kkj/kkj.tex,backgrounds/kpa.bg.png,Final Boss Arena (6F) 70 + kkj_26,areas/kkj/kkj_26.map/shape.bin,areas/kkj/kkj_26.map/hit.bin,areas/kkj/kkj.tex,backgrounds/kpa.bg.png,Balcony (2F) 71 + kkj_27,areas/kkj/kkj_27.map/shape.bin,areas/kkj/kkj_27.map/hit.bin,areas/kkj/kkj.tex,none,Secret Passage (2F) 72 + kkj_28,areas/kkj/kkj_28.map/shape.bin,areas/kkj/kkj_28.map/hit.bin,areas/kkj/kkj.tex,none,Darkened Quiz-Off (1F) 73 + kkj_29,areas/kkj/kkj_29.map/shape.bin,areas/kkj/kkj_29.map/hit.bin,areas/kkj/kkj.tex,none,Quiz-Off Room (1F) 74 + hos_00,areas/hos/hos_00.map/shape.bin,areas/hos/hos_00.map/hit.bin,areas/hos/hos.tex,backgrounds/nok.bg.png,Shooting Star Path 75 + hos_01,areas/hos/hos_01.map/shape.bin,areas/hos/hos_01.map/hit.bin,areas/hos/hos.tex,backgrounds/hos.bg.png,Shooting Star Summit 76 + hos_02,areas/hos/hos_02.map/shape.bin,areas/hos/hos_02.map/hit.bin,areas/hos/hos.tex,backgrounds/hos.bg.png,Star Way 77 + hos_03,areas/hos/hos_03.map/shape.bin,areas/hos/hos_03.map/hit.bin,areas/hos/hos.tex,backgrounds/hos.bg.png,Star Haven 78 + hos_04,areas/hos/hos_04.map/shape.bin,areas/hos/hos_04.map/hit.bin,areas/hos/hos.tex,backgrounds/hos.bg.png,Outside the Sanctuary 79 + hos_05,areas/hos/hos_05.map/shape.bin,areas/hos/hos_05.map/hit.bin,areas/hos/hos.tex,backgrounds/hos.bg.png,Star Sanctuary 80 + hos_06,areas/hos/hos_06.map/shape.bin,areas/hos/hos_06.map/hit.bin,areas/hos/hos.tex,backgrounds/hos.bg.png,Merluvlee's House 81 + hos_10,areas/hos/hos_10.map/shape.bin,areas/hos/hos_10.map/hit.bin,areas/hos/hos.tex,backgrounds/hos.bg.png,Ending Descent Scene 82 + hos_20,areas/hos/hos_20.map/shape.bin,areas/hos/hos_20.map/hit.bin,areas/hos/hos.tex,backgrounds/hos.bg.png,Riding Star Ship Scene 83 + nok_01,areas/nok/nok_01.map/shape.bin,areas/nok/nok_01.map/hit.bin,areas/nok/nok.tex,backgrounds/nok.bg.png,Koopa Village 1 84 + nok_02,areas/nok/nok_02.map/shape.bin,areas/nok/nok_02.map/hit.bin,areas/nok/nok.tex,backgrounds/nok.bg.png,Koopa Village 2 85 + nok_03,areas/nok/nok_03.map/shape.bin,areas/nok/nok_03.map/hit.bin,areas/nok/nok.tex,backgrounds/nok.bg.png,Behind Koopa Village 86 + nok_04,areas/nok/nok_04.map/shape.bin,areas/nok/nok_04.map/hit.bin,areas/nok/nok.tex,backgrounds/nok.bg.png,Fuzzy Forest 87 + nok_11,areas/nok/nok_11.map/shape.bin,areas/nok/nok_11.map/hit.bin,areas/nok/nok.tex,backgrounds/nok.bg.png,Pleasant Path Entry 88 + nok_12,areas/nok/nok_12.map/shape.bin,areas/nok/nok_12.map/hit.bin,areas/nok/nok.tex,backgrounds/nok.bg.png,Pleasant Path Bridge 89 + nok_13,areas/nok/nok_13.map/shape.bin,areas/nok/nok_13.map/hit.bin,areas/nok/nok.tex,backgrounds/nok.bg.png,Pleasant Crossroads 90 + nok_14,areas/nok/nok_14.map/shape.bin,areas/nok/nok_14.map/hit.bin,areas/nok/nok.tex,backgrounds/nok.bg.png,Path to Fortress 1 91 + nok_15,areas/nok/nok_15.map/shape.bin,areas/nok/nok_15.map/hit.bin,areas/nok/nok.tex,backgrounds/nok.bg.png,Path to Fortress 2 92 + trd_00,areas/trd/trd_00.map/shape.bin,areas/trd/trd_00.map/hit.bin,areas/trd/trd.tex,backgrounds/nok.bg.png,Fortress Exterior 93 + trd_01,areas/trd/trd_01.map/shape.bin,areas/trd/trd_01.map/hit.bin,areas/trd/trd.tex,none,Left Tower 94 + trd_02,areas/trd/trd_02.map/shape.bin,areas/trd/trd_02.map/hit.bin,areas/trd/trd.tex,none,Left Stairway 95 + trd_03,areas/trd/trd_03.map/shape.bin,areas/trd/trd_03.map/hit.bin,areas/trd/trd.tex,none,Central Hall 96 + trd_04,areas/trd/trd_04.map/shape.bin,areas/trd/trd_04.map/hit.bin,areas/trd/trd.tex,none,Right Stairway 97 + trd_05,areas/trd/trd_05.map/shape.bin,areas/trd/trd_05.map/hit.bin,areas/trd/trd.tex,none,Right Tower 98 + trd_06,areas/trd/trd_06.map/shape.bin,areas/trd/trd_06.map/hit.bin,areas/trd/trd.tex,none,Jail 99 + trd_07,areas/trd/trd_07.map/shape.bin,areas/trd/trd_07.map/hit.bin,areas/trd/trd.tex,none,Dungeon Trap 100 + trd_08,areas/trd/trd_08.map/shape.bin,areas/trd/trd_08.map/hit.bin,areas/trd/trd.tex,none,Dungeon Fire Room 101 + trd_09,areas/trd/trd_09.map/shape.bin,areas/trd/trd_09.map/hit.bin,areas/trd/trd.tex,backgrounds/nok.bg.png,Battlement 102 + trd_10,areas/trd/trd_10.map/shape.bin,areas/trd/trd_10.map/hit.bin,areas/trd/trd.tex,none,Boss Battle Room 103 + iwa_00,areas/iwa/iwa_00.map/shape.bin,areas/iwa/iwa_00.map/hit.bin,areas/iwa/iwa.tex,backgrounds/iwa.bg.png,Mt Rugged 1 104 + iwa_01,areas/iwa/iwa_01.map/shape.bin,areas/iwa/iwa_01.map/hit.bin,areas/iwa/iwa.tex,backgrounds/iwa.bg.png,Mt Rugged 2 105 + iwa_02,areas/iwa/iwa_02.map/shape.bin,areas/iwa/iwa_02.map/hit.bin,areas/iwa/iwa.tex,backgrounds/iwa.bg.png,Mt Rugged 3 106 + iwa_03,areas/iwa/iwa_03.map/shape.bin,areas/iwa/iwa_03.map/hit.bin,areas/iwa/iwa.tex,backgrounds/iwa.bg.png,Mt Rugged 4 107 + iwa_04,areas/iwa/iwa_04.map/shape.bin,areas/iwa/iwa_04.map/hit.bin,areas/iwa/iwa.tex,backgrounds/iwa.bg.png,Suspension Bridge 108 + iwa_10,areas/iwa/iwa_10.map/shape.bin,areas/iwa/iwa_10.map/hit.bin,areas/iwa/iwa.tex,backgrounds/iwa.bg.png,Train Station 109 + iwa_11,areas/iwa/iwa_11.map/shape.bin,areas/iwa/iwa_11.map/hit.bin,areas/iwa/iwa.tex,backgrounds/iwa.bg.png,Train Ride Scene 110 + dro_01,areas/dro/dro_01.map/shape.bin,areas/dro/dro_01.map/hit.bin,areas/dro/dro.tex,backgrounds/sbk.bg.png,Outpost 1 111 + dro_02,areas/dro/dro_02.map/shape.bin,areas/dro/dro_02.map/hit.bin,areas/dro/dro.tex,backgrounds/sbk.bg.png,Outpost 2 112 + sbk_00,areas/sbk/sbk_00.map/shape.bin,areas/sbk/sbk_00.map/hit.bin,areas/sbk/sbk.tex,backgrounds/sbk.bg.png,N3W3 113 + sbk_01,areas/sbk/sbk_01.map/shape.bin,areas/sbk/sbk_01.map/hit.bin,areas/sbk/sbk.tex,backgrounds/sbk.bg.png,N3W2 114 + sbk_02,areas/sbk/sbk_02.map/shape.bin,areas/sbk/sbk_02.map/hit.bin,areas/sbk/sbk.tex,backgrounds/sbk.bg.png,N3W1 Ruins Entrance 115 + sbk_03,areas/sbk/sbk_03.map/shape.bin,areas/sbk/sbk_03.map/hit.bin,areas/sbk/sbk.tex,backgrounds/sbk.bg.png,N3 116 + sbk_04,areas/sbk/sbk_04.map/shape.bin,areas/sbk/sbk_04.map/hit.bin,areas/sbk/sbk.tex,backgrounds/sbk.bg.png,N3E1 117 + sbk_05,areas/sbk/sbk_05.map/shape.bin,areas/sbk/sbk_05.map/hit.bin,areas/sbk/sbk.tex,backgrounds/sbk.bg.png,N3E2 Pokey Army 118 + sbk_06,areas/sbk/sbk_06.map/shape.bin,areas/sbk/sbk_06.map/hit.bin,areas/sbk/sbk.tex,backgrounds/sbk.bg.png,N3E3 119 + sbk_10,areas/sbk/sbk_10.map/shape.bin,areas/sbk/sbk_10.map/hit.bin,areas/sbk/sbk.tex,backgrounds/sbk.bg.png,N2W3 120 + sbk_11,areas/sbk/sbk_11.map/shape.bin,areas/sbk/sbk_11.map/hit.bin,areas/sbk/sbk.tex,backgrounds/sbk.bg.png,N2W2 121 + sbk_12,areas/sbk/sbk_12.map/shape.bin,areas/sbk/sbk_12.map/hit.bin,areas/sbk/sbk.tex,backgrounds/sbk.bg.png,N2W1 122 + sbk_13,areas/sbk/sbk_13.map/shape.bin,areas/sbk/sbk_13.map/hit.bin,areas/sbk/sbk.tex,backgrounds/sbk.bg.png,N2 123 + sbk_14,areas/sbk/sbk_14.map/shape.bin,areas/sbk/sbk_14.map/hit.bin,areas/sbk/sbk.tex,backgrounds/sbk.bg.png,N2E1 (Tweester A) 124 + sbk_15,areas/sbk/sbk_15.map/shape.bin,areas/sbk/sbk_15.map/hit.bin,areas/sbk/sbk.tex,backgrounds/sbk.bg.png,N2E2 125 + sbk_16,areas/sbk/sbk_16.map/shape.bin,areas/sbk/sbk_16.map/hit.bin,areas/sbk/sbk.tex,backgrounds/sbk.bg.png,N2E3 126 + sbk_20,areas/sbk/sbk_20.map/shape.bin,areas/sbk/sbk_20.map/hit.bin,areas/sbk/sbk.tex,backgrounds/sbk.bg.png,N1W3 Special Block 127 + sbk_21,areas/sbk/sbk_21.map/shape.bin,areas/sbk/sbk_21.map/hit.bin,areas/sbk/sbk.tex,backgrounds/sbk.bg.png,N1W2 128 + sbk_22,areas/sbk/sbk_22.map/shape.bin,areas/sbk/sbk_22.map/hit.bin,areas/sbk/sbk.tex,backgrounds/sbk.bg.png,N1W1 129 + sbk_23,areas/sbk/sbk_23.map/shape.bin,areas/sbk/sbk_23.map/hit.bin,areas/sbk/sbk.tex,backgrounds/sbk.bg.png,N1 (Tweester B) 130 + sbk_24,areas/sbk/sbk_24.map/shape.bin,areas/sbk/sbk_24.map/hit.bin,areas/sbk/sbk.tex,backgrounds/sbk.bg.png,N1E1 Palm Trio 131 + sbk_25,areas/sbk/sbk_25.map/shape.bin,areas/sbk/sbk_25.map/hit.bin,areas/sbk/sbk.tex,backgrounds/sbk.bg.png,N1E2 132 + sbk_26,areas/sbk/sbk_26.map/shape.bin,areas/sbk/sbk_26.map/hit.bin,areas/sbk/sbk.tex,backgrounds/sbk.bg.png,N1E3 133 + sbk_30,areas/sbk/sbk_30.map/shape.bin,areas/sbk/sbk_30.map/hit.bin,areas/sbk/sbk.tex,backgrounds/sbk.bg.png,W3 Kolorado's Camp 134 + sbk_31,areas/sbk/sbk_31.map/shape.bin,areas/sbk/sbk_31.map/hit.bin,areas/sbk/sbk.tex,backgrounds/sbk.bg.png,W2 135 + sbk_32,areas/sbk/sbk_32.map/shape.bin,areas/sbk/sbk_32.map/hit.bin,areas/sbk/sbk.tex,backgrounds/sbk.bg.png,W1 136 + sbk_33,areas/sbk/sbk_33.map/shape.bin,areas/sbk/sbk_33.map/hit.bin,areas/sbk/sbk.tex,backgrounds/sbk.bg.png,Center (Tweester C) 137 + sbk_34,areas/sbk/sbk_34.map/shape.bin,areas/sbk/sbk_34.map/hit.bin,areas/sbk/sbk.tex,backgrounds/sbk.bg.png,E1 Nomadimouse 138 + sbk_35,areas/sbk/sbk_35.map/shape.bin,areas/sbk/sbk_35.map/hit.bin,areas/sbk/sbk.tex,backgrounds/sbk.bg.png,E2 139 + sbk_36,areas/sbk/sbk_36.map/shape.bin,areas/sbk/sbk_36.map/hit.bin,areas/sbk/sbk.tex,backgrounds/sbk.bg.png,E3 Outside Outpost 140 + sbk_40,areas/sbk/sbk_40.map/shape.bin,areas/sbk/sbk_40.map/hit.bin,areas/sbk/sbk.tex,backgrounds/sbk.bg.png,S1W3 141 + sbk_41,areas/sbk/sbk_41.map/shape.bin,areas/sbk/sbk_41.map/hit.bin,areas/sbk/sbk.tex,backgrounds/sbk.bg.png,S1W2 (Tweester D) 142 + sbk_42,areas/sbk/sbk_42.map/shape.bin,areas/sbk/sbk_42.map/hit.bin,areas/sbk/sbk.tex,backgrounds/sbk.bg.png,S1W1 143 + sbk_43,areas/sbk/sbk_43.map/shape.bin,areas/sbk/sbk_43.map/hit.bin,areas/sbk/sbk.tex,backgrounds/sbk.bg.png,S1 144 + sbk_44,areas/sbk/sbk_44.map/shape.bin,areas/sbk/sbk_44.map/hit.bin,areas/sbk/sbk.tex,backgrounds/sbk.bg.png,S1E1 145 + sbk_45,areas/sbk/sbk_45.map/shape.bin,areas/sbk/sbk_45.map/hit.bin,areas/sbk/sbk.tex,backgrounds/sbk.bg.png,S1E2 Small Bluffs 146 + sbk_46,areas/sbk/sbk_46.map/shape.bin,areas/sbk/sbk_46.map/hit.bin,areas/sbk/sbk.tex,backgrounds/sbk.bg.png,S1E3 North of Oasis 147 + sbk_50,areas/sbk/sbk_50.map/shape.bin,areas/sbk/sbk_50.map/hit.bin,areas/sbk/sbk.tex,backgrounds/sbk.bg.png,S2W3 148 + sbk_51,areas/sbk/sbk_51.map/shape.bin,areas/sbk/sbk_51.map/hit.bin,areas/sbk/sbk.tex,backgrounds/sbk.bg.png,S2W2 149 + sbk_52,areas/sbk/sbk_52.map/shape.bin,areas/sbk/sbk_52.map/hit.bin,areas/sbk/sbk.tex,backgrounds/sbk.bg.png,S2W1 150 + sbk_53,areas/sbk/sbk_53.map/shape.bin,areas/sbk/sbk_53.map/hit.bin,areas/sbk/sbk.tex,backgrounds/sbk.bg.png,S2 151 + sbk_54,areas/sbk/sbk_54.map/shape.bin,areas/sbk/sbk_54.map/hit.bin,areas/sbk/sbk.tex,backgrounds/sbk.bg.png,S2E1 Blue Cactus 152 + sbk_55,areas/sbk/sbk_55.map/shape.bin,areas/sbk/sbk_55.map/hit.bin,areas/sbk/sbk.tex,backgrounds/sbk.bg.png,S2E2 West of Oasis 153 + sbk_56,areas/sbk/sbk_56.map/shape.bin,areas/sbk/sbk_56.map/hit.bin,areas/sbk/sbk.tex,backgrounds/sbk.bg.png,S2E3 Oasis 154 + sbk_60,areas/sbk/sbk_60.map/shape.bin,areas/sbk/sbk_60.map/hit.bin,areas/sbk/sbk.tex,backgrounds/sbk.bg.png,S3W3 155 + sbk_61,areas/sbk/sbk_61.map/shape.bin,areas/sbk/sbk_61.map/hit.bin,areas/sbk/sbk.tex,backgrounds/sbk.bg.png,S3W2 Hidden AttackFX 156 + sbk_62,areas/sbk/sbk_62.map/shape.bin,areas/sbk/sbk_62.map/hit.bin,areas/sbk/sbk.tex,backgrounds/sbk.bg.png,S3W1 157 + sbk_63,areas/sbk/sbk_63.map/shape.bin,areas/sbk/sbk_63.map/hit.bin,areas/sbk/sbk.tex,backgrounds/sbk.bg.png,S3 158 + sbk_64,areas/sbk/sbk_64.map/shape.bin,areas/sbk/sbk_64.map/hit.bin,areas/sbk/sbk.tex,backgrounds/sbk.bg.png,S3E1 159 + sbk_65,areas/sbk/sbk_65.map/shape.bin,areas/sbk/sbk_65.map/hit.bin,areas/sbk/sbk.tex,backgrounds/sbk.bg.png,S3E2 160 + sbk_66,areas/sbk/sbk_66.map/shape.bin,areas/sbk/sbk_66.map/hit.bin,areas/sbk/sbk.tex,backgrounds/sbk.bg.png,S3E3 South of Oasis 161 + sbk_99,areas/sbk/sbk_99.map/shape.bin,areas/sbk/sbk_99.map/hit.bin,areas/sbk/sbk.tex,backgrounds/sbk.bg.png,Entrance 162 + isk_01,areas/isk/isk_01.map/shape.bin,areas/isk/isk_01.map/hit.bin,areas/isk/isk.tex,backgrounds/sbk3.bg.png,Entrance 163 + isk_02,areas/isk/isk_02.map/shape.bin,areas/isk/isk_02.map/hit.bin,areas/isk/isk.tex,backgrounds/sbk3.bg.png,Sarcophagus Hall 1 164 + isk_03,areas/isk/isk_03.map/shape.bin,areas/isk/isk_03.map/hit.bin,areas/isk/isk.tex,none,Sand Drainage Room 1 165 + isk_04,areas/isk/isk_04.map/shape.bin,areas/isk/isk_04.map/hit.bin,areas/isk/isk.tex,backgrounds/sbk3.bg.png,Descending Stairs 1 166 + isk_05,areas/isk/isk_05.map/shape.bin,areas/isk/isk_05.map/hit.bin,areas/isk/isk.tex,none,Pyramid Stone Room 167 + isk_06,areas/isk/isk_06.map/shape.bin,areas/isk/isk_06.map/hit.bin,areas/isk/isk.tex,none,Sand Drainage Room 2 168 + isk_07,areas/isk/isk_07.map/shape.bin,areas/isk/isk_07.map/hit.bin,areas/isk/isk.tex,none,Sarcophagus Hall 2 169 + isk_08,areas/isk/isk_08.map/shape.bin,areas/isk/isk_08.map/hit.bin,areas/isk/isk.tex,none,Descending Stairs 2 170 + isk_09,areas/isk/isk_09.map/shape.bin,areas/isk/isk_09.map/hit.bin,areas/isk/isk.tex,none,Super Hammer Room 171 + isk_10,areas/isk/isk_10.map/shape.bin,areas/isk/isk_10.map/hit.bin,areas/isk/isk.tex,none,Vertical Shaft 172 + isk_11,areas/isk/isk_11.map/shape.bin,areas/isk/isk_11.map/hit.bin,areas/isk/isk.tex,none,Stone Puzzle Room 173 + isk_12,areas/isk/isk_12.map/shape.bin,areas/isk/isk_12.map/hit.bin,areas/isk/isk.tex,none,Sand Drainage Room 3 174 + isk_13,areas/isk/isk_13.map/shape.bin,areas/isk/isk_13.map/hit.bin,areas/isk/isk.tex,none,Lunar Stone Room 175 + isk_14,areas/isk/isk_14.map/shape.bin,areas/isk/isk_14.map/hit.bin,areas/isk/isk.tex,none,Diamond Stone Room 176 + isk_16,areas/isk/isk_16.map/shape.bin,areas/isk/isk_16.map/hit.bin,areas/isk/isk.tex,none,Tutankoopa Room 177 + isk_18,areas/isk/isk_18.map/shape.bin,areas/isk/isk_18.map/hit.bin,areas/isk/isk.tex,none,Deep Tunnel 178 + isk_19,areas/isk/isk_19.map/shape.bin,areas/isk/isk_19.map/hit.bin,areas/isk/isk.tex,none,Boss Antechamber 179 + mim_01,areas/mim/mim_01.map/shape.bin,areas/mim/mim_01.map/hit.bin,areas/mim/mim.tex,backgrounds/obk.bg.png,Flower Sounds 180 + mim_02,areas/mim/mim_02.map/shape.bin,areas/mim/mim_02.map/hit.bin,areas/mim/mim.tex,backgrounds/obk.bg.png,Stump Eyes 181 + mim_03,areas/mim/mim_03.map/shape.bin,areas/mim/mim_03.map/hit.bin,areas/mim/mim.tex,backgrounds/obk.bg.png,Flowers (Oaklie) 182 + mim_04,areas/mim/mim_04.map/shape.bin,areas/mim/mim_04.map/hit.bin,areas/mim/mim.tex,backgrounds/obk.bg.png,Tree Face (Bub-ulb) 183 + mim_05,areas/mim/mim_05.map/shape.bin,areas/mim/mim_05.map/hit.bin,areas/mim/mim.tex,backgrounds/obk.bg.png,Mushrooms (Path Splits) 184 + mim_06,areas/mim/mim_06.map/shape.bin,areas/mim/mim_06.map/hit.bin,areas/mim/mim.tex,backgrounds/obk.bg.png,Flowers Vanish 185 + mim_07,areas/mim/mim_07.map/shape.bin,areas/mim/mim_07.map/hit.bin,areas/mim/mim.tex,backgrounds/obk.bg.png,Laughing Rock 186 + mim_08,areas/mim/mim_08.map/shape.bin,areas/mim/mim_08.map/hit.bin,areas/mim/mim.tex,backgrounds/obk.bg.png,Bee Hive (HP Plus) 187 + mim_09,areas/mim/mim_09.map/shape.bin,areas/mim/mim_09.map/hit.bin,areas/mim/mim.tex,backgrounds/obk.bg.png,Flowers Appear (FP Plus) 188 + mim_10,areas/mim/mim_10.map/shape.bin,areas/mim/mim_10.map/hit.bin,areas/mim/mim.tex,backgrounds/nok.bg.png,Exit to Toad Town 189 + mim_11,areas/mim/mim_11.map/shape.bin,areas/mim/mim_11.map/hit.bin,areas/mim/mim.tex,backgrounds/obk.bg.png,Outside Boo's Mansion 190 + mim_12,areas/mim/mim_12.map/shape.bin,areas/mim/mim_12.map/hit.bin,areas/mim/mim.tex,backgrounds/arn.bg.png,Exit to Gusty Gulch 191 + obk_01,areas/obk/obk_01.map/shape.bin,areas/obk/obk_01.map/hit.bin,areas/obk/obk.tex,none,Foyer 192 + obk_02,areas/obk/obk_02.map/shape.bin,areas/obk/obk_02.map/hit.bin,areas/obk/obk.tex,backgrounds/obk.bg.png,Basement Stairs 193 + obk_03,areas/obk/obk_03.map/shape.bin,areas/obk/obk_03.map/hit.bin,areas/obk/obk.tex,none,Basement 194 + obk_04,areas/obk/obk_04.map/shape.bin,areas/obk/obk_04.map/hit.bin,areas/obk/obk.tex,none,Super Boots Room 195 + obk_05,areas/obk/obk_05.map/shape.bin,areas/obk/obk_05.map/hit.bin,areas/obk/obk.tex,backgrounds/obk.bg.png,Pot Room 196 + obk_06,areas/obk/obk_06.map/shape.bin,areas/obk/obk_06.map/hit.bin,areas/obk/obk.tex,none,Library 197 + obk_07,areas/obk/obk_07.map/shape.bin,areas/obk/obk_07.map/hit.bin,areas/obk/obk.tex,backgrounds/obk.bg.png,Record Player Room 198 + obk_08,areas/obk/obk_08.map/shape.bin,areas/obk/obk_08.map/hit.bin,areas/obk/obk.tex,backgrounds/obk.bg.png,Record Room 199 + obk_09,areas/obk/obk_09.map/shape.bin,areas/obk/obk_09.map/hit.bin,areas/obk/obk.tex,none,Lady Bow's Room 200 + arn_02,areas/arn/arn_02.map/shape.bin,areas/arn/arn_02.map/hit.bin,areas/arn/arn.tex,backgrounds/arn.bg.png,Wasteland Ascent 1 201 + arn_03,areas/arn/arn_03.map/shape.bin,areas/arn/arn_03.map/hit.bin,areas/arn/arn.tex,backgrounds/arn.bg.png,Ghost Town 1 202 + arn_04,areas/arn/arn_04.map/shape.bin,areas/arn/arn_04.map/hit.bin,areas/arn/arn.tex,backgrounds/arn.bg.png,Wasteland Ascent 2 203 + arn_05,areas/arn/arn_05.map/shape.bin,areas/arn/arn_05.map/hit.bin,areas/arn/arn.tex,backgrounds/arn.bg.png,Ghost Town 2 204 + arn_07,areas/arn/arn_07.map/shape.bin,areas/arn/arn_07.map/hit.bin,areas/arn/arn.tex,backgrounds/arn.bg.png,Windmill Exterior 205 + arn_08,areas/arn/arn_08.map/shape.bin,areas/arn/arn_08.map/hit.bin,areas/arn/arn.tex,none,Windmill Interior 206 + arn_09,areas/arn/arn_09.map/shape.bin,areas/arn/arn_09.map/hit.bin,areas/arn/arn.tex,none,Windmill Tunnel Entry 207 + arn_10,areas/arn/arn_10.map/shape.bin,areas/arn/arn_10.map/hit.bin,areas/arn/arn.tex,none,Tunnel 1 208 + arn_11,areas/arn/arn_11.map/shape.bin,areas/arn/arn_11.map/hit.bin,areas/arn/arn.tex,none,Tubba's Heart Chamber 209 + arn_12,areas/arn/arn_12.map/shape.bin,areas/arn/arn_12.map/hit.bin,areas/arn/arn.tex,none,Tunnel 2 210 + arn_13,areas/arn/arn_13.map/shape.bin,areas/arn/arn_13.map/hit.bin,areas/arn/arn.tex,none,Tunnel 3 211 + arn_20,areas/arn/arn_20.map/shape.bin,areas/arn/arn_20.map/hit.bin,areas/arn/arn.tex,none,Tubba's Manor Exterior 212 + dgb_00,areas/arn/arn_20.map/shape.bin,areas/arn/arn_20.map/hit.bin,areas/arn/arn.tex,backgrounds/arn.bg.png,Escape Scene 213 + dgb_01,areas/dgb/dgb_01.map/shape.bin,areas/dgb/dgb_01.map/hit.bin,areas/dgb/dgb.tex,none,Great Hall 214 + dgb_02,areas/dgb/dgb_02.map/shape.bin,areas/dgb/dgb_02.map/hit.bin,areas/dgb/dgb.tex,none,West Hall (1F) 215 + dgb_03,areas/dgb/dgb_03.map/shape.bin,areas/dgb/dgb_03.map/hit.bin,areas/dgb/dgb.tex,none,Table/Clock Room (1/2F) 216 + dgb_04,areas/dgb/dgb_04.map/shape.bin,areas/dgb/dgb_04.map/hit.bin,areas/dgb/dgb.tex,none,Stairs to Basement 217 + dgb_05,areas/dgb/dgb_05.map/shape.bin,areas/dgb/dgb_05.map/hit.bin,areas/dgb/dgb.tex,none,Stairs Above Basement 218 + dgb_06,areas/dgb/dgb_06.map/shape.bin,areas/dgb/dgb_06.map/hit.bin,areas/dgb/dgb.tex,none,Basement 219 + dgb_07,areas/dgb/dgb_07.map/shape.bin,areas/dgb/dgb_07.map/hit.bin,areas/dgb/dgb.tex,none,Study (1F) 220 + dgb_08,areas/dgb/dgb_08.map/shape.bin,areas/dgb/dgb_08.map/hit.bin,areas/dgb/dgb.tex,none,East Hall (1/2F) 221 + dgb_09,areas/dgb/dgb_09.map/shape.bin,areas/dgb/dgb_09.map/hit.bin,areas/dgb/dgb.tex,none,West Hall (2F) 222 + dgb_10,areas/dgb/dgb_10.map/shape.bin,areas/dgb/dgb_10.map/hit.bin,areas/dgb/dgb.tex,none,Sealed Room (2F) 223 + dgb_11,areas/dgb/dgb_11.map/shape.bin,areas/dgb/dgb_11.map/hit.bin,areas/dgb/dgb.tex,none,Covered Tables Room (1F) 224 + dgb_12,areas/dgb/dgb_12.map/shape.bin,areas/dgb/dgb_12.map/hit.bin,areas/dgb/dgb.tex,none,Spike Trap Room (2F) 225 + dgb_13,areas/dgb/dgb_13.map/shape.bin,areas/dgb/dgb_13.map/hit.bin,areas/dgb/dgb.tex,none,Hidden Bedroom (2F) 226 + dgb_14,areas/dgb/dgb_14.map/shape.bin,areas/dgb/dgb_14.map/hit.bin,areas/dgb/dgb.tex,none,Stairs to Third Floor 227 + dgb_15,areas/dgb/dgb_15.map/shape.bin,areas/dgb/dgb_15.map/hit.bin,areas/dgb/dgb.tex,none,West Hall (3F) 228 + dgb_16,areas/dgb/dgb_16.map/shape.bin,areas/dgb/dgb_16.map/hit.bin,areas/dgb/dgb.tex,none,Sleeping Clubbas Room (3F) 229 + dgb_17,areas/dgb/dgb_17.map/shape.bin,areas/dgb/dgb_17.map/hit.bin,areas/dgb/dgb.tex,none,Save Room (3F) 230 + dgb_18,areas/dgb/dgb_18.map/shape.bin,areas/dgb/dgb_18.map/hit.bin,areas/dgb/dgb.tex,none,Master Bedroom (3F) 231 + omo_01,areas/omo/omo_01.map/shape.bin,areas/omo/omo_01.map/hit.bin,areas/omo/omo.tex,backgrounds/omo.bg.png,BLU Large Playroom 232 + omo_02,areas/omo/omo_02.map/shape.bin,areas/omo/omo_02.map/hit.bin,areas/omo/omo.tex,backgrounds/omo.bg.png,RED Boss Barricade 233 + omo_03,areas/omo/omo_03.map/shape.bin,areas/omo/omo_03.map/hit.bin,areas/omo/omo.tex,backgrounds/omo.bg.png,BLU Station 234 + omo_04,areas/omo/omo_04.map/shape.bin,areas/omo/omo_04.map/hit.bin,areas/omo/omo.tex,backgrounds/omo.bg.png,BLU Block City 235 + omo_05,areas/omo/omo_05.map/shape.bin,areas/omo/omo_05.map/hit.bin,areas/omo/omo.tex,backgrounds/omo.bg.png,PNK Gourmet Guy Crossing 236 + omo_06,areas/omo/omo_06.map/shape.bin,areas/omo/omo_06.map/hit.bin,areas/omo/omo.tex,backgrounds/omo.bg.png,PNK Station 237 + omo_07,areas/omo/omo_07.map/shape.bin,areas/omo/omo_07.map/hit.bin,areas/omo/omo.tex,backgrounds/omo.bg.png,PNK Playhouse 238 + omo_08,areas/omo/omo_08.map/shape.bin,areas/omo/omo_08.map/hit.bin,areas/omo/omo.tex,backgrounds/omo.bg.png,GRN Station 239 + omo_09,areas/omo/omo_09.map/shape.bin,areas/omo/omo_09.map/hit.bin,areas/omo/omo.tex,backgrounds/omo.bg.png,GRN Treadmills/Slot Machine 240 + omo_10,areas/omo/omo_10.map/shape.bin,areas/omo/omo_10.map/hit.bin,areas/omo/omo.tex,backgrounds/omo.bg.png,RED Station 241 + omo_11,areas/omo/omo_11.map/shape.bin,areas/omo/omo_11.map/hit.bin,areas/omo/omo.tex,backgrounds/omo.bg.png,RED Moving Platforms 242 + omo_12,areas/omo/omo_12.map/shape.bin,areas/omo/omo_12.map/hit.bin,areas/omo/omo.tex,none,RED Lantern Ghost 243 + omo_13,areas/omo/omo_13.map/shape.bin,areas/omo/omo_13.map/hit.bin,areas/omo/omo.tex,backgrounds/omo.bg.png,BLU Anti-Guy Hall 244 + omo_14,areas/omo/omo_14.map/shape.bin,areas/omo/omo_14.map/hit.bin,areas/omo/omo.tex,none,RED Boss Antechamber 245 + omo_15,areas/omo/omo_15.map/shape.bin,areas/omo/omo_15.map/hit.bin,areas/omo/omo.tex,backgrounds/omo.bg.png,RED General Guy Room 246 + omo_16,areas/omo/omo_16.map/shape.bin,areas/omo/omo_16.map/hit.bin,areas/omo/omo.tex,backgrounds/omo.bg.png,Riding the Train 247 + omo_17,areas/omo/omo_17.map/shape.bin,areas/omo/omo_17.map/hit.bin,areas/omo/omo.tex,backgrounds/omo.bg.png,PNK Tracks Hallway 248 + jan_00,areas/jan/jan_00.map/shape.bin,areas/jan/jan_00.map/hit.bin,areas/jan/jan.tex,backgrounds/yos.bg.png,Whale Cove 249 + jan_01,areas/jan/jan_01.map/shape.bin,areas/jan/jan_01.map/hit.bin,areas/jan/jan.tex,backgrounds/yos.bg.png,Beach 250 + jan_02,areas/jan/jan_02.map/shape.bin,areas/jan/jan_02.map/hit.bin,areas/jan/jan.tex,backgrounds/yos.bg.png,Village Cove 251 + jan_03,areas/jan/jan_03.map/shape.bin,areas/jan/jan_03.map/hit.bin,areas/jan/jan.tex,backgrounds/yos.bg.png,Village Buildings 252 + jan_04,areas/jan/jan_04.map/shape.bin,areas/jan/jan_04.map/hit.bin,areas/jan/jan.tex,backgrounds/yos.bg.png,Sushi Tree 253 + jan_05,areas/jan/jan_05.map/shape.bin,areas/jan/jan_05.map/hit.bin,areas/jan/jan.tex,backgrounds/yos.bg.png,SE Jungle (Quake Hammer) 254 + jan_06,areas/jan/jan_06.map/shape.bin,areas/jan/jan_06.map/hit.bin,areas/jan/jan.tex,backgrounds/jan.bg.png,NE Jungle (Raven Statue) 255 + jan_07,areas/jan/jan_07.map/shape.bin,areas/jan/jan_07.map/hit.bin,areas/jan/jan.tex,backgrounds/yos.bg.png,Small Jungle Ledge 256 + jan_08,areas/jan/jan_08.map/shape.bin,areas/jan/jan_08.map/hit.bin,areas/jan/jan.tex,backgrounds/yos.bg.png,SW Jungle (Super Block) 257 + jan_09,areas/jan/jan_09.map/shape.bin,areas/jan/jan_09.map/hit.bin,areas/jan/jan.tex,backgrounds/yos.bg.png,NW Jungle (Large Ledge) 258 + jan_10,areas/jan/jan_10.map/shape.bin,areas/jan/jan_10.map/hit.bin,areas/jan/jan.tex,backgrounds/yos.bg.png,Western Dead End 259 + jan_11,areas/jan/jan_11.map/shape.bin,areas/jan/jan_11.map/hit.bin,areas/jan/jan.tex,backgrounds/jan.bg.png,Root Cavern 260 + jan_12,areas/jan/jan_12.map/shape.bin,areas/jan/jan_12.map/hit.bin,areas/jan/jan.tex,backgrounds/jan.bg.png,Deep Jungle 1 261 + jan_13,areas/jan/jan_13.map/shape.bin,areas/jan/jan_13.map/hit.bin,areas/jan/jan.tex,backgrounds/jan.bg.png,Deep Jungle 2 (Block Puzzle) 262 + jan_14,areas/jan/jan_14.map/shape.bin,areas/jan/jan_14.map/hit.bin,areas/jan/jan.tex,backgrounds/jan.bg.png,Deep Jungle 3 263 + jan_15,areas/jan/jan_15.map/shape.bin,areas/jan/jan_15.map/hit.bin,areas/jan/jan.tex,backgrounds/jan.bg.png,Deep Jungle 4 (Ambush) 264 + jan_16,areas/jan/jan_16.map/shape.bin,areas/jan/jan_16.map/hit.bin,areas/jan/jan.tex,backgrounds/jan.bg.png,Base of Great Tree 265 + jan_17,areas/jan/jan_17.map/shape.bin,areas/jan/jan_17.map/hit.bin,areas/jan/jan.tex,backgrounds/jan.bg.png,Lower Great Tree Interior 266 + jan_18,areas/jan/jan_18.map/shape.bin,areas/jan/jan_18.map/hit.bin,areas/jan/jan.tex,backgrounds/yos.bg.png,Great Tree Vine Ascent 267 + jan_19,areas/jan/jan_19.map/shape.bin,areas/jan/jan_19.map/hit.bin,areas/jan/jan.tex,backgrounds/jan.bg.png,Upper Great Tree Interior 268 + jan_22,areas/jan/jan_22.map/shape.bin,areas/jan/jan_22.map/hit.bin,areas/jan/jan.tex,backgrounds/jan.bg.png,Path to the Volcano 269 + jan_23,areas/jan/jan_23.map/shape.bin,areas/jan/jan_23.map/hit.bin,areas/jan/jan.tex,backgrounds/yos.bg.png,Great Treetop Roost 270 + kzn_01,areas/kzn/kzn_01.map/shape.bin,areas/kzn/kzn_01.map/hit.bin,areas/kzn/kzn.tex,none,Volcano Entrance 271 + kzn_02,areas/kzn/kzn_02.map/shape.bin,areas/kzn/kzn_02.map/hit.bin,areas/kzn/kzn.tex,none,First Lava Lake 272 + kzn_03,areas/kzn/kzn_03.map/shape.bin,areas/kzn/kzn_03.map/hit.bin,areas/kzn/kzn.tex,none,Central Cavern 273 + kzn_04,areas/kzn/kzn_04.map/shape.bin,areas/kzn/kzn_04.map/hit.bin,areas/kzn/kzn.tex,none,Fire Bar Bridge 274 + kzn_05,areas/kzn/kzn_05.map/shape.bin,areas/kzn/kzn_05.map/hit.bin,areas/kzn/kzn.tex,none,Descent Toward Ultra Hammer 275 + kzn_06,areas/kzn/kzn_06.map/shape.bin,areas/kzn/kzn_06.map/hit.bin,areas/kzn/kzn.tex,none,Flowing Lava Puzzle 276 + kzn_07,areas/kzn/kzn_07.map/shape.bin,areas/kzn/kzn_07.map/hit.bin,areas/kzn/kzn.tex,none,Ultra Hammer Room 277 + kzn_08,areas/kzn/kzn_08.map/shape.bin,areas/kzn/kzn_08.map/hit.bin,areas/kzn/kzn.tex,none,Dizzy Stomp Room 278 + kzn_09,areas/kzn/kzn_09.map/shape.bin,areas/kzn/kzn_09.map/hit.bin,areas/kzn/kzn.tex,none,Zipline Cavern 279 + kzn_10,areas/kzn/kzn_10.map/shape.bin,areas/kzn/kzn_10.map/hit.bin,areas/kzn/kzn.tex,none,Descent Toward Boss 280 + kzn_11,areas/kzn/kzn_11.map/shape.bin,areas/kzn/kzn_11.map/hit.bin,areas/kzn/kzn.tex,none,Second Lava Lake 281 + kzn_17,areas/kzn/kzn_17.map/shape.bin,areas/kzn/kzn_17.map/hit.bin,areas/kzn/kzn.tex,none,Spike Roller Trap 282 + kzn_18,areas/kzn/kzn_18.map/shape.bin,areas/kzn/kzn_18.map/hit.bin,areas/kzn/kzn.tex,none,Boss Antechamber 283 + kzn_19,areas/kzn/kzn_19.map/shape.bin,areas/kzn/kzn_19.map/hit.bin,areas/kzn/kzn.tex,none,Boss Room 284 + kzn_20,areas/kzn/kzn_20.map/shape.bin,areas/kzn/kzn_20.map/hit.bin,areas/kzn/kzn.tex,none,Rising Lava 1 285 + kzn_22,areas/kzn/kzn_22.map/shape.bin,areas/kzn/kzn_22.map/hit.bin,areas/kzn/kzn.tex,none,Rising Lava 2 286 + kzn_23,areas/kzn/kzn_23.map/shape.bin,areas/kzn/kzn_23.map/hit.bin,areas/kzn/kzn.tex,backgrounds/yos.bg.png,Volcano Escape 287 + flo_00,areas/flo/flo_00.map/shape.bin,areas/flo/flo_00.map/hit.bin,areas/flo/flo.tex,backgrounds/fla.bg.png,Center 288 + flo_03,areas/flo/flo_03.map/shape.bin,areas/flo/flo_03.map/hit.bin,areas/flo/flo.tex,backgrounds/fla.bg.png,(East) Petunia's Field 289 + flo_07,areas/flo/flo_07.map/shape.bin,areas/flo/flo_07.map/hit.bin,areas/flo/flo.tex,backgrounds/fla.bg.png,(SW) Posie and Crystal Tree 290 + flo_08,areas/flo/flo_08.map/shape.bin,areas/flo/flo_08.map/hit.bin,areas/flo/flo.tex,backgrounds/fla.bg.png,(SE) Briar Platforming 291 + flo_09,areas/flo/flo_09.map/shape.bin,areas/flo/flo_09.map/hit.bin,areas/flo/flo.tex,backgrounds/fla.bg.png,(East) Triple Tree Path 292 + flo_10,areas/flo/flo_10.map/shape.bin,areas/flo/flo_10.map/hit.bin,areas/flo/flo.tex,backgrounds/fla.bg.png,(SE) Lily's Fountain 293 + flo_11,areas/flo/flo_11.map/shape.bin,areas/flo/flo_11.map/hit.bin,areas/flo/flo.tex,backgrounds/fla.bg.png,(West) Maze 294 + flo_12,areas/flo/flo_12.map/shape.bin,areas/flo/flo_12.map/hit.bin,areas/flo/flo.tex,backgrounds/fla.bg.png,(West) Rosie's Trellis 295 + flo_13,areas/flo/flo_13.map/shape.bin,areas/flo/flo_13.map/hit.bin,areas/flo/flo.tex,backgrounds/fla.bg.png,(NW) Lakilester 296 + flo_14,areas/flo/flo_14.map/shape.bin,areas/flo/flo_14.map/hit.bin,areas/flo/flo.tex,backgrounds/fla.bg.png,(NW) Bubble Flower 297 + flo_15,areas/flo/flo_15.map/shape.bin,areas/flo/flo_15.map/hit.bin,areas/flo/flo.tex,backgrounds/fla.bg.png,(NW) Sun Tower 298 + flo_16,areas/flo/flo_16.map/shape.bin,areas/flo/flo_16.map/hit.bin,areas/flo/flo.tex,backgrounds/fla.bg.png,(NE) Elevators 299 + flo_17,areas/flo/flo_17.map/shape.bin,areas/flo/flo_17.map/hit.bin,areas/flo/flo.tex,backgrounds/fla.bg.png,(NE) Fallen Logs 300 + flo_18,areas/flo/flo_18.map/shape.bin,areas/flo/flo_18.map/hit.bin,areas/flo/flo.tex,backgrounds/fla.bg.png,(NE) Puff Puff Machine 301 + flo_19,areas/flo/flo_19.map/shape.bin,areas/flo/flo_19.map/hit.bin,areas/flo/flo.tex,backgrounds/sra.bg.png,Cloudy Climb 302 + flo_21,areas/flo/flo_21.map/shape.bin,areas/flo/flo_21.map/hit.bin,areas/flo/flo.tex,backgrounds/sra.bg.png,Huff N Puff Room 303 + flo_22,areas/flo/flo_22.map/shape.bin,areas/flo/flo_22.map/hit.bin,areas/flo/flo.tex,backgrounds/fla.bg.png,(East) Old Well 304 + flo_23,areas/flo/flo_23.map/shape.bin,areas/flo/flo_23.map/hit.bin,areas/flo/flo.tex,backgrounds/fla.bg.png,(West) Path to Maze 305 + flo_24,areas/flo/flo_24.map/shape.bin,areas/flo/flo_24.map/hit.bin,areas/flo/flo.tex,backgrounds/fla.bg.png,(SE) Water Level Room 306 + flo_25,areas/flo/flo_25.map/shape.bin,areas/flo/flo_25.map/hit.bin,areas/flo/flo.tex,backgrounds/fla.bg.png,(SW) Path to Crystal Tree 307 + sam_01,areas/sam/sam_01.map/shape.bin,areas/sam/sam_01.map/hit.bin,areas/sam/sam.tex,backgrounds/yki.bg.png,Shiver City Mayor Area 308 + sam_02,areas/sam/sam_02.map/shape.bin,areas/sam/sam_02.map/hit.bin,areas/sam/sam.tex,backgrounds/yki.bg.png,Shiver City Center 309 + sam_03,areas/sam/sam_03.map/shape.bin,areas/sam/sam_03.map/hit.bin,areas/sam/sam.tex,backgrounds/yki.bg.png,Road to Shiver Snowfield 310 + sam_04,areas/sam/sam_04.map/shape.bin,areas/sam/sam_04.map/hit.bin,areas/sam/sam.tex,backgrounds/yki.bg.png,Shiver Snowfield 311 + sam_05,areas/sam/sam_05.map/shape.bin,areas/sam/sam_05.map/hit.bin,areas/sam/sam.tex,backgrounds/sam.bg.png,Path to Starborn Valley 312 + sam_06,areas/sam/sam_06.map/shape.bin,areas/sam/sam_06.map/hit.bin,areas/sam/sam.tex,backgrounds/sam.bg.png,Starborn Valley 313 + sam_07,areas/sam/sam_07.map/shape.bin,areas/sam/sam_07.map/hit.bin,areas/sam/sam.tex,backgrounds/yki.bg.png,Shiver Mountain Passage 314 + sam_08,areas/sam/sam_08.map/shape.bin,areas/sam/sam_08.map/hit.bin,areas/sam/sam.tex,backgrounds/yki.bg.png,Shiver Mountain Hills 315 + sam_09,areas/sam/sam_09.map/shape.bin,areas/sam/sam_09.map/hit.bin,areas/sam/sam.tex,backgrounds/yki.bg.png,Shiver Mountain Tunnel 316 + sam_10,areas/sam/sam_10.map/shape.bin,areas/sam/sam_10.map/hit.bin,areas/sam/sam.tex,backgrounds/yki.bg.png,Shiver Mountain Peaks 317 + sam_11,areas/sam/sam_11.map/shape.bin,areas/sam/sam_11.map/hit.bin,areas/sam/sam.tex,backgrounds/yki.bg.png,Shiver City Pond Area 318 + sam_12,areas/sam/sam_12.map/shape.bin,areas/sam/sam_12.map/hit.bin,areas/sam/sam.tex,backgrounds/yki.bg.png,Merlar's Sanctuary 319 + pra_01,areas/pra/pra_01.map/shape.bin,areas/pra/pra_01.map/hit.bin,areas/pra/pra.tex,backgrounds/yki.bg.png,Entrance 320 + pra_02,areas/pra/pra_02.map/shape.bin,areas/pra/pra_02.map/hit.bin,areas/pra/pra.tex,none,Entry Hall 321 + pra_03,areas/pra/pra_03.map/shape.bin,areas/pra/pra_03.map/hit.bin,areas/pra/pra.tex,none,Save Room 322 + pra_04,areas/pra/pra_04.map/shape.bin,areas/pra/pra_04.map/hit.bin,areas/pra/pra.tex,none,Reflected Save Room 323 + pra_05,areas/pra/pra_05.map/shape.bin,areas/pra/pra_05.map/hit.bin,areas/pra/pra.tex,none,Blue Key Room 324 + pra_06,areas/pra/pra_05.map/shape.bin,areas/pra/pra_05.map/hit.bin,areas/pra/pra.tex,none,Shooting Star Room 325 + pra_09,areas/pra/pra_09.map/shape.bin,areas/pra/pra_09.map/hit.bin,areas/pra/pra.tex,none,Red Key Hall 326 + pra_10,areas/pra/pra_10.map/shape.bin,areas/pra/pra_10.map/hit.bin,areas/pra/pra.tex,none,P-Down D-Up Hall 327 + pra_11,areas/pra/pra_11.map/shape.bin,areas/pra/pra_11.map/hit.bin,areas/pra/pra.tex,none,Red Key Room 328 + pra_12,areas/pra/pra_05.map/shape.bin,areas/pra/pra_05.map/hit.bin,areas/pra/pra.tex,none,P-Down D-Up Room 329 + pra_13,areas/pra/pra_13.map/shape.bin,areas/pra/pra_13.map/hit.bin,areas/pra/pra.tex,none,Blue Mirror Hall 1 330 + pra_14,areas/pra/pra_14.map/shape.bin,areas/pra/pra_14.map/hit.bin,areas/pra/pra.tex,none,Blue Mirror Hall 2 331 + pra_15,areas/pra/pra_15.map/shape.bin,areas/pra/pra_15.map/hit.bin,areas/pra/pra.tex,backgrounds/yki.bg.png,Star Piece Cave 332 + pra_16,areas/pra/pra_16.map/shape.bin,areas/pra/pra_16.map/hit.bin,areas/pra/pra.tex,none,Red Mirror Hall 333 + pra_18,areas/pra/pra_18.map/shape.bin,areas/pra/pra_18.map/hit.bin,areas/pra/pra.tex,none,Bridge Mirror Hall 334 + pra_19,areas/pra/pra_19.map/shape.bin,areas/pra/pra_19.map/hit.bin,areas/pra/pra.tex,none,Reflection Mimic Room 335 + pra_20,areas/pra/pra_20.map/shape.bin,areas/pra/pra_20.map/hit.bin,areas/pra/pra.tex,none,Mirrored Door Room 336 + pra_21,areas/pra/pra_21.map/shape.bin,areas/pra/pra_21.map/hit.bin,areas/pra/pra.tex,none,Huge Statue Room 337 + pra_22,areas/pra/pra_22.map/shape.bin,areas/pra/pra_22.map/hit.bin,areas/pra/pra.tex,none,Small Statue Room 338 + pra_27,areas/pra/pra_05.map/shape.bin,areas/pra/pra_05.map/hit.bin,areas/pra/pra.tex,none,Palace Key Room 339 + pra_28,areas/pra/pra_05.map/shape.bin,areas/pra/pra_05.map/hit.bin,areas/pra/pra.tex,none,P-Up D-Down Room 340 + pra_29,areas/pra/pra_29.map/shape.bin,areas/pra/pra_29.map/hit.bin,areas/pra/pra.tex,none,Hidden Bridge Room 341 + pra_31,areas/pra/pra_31.map/shape.bin,areas/pra/pra_31.map/hit.bin,areas/pra/pra.tex,none,Dino Puzzle Room 342 + pra_32,areas/pra/pra_32.map/shape.bin,areas/pra/pra_32.map/hit.bin,areas/pra/pra.tex,backgrounds/sam.bg.png,Crystal Summit 343 + pra_33,areas/pra/pra_33.map/shape.bin,areas/pra/pra_33.map/hit.bin,areas/pra/pra.tex,none,Turnstyle Room 344 + pra_34,areas/pra/pra_34.map/shape.bin,areas/pra/pra_34.map/hit.bin,areas/pra/pra.tex,none,Mirror Hole Room 345 + pra_35,areas/pra/pra_35.map/shape.bin,areas/pra/pra_35.map/hit.bin,areas/pra/pra.tex,none,Triple Dip Room 346 + pra_36,areas/pra/pra_10.map/shape.bin,areas/pra/pra_10.map/hit.bin,areas/pra/pra.tex,none,Palace Key Hall 347 + pra_37,areas/pra/pra_10.map/shape.bin,areas/pra/pra_10.map/hit.bin,areas/pra/pra.tex,none,P-Up D-Down Hall 348 + pra_38,areas/pra/pra_10.map/shape.bin,areas/pra/pra_10.map/hit.bin,areas/pra/pra.tex,none,Blue Key Hall 349 + pra_39,areas/pra/pra_10.map/shape.bin,areas/pra/pra_10.map/hit.bin,areas/pra/pra.tex,none,Shooting Star Hall 350 + pra_40,areas/pra/pra_40.map/shape.bin,areas/pra/pra_40.map/hit.bin,areas/pra/pra.tex,none,Boss Antechamber 351 + kpa_01,areas/kpa/kpa_01.map/shape.bin,areas/kpa/kpa_01.map/hit.bin,areas/kpa/kpa.tex,none,Dark Cave 1 352 + kpa_03,areas/kpa/kpa_03.map/shape.bin,areas/kpa/kpa_03.map/hit.bin,areas/kpa/kpa.tex,none,Dark Cave 2 353 + kpa_04,areas/kpa/kpa_04.map/shape.bin,areas/kpa/kpa_04.map/hit.bin,areas/kpa/kpa.tex,none,Cave Exit 354 + kpa_08,areas/kpa/kpa_08.map/shape.bin,areas/kpa/kpa_08.map/hit.bin,areas/kpa/kpa.tex,none,Castle Key Timing Puzzle 355 + kpa_09,areas/kpa/kpa_09.map/shape.bin,areas/kpa/kpa_09.map/hit.bin,areas/kpa/kpa.tex,none,Ultra Shroom Timing Puzzle 356 + kpa_10,areas/kpa/kpa_10.map/shape.bin,areas/kpa/kpa_10.map/hit.bin,areas/kpa/kpa.tex,none,Outside Lower Jail (No Lava) 357 + kpa_11,areas/kpa/kpa_11.map/shape.bin,areas/kpa/kpa_11.map/hit.bin,areas/kpa/kpa.tex,none,Outside Lower Jail (Lava) 358 + kpa_12,areas/kpa/kpa_12.map/shape.bin,areas/kpa/kpa_12.map/hit.bin,areas/kpa/kpa.tex,none,Lava Channel 1 359 + kpa_13,areas/kpa/kpa_13.map/shape.bin,areas/kpa/kpa_13.map/hit.bin,areas/kpa/kpa.tex,none,Lava Channel 2 360 + kpa_14,areas/kpa/kpa_14.map/shape.bin,areas/kpa/kpa_14.map/hit.bin,areas/kpa/kpa.tex,none,Lava Channel 3 361 + kpa_15,areas/kpa/kpa_15.map/shape.bin,areas/kpa/kpa_15.map/hit.bin,areas/kpa/kpa.tex,none,Lava Key Room 362 + kpa_16,areas/kpa/kpa_16.map/shape.bin,areas/kpa/kpa_16.map/hit.bin,areas/kpa/kpa.tex,none,Lava Control Room 363 + kpa_17,areas/kpa/kpa_17.map/shape.bin,areas/kpa/kpa_17.map/hit.bin,areas/kpa/kpa.tex,none,Lower Jail 364 + kpa_32,areas/kpa/kpa_32.map/shape.bin,areas/kpa/kpa_32.map/hit.bin,areas/kpa/kpa.tex,backgrounds/kpa.bg.png,Lower Grand Hall 365 + kpa_33,areas/kpa/kpa_33.map/shape.bin,areas/kpa/kpa_33.map/hit.bin,areas/kpa/kpa.tex,backgrounds/kpa.bg.png,Upper Grand Hall 366 + kpa_40,areas/kpa/kpa_40.map/shape.bin,areas/kpa/kpa_40.map/hit.bin,areas/kpa/kpa.tex,none,Maze Guide Room 367 + kpa_41,areas/kpa/kpa_41.map/shape.bin,areas/kpa/kpa_41.map/hit.bin,areas/kpa/kpa.tex,none,Maze Room 368 + kpa_50,areas/kpa/kpa_50.map/shape.bin,areas/kpa/kpa_50.map/hit.bin,areas/kpa/kpa.tex,none,Hall to Guard Door 1 369 + kpa_51,areas/kpa/kpa_50.map/shape.bin,areas/kpa/kpa_50.map/hit.bin,areas/kpa/kpa.tex,none,Hall to Water Puzzle 370 + kpa_52,areas/kpa/kpa_52.map/shape.bin,areas/kpa/kpa_52.map/hit.bin,areas/kpa/kpa.tex,none,Split Level Hall 371 + kpa_53,areas/kpa/kpa_50.map/shape.bin,areas/kpa/kpa_50.map/hit.bin,areas/kpa/kpa.tex,none,Fake Peach Hallway 372 + kpa_60,areas/kpa/kpa_60.map/shape.bin,areas/kpa/kpa_60.map/hit.bin,areas/kpa/kpa.tex,backgrounds/kpa.bg.png,Ship Enter/Exit Scenes 373 + kpa_61,areas/kpa/kpa_61.map/shape.bin,areas/kpa/kpa_61.map/hit.bin,areas/kpa/kpa.tex,backgrounds/kpa.bg.png,Battlement 374 + kpa_62,areas/kpa/kpa_62.map/shape.bin,areas/kpa/kpa_62.map/hit.bin,areas/kpa/kpa.tex,backgrounds/kpa.bg.png,Front Door Exterior 375 + kpa_63,areas/kpa/kpa_63.map/shape.bin,areas/kpa/kpa_63.map/hit.bin,areas/kpa/kpa.tex,none,Hanger 376 + kpa_70,areas/kpa/kpa_70.map/shape.bin,areas/kpa/kpa_70.map/hit.bin,areas/kpa/kpa.tex,none,Entry Lava Hall 377 + kpa_81,areas/kpa/kpa_80.map/shape.bin,areas/kpa/kpa_80.map/hit.bin,areas/kpa/kpa.tex,none,Guard Door 1 378 + kpa_82,areas/kpa/kpa_80.map/shape.bin,areas/kpa/kpa_80.map/hit.bin,areas/kpa/kpa.tex,none,Guard Door 2 379 + kpa_83,areas/kpa/kpa_80.map/shape.bin,areas/kpa/kpa_80.map/hit.bin,areas/kpa/kpa.tex,none,Guard Door 3 380 + kpa_90,areas/kpa/kpa_90.map/shape.bin,areas/kpa/kpa_90.map/hit.bin,areas/kpa/kpa.tex,none,Stairs to East Upper Jail 381 + kpa_91,areas/kpa/kpa_91.map/shape.bin,areas/kpa/kpa_91.map/hit.bin,areas/kpa/kpa.tex,none,East Upper Jail 382 + kpa_94,areas/kpa/kpa_94.map/shape.bin,areas/kpa/kpa_94.map/hit.bin,areas/kpa/kpa.tex,none,Stairs to West Upper Jail 383 + kpa_95,areas/kpa/kpa_95.map/shape.bin,areas/kpa/kpa_95.map/hit.bin,areas/kpa/kpa.tex,none,West Upper Jail 384 + kpa_96,areas/kpa/kpa_96.map/shape.bin,areas/kpa/kpa_96.map/hit.bin,areas/kpa/kpa.tex,none,Item Shop 385 + kpa_100,areas/kpa/kpa_117.map/shape.bin,areas/kpa/kpa_117.map/hit.bin,areas/kpa/kpa.tex,none,Castle Key Room 386 + kpa_101,areas/kpa/kpa_119.map/shape.bin,areas/kpa/kpa_119.map/hit.bin,areas/kpa/kpa.tex,none,Ultra Shroom Room 387 + kpa_102,areas/kpa/kpa_102.map/shape.bin,areas/kpa/kpa_102.map/hit.bin,areas/kpa/kpa.tex,none,Blue Fire Bridge 388 + kpa_111,areas/kpa/kpa_111.map/shape.bin,areas/kpa/kpa_111.map/hit.bin,areas/kpa/kpa.tex,none,Room with Hidden Door 1 389 + kpa_112,areas/kpa/kpa_112.map/shape.bin,areas/kpa/kpa_112.map/hit.bin,areas/kpa/kpa.tex,none,Hidden Passage 1 390 + kpa_113,areas/kpa/kpa_113.map/shape.bin,areas/kpa/kpa_113.map/hit.bin,areas/kpa/kpa.tex,none,Room with Hidden Door 2 391 + kpa_114,areas/kpa/kpa_112.map/shape.bin,areas/kpa/kpa_112.map/hit.bin,areas/kpa/kpa.tex,none,Hidden Passage 2 392 + kpa_115,areas/kpa/kpa_115.map/shape.bin,areas/kpa/kpa_115.map/hit.bin,areas/kpa/kpa.tex,none,Room with Hidden Door 3 393 + kpa_116,areas/kpa/kpa_116.map/shape.bin,areas/kpa/kpa_116.map/hit.bin,areas/kpa/kpa.tex,none,Dead End Passage 394 + kpa_117,areas/kpa/kpa_117.map/shape.bin,areas/kpa/kpa_117.map/hit.bin,areas/kpa/kpa.tex,none,Dead End Room 395 + kpa_118,areas/kpa/kpa_118.map/shape.bin,areas/kpa/kpa_118.map/hit.bin,areas/kpa/kpa.tex,none,Hidden Passage 3 396 + kpa_119,areas/kpa/kpa_119.map/shape.bin,areas/kpa/kpa_119.map/hit.bin,areas/kpa/kpa.tex,none,Hidden Key Room 397 + kpa_121,areas/kpa/kpa_121.map/shape.bin,areas/kpa/kpa_121.map/hit.bin,areas/kpa/kpa.tex,none,Exit to Peach's Castle 398 + kpa_130,areas/kpa/kpa_130.map/shape.bin,areas/kpa/kpa_130.map/hit.bin,areas/kpa/kpa.tex,none,Bill Blaster Hall 399 + kpa_133,areas/kpa/kpa_133.map/shape.bin,areas/kpa/kpa_133.map/hit.bin,areas/kpa/kpa.tex,none,Left Water Puzzle 400 + kpa_134,areas/kpa/kpa_134.map/shape.bin,areas/kpa/kpa_134.map/hit.bin,areas/kpa/kpa.tex,none,Right Water Puzzle 401 + kpa_80,areas/kpa/kpa_80.map/shape.bin,areas/kpa/kpa_80.map/hit.bin,areas/kpa/kpa.tex,none,Guard Door Geometry 402 + osr_00,areas/osr/osr_00.map/shape.bin,areas/osr/osr_00.map/hit.bin,areas/osr/osr.tex,backgrounds/nok.bg.png,Intro Castle Grounds 403 + osr_01,areas/osr/osr_01.map/shape.bin,areas/osr/osr_01.map/hit.bin,areas/osr/osr.tex,backgrounds/nok.bg.png,Ruined Castle Grounds 404 + osr_02,areas/osr/osr_02.map/shape.bin,areas/osr/osr_02.map/hit.bin,areas/osr/osr.tex,backgrounds/kpa.bg.png,Hijacked Castle Entrance 405 + osr_03,areas/osr/osr_03.map/shape.bin,areas/osr/osr_03.map/hit.bin,areas/osr/osr.tex,backgrounds/kpa.bg.png,Outside Hijacked Castle 406 + osr_04,areas/osr/osr_03.map/shape.bin,areas/osr/osr_03.map/hit.bin,areas/osr/osr.tex,backgrounds/nok.bg.png,Castle Hijacking Scene 407 + end_00,areas/end/end_00.map/shape.bin,areas/end/end_00.map/hit.bin,areas/end/end.tex,none,Parade (Day) 408 + end_01,areas/end/end_01.map/shape.bin,areas/end/end_01.map/hit.bin,areas/end/end.tex,none,Parade (Night) 409 + mgm_00,areas/mgm/mgm_00.map/shape.bin,areas/mgm/mgm_00.map/hit.bin,areas/mgm/mgm.tex,none,Playroom Lobby 410 + mgm_01,areas/mgm/mgm_01.map/shape.bin,areas/mgm/mgm_01.map/hit.bin,areas/mgm/mgm.tex,none,Jump Attack Minigame 411 + mgm_02,areas/mgm/mgm_02.map/shape.bin,areas/mgm/mgm_02.map/hit.bin,areas/mgm/mgm.tex,none,Smash Attack Minigame 412 + mgm_03,areas/mgm/mgm_03.map/shape.bin,areas/mgm/mgm_03.map/hit.bin,areas/mgm/mgm.tex,none,Large Debug Room 413 + gv_01,areas/gv_/gv_01.map/shape.bin,areas/gv_/gv_01.map/hit.bin,areas/gv_/gv_.tex,none,Game Over Screen 414 + tst_01,areas/tst/tst_01.map/shape.bin,areas/tst/tst_01.map/hit.bin,areas/tst/tst.tex,backgrounds/nok.bg.png,Jump Width Test 415 + tst_02,areas/tst/tst_02.map/shape.bin,areas/tst/tst_02.map/hit.bin,areas/tst/tst.tex,backgrounds/nok.bg.png,Jump Height Test 416 + tst_03,areas/tst/tst_03.map/shape.bin,areas/tst/tst_03.map/hit.bin,areas/tst/tst.tex,backgrounds/nok.bg.png,Entity Test 417 + tst_04,areas/tst/tst_04.map/shape.bin,areas/tst/tst_04.map/hit.bin,areas/tst/tst.tex,backgrounds/nok.bg.png,Moving Platforms Test 418 + tst_10,areas/tst/tst_10.map/shape.bin,areas/tst/tst_10.map/hit.bin,areas/tst/tst.tex,backgrounds/nok.bg.png,Entry and Camera Test 419 + tst_11,areas/tst/tst_11.map/shape.bin,areas/tst/tst_11.map/hit.bin,areas/tst/tst.tex,backgrounds/nok.bg.png,Reflection Test 420 + tst_12,areas/tst/tst_12.map/shape.bin,areas/tst/tst_12.map/hit.bin,areas/tst/tst.tex,backgrounds/nok.bg.png,Flower Fields Test 421 + tst_13,areas/tst/tst_13.map/shape.bin,areas/tst/tst_13.map/hit.bin,areas/tst/tst.tex,backgrounds/nok.bg.png,Partners and Shockwave 422 + tst_20,areas/tst/tst_20.map/shape.bin,areas/tst/tst_20.map/hit.bin,areas/tst/tst.tex,backgrounds/nok.bg.png,Pipes Gallery
+90 -90
src/main/resources/extract/stages.csv
··· 1 - arn_bt01,arn_bt01_shape,arn_bt01_hit,arn_tex,arn_bg,Wasteland 1 2 - arn_bt02,arn_bt02_shape,arn_bt02_hit,arn_tex,arn_bg,Outside Windmill 3 - arn_bt03,arn_bt03_shape,arn_bt03_hit,arn_tex,arn_bg,Wasteland 2 4 - arn_bt04,arn_bt04_shape,arn_bt04_hit,arn_tex,none,Windmill Tunnel Exit 5 - arn_bt05,arn_bt05_shape,arn_bt05_hit,arn_tex,none,Windmill Tunnel 6 - arn_bt06,arn_bt06_shape,arn_bt06_hit,arn_tex,none,Tubba's Heart Arena 7 - dgb_bt01,dgb_bt01_shape,dgb_bt01_hit,dgb_tex,none,Hall with Door 8 - dgb_bt02,dgb_bt02_shape,dgb_bt02_hit,dgb_tex,none,Dining Hall 9 - dgb_bt03,dgb_bt03_shape,dgb_bt03_hit,dgb_tex,none,Hall with Windows 10 - dgb_bt04,dgb_bt04_shape,dgb_bt04_hit,dgb_tex,none,Basement 11 - dgb_bt05,dgb_bt05_shape,dgb_bt05_hit,dgb_tex,none,Master Bedroom 12 - flo_bt01,flo_bt01_shape,flo_bt01_hit,flo_tex,fla_bg,Wide Open Path 13 - flo_bt02,flo_bt02_shape,flo_bt02_hit,flo_tex,fla_bg,Walls and Tree 14 - flo_bt03,flo_bt03_shape,flo_bt03_hit,flo_tex,fla_bg,Puff Puff Machine 15 - flo_bt04,flo_bt04_shape,flo_bt04_hit,flo_tex,sra_bg,Clouds 16 - flo_bt05,flo_bt05_shape,flo_bt05_hit,flo_tex,fla_bg,Hedge Maze 17 - flo_bt06,flo_bt06_shape,flo_bt06_hit,flo_tex,fla_bg,Empty Wasteland 18 - hos_bt01,hos_bt01_shape,hos_bt01_hit,hos_tex,hos_bg,Star Way 19 - hos_bt02,hos_bt02_shape,hos_bt02_hit,hos_tex,nok_bg,Shooting Star Path 20 - isk_bt01,isk_bt01_shape,isk_bt01_hit,isk_tex,none,Tutankoopa Arena 21 - isk_bt02,isk_bt02_shape,isk_bt02_hit,isk_tex,none,Generic Ruins Hall 22 - isk_bt03,isk_bt03_shape,isk_bt03_hit,isk_tex,none,Surface Level Hall 23 - isk_bt04,isk_bt04_shape,isk_bt04_hit,isk_tex,none,Statues Room 24 - isk_bt05,isk_bt05_shape,isk_bt05_hit,isk_tex,none,Dark Hall 25 - isk_bt06,isk_bt06_shape,isk_bt06_hit,isk_tex,none,Right Platform Room 26 - isk_bt07,isk_bt07_shape,isk_bt07_hit,isk_tex,none,Left Platform Room 27 - isk_bt08,isk_bt08_shape,isk_bt08_hit,isk_tex,none,Dark Sarcophagus Hall 28 - iwa_bt01,iwa_bt01_shape,iwa_bt01_hit,iwa_tex,iwa_bg,Generic Mt Rugged 29 - iwa_bt02,iwa_bt02_shape,iwa_bt02_hit,iwa_tex,iwa_bg,Buzzar Arena 30 - jan_bt00,jan_bt00_shape,jan_bt00_hit,jan_tex,yos_bg,Beach 31 - jan_bt01,jan_bt01_shape,jan_bt01_hit,jan_tex,yos_bg,Jungle Riverside 32 - jan_bt02,jan_bt02_shape,jan_bt02_hit,jan_tex,yos_bg,Jungle Cliffside 33 - jan_bt03,jan_bt03_shape,jan_bt03_hit,jan_tex,jan_bg,Jungle Trees 34 - jan_bt04,jan_bt04_shape,jan_bt04_hit,jan_tex,jan_bg,Deep Jungle 35 - kgr_bt01,kgr_bt01_shape,kgr_bt01_hit,kgr_tex,none,Inside the Whale 36 - kkj_bt01,kkj_bt01_shape,kkj_bt01_hit,kkj_tex,kpa_bg,Hallway Bowser Arena 37 - kkj_bt02,kkj_bt02_shape,kkj_bt02_hit,kkj_tex,kpa_bg,Final Bowser Arena 38 - kmr_bt03,kmr_bt03_shape,kmr_bt03_hit,kmr_tex,kmr_bg,Goomba Bros Arena 39 - kmr_bt04,kmr_bt04_shape,kmr_bt04_hit,kmr_tex,kmr_bg,Forest Path 40 - kmr_bt05,kmr_bt05_shape,kmr_bt05_hit,kmr_tex,kmr_bg,Rocky Forest 41 - kmr_bt06,kmr_bt06_shape,kmr_bt06_hit,kmr_tex,kmr_bg,Goomba King Arena 42 - kpa_bt01,kpa_bt01_shape,kpa_bt01_hit,kpa_tex,none,Blocks Hall 43 - kpa_bt02,kpa_bt02_shape,kpa_bt02_hit,kpa_tex,none,Lava Chamber 44 - kpa_bt03,kpa_bt03_shape,kpa_bt03_hit,kpa_tex,none,Cavern 45 - kpa_bt04,kpa_bt04_shape,kpa_bt04_hit,kpa_tex,none,Blue Fire Hall 46 - kpa_bt05,kpa_bt05_shape,kpa_bt05_hit,kpa_tex,none,Water Levels Chamber 47 - kpa_bt07,kpa_bt07_shape,kpa_bt07_hit,kpa_tex,none,Castle Roof 48 - kpa_bt08,kpa_bt08_shape,kpa_bt08_hit,kpa_tex,none,Spiral Chamber 49 - kpa_bt09,kpa_bt09_shape,kpa_bt09_hit,kpa_tex,none,Dark Bridge 50 - kpa_bt11,kpa_bt11_shape,kpa_bt11_hit,kpa_tex,none,Pillar Hall 51 - kpa_bt13,kpa_bt13_shape,kpa_bt13_hit,kpa_tex,none,Bowser Door Room 52 - kpa_bt14,kpa_bt14_shape,kpa_bt14_hit,kpa_tex,none,Jail Cell 53 - kzn_bt01,kzn_bt01_shape,kzn_bt01_hit,kzn_tex,none,Smoky Cave 54 - kzn_bt02,kzn_bt02_shape,kzn_bt02_hit,kzn_tex,none,Lava Chamber 55 - kzn_bt04,kzn_bt04_shape,kzn_bt04_hit,kzn_tex,none,Lava Hall 56 - kzn_bt05,kzn_bt05_shape,kzn_bt05_hit,kzn_tex,none,Lava Piranha Arena 57 - mac_bt01,mac_bt01_shape,mac_bt01_hit,mac_tex,nok_bg,Docks 58 - mac_bt02,mac_bt02_shape,mac_bt02_hit,mac_tex,none,Dojo 59 - mim_bt01,mim_bt01_shape,mim_bt01_hit,mim_tex,none,Haunted Forest 60 - nok_bt01,nok_bt01_shape,nok_bt01_hit,nok_tex,nok_bg,Forest Ledges 61 - nok_bt02,nok_bt02_shape,nok_bt02_hit,nok_tex,nok_bg,Forest Bushes 62 - nok_bt03,nok_bt03_shape,nok_bt03_hit,nok_tex,nok_bg,Bridge 63 - nok_bt04,nok_bt04_shape,nok_bt04_hit,nok_tex,nok_bg,Forest Path 64 - omo_bt01,omo_bt01_shape,omo_bt01_hit,omo_tex,omo_bg,Generic Toybox 65 - omo_bt02,omo_bt02_shape,omo_bt02_hit,omo_tex,omo_bg,Block Fortress 66 - omo_bt03,omo_bt03_shape,omo_bt03_hit,omo_tex,omo_bg,Dark Room 67 - omo_bt04,omo_bt04_shape,omo_bt04_hit,omo_tex,omo_bg,Slot Machine 68 - omo_bt05,omo_bt05_shape,omo_bt05_hit,omo_tex,omo_bg,Train Tracks 69 - omo_bt06,omo_bt06_shape,omo_bt06_hit,omo_tex,omo_bg,Block Towers 70 - omo_bt07,omo_bt07_shape,omo_bt07_hit,omo_tex,none,General Guy Arena 71 - pra_bt01,pra_bt01_shape,pra_bt01_hit,pra_tex,none,Generic Hall 72 - pra_bt02,pra_bt02_shape,pra_bt02_hit,pra_tex,none,Mirror Hall 73 - pra_bt03,pra_bt03_shape,pra_bt03_hit,pra_tex,none,Mirror Bridge 74 - pra_bt04,pra_bt04_shape,pra_bt04_hit,pra_tex,none,Mirror Room 75 - sam_bt01,sam_bt01_shape,sam_bt01_hit,sam_tex,yki_bg,Snowfield 76 - sam_bt02,sam_bt02_shape,sam_bt02_hit,sam_tex,yki_bg,Mountain Caves 77 - sam_bt03,sam_bt03_shape,sam_bt03_hit,sam_tex,sam_bg,Mountain Cliffs 78 - sam_bt04,sam_bt04_shape,sam_bt04_hit,sam_tex,sam_bg,Crystal King Arena 79 - sbk_bt02,sbk_bt02_shape,sbk_bt02_hit,sbk_tex,sbk_bg,Desert Dunes 80 - tik_bt01,tik_bt01_shape,tik_bt01_hit,tik_tex,none,Generic Tunnel 81 - tik_bt02,tik_bt02_shape,tik_bt02_hit,tik_tex,none,Ledges Tunnel 82 - tik_bt03,tik_bt03_shape,tik_bt03_hit,tik_tex,none,Pipe Bridge 83 - tik_bt04,tik_bt04_shape,tik_bt04_hit,tik_tex,none,Sewer Stream 84 - tik_bt05,tik_bt05_shape,tik_bt05_hit,tik_tex,none,Waterfalls 85 - trd_bt00,trd_bt00_shape,trd_bt00_hit,trd_tex,none,Koopa Bros Arena 86 - trd_bt01,trd_bt01_shape,trd_bt01_hit,trd_tex,none,Cell Block 87 - trd_bt02,trd_bt02_shape,trd_bt02_hit,trd_tex,none,Water Passage 88 - trd_bt03,trd_bt03_shape,trd_bt03_hit,trd_tex,nok_bg,Battlement 89 - trd_bt04,trd_bt04_shape,trd_bt04_hit,trd_tex,none,Spiral Stairwell 90 - trd_bt05,trd_bt05_shape,trd_bt05_hit,trd_tex,nok_bg,Basement 1 + arn_bt01,areas/arn/arn_bt01.stage/shape.bin,areas/arn/arn_bt01.stage/hit.bin,areas/arn/arn.tex,backgrounds/arn.bg.png,Wasteland 1 2 + arn_bt02,areas/arn/arn_bt02.stage/shape.bin,areas/arn/arn_bt02.stage/hit.bin,areas/arn/arn.tex,backgrounds/arn.bg.png,Outside Windmill 3 + arn_bt03,areas/arn/arn_bt03.stage/shape.bin,areas/arn/arn_bt03.stage/hit.bin,areas/arn/arn.tex,backgrounds/arn.bg.png,Wasteland 2 4 + arn_bt04,areas/arn/arn_bt04.stage/shape.bin,areas/arn/arn_bt04.stage/hit.bin,areas/arn/arn.tex,none,Windmill Tunnel Exit 5 + arn_bt05,areas/arn/arn_bt05.stage/shape.bin,areas/arn/arn_bt05.stage/hit.bin,areas/arn/arn.tex,none,Windmill Tunnel 6 + arn_bt06,areas/arn/arn_bt06.stage/shape.bin,areas/arn/arn_bt06.stage/hit.bin,areas/arn/arn.tex,none,Tubba's Heart Arena 7 + dgb_bt01,areas/dgb/dgb_bt01.stage/shape.bin,areas/dgb/dgb_bt01.stage/hit.bin,areas/dgb/dgb.tex,none,Hall with Door 8 + dgb_bt02,areas/dgb/dgb_bt02.stage/shape.bin,areas/dgb/dgb_bt02.stage/hit.bin,areas/dgb/dgb.tex,none,Dining Hall 9 + dgb_bt03,areas/dgb/dgb_bt03.stage/shape.bin,areas/dgb/dgb_bt03.stage/hit.bin,areas/dgb/dgb.tex,none,Hall with Windows 10 + dgb_bt04,areas/dgb/dgb_bt04.stage/shape.bin,areas/dgb/dgb_bt04.stage/hit.bin,areas/dgb/dgb.tex,none,Basement 11 + dgb_bt05,areas/dgb/dgb_bt05.stage/shape.bin,areas/dgb/dgb_bt05.stage/hit.bin,areas/dgb/dgb.tex,none,Master Bedroom 12 + flo_bt01,areas/flo/flo_bt01.stage/shape.bin,areas/flo/flo_bt01.stage/hit.bin,areas/flo/flo.tex,backgrounds/fla.bg.png,Wide Open Path 13 + flo_bt02,areas/flo/flo_bt02.stage/shape.bin,areas/flo/flo_bt02.stage/hit.bin,areas/flo/flo.tex,backgrounds/fla.bg.png,Walls and Tree 14 + flo_bt03,areas/flo/flo_bt03.stage/shape.bin,areas/flo/flo_bt03.stage/hit.bin,areas/flo/flo.tex,backgrounds/fla.bg.png,Puff Puff Machine 15 + flo_bt04,areas/flo/flo_bt04.stage/shape.bin,areas/flo/flo_bt04.stage/hit.bin,areas/flo/flo.tex,backgrounds/sra.bg.png,Clouds 16 + flo_bt05,areas/flo/flo_bt05.stage/shape.bin,areas/flo/flo_bt05.stage/hit.bin,areas/flo/flo.tex,backgrounds/fla.bg.png,Hedge Maze 17 + flo_bt06,areas/flo/flo_bt06.stage/shape.bin,areas/flo/flo_bt06.stage/hit.bin,areas/flo/flo.tex,backgrounds/fla.bg.png,Empty Wasteland 18 + hos_bt01,areas/hos/hos_bt01.stage/shape.bin,areas/hos/hos_bt01.stage/hit.bin,areas/hos/hos.tex,backgrounds/hos.bg.png,Star Way 19 + hos_bt02,areas/hos/hos_bt02.stage/shape.bin,areas/hos/hos_bt02.stage/hit.bin,areas/hos/hos.tex,backgrounds/nok.bg.png,Shooting Star Path 20 + isk_bt01,areas/isk/isk_bt01.stage/shape.bin,areas/isk/isk_bt01.stage/hit.bin,areas/isk/isk.tex,none,Tutankoopa Arena 21 + isk_bt02,areas/isk/isk_bt02.stage/shape.bin,areas/isk/isk_bt02.stage/hit.bin,areas/isk/isk.tex,none,Generic Ruins Hall 22 + isk_bt03,areas/isk/isk_bt03.stage/shape.bin,areas/isk/isk_bt03.stage/hit.bin,areas/isk/isk.tex,none,Surface Level Hall 23 + isk_bt04,areas/isk/isk_bt04.stage/shape.bin,areas/isk/isk_bt04.stage/hit.bin,areas/isk/isk.tex,none,Statues Room 24 + isk_bt05,areas/isk/isk_bt05.stage/shape.bin,areas/isk/isk_bt05.stage/hit.bin,areas/isk/isk.tex,none,Dark Hall 25 + isk_bt06,areas/isk/isk_bt06.stage/shape.bin,areas/isk/isk_bt06.stage/hit.bin,areas/isk/isk.tex,none,Right Platform Room 26 + isk_bt07,areas/isk/isk_bt07.stage/shape.bin,areas/isk/isk_bt07.stage/hit.bin,areas/isk/isk.tex,none,Left Platform Room 27 + isk_bt08,areas/isk/isk_bt08.stage/shape.bin,areas/isk/isk_bt08.stage/hit.bin,areas/isk/isk.tex,none,Dark Sarcophagus Hall 28 + iwa_bt01,areas/iwa/iwa_bt01.stage/shape.bin,areas/iwa/iwa_bt01.stage/hit.bin,areas/iwa/iwa.tex,backgrounds/iwa.bg.png,Generic Mt Rugged 29 + iwa_bt02,areas/iwa/iwa_bt02.stage/shape.bin,areas/iwa/iwa_bt02.stage/hit.bin,areas/iwa/iwa.tex,backgrounds/iwa.bg.png,Buzzar Arena 30 + jan_bt00,areas/jan/jan_bt00.stage/shape.bin,areas/jan/jan_bt00.stage/hit.bin,areas/jan/jan.tex,backgrounds/yos.bg.png,Beach 31 + jan_bt01,areas/jan/jan_bt01.stage/shape.bin,areas/jan/jan_bt01.stage/hit.bin,areas/jan/jan.tex,backgrounds/yos.bg.png,Jungle Riverside 32 + jan_bt02,areas/jan/jan_bt02.stage/shape.bin,areas/jan/jan_bt02.stage/hit.bin,areas/jan/jan.tex,backgrounds/yos.bg.png,Jungle Cliffside 33 + jan_bt03,areas/jan/jan_bt03.stage/shape.bin,areas/jan/jan_bt03.stage/hit.bin,areas/jan/jan.tex,backgrounds/jan.bg.png,Jungle Trees 34 + jan_bt04,areas/jan/jan_bt04.stage/shape.bin,areas/jan/jan_bt04.stage/hit.bin,areas/jan/jan.tex,backgrounds/jan.bg.png,Deep Jungle 35 + kgr_bt01,areas/kgr/kgr_bt01.stage/shape.bin,areas/kgr/kgr_bt01.stage/hit.bin,areas/kgr/kgr.tex,none,Inside the Whale 36 + kkj_bt01,areas/kkj/kkj_bt01.stage/shape.bin,areas/kkj/kkj_bt01.stage/hit.bin,areas/kkj/kkj.tex,backgrounds/kpa.bg.png,Hallway Bowser Arena 37 + kkj_bt02,areas/kkj/kkj_bt02.stage/shape.bin,areas/kkj/kkj_bt02.stage/hit.bin,areas/kkj/kkj.tex,backgrounds/kpa.bg.png,Final Bowser Arena 38 + kmr_bt03,areas/kmr/kmr_bt03.stage/shape.bin,areas/kmr/kmr_bt03.stage/hit.bin,areas/kmr/kmr.tex,backgrounds/kmr.bg.png,Goomba Bros Arena 39 + kmr_bt04,areas/kmr/kmr_bt04.stage/shape.bin,areas/kmr/kmr_bt04.stage/hit.bin,areas/kmr/kmr.tex,backgrounds/kmr.bg.png,Forest Path 40 + kmr_bt05,areas/kmr/kmr_bt05.stage/shape.bin,areas/kmr/kmr_bt05.stage/hit.bin,areas/kmr/kmr.tex,backgrounds/kmr.bg.png,Rocky Forest 41 + kmr_bt06,areas/kmr/kmr_bt06.stage/shape.bin,areas/kmr/kmr_bt06.stage/hit.bin,areas/kmr/kmr.tex,backgrounds/kmr.bg.png,Goomba King Arena 42 + kpa_bt01,areas/kpa/kpa_bt01.stage/shape.bin,areas/kpa/kpa_bt01.stage/hit.bin,areas/kpa/kpa.tex,none,Blocks Hall 43 + kpa_bt02,areas/kpa/kpa_bt02.stage/shape.bin,areas/kpa/kpa_bt02.stage/hit.bin,areas/kpa/kpa.tex,none,Lava Chamber 44 + kpa_bt03,areas/kpa/kpa_bt03.stage/shape.bin,areas/kpa/kpa_bt03.stage/hit.bin,areas/kpa/kpa.tex,none,Cavern 45 + kpa_bt04,areas/kpa/kpa_bt04.stage/shape.bin,areas/kpa/kpa_bt04.stage/hit.bin,areas/kpa/kpa.tex,none,Blue Fire Hall 46 + kpa_bt05,areas/kpa/kpa_bt05.stage/shape.bin,areas/kpa/kpa_bt05.stage/hit.bin,areas/kpa/kpa.tex,none,Water Levels Chamber 47 + kpa_bt07,areas/kpa/kpa_bt07.stage/shape.bin,areas/kpa/kpa_bt07.stage/hit.bin,areas/kpa/kpa.tex,none,Castle Roof 48 + kpa_bt08,areas/kpa/kpa_bt08.stage/shape.bin,areas/kpa/kpa_bt08.stage/hit.bin,areas/kpa/kpa.tex,none,Spiral Chamber 49 + kpa_bt09,areas/kpa/kpa_bt09.stage/shape.bin,areas/kpa/kpa_bt09.stage/hit.bin,areas/kpa/kpa.tex,none,Dark Bridge 50 + kpa_bt11,areas/kpa/kpa_bt11.stage/shape.bin,areas/kpa/kpa_bt11.stage/hit.bin,areas/kpa/kpa.tex,none,Pillar Hall 51 + kpa_bt13,areas/kpa/kpa_bt13.stage/shape.bin,areas/kpa/kpa_bt13.stage/hit.bin,areas/kpa/kpa.tex,none,Bowser Door Room 52 + kpa_bt14,areas/kpa/kpa_bt14.stage/shape.bin,areas/kpa/kpa_bt14.stage/hit.bin,areas/kpa/kpa.tex,none,Jail Cell 53 + kzn_bt01,areas/kzn/kzn_bt01.stage/shape.bin,areas/kzn/kzn_bt01.stage/hit.bin,areas/kzn/kzn.tex,none,Smoky Cave 54 + kzn_bt02,areas/kzn/kzn_bt02.stage/shape.bin,areas/kzn/kzn_bt02.stage/hit.bin,areas/kzn/kzn.tex,none,Lava Chamber 55 + kzn_bt04,areas/kzn/kzn_bt04.stage/shape.bin,areas/kzn/kzn_bt04.stage/hit.bin,areas/kzn/kzn.tex,none,Lava Hall 56 + kzn_bt05,areas/kzn/kzn_bt05.stage/shape.bin,areas/kzn/kzn_bt05.stage/hit.bin,areas/kzn/kzn.tex,none,Lava Piranha Arena 57 + mac_bt01,areas/mac/mac_bt01.stage/shape.bin,areas/mac/mac_bt01.stage/hit.bin,areas/mac/mac.tex,backgrounds/nok.bg.png,Docks 58 + mac_bt02,areas/mac/mac_bt02.stage/shape.bin,areas/mac/mac_bt02.stage/hit.bin,areas/mac/mac.tex,none,Dojo 59 + mim_bt01,areas/mim/mim_bt01.stage/shape.bin,areas/mim/mim_bt01.stage/hit.bin,areas/mim/mim.tex,none,Haunted Forest 60 + nok_bt01,areas/nok/nok_bt01.stage/shape.bin,areas/nok/nok_bt01.stage/hit.bin,areas/nok/nok.tex,backgrounds/nok.bg.png,Forest Ledges 61 + nok_bt02,areas/nok/nok_bt02.stage/shape.bin,areas/nok/nok_bt02.stage/hit.bin,areas/nok/nok.tex,backgrounds/nok.bg.png,Forest Bushes 62 + nok_bt03,areas/nok/nok_bt03.stage/shape.bin,areas/nok/nok_bt03.stage/hit.bin,areas/nok/nok.tex,backgrounds/nok.bg.png,Bridge 63 + nok_bt04,areas/nok/nok_bt04.stage/shape.bin,areas/nok/nok_bt04.stage/hit.bin,areas/nok/nok.tex,backgrounds/nok.bg.png,Forest Path 64 + omo_bt01,areas/omo/omo_bt01.stage/shape.bin,areas/omo/omo_bt01.stage/hit.bin,areas/omo/omo.tex,backgrounds/omo.bg.png,Generic Toybox 65 + omo_bt02,areas/omo/omo_bt02.stage/shape.bin,areas/omo/omo_bt02.stage/hit.bin,areas/omo/omo.tex,backgrounds/omo.bg.png,Block Fortress 66 + omo_bt03,areas/omo/omo_bt03.stage/shape.bin,areas/omo/omo_bt03.stage/hit.bin,areas/omo/omo.tex,backgrounds/omo.bg.png,Dark Room 67 + omo_bt04,areas/omo/omo_bt04.stage/shape.bin,areas/omo/omo_bt04.stage/hit.bin,areas/omo/omo.tex,backgrounds/omo.bg.png,Slot Machine 68 + omo_bt05,areas/omo/omo_bt05.stage/shape.bin,areas/omo/omo_bt05.stage/hit.bin,areas/omo/omo.tex,backgrounds/omo.bg.png,Train Tracks 69 + omo_bt06,areas/omo/omo_bt06.stage/shape.bin,areas/omo/omo_bt06.stage/hit.bin,areas/omo/omo.tex,backgrounds/omo.bg.png,Block Towers 70 + omo_bt07,areas/omo/omo_bt07.stage/shape.bin,areas/omo/omo_bt07.stage/hit.bin,areas/omo/omo.tex,none,General Guy Arena 71 + pra_bt01,areas/pra/pra_bt01.stage/shape.bin,areas/pra/pra_bt01.stage/hit.bin,areas/pra/pra.tex,none,Generic Hall 72 + pra_bt02,areas/pra/pra_bt02.stage/shape.bin,areas/pra/pra_bt02.stage/hit.bin,areas/pra/pra.tex,none,Mirror Hall 73 + pra_bt03,areas/pra/pra_bt03.stage/shape.bin,areas/pra/pra_bt03.stage/hit.bin,areas/pra/pra.tex,none,Mirror Bridge 74 + pra_bt04,areas/pra/pra_bt04.stage/shape.bin,areas/pra/pra_bt04.stage/hit.bin,areas/pra/pra.tex,none,Mirror Room 75 + sam_bt01,areas/sam/sam_bt01.stage/shape.bin,areas/sam/sam_bt01.stage/hit.bin,areas/sam/sam.tex,backgrounds/yki.bg.png,Snowfield 76 + sam_bt02,areas/sam/sam_bt02.stage/shape.bin,areas/sam/sam_bt02.stage/hit.bin,areas/sam/sam.tex,backgrounds/yki.bg.png,Mountain Caves 77 + sam_bt03,areas/sam/sam_bt03.stage/shape.bin,areas/sam/sam_bt03.stage/hit.bin,areas/sam/sam.tex,backgrounds/sam.bg.png,Mountain Cliffs 78 + sam_bt04,areas/sam/sam_bt04.stage/shape.bin,areas/sam/sam_bt04.stage/hit.bin,areas/sam/sam.tex,backgrounds/sam.bg.png,Crystal King Arena 79 + sbk_bt02,areas/sbk/sbk_bt02.stage/shape.bin,areas/sbk/sbk_bt02.stage/hit.bin,areas/sbk/sbk.tex,backgrounds/sbk.bg.png,Desert Dunes 80 + tik_bt01,areas/tik/tik_bt01.stage/shape.bin,areas/tik/tik_bt01.stage/hit.bin,areas/tik/tik.tex,none,Generic Tunnel 81 + tik_bt02,areas/tik/tik_bt02.stage/shape.bin,areas/tik/tik_bt02.stage/hit.bin,areas/tik/tik.tex,none,Ledges Tunnel 82 + tik_bt03,areas/tik/tik_bt03.stage/shape.bin,areas/tik/tik_bt03.stage/hit.bin,areas/tik/tik.tex,none,Pipe Bridge 83 + tik_bt04,areas/tik/tik_bt04.stage/shape.bin,areas/tik/tik_bt04.stage/hit.bin,areas/tik/tik.tex,none,Sewer Stream 84 + tik_bt05,areas/tik/tik_bt05.stage/shape.bin,areas/tik/tik_bt05.stage/hit.bin,areas/tik/tik.tex,none,Waterfalls 85 + trd_bt00,areas/trd/trd_bt00.stage/shape.bin,areas/trd/trd_bt00.stage/hit.bin,areas/trd/trd.tex,none,Koopa Bros Arena 86 + trd_bt01,areas/trd/trd_bt01.stage/shape.bin,areas/trd/trd_bt01.stage/hit.bin,areas/trd/trd.tex,none,Cell Block 87 + trd_bt02,areas/trd/trd_bt02.stage/shape.bin,areas/trd/trd_bt02.stage/hit.bin,areas/trd/trd.tex,none,Water Passage 88 + trd_bt03,areas/trd/trd_bt03.stage/shape.bin,areas/trd/trd_bt03.stage/hit.bin,areas/trd/trd.tex,backgrounds/nok.bg.png,Battlement 89 + trd_bt04,areas/trd/trd_bt04.stage/shape.bin,areas/trd/trd_bt04.stage/hit.bin,areas/trd/trd.tex,none,Spiral Stairwell 90 + trd_bt05,areas/trd/trd_bt05.stage/shape.bin,areas/trd/trd_bt05.stage/hit.bin,areas/trd/trd.tex,backgrounds/nok.bg.png,Basement
+1
src/main/resources/icon/eye.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-eye" aria-hidden="true"><path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"></path><circle cx="12" cy="12" r="3"></circle></svg>
+1
src/main/resources/icon/eye_off.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-eye-off" aria-hidden="true"><path d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49"></path><path d="M14.084 14.158a3 3 0 0 1-4.242-4.242"></path><path d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143"></path><path d="m2 2 20 20"></path></svg>