this repo has no description

add Engine which clones papermario-dx inside the BuildEnvironment

+911 -559
+20 -39
src/main/java/app/BuildOutputDialog.java
··· 3 3 import java.awt.Color; 4 4 import java.awt.Dimension; 5 5 import java.awt.Frame; 6 + import java.io.File; 6 7 import java.io.IOException; 7 8 import java.util.regex.Matcher; 8 9 import java.util.regex.Pattern; ··· 17 18 import javax.swing.WindowConstants; 18 19 import javax.swing.text.DefaultCaret; 19 20 20 - import app.build.BuildEnvironment; 21 - import app.build.BuildException; 22 - import app.build.BuildOutputListener; 23 - import app.build.BuildResult; 24 - import app.build.NixEnvironment; 25 - import app.build.WslNixOsEnvironment; 21 + import project.engine.BuildEnvironment; 22 + import project.engine.BuildException; 23 + import project.engine.BuildOutputListener; 24 + import project.engine.BuildResult; 26 25 import net.miginfocom.swing.MigLayout; 27 26 28 27 /** ··· 100 99 101 100 // Create build environment on background thread 102 101 Environment.getExecutor().submit(() -> { 103 - try { 104 - if (Environment.isWindows()) { 105 - buildEnv = new WslNixOsEnvironment(); 106 - } 107 - else { 108 - buildEnv = new NixEnvironment(); 109 - } 110 - 111 - try { 112 - buildEnv.configure(getOutputListener()); 113 - } 114 - catch (IOException | BuildException e) { 115 - SwingUtilities.invokeLater(() -> { 116 - appendOutput("Configuration error: " + e.getMessage(), true); 117 - handleBuildError(e); 118 - }); 119 - return; 120 - } 102 + File projectDir = Environment.getProject().getDirectory(); 103 + buildEnv = Environment.getProject().getEngine().getBuildEnvironment(); 121 104 122 - buildEnv.buildAsync(getOutputListener()).thenAccept(result -> { 123 - SwingUtilities.invokeLater(() -> handleBuildComplete(result)); 124 - }).exceptionally(ex -> { 125 - SwingUtilities.invokeLater(() -> handleBuildError(ex)); 126 - return null; 127 - }); 105 + try { 106 + buildEnv.configure(projectDir, getOutputListener()); 128 107 } 129 - catch (BuildException e) { 108 + catch (IOException | BuildException e) { 130 109 SwingUtilities.invokeLater(() -> { 131 - if (!e.isSilent()) { 132 - appendOutput("Build environment error: " + e.getMessage(), true); 133 - handleBuildError(e); 134 - } 135 - else { 136 - appendOutput(e.getMessage(), false); 137 - handleBuildComplete(BuildResult.cancelled(java.time.Duration.ZERO)); 138 - } 110 + appendOutput("Configuration error: " + e.getMessage(), true); 111 + handleBuildError(e); 139 112 }); 113 + return; 140 114 } 115 + 116 + buildEnv.buildAsync(projectDir, getOutputListener()).thenAccept(result -> { 117 + SwingUtilities.invokeLater(() -> handleBuildComplete(result)); 118 + }).exceptionally(ex -> { 119 + SwingUtilities.invokeLater(() -> handleBuildError(ex)); 120 + return null; 121 + }); 141 122 }); 142 123 } 143 124
+14 -7
src/main/java/app/Directories.java
··· 45 45 46 46 PROJ_STAR_ROD (Root.PROJECT, "/.starrod/"), 47 47 PROJ_CFG (Root.PROJECT, PROJ_STAR_ROD, "/cfg/"), 48 - PROJ_THUMBNAIL (Root.PROJECT, "/thumbnail/"), 49 - PROJ_SRC (Root.PROJECT, "/src/"), 50 - PROJ_SRC_WORLD (Root.PROJECT, PROJ_SRC, "/world/"), 51 - PROJ_SRC_STAGE (Root.PROJECT, PROJ_SRC, "/battle/common/stage/"), 52 - PROJ_INCLUDE (Root.PROJECT, "/include/"), 53 - PROJ_INCLUDE_MAPFS (Root.PROJECT, PROJ_INCLUDE, "/mapfs/"); 48 + PROJ_THUMBNAIL (Root.PROJECT, PROJ_STAR_ROD, "/thumbnail/"), 49 + 50 + //======================================================================================= 51 + // Directories relative to the current project's engine (papermario-dx) 52 + 53 + ENGINE_SRC (Root.ENGINE, "/src/"), 54 + ENGINE_SRC_WORLD (Root.ENGINE, ENGINE_SRC, "/world/"), 55 + ENGINE_SRC_STAGE (Root.ENGINE, ENGINE_SRC, "/battle/common/stage/"), 56 + ENGINE_INCLUDE (Root.ENGINE, "/include/"), 57 + ENGINE_INCLUDE_MAPFS (Root.ENGINE, ENGINE_INCLUDE, "/mapfs/"), 58 + ENGINE_ASSETS_US (Root.ENGINE, "/assets/us/"); 54 59 55 60 // @formatter:on 56 61 //======================================================================================= ··· 129 134 130 135 private enum Root 131 136 { 132 - NONE, DUMP, PROJECT, CONFIG, STATE 137 + NONE, DUMP, PROJECT, CONFIG, STATE, ENGINE 133 138 } 134 139 135 140 private static String getRootPath(Root root) ··· 145 150 return Environment.getUserConfigDir().getAbsolutePath(); 146 151 case STATE: 147 152 return Environment.getUserStateDir().getAbsolutePath(); 153 + case ENGINE: 154 + return Environment.getProject().getEngine().getDirectory().getAbsolutePath(); 148 155 149 156 } 150 157 return null;
+83 -47
src/main/java/app/Environment.java
··· 43 43 import app.config.Options.Scope; 44 44 import app.input.IOUtils; 45 45 import project.Project; 46 + import project.ProjectListing; 46 47 import project.ProjectManager; 47 48 import project.Manifest; 48 49 import project.ui.ProjectSwitcherDialog; ··· 53 54 import game.entity.EntityExtractor; 54 55 import game.map.editor.ui.dialogs.ChooseDialogResult; 55 56 import game.map.editor.ui.dialogs.DirChooser; 57 + import game.map.editor.ui.dialogs.OpenFileChooser; 56 58 import game.message.font.FontManager; 57 59 import util.Logger; 58 60 import util.Priority; ··· 90 92 private static File codeSource; 91 93 92 94 public static Config mainConfig = null; 93 - public static Config projectConfig = null; 94 95 95 96 private static Project project = null; 96 97 ··· 258 259 } 259 260 260 261 try { 261 - Project project = chooseProject(); 262 - if (project == null) 262 + ProjectListing listing = chooseProject(); 263 + if (listing == null) 263 264 exit(); 264 - LoadingBar.show("Loading " + project.getName(), true); 265 - boolean validProject = loadProject(project); 265 + LoadingBar.show("Loading " + listing.getName(), true); 266 + boolean validProject = loadProject(listing); 266 267 if (!validProject) 267 268 exit(1); 268 269 } catch (IOException | KdlParseException e) { ··· 492 493 } 493 494 } 494 495 495 - private static Project chooseProject() throws IOException, KdlParseException 496 + private static ProjectListing chooseProject() throws IOException, KdlParseException 496 497 { 497 498 // Search current directory and its parents for a project manifest 498 499 File currentDir = new File("."); 499 500 while (currentDir != null) { 500 501 File projectDir = new File(currentDir, Manifest.FILENAME); 501 502 if (projectDir.isFile()) { 502 - return new Project(projectDir); 503 + return new ProjectListing(projectDir.getParentFile()); 503 504 } 504 505 currentDir = currentDir.getParentFile(); 505 506 } ··· 512 513 return ProjectSwitcherDialog.showPrompt(); 513 514 } 514 515 515 - public static boolean loadProject(Project newProject) throws IOException 516 + public static boolean loadProject(ProjectListing listing) throws IOException 516 517 { 517 - project = newProject; 518 - 519 - // TODO: get similar to classic 520 - /* 521 - // get US baserom 522 - usBaseRom = new File(project.getDirectory(), FN_BASEROM); 523 - if (!usBaseRom.exists()) { 524 - showErrorMessage("Missing US Base ROM", 525 - "Could not find US baserom for project. %n" + 526 - "Star Rod requries one for asset extraction."); 518 + try { 519 + project = new Project(listing.getDirectory()); 520 + } catch (IOException | KdlParseException e) { 521 + Logger.logError(e.getMessage()); 522 + showErrorMessage("Error Loading Project", "Failed to load project: %s", listing.getName()); 527 523 return false; 528 524 } 529 - */ 530 525 531 - // save project dir 532 526 Directories.setProjectDirectory(project.getPath()); 527 + Directories.setDumpDirectory(project.getEngine().getDumpDir().getAbsolutePath()); 533 528 534 - reloadIcons(); 529 + // Asset stack (TODO: move to Project?) 530 + assetDirectories = new ArrayList<>(); 531 + assetDirectories.add(project.getDirectory()); 532 + // ...dependencies... 533 + assetDirectories.add(Directories.ENGINE_ASSETS_US.toFile()); 535 534 536 - ProjectDatabase.initialize(); 537 - 538 - // set dump dir 539 - File dumpDir = new File(usBaseRom.getParentFile(), "/dump/"); 540 - Directories.setDumpDirectory(dumpDir.getAbsolutePath()); 541 - 542 - // dump if missing 543 - if (!dumpDir.exists()) { 544 - LoadingBar.show("Extracting Baserom"); 545 - Logger.log("Extracting assets from baserom"); 546 - Directories.createDumpDirectories(); 547 - EntityExtractor.extractAll(); 548 - FontManager.dump(); 535 + usBaseRom = project.getEngine().getBaseRom(); 536 + if (!ensureDumpExtracted()) { 537 + return false; 549 538 } 550 539 551 - AssetExtractor.extractAll(); 540 + reloadIcons(); 541 + ProjectDatabase.initialize(); 552 542 553 - // Record that this project was opened 554 543 ProjectManager.getInstance().recordProjectOpened(project); 555 - 556 544 return true; 557 545 } 558 546 ··· 586 574 587 575 public static ByteBuffer getBaseRomBuffer() 588 576 { 589 - // lazy load 590 577 if (romBytes != null) 591 578 return romBytes; 592 579 580 + if (usBaseRom == null || !usBaseRom.exists()) 581 + return null; 582 + 593 583 try { 594 584 romBytes = IOUtils.getDirectBuffer(usBaseRom).asReadOnlyBuffer(); 595 585 } ··· 603 593 return romBytes; 604 594 } 605 595 606 - private static List<File> getAssetDirs(File directory, File splatFile) throws IOException 596 + /** 597 + * Prompts the user to select a baserom file, validates it, and copies 598 + * it to the appropriate location. 599 + * @return the validated ROM file, or null if cancelled 600 + */ 601 + public static File promptForBaserom() 607 602 { 608 - Map<String, Object> topLevelMap = new Yaml().load(new FileInputStream(splatFile)); 603 + OpenFileChooser chooser = new OpenFileChooser( 604 + null, "Select Paper Mario (US) ROM", "N64 ROM", "z64", "n64", "v64"); 605 + 606 + if (chooser.prompt() != ChooseDialogResult.APPROVE) 607 + return null; 608 + 609 + File selected = chooser.getSelectedFile(); 610 + if (selected == null) 611 + return null; 612 + 613 + try { 614 + File validated = RomValidator.validateROM(selected); 615 + if (validated == null) 616 + return null; 617 + 618 + // Copy to target location 619 + FileUtils.copyFile(validated, usBaseRom); 620 + Logger.log("Baserom installed to " + usBaseRom.getAbsolutePath()); 621 + 622 + return usBaseRom; 623 + } 624 + catch (IOException e) { 625 + Logger.printStackTrace(e); 626 + showErrorMessage("ROM Copy Error", "Failed to copy ROM: %s", e.getMessage()); 627 + return null; 628 + } 629 + } 609 630 610 - @SuppressWarnings("unchecked") 611 - List<String> assetDirNames = (List<String>) topLevelMap.get("asset_stack"); 631 + /** 632 + * Ensures the dump directory is extracted. If it doesn't exist, 633 + * extracts from the baserom (prompting for the ROM if needed). 634 + * Sets usBaseRom and dumpPath as side effects when the user provides a ROM. 635 + * @return true if dump is available, false if user cancelled or extraction failed 636 + */ 637 + public static boolean ensureDumpExtracted() throws IOException 638 + { 639 + String dumpPath = Directories.getDumpPath(); 612 640 613 - File assetsDir = new File(directory, "assets"); 641 + if (dumpPath == null || !(new File(dumpPath).exists())) { 642 + // Need baserom to create dump 643 + if (usBaseRom == null || !usBaseRom.exists()) { 644 + Logger.logError("Cannot extract dump: baserom not found"); 645 + return false; 646 + } 614 647 615 - List<File> assetDirectories = new ArrayList<>(); 616 - for (String dirName : assetDirNames) { 617 - assetDirectories.add(new File(assetsDir, dirName)); 648 + Logger.log("Extracting assets"); 649 + Directories.createDumpDirectories(); 650 + EntityExtractor.extractAll(); 651 + FontManager.dump(); 618 652 } 619 - return assetDirectories; 653 + 654 + AssetExtractor.extractAll(); 655 + return true; 620 656 } 621 657 622 658 public static boolean isWindows()
+1
src/main/java/app/LoadingBar.java
··· 77 77 setTitle("Initializing"); 78 78 79 79 setMinimumSize(new Dimension(320, 64)); 80 + setMaximumSize(new Dimension(320, 64)); 80 81 setLocationRelativeTo(null); 81 82 setUndecorated(true); 82 83 progressBar = new JProgressBar();
+23 -86
src/main/java/app/StarRodMain.java
··· 18 18 19 19 import javax.imageio.ImageIO; 20 20 import javax.swing.AbstractButton; 21 + import javax.swing.BorderFactory; 21 22 import javax.swing.ImageIcon; 22 23 import javax.swing.JButton; 23 24 import javax.swing.JLabel; ··· 39 40 40 41 import org.apache.commons.io.FilenameUtils; 41 42 42 - import app.build.BuildEnvironment; 43 - import app.build.BuildOutputListener; 44 - import app.build.BuildResult; 45 - import app.build.NixEnvironment; 46 - import app.build.WslNixOsEnvironment; 43 + import project.engine.BuildEnvironment; 44 + import project.engine.BuildOutputListener; 45 + import project.engine.BuildResult; 47 46 import app.config.Options; 48 47 import app.input.InvalidInputException; 49 48 import app.pane.Dock; ··· 51 50 import assets.AssetManager; 52 51 import assets.ExpectedAsset; 53 52 import common.BaseEditor; 53 + import project.engine.Engine; 54 54 import game.globals.editor.GlobalsEditor; 55 55 import game.map.Map; 56 56 import game.map.compiler.BuildException; ··· 64 64 import game.texture.editor.ImageEditor; 65 65 import game.worldmap.WorldMapEditor; 66 66 import net.miginfocom.swing.MigLayout; 67 + import project.Project; 67 68 import util.Logger; 68 69 import util.Priority; 69 70 ··· 101 102 setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); 102 103 setMinimumSize(new Dimension(MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT)); 103 104 104 - 105 - // Display current project path (read-only, restart app to change projects) 106 - JLabel projectPathLabel = new JLabel(Environment.getProjectDirectory().getAbsolutePath()); 107 - SwingUtils.setFontSize(projectPathLabel, 11); 105 + // TODO: click this to change project 106 + JLabel projectIdLabel = new JLabel(Environment.getProject().getManifest().getId()); 107 + SwingUtils.setFontSize(projectIdLabel, 11); 108 108 109 109 JButton mapEditorButton = new JButton("Map Editor"); 110 110 trySetIcon(mapEditorButton, ExpectedAsset.ICON_MAP_EDITOR); ··· 162 162 }); 163 163 buttons.add(themesMenuButton); 164 164 165 - // Extract Data button 166 - JButton extractDataButton = new JButton("Extract Map Data"); 167 - trySetIcon(extractDataButton, ExpectedAsset.ICON_EXTRACT); 168 - SwingUtils.setFontSize(extractDataButton, 12); 169 - extractDataButton.addActionListener((e) -> { 170 - action_extractMapData(); 171 - }); 172 - buttons.add(extractDataButton); 173 - 174 165 // Build Project button 175 166 JButton buildProjectButton = new JButton("Build Project"); 176 167 trySetIcon(buildProjectButton, ExpectedAsset.ICON_GOLD); ··· 222 213 223 214 // Left pane - buttons panel 224 215 JPanel leftPane = new JPanel(new MigLayout("fill, ins 8, wrap 1")); 225 - leftPane.add(new JLabel("Project:"), "split 2"); 226 - leftPane.add(projectPathLabel, "pushx, growx, gapbottom 8, wrap"); 227 216 228 217 JPanel buttonsPanel = new JPanel(new MigLayout("fillx, wrap 1, hidemode 3")); 229 218 buttonsPanel.add(mapEditorButton, "growx"); ··· 233 222 buttonsPanel.add(worldEditorButton, "growx"); 234 223 buttonsPanel.add(imageEditorButton, "growx"); 235 224 buttonsPanel.add(themesMenuButton, "growx"); 236 - buttonsPanel.add(extractDataButton, "growx"); 237 225 buttonsPanel.add(openConfigDirButton, "growx"); 238 226 buttonsPanel.add(openProjectDirButton, "growx"); 239 227 buttonsPanel.add(buildProjectButton, "growx, gaptop 8"); ··· 272 260 verticalSplit.setDividerSize(4); 273 261 verticalSplit.setResizeWeight(1.0); // Give most space to top pane 274 262 263 + // Status bar 264 + JLabel statusBarLabel = new JLabel("Status bar"); 265 + statusBarLabel.setBorder(BorderFactory.createCompoundBorder( 266 + BorderFactory.createMatteBorder(1, 0, 0, 0, UIManager.getColor("Separator.foreground")), 267 + BorderFactory.createEmptyBorder(2, 8, 2, 8) 268 + )); 269 + SwingUtils.setFontSize(statusBarLabel, 11); 270 + 275 271 // Layout 276 - setLayout(new MigLayout("fill, ins 0")); 272 + setLayout(new MigLayout("fill, ins 0, wrap")); 277 273 add(verticalSplit, "grow, push"); 274 + add(statusBarLabel, "growx, h 20!"); 278 275 279 276 pack(); 280 277 setLocationRelativeTo(null); ··· 376 373 }); 377 374 } 378 375 379 - private void action_extractMapData() 380 - { 381 - new EditorWorker(() -> { 382 - if (!Environment.projectConfig.getBoolean(Options.ExtractedMapData)) { 383 - int choice = SwingUtils.getConfirmDialog() 384 - .setTitle("Extraction Warning") 385 - .setMessage("This action will modify the source files of almost every map.", 386 - "Consider creating a backup or committing any changes before proceeding.", 387 - "Are you ready to begin extracting?") 388 - .setMessageType(JOptionPane.WARNING_MESSAGE) 389 - .setOptionsType(JOptionPane.YES_NO_CANCEL_OPTION) 390 - .choose(); 391 - 392 - if (choice == JOptionPane.YES_OPTION) { 393 - Logger.log("Extracting map data...", Priority.MILESTONE); 394 - Extractor.extractAll(); 395 - 396 - SwingUtils.getMessageDialog() 397 - .setTitle("All Data Extracted") 398 - .setMessage("Complete!") 399 - .setMessageType(JOptionPane.PLAIN_MESSAGE) 400 - .show(); 401 - 402 - Environment.projectConfig.setBoolean(Options.ExtractedMapData, true); 403 - Environment.projectConfig.saveConfigFile(); 404 - } 405 - } 406 - else { 407 - SwingUtils.getWarningDialog() 408 - .setTitle("Data Already Extracted") 409 - .setMessage("Map data has already been extracted for this project.") 410 - .show(); 411 - } 412 - }); 413 - } 414 - 415 376 private void action_buildProject() 416 377 { 417 378 BuildOutputDialog dialog = new BuildOutputDialog(this); ··· 607 568 break; 608 569 609 570 case "-BUILDPROJECT": 610 - BuildEnvironment env = null; 611 571 try { 612 - if (Environment.isWindows()) { 613 - env = new WslNixOsEnvironment(); 614 - } 615 - else { 616 - env = new NixEnvironment(); 617 - } 618 - 619 - BuildResult result = env.configure(BuildOutputListener.toLogger()); 620 - if (result.isSuccess()) { 621 - result = env.build(BuildOutputListener.toLogger()); 622 - } 623 - 624 - if (!result.isSuccess()) { 625 - throw new StarRodException("Build failed: " + result.getErrorMessage().orElse("unknown")); 626 - } 627 - Logger.log("ROM built: " + result.getOutputRom().get()); 628 - } 629 - catch (app.build.BuildException e) { 630 - if (!e.isSilent()) { 631 - Logger.logError("Build environment error: " + e.getMessage()); 632 - } 633 - else { 634 - Logger.log(e.getMessage()); 635 - } 572 + Environment.getProject().build(); 636 573 } 637 - catch (IOException e) { 638 - Logger.logError("Build environment error: " + e.getMessage()); 574 + catch (Exception e) { 575 + Logger.logError("Build failed: " + e.getMessage()); 639 576 } 640 577 break; 641 578 ··· 647 584 648 585 private static final void trySetIcon(AbstractButton button, ExpectedAsset asset) 649 586 { 650 - if (!(new File(Directories.getDumpPath())).exists()) { 587 + if (Directories.getDumpPath() == null || !(new File(Directories.getDumpPath())).exists()) { 651 588 Logger.log("Dump directory could not be found."); 652 589 SwingUtils.addBorderPadding(button); 653 590 return;
-61
src/main/java/app/build/BuildEnvironment.java
··· 1 - package app.build; 2 - 3 - import java.io.IOException; 4 - import java.util.concurrent.CompletableFuture; 5 - 6 - /** 7 - * Interface for building Paper Mario decomp projects. 8 - * Implementations handle different build environments (native Nix, WSL+NixOS). 9 - */ 10 - public interface BuildEnvironment 11 - { 12 - /** 13 - * Returns a human-readable name for this environment type. 14 - */ 15 - String getName(); 16 - 17 - /** 18 - * Runs configure (./configure). 19 - * @param listener Callback for real-time build output 20 - * @return The result of the configure operation 21 - * @throws BuildException If the build environment is not properly set up 22 - * @throws IOException If an I/O error occurs 23 - */ 24 - BuildResult configure(BuildOutputListener listener) throws BuildException, IOException; 25 - 26 - /** 27 - * Builds the project (ninja). 28 - * @param listener Callback for real-time build output 29 - * @return The result of the build operation 30 - * @throws BuildException If the build environment is not properly set up 31 - * @throws IOException If an I/O error occurs 32 - */ 33 - BuildResult build(BuildOutputListener listener) throws BuildException, IOException; 34 - 35 - /** 36 - * Cleans the build directory (./configure --clean). 37 - * @param listener Callback for real-time build output 38 - * @return The result of the clean operation 39 - * @throws BuildException If the build environment is not properly set up 40 - * @throws IOException If an I/O error occurs 41 - */ 42 - BuildResult clean(BuildOutputListener listener) throws BuildException, IOException; 43 - 44 - /** 45 - * Builds the project asynchronously. 46 - * @param listener Callback for real-time build output 47 - * @return A CompletableFuture that completes with the build result 48 - */ 49 - CompletableFuture<BuildResult> buildAsync(BuildOutputListener listener); 50 - 51 - /** 52 - * Cancels any running build operation. 53 - * @return True if a build was cancelled, false if no build was running 54 - */ 55 - boolean cancel(); 56 - 57 - /** 58 - * Returns whether a build is currently in progress. 59 - */ 60 - boolean isBuilding(); 61 - }
+1 -1
src/main/java/app/build/BuildException.java src/main/java/project/engine/BuildException.java
··· 1 - package app.build; 1 + package project.engine; 2 2 3 3 /** 4 4 * Exception thrown when a build operation fails.
+2 -7
src/main/java/app/build/BuildOutputListener.java src/main/java/project/engine/BuildOutputListener.java
··· 1 - package app.build; 1 + package project.engine; 2 2 3 3 import util.Logger; 4 4 ··· 21 21 static BuildOutputListener toLogger() 22 22 { 23 23 return (line, isError) -> { 24 - if (isError) { 25 - Logger.logError(line); 26 - } 27 - else { 28 - Logger.log(line); 29 - } 24 + Logger.log(line); 30 25 }; 31 26 } 32 27
+1 -1
src/main/java/app/build/BuildResult.java src/main/java/project/engine/BuildResult.java
··· 1 - package app.build; 1 + package project.engine; 2 2 3 3 import java.io.File; 4 4 import java.time.Duration;
-145
src/main/java/app/build/NixEnvironment.java
··· 1 - package app.build; 2 - 3 - import java.io.File; 4 - import java.io.IOException; 5 - import java.util.concurrent.CompletableFuture; 6 - 7 - import app.BuildOutputDialog; 8 - import app.Environment; 9 - 10 - /** 11 - * Build environment implementation for native Nix (Linux/macOS). 12 - * Runs commands via `nix develop -c bash -c "<command>"`. 13 - */ 14 - public class NixEnvironment implements BuildEnvironment 15 - { 16 - private static final String ROM_PATH = "ver/us/build/papermario.z64"; 17 - 18 - private final ProcessRunner runner = new ProcessRunner(); 19 - 20 - @Override 21 - public String getName() 22 - { 23 - return "Nix"; 24 - } 25 - 26 - @Override 27 - public BuildResult configure(BuildOutputListener listener) throws BuildException, IOException 28 - { 29 - validateEnvironment(); 30 - return runNixCommand("./configure", listener); 31 - } 32 - 33 - @Override 34 - public BuildResult build(BuildOutputListener listener) throws BuildException, IOException 35 - { 36 - validateEnvironment(); 37 - ProcessRunner.ProcessResult result = runNixCommandRaw("NINJA_STATUS='" + BuildOutputDialog.NINJA_STATUS + "' ninja", listener); 38 - 39 - if (result.wasCancelled()) { 40 - return BuildResult.cancelled(result.getDuration()); 41 - } 42 - 43 - File rom = new File(Environment.getProjectDirectory(), ROM_PATH); 44 - if (result.isSuccess() && rom.exists()) { 45 - return BuildResult.success(result.getExitCode(), result.getDuration(), rom); 46 - } 47 - else { 48 - String error = result.getExitCode() == 0 ? "ROM file not found" : "Build failed with exit code " + result.getExitCode(); 49 - return BuildResult.failure(result.getExitCode(), result.getDuration(), error); 50 - } 51 - } 52 - 53 - @Override 54 - public BuildResult clean(BuildOutputListener listener) throws BuildException, IOException 55 - { 56 - validateEnvironment(); 57 - return runNixCommand("./configure --clean", listener); 58 - } 59 - 60 - @Override 61 - public CompletableFuture<BuildResult> buildAsync(BuildOutputListener listener) 62 - { 63 - return CompletableFuture.supplyAsync(() -> { 64 - try { 65 - return build(listener); 66 - } 67 - catch (BuildException | IOException e) { 68 - return BuildResult.failure(-1, java.time.Duration.ZERO, e.getMessage()); 69 - } 70 - }, Environment.getExecutor()); 71 - } 72 - 73 - @Override 74 - public boolean cancel() 75 - { 76 - return runner.cancel(); 77 - } 78 - 79 - @Override 80 - public boolean isBuilding() 81 - { 82 - return runner.isRunning(); 83 - } 84 - 85 - private void validateEnvironment() throws BuildException 86 - { 87 - // Check for nix binary 88 - if (!isNixInstalled()) { 89 - throw new BuildException( 90 - "Nix is not installed. Please install Nix from https://nixos.org/download.html\n" + 91 - "Run: curl -L https://nixos.org/nix/install | sh -s -- --daemon"); 92 - } 93 - 94 - // Check for flake.nix in project directory 95 - File projectDir = Environment.getProjectDirectory(); 96 - if (projectDir == null) { 97 - throw new BuildException("No project directory is set"); 98 - } 99 - 100 - File flakeFile = new File(projectDir, "flake.nix"); 101 - if (!flakeFile.exists()) { 102 - throw new BuildException("No flake.nix found in project directory: " + projectDir.getAbsolutePath()); 103 - } 104 - } 105 - 106 - private boolean isNixInstalled() 107 - { 108 - try { 109 - ProcessBuilder pb = new ProcessBuilder("which", "nix"); 110 - Process process = pb.start(); 111 - int exitCode = process.waitFor(); 112 - return exitCode == 0; 113 - } 114 - catch (IOException | InterruptedException e) { 115 - return false; 116 - } 117 - } 118 - 119 - private BuildResult runNixCommand(String command, BuildOutputListener listener) throws IOException 120 - { 121 - ProcessRunner.ProcessResult result = runNixCommandRaw(command, listener); 122 - 123 - if (result.wasCancelled()) { 124 - return BuildResult.cancelled(result.getDuration()); 125 - } 126 - else if (result.isSuccess()) { 127 - return BuildResult.success(result.getExitCode(), result.getDuration(), null); 128 - } 129 - else { 130 - return BuildResult.failure(result.getExitCode(), result.getDuration(), 131 - "Command failed with exit code " + result.getExitCode()); 132 - } 133 - } 134 - 135 - private ProcessRunner.ProcessResult runNixCommandRaw(String command, BuildOutputListener listener) throws IOException 136 - { 137 - File projectDir = Environment.getProjectDirectory(); 138 - 139 - String[] cmd = new String[] { 140 - "nix", "develop", "-c", "bash", "-c", command 141 - }; 142 - 143 - return runner.run(cmd, projectDir, listener); 144 - } 145 - }
+1 -1
src/main/java/app/build/ProcessRunner.java src/main/java/project/engine/ProcessRunner.java
··· 1 - package app.build; 1 + package project.engine; 2 2 3 3 import java.io.BufferedReader; 4 4 import java.io.File;
+88 -14
src/main/java/app/build/WslNixOsEnvironment.java src/main/java/project/engine/WslNixOsEnvironment.java
··· 1 - package app.build; 1 + package project.engine; 2 2 3 3 import java.io.BufferedReader; 4 4 import java.io.File; ··· 42 42 registerShutdownHook(); 43 43 } 44 44 45 + // --- Engine storage and git operations --- 46 + 47 + private static final String WSL_ENGINE_BASE = "/home/nixos/star-rod/engine"; 48 + 49 + @Override 50 + public File getEngineBaseDir() 51 + { 52 + return new File("\\\\" + "wsl$\\" + DISTRO_NAME + WSL_ENGINE_BASE.replace('/', '\\')); 53 + } 54 + 55 + @Override 56 + public void gitCloneBare(String url, File targetDir, BuildOutputListener listener) throws IOException 57 + { 58 + String wslTarget = convertToWslPath(targetDir); 59 + runWslGitCommand(null, listener, "clone", "--bare", url, wslTarget); 60 + } 61 + 62 + @Override 63 + public void gitWorktreeAdd(File bareRepo, File worktreeDir, String ref, BuildOutputListener listener) throws IOException 64 + { 65 + String wslBareRepo = convertToWslPath(bareRepo); 66 + String wslWorktree = convertToWslPath(worktreeDir); 67 + runWslGitCommand(wslBareRepo, listener, "worktree", "add", wslWorktree, ref); 68 + } 69 + 70 + @Override 71 + public void gitFetchAll(File repo, BuildOutputListener listener) throws IOException 72 + { 73 + runWslGitCommand(convertToWslPath(repo), listener, "fetch", "--all"); 74 + } 75 + 76 + @Override 77 + public void gitCheckout(File dir, String ref, BuildOutputListener listener) throws IOException 78 + { 79 + runWslGitCommand(convertToWslPath(dir), listener, "checkout", ref); 80 + } 81 + 82 + /** 83 + * Runs a git command inside WSL. 84 + * @param wslWorkingDir WSL-native path for working directory, or null 85 + * @param listener Callback for output 86 + * @param args git arguments (paths must already be WSL-native) 87 + */ 88 + private void runWslGitCommand(String wslWorkingDir, BuildOutputListener listener, String... args) throws IOException 89 + { 90 + int baseLen = 3; // wsl -d <distro> 91 + if (wslWorkingDir != null) 92 + baseLen += 2; // --cd <path> 93 + baseLen += 1; // git 94 + 95 + String[] cmd = new String[baseLen + args.length]; 96 + int i = 0; 97 + cmd[i++] = "wsl"; 98 + cmd[i++] = "-d"; 99 + cmd[i++] = DISTRO_NAME; 100 + if (wslWorkingDir != null) { 101 + cmd[i++] = "--cd"; 102 + cmd[i++] = wslWorkingDir; 103 + } 104 + cmd[i++] = "git"; 105 + System.arraycopy(args, 0, cmd, i, args.length); 106 + 107 + ProcessRunner.ProcessResult result = runner.run(cmd, null, listener); 108 + if (!result.isSuccess()) { 109 + throw new IOException("WSL git command failed with exit code " + result.getExitCode()); 110 + } 111 + } 112 + 113 + // --- Build operations --- 114 + 45 115 @Override 46 116 public String getName() 47 117 { ··· 49 119 } 50 120 51 121 @Override 52 - public BuildResult configure(BuildOutputListener listener) throws BuildException, IOException 122 + public BuildResult configure(File projectDir, BuildOutputListener listener) throws BuildException, IOException 53 123 { 54 124 ensureDistroExists(listener); 55 - return runWslCommand("./configure", listener); 125 + return runWslCommand(projectDir, "./configure", listener); 56 126 } 57 127 58 128 @Override 59 - public BuildResult build(BuildOutputListener listener) throws BuildException, IOException 129 + public BuildResult build(File projectDir, BuildOutputListener listener) throws BuildException, IOException 60 130 { 61 131 ensureDistroExists(listener); 62 - ProcessRunner.ProcessResult result = runWslCommandRaw("NINJA_STATUS='" + BuildOutputDialog.NINJA_STATUS + "' ./configure && ninja", listener); 132 + ProcessRunner.ProcessResult result = runWslCommandRaw(projectDir, "NINJA_STATUS='" + BuildOutputDialog.NINJA_STATUS + "' ./configure && ninja", listener); 63 133 64 134 if (result.wasCancelled()) { 65 135 return BuildResult.cancelled(result.getDuration()); 66 136 } 67 137 68 - File rom = new File(Environment.getProjectDirectory(), ROM_PATH); 138 + File rom = new File(projectDir, ROM_PATH); 69 139 if (result.isSuccess() && rom.exists()) { 70 140 return BuildResult.success(result.getExitCode(), result.getDuration(), rom); 71 141 } ··· 76 146 } 77 147 78 148 @Override 79 - public BuildResult clean(BuildOutputListener listener) throws BuildException, IOException 149 + public BuildResult clean(File projectDir, BuildOutputListener listener) throws BuildException, IOException 80 150 { 81 151 ensureDistroExists(listener); 82 - return runWslCommand("./configure --clean", listener); 152 + return runWslCommand(projectDir, "./configure --clean", listener); 83 153 } 84 154 85 155 @Override 86 - public CompletableFuture<BuildResult> buildAsync(BuildOutputListener listener) 156 + public CompletableFuture<BuildResult> buildAsync(File projectDir, BuildOutputListener listener) 87 157 { 88 158 return CompletableFuture.supplyAsync(() -> { 89 159 try { 90 - return build(listener); 160 + return build(projectDir, listener); 91 161 } 92 162 catch (BuildException | IOException e) { 93 163 return BuildResult.failure(-1, java.time.Duration.ZERO, e.getMessage()); ··· 457 527 private String convertToWslPath(File windowsPath) 458 528 { 459 529 String absPath = windowsPath.getAbsolutePath(); 530 + // Convert WSL paths to local ones 531 + if (absPath.startsWith("\\\\wsl$\\" + DISTRO_NAME + "\\")) { 532 + String rest = absPath.substring(absPath.indexOf(DISTRO_NAME) + DISTRO_NAME.length() + 1).replace('\\', '/'); 533 + return rest.replace("\\", "/"); 534 + } 460 535 // Convert C:\foo\bar to /mnt/c/foo/bar 461 536 if (absPath.length() >= 2 && absPath.charAt(1) == ':') { 462 537 char driveLetter = Character.toLowerCase(absPath.charAt(0)); ··· 466 541 return absPath.replace('\\', '/'); 467 542 } 468 543 469 - private BuildResult runWslCommand(String command, BuildOutputListener listener) throws IOException 544 + private BuildResult runWslCommand(File projectDir, String command, BuildOutputListener listener) throws IOException 470 545 { 471 - ProcessRunner.ProcessResult result = runWslCommandRaw(command, listener); 546 + ProcessRunner.ProcessResult result = runWslCommandRaw(projectDir, command, listener); 472 547 473 548 if (result.wasCancelled()) { 474 549 return BuildResult.cancelled(result.getDuration()); ··· 482 557 } 483 558 } 484 559 485 - private ProcessRunner.ProcessResult runWslCommandRaw(String command, BuildOutputListener listener) throws IOException 560 + private ProcessRunner.ProcessResult runWslCommandRaw(File projectDir, String command, BuildOutputListener listener) throws IOException 486 561 { 487 - File projectDir = Environment.getProjectDirectory(); 488 562 String wslPath = convertToWslPath(projectDir); 489 563 490 564 String[] cmd = new String[] {
+6 -6
src/main/java/game/ProjectDatabase.java
··· 44 44 if (initialized) 45 45 return; 46 46 47 - savedFlags = loadSavedVarNames(Directories.PROJ_INCLUDE.file("saved_flag_names.h")); 48 - savedBytes = loadSavedVarNames(Directories.PROJ_INCLUDE.file("saved_byte_names.h")); 47 + savedFlags = loadSavedVarNames(Directories.ENGINE_INCLUDE.file("saved_flag_names.h")); 48 + savedBytes = loadSavedVarNames(Directories.ENGINE_INCLUDE.file("saved_byte_names.h")); 49 49 50 50 // TODO the following block takes a half-second at startup and could be optimized better 51 51 { 52 52 decompEnums = new CaseInsensitiveMap<>(); 53 - DecompEnum.addEnums(decompEnums, Directories.PROJ_INCLUDE.file("enums.h").getAbsolutePath()); 54 - DecompEnum.addEnums(decompEnums, Directories.PROJ_INCLUDE.file("effects.h").getAbsolutePath()); 55 - DecompEnum.addEnums(decompEnums, Directories.PROJ_SRC.file("battle/battle_names.h").getAbsolutePath()); 56 - DecompEnum.addEnums(decompEnums, Directories.PROJ_SRC.file("battle/stage_names.h").getAbsolutePath()); 53 + DecompEnum.addEnums(decompEnums, Directories.ENGINE_INCLUDE.file("enums.h").getAbsolutePath()); 54 + DecompEnum.addEnums(decompEnums, Directories.ENGINE_INCLUDE.file("effects.h").getAbsolutePath()); 55 + DecompEnum.addEnums(decompEnums, Directories.ENGINE_SRC.file("battle/battle_names.h").getAbsolutePath()); 56 + DecompEnum.addEnums(decompEnums, Directories.ENGINE_SRC.file("battle/stage_names.h").getAbsolutePath()); 57 57 } 58 58 59 59 ESurfaceTypes = decompEnums.get("SurfaceType");
+1 -1
src/main/java/game/SimpleItem.java
··· 52 52 { 53 53 ArrayList<SimpleItemTemplate> templates = new ArrayList<>(); 54 54 55 - File yamlFile = Directories.PROJ_SRC.file("item_table.yaml"); 55 + File yamlFile = Directories.ENGINE_SRC.file("item_table.yaml"); 56 56 ArrayList<Object> itemList = YamlHelper.readAsList(yamlFile); 57 57 if (itemList == null) 58 58 return null;
+13
src/main/java/game/entity/EntityInfo.java
··· 267 267 268 268 public static void loadModels() 269 269 { 270 + // Ensure dump is extracted (will prompt for baserom if needed) 271 + if (!DUMP_ENTITY_SRC.toFile().exists()) { 272 + try { 273 + if (!Environment.ensureDumpExtracted()) 274 + return; // user cancelled baserom selection 275 + } 276 + catch (IOException e) { 277 + Logger.logError("Failed to extract dump: " + e.getMessage()); 278 + Logger.printStackTrace(e); 279 + return; 280 + } 281 + } 282 + 270 283 for (EntityType type : EntityType.values()) { 271 284 if (type.model != null) 272 285 type.model.freeTextures();
+4 -4
src/main/java/game/globals/editor/GlobalsData.java
··· 81 81 82 82 private void loadItems() 83 83 { 84 - File yamlFile = Directories.PROJ_SRC.file("item_table.yaml"); 84 + File yamlFile = Directories.ENGINE_SRC.file("item_table.yaml"); 85 85 List<ItemRecord> itemList = ItemRecord.fromYAML(yamlFile); 86 86 87 87 items.clear(); ··· 103 103 item.saveBackup(); 104 104 } 105 105 106 - File yamlFile = Directories.PROJ_SRC.file("item_table.yaml"); 106 + File yamlFile = Directories.ENGINE_SRC.file("item_table.yaml"); 107 107 ItemRecord.toYAML(yamlFile, itemList); 108 108 109 109 Logger.log("Saved item table"); ··· 111 111 112 112 private void loadMoves() 113 113 { 114 - File yamlFile = Directories.PROJ_SRC.file("move_table.yaml"); 114 + File yamlFile = Directories.ENGINE_SRC.file("move_table.yaml"); 115 115 List<MoveRecord> moveList = MoveRecord.fromYAML(yamlFile); 116 116 117 117 moves.clear(); ··· 133 133 move.saveBackup(); 134 134 } 135 135 136 - File yamlFile = Directories.PROJ_SRC.file("move_table.yaml"); 136 + File yamlFile = Directories.ENGINE_SRC.file("move_table.yaml"); 137 137 MoveRecord.toYAML(yamlFile, moveList); 138 138 139 139 Logger.log("Saved moves table");
+3 -3
src/main/java/game/map/Map.java
··· 321 321 } 322 322 323 323 areaName = "area_" + areaAffix; 324 - projDir = Directories.PROJ_SRC_WORLD.file(areaName + "/" + name); 324 + projDir = Directories.ENGINE_SRC_WORLD.file(areaName + "/" + name); 325 325 } 326 326 327 327 public String getName() ··· 709 709 public void loadVarNames() 710 710 { 711 711 String areaHeaderName = String.format("%s/%s.h", areaName, areaAffix); 712 - File areaHeader = Directories.PROJ_SRC_WORLD.file(areaHeaderName); 712 + File areaHeader = Directories.ENGINE_SRC_WORLD.file(areaHeaderName); 713 713 714 714 areaByteNames.clear(); 715 715 areaFlagNames.clear(); ··· 737 737 } 738 738 739 739 String mapHeaderName = String.format("%s/%s/%s.h", areaName, name, name); 740 - File mapHeader = Directories.PROJ_SRC_WORLD.file(mapHeaderName); 740 + File mapHeader = Directories.ENGINE_SRC_WORLD.file(mapHeaderName); 741 741 742 742 mapVarNames.clear(); 743 743 mapFlagNames.clear();
+1 -1
src/main/java/game/map/compiler/CollisionCompiler.java
··· 44 44 raf.writeInt(zoneHeaderOffset); 45 45 raf.close(); 46 46 47 - File headerFile = Directories.PROJ_INCLUDE_MAPFS.file(map.getName() + "_hit.h"); 47 + File headerFile = Directories.ENGINE_INCLUDE_MAPFS.file(map.getName() + "_hit.h"); 48 48 try (PrintWriter pw = IOUtils.getBufferedPrintWriter(headerFile)) { 49 49 for (Collider c : map.colliderTree.getList()) { 50 50 pw.printf("#define %-23s 0x%X%n", "COLLIDER_" + c.getName(), c.getNode().getTreeIndex());
+1 -1
src/main/java/game/map/compiler/GeometryCompiler.java
··· 164 164 throw new BuildException("Build failed: " + mapType + " size exceeds engine limit.\n" + breakdown); 165 165 } 166 166 167 - File headerFile = Directories.PROJ_INCLUDE_MAPFS.file(map.getName() + "_shape.h"); 167 + File headerFile = Directories.ENGINE_INCLUDE_MAPFS.file(map.getName() + "_shape.h"); 168 168 try (PrintWriter pw = IOUtils.getBufferedPrintWriter(headerFile)) { 169 169 for (Model mdl : map.modelTree.getList()) { 170 170 pw.printf("#define %-23s 0x%X%n", "MODEL_" + mdl.getName(), mdl.getNode().getTreeIndex());
+1 -1
src/main/java/game/map/scripts/extract/Extractor.java
··· 55 55 { 56 56 LoadingBar.show("Extracting Map Data", Priority.IMPORTANT); 57 57 58 - File f = Directories.PROJ_SRC_WORLD.toFile(); 58 + File f = Directories.ENGINE_SRC_WORLD.toFile(); 59 59 File[] worldDirs = f.listFiles(); 60 60 Arrays.sort(worldDirs); 61 61
+3 -3
src/main/java/game/map/scripts/extract/HeaderEntry.java
··· 34 34 Environment.initialize(); 35 35 LoadingBar.show("Testing Map Data"); 36 36 37 - File f = Directories.PROJ_SRC_WORLD.toFile(); 37 + File f = Directories.ENGINE_SRC_WORLD.toFile(); 38 38 File[] worldDirs = f.listFiles(); 39 39 Arrays.sort(worldDirs); 40 40 int mismatchCount = 0; ··· 69 69 70 70 public static boolean test(String mapPath) throws IOException 71 71 { 72 - File in = Directories.PROJ_SRC_WORLD.file(mapPath + "/generated.h"); 73 - File out = Directories.PROJ_SRC_WORLD.file(mapPath + "/test.h"); 72 + File in = Directories.ENGINE_SRC_WORLD.file(mapPath + "/generated.h"); 73 + File out = Directories.ENGINE_SRC_WORLD.file(mapPath + "/test.h"); 74 74 75 75 List<HeaderEntry> entries = parseFile(in); 76 76
+1 -1
src/main/java/game/map/scripts/extract/StageExtractor.java
··· 26 26 Environment.initialize(); 27 27 LoadingBar.show("Extracting Stage Data"); 28 28 29 - File f = Directories.PROJ_SRC_STAGE.toFile(); 29 + File f = Directories.ENGINE_SRC_STAGE.toFile(); 30 30 File[] areaDirs = f.listFiles(); 31 31 Arrays.sort(areaDirs); 32 32
+8 -1
src/main/java/game/message/font/FontManager.java
··· 62 62 63 63 public static void loadData() throws IOException 64 64 { 65 - XmlReader xmr = new XmlReader(new File(DUMP_MSG_FONT.toFile(), "fonts.xml")); 65 + // Ensure dump is extracted (will prompt for baserom if needed) 66 + File fontsXml = new File(DUMP_MSG_FONT.toFile(), "fonts.xml"); 67 + if (!fontsXml.exists()) { 68 + if (!Environment.ensureDumpExtracted()) 69 + return; // user cancelled baserom selection 70 + } 71 + 72 + XmlReader xmr = new XmlReader(fontsXml); 66 73 67 74 List<Element> fontElems = xmr.getTags(xmr.getRootElement(), TAG_FONT); 68 75 if (fontElems.size() != 4)
+2 -2
src/main/java/game/worldmap/WorldMapModder.java
··· 169 169 170 170 public static List<WorldLocation> loadLocations() throws IOException 171 171 { 172 - List<WorldLocation> locations = readXML(Directories.PROJ_SRC.file(Directories.FN_WORLD_MAP)); 172 + List<WorldLocation> locations = readXML(Directories.ENGINE_SRC.file(Directories.FN_WORLD_MAP)); 173 173 174 174 HashMap<String, WorldLocation> locationMap = new HashMap<>(); 175 175 for (WorldLocation loc : locations) { ··· 192 192 loc.parentName = loc.parent.name; 193 193 } 194 194 195 - writeXML(locations, Directories.PROJ_SRC.file(Directories.FN_WORLD_MAP)); 195 + writeXML(locations, Directories.ENGINE_SRC.file(Directories.FN_WORLD_MAP)); 196 196 } 197 197 198 198 public static String stripPrefix(String s, String prefix)
+11 -11
src/main/java/project/JsonProjectRepository.java
··· 38 38 } 39 39 40 40 @Override 41 - public synchronized List<Project> getAllProjects() 41 + public synchronized List<ProjectListing> getAllProjects() 42 42 { 43 43 List<ProjectData> dataList = loadProjectData(); 44 - List<Project> projects = new ArrayList<>(); 44 + List<ProjectListing> projects = new ArrayList<>(); 45 45 46 - // Convert to Project objects, filtering out invalid entries 46 + // Convert to ProjectListing objects, filtering out invalid entries 47 47 Iterator<ProjectData> iter = dataList.iterator(); 48 48 boolean modified = false; 49 49 ··· 51 51 ProjectData data = iter.next(); 52 52 53 53 try { 54 - projects.add(new Project(new File(data.path), data.lastOpened)); 54 + projects.add(new ProjectListing(new File(data.path), data.lastOpened)); 55 55 } catch (IOException | KdlParseException e) { 56 56 Logger.logWarning("Ignoring invalid project: " + data.path); 57 57 iter.remove(); ··· 71 71 } 72 72 73 73 @Override 74 - public synchronized void addProject(Project project) 74 + public synchronized void addProject(ProjectListing listing) 75 75 { 76 76 List<ProjectData> dataList = loadProjectData(); 77 77 78 78 // Remove existing entry with same path (will be re-added with new timestamp) 79 - String absolutePath = project.getPath(); 79 + String absolutePath = listing.getPath(); 80 80 dataList.removeIf(data -> data.path.equals(absolutePath)); 81 81 82 82 // Add new entry 83 83 ProjectData newData = new ProjectData(); 84 84 newData.path = absolutePath; 85 - newData.lastOpened = project.getLastOpened(); 85 + newData.lastOpened = listing.getLastOpened(); 86 86 dataList.add(0, newData); // Add to beginning (most recent) 87 87 88 88 saveProjectData(dataList); 89 89 } 90 90 91 91 @Override 92 - public synchronized void removeProject(Project project) 92 + public synchronized void removeProject(ProjectListing listing) 93 93 { 94 94 List<ProjectData> dataList = loadProjectData(); 95 - String absolutePath = project.getPath(); 95 + String absolutePath = listing.getPath(); 96 96 dataList.removeIf(data -> data.path.equals(absolutePath)); 97 97 saveProjectData(dataList); 98 98 } 99 99 100 100 @Override 101 - public synchronized void updateLastOpened(Project project) 101 + public synchronized void updateLastOpened(ProjectListing listing) 102 102 { 103 103 List<ProjectData> dataList = loadProjectData(); 104 - String absolutePath = project.getPath(); 104 + String absolutePath = listing.getPath(); 105 105 106 106 for (ProjectData data : dataList) { 107 107 if (data.path.equals(absolutePath)) {
+32 -2
src/main/java/project/Manifest.java
··· 15 15 private final File file; 16 16 private final KdlDocument doc; 17 17 18 - public Manifest(Project project) throws IOException, KdlParseException { 19 - file = new File(project.getPath(), FILENAME); 18 + public Manifest(File directory) throws IOException, KdlParseException { 19 + file = new File(directory, FILENAME); 20 20 21 21 if (!file.exists()) { 22 22 throw new IOException(FILENAME + " does not exist"); ··· 24 24 25 25 var parser = KdlParser.v2(); 26 26 doc = parser.parse(Path.of(file.getAbsolutePath())); 27 + 28 + if (!hasEngine()) 29 + throw new IOException(FILENAME + " is missing required 'engine' node"); 27 30 } 28 31 public String toString() { 29 32 return "Manifest(" + file.getPath() + ")"; ··· 35 38 .findFirst() 36 39 .map(n -> n.arguments().get(0).value().toString()) 37 40 .orElse(file.getParentFile().getName()); 41 + } 42 + 43 + public String getId() { 44 + return doc.nodes().stream() 45 + .filter(n -> n.name().equals("id")) 46 + .findFirst() 47 + .map(n -> n.arguments().get(0).value().toString()) 48 + .orElse(null); 49 + } 50 + 51 + private boolean hasEngine() { 52 + return doc.nodes().stream() 53 + .anyMatch(n -> n.name().equals("engine")); 54 + } 55 + 56 + /** 57 + * Returns the engine ref (git ref to checkout). 58 + * Defaults to "main" if the ref property is omitted. 59 + */ 60 + public String getEngineRef() { 61 + return doc.nodes().stream() 62 + .filter(n -> n.name().equals("engine")) 63 + .findFirst() 64 + .map(n -> n.getProperty("ref") 65 + .map(v -> v.value().toString()) 66 + .orElse("main")) 67 + .orElse("main"); 38 68 } 39 69 }
+25 -70
src/main/java/project/Project.java
··· 8 8 import org.apache.commons.io.FileUtils; 9 9 10 10 import dev.kdl.parse.KdlParseException; 11 + import project.engine.BuildException; 12 + import project.engine.Engine; 11 13 12 - public class Project implements Comparable<Project> 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 13 19 { 14 - private final File directory; 15 - private final long lastOpened; // TODO: move this, Comparable, and compareTo to a new class 16 - private final Manifest manifest; 17 - 18 - /** Loads a project from a directory. */ 19 - public Project(File path, long lastOpened) throws IOException, KdlParseException 20 - { 21 - if (!path.isDirectory()) 22 - throw new IllegalArgumentException("Project path must be a directory: " + path); 23 - this.directory = path.getAbsoluteFile(); 24 - this.lastOpened = lastOpened; 25 - this.manifest = new Manifest(this); 26 - } 20 + private final Engine engine; 27 21 28 - /** Loads a project from a directory. */ 22 + /** Loads a project from a directory, initializing the engine. */ 29 23 public Project(File path) throws IOException, KdlParseException 30 24 { 31 - this(path, System.currentTimeMillis()); 32 - } 33 - 34 - public String getPath() 35 - { 36 - return directory.getPath(); 37 - } 38 - 39 - public File getDirectory() 40 - { 41 - return directory; 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 + } 42 32 } 43 33 44 - public long getLastOpened() 34 + public Engine getEngine() 45 35 { 46 - return lastOpened; 47 - } 48 - 49 - public String getName() 50 - { 51 - return manifest.getName(); 52 - } 53 - 54 - public Manifest getManifest() 55 - { 56 - return manifest; 36 + return engine; 57 37 } 58 38 59 39 /** Creates a new project from a template. */ 60 - public static Project create(File path, String template, String id, String name) throws IOException, KdlParseException 40 + public static ProjectListing create(File path, String template, String id, String name) throws IOException, KdlParseException 61 41 { 62 42 if (!path.exists()) 63 43 path.mkdirs(); ··· 74 54 File manifestFile = new File(path, Manifest.FILENAME); 75 55 if (manifestFile.exists()) { 76 56 String content = FileUtils.readFileToString(manifestFile, "UTF-8"); 77 - content = content.replace("$PROJECT_ID", id); 78 - content = content.replace("$PROJECT_NAME", name); 79 - content = content.replace("$PROJECT_DESCRIPTION", ""); 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 80 60 FileUtils.writeStringToFile(manifestFile, content, "UTF-8"); 81 61 } 82 62 83 - return new Project(path); 63 + return new ProjectListing(path); 84 64 } 85 65 86 - @Override 87 - public int compareTo(Project other) 66 + public void build() 88 67 { 89 - // Sort by lastOpened descending (most recent first) 90 - return Long.compare(other.lastOpened, this.lastOpened); 91 - } 92 - 93 - @Override 94 - public boolean equals(Object obj) 95 - { 96 - if (this == obj) 97 - return true; 98 - if (obj == null || getClass() != obj.getClass()) 99 - return false; 100 - Project other = (Project) obj; 101 - return directory.equals(other.directory); 102 - } 103 - 104 - @Override 105 - public int hashCode() 106 - { 107 - return directory.hashCode(); 108 - } 109 - 110 - @Override 111 - public String toString() 112 - { 113 - return getName() + " (" + directory.getAbsolutePath() + ")"; 68 + // TODO 114 69 } 115 70 }
+85
src/main/java/project/ProjectListing.java
··· 1 + package project; 2 + 3 + import java.io.File; 4 + import java.io.IOException; 5 + 6 + import dev.kdl.parse.KdlParseException; 7 + 8 + /** 9 + * Lightweight project metadata for display in the project switcher. 10 + * Parses the manifest for name/path but does not initialize the engine. 11 + */ 12 + public class ProjectListing implements Comparable<ProjectListing> 13 + { 14 + private final File directory; 15 + private final long lastOpened; 16 + private final Manifest manifest; 17 + 18 + public ProjectListing(File path, long lastOpened) throws IOException, KdlParseException 19 + { 20 + if (!path.isDirectory()) 21 + throw new IllegalArgumentException("Project path must be a directory: " + path); 22 + this.directory = path.getAbsoluteFile(); 23 + this.lastOpened = lastOpened; 24 + this.manifest = new Manifest(this.directory); 25 + } 26 + 27 + public ProjectListing(File path) throws IOException, KdlParseException 28 + { 29 + this(path, System.currentTimeMillis()); 30 + } 31 + 32 + public String getPath() 33 + { 34 + return directory.getPath(); 35 + } 36 + 37 + public File getDirectory() 38 + { 39 + return directory; 40 + } 41 + 42 + public long getLastOpened() 43 + { 44 + return lastOpened; 45 + } 46 + 47 + public String getName() 48 + { 49 + return manifest.getName(); 50 + } 51 + 52 + public Manifest getManifest() 53 + { 54 + return manifest; 55 + } 56 + 57 + @Override 58 + public int compareTo(ProjectListing other) 59 + { 60 + // Sort by lastOpened descending (most recent first) 61 + return Long.compare(other.lastOpened, this.lastOpened); 62 + } 63 + 64 + @Override 65 + public boolean equals(Object obj) 66 + { 67 + if (this == obj) 68 + return true; 69 + if (!(obj instanceof ProjectListing other)) 70 + return false; 71 + return directory.equals(other.directory); 72 + } 73 + 74 + @Override 75 + public int hashCode() 76 + { 77 + return directory.hashCode(); 78 + } 79 + 80 + @Override 81 + public String toString() 82 + { 83 + return getName() + " (" + directory.getAbsolutePath() + ")"; 84 + } 85 + }
+8 -10
src/main/java/project/ProjectManager.java
··· 38 38 * Gets all recent projects, sorted by last opened (most recent first). 39 39 * Invalid projects (non-existent paths) are automatically removed. 40 40 */ 41 - public List<Project> getRecentProjects() 41 + public List<ProjectListing> getRecentProjects() 42 42 { 43 43 return repository.getAllProjects(); 44 44 } ··· 46 46 /** 47 47 * Records that a project was opened (adds or updates its timestamp). 48 48 */ 49 - public void recordProjectOpened(Project project) 49 + public void recordProjectOpened(ProjectListing listing) 50 50 { 51 - repository.updateLastOpened(project); 51 + repository.updateLastOpened(listing); 52 52 } 53 53 54 54 /** 55 55 * Removes a project from the recent projects list. 56 56 * Does NOT delete files from disk. 57 - * @param project The project to remove 58 57 */ 59 - public void removeFromHistory(Project project) 58 + public void removeFromHistory(ProjectListing listing) 60 59 { 61 - repository.removeProject(project); 60 + repository.removeProject(listing); 62 61 } 63 62 64 63 /** 65 64 * Deletes a project from disk and removes it from the history. 66 - * @param project The project to delete 67 65 * @return true if deletion was successful, false otherwise 68 66 */ 69 - public boolean deleteFromDisk(Project project) 67 + public boolean deleteFromDisk(ProjectListing listing) 70 68 { 71 - File projectDir = new File(project.getPath()); 69 + File projectDir = new File(listing.getPath()); 72 70 73 - repository.removeProject(project); 71 + repository.removeProject(listing); 74 72 75 73 try { 76 74 FileUtils.deleteDirectory(projectDir);
+7 -7
src/main/java/project/ProjectRepository.java
··· 10 10 { 11 11 /** 12 12 * Gets all projects sorted by last opened (most recent first). 13 - * @return List of projects, or empty list if none exist 13 + * @return List of project listings, or empty list if none exist 14 14 */ 15 - List<Project> getAllProjects(); 15 + List<ProjectListing> getAllProjects(); 16 16 17 17 /** 18 18 * Adds a project to the repository or updates its timestamp if it already exists. 19 - * @param project The project to add or update 19 + * @param listing The project listing to add or update 20 20 */ 21 - void addProject(Project project); 21 + void addProject(ProjectListing listing); 22 22 23 23 /** 24 24 * Removes a project from the repository. 25 25 */ 26 - void removeProject(Project project); 26 + void removeProject(ProjectListing listing); 27 27 28 28 /** 29 29 * Updates the last opened timestamp for a project. 30 - */ 31 - void updateLastOpened(Project project); 30 + */ 31 + void updateLastOpened(ProjectListing listing); 32 32 }
+112
src/main/java/project/engine/BuildEnvironment.java
··· 1 + package project.engine; 2 + 3 + import java.io.File; 4 + import java.io.IOException; 5 + import java.util.concurrent.CompletableFuture; 6 + 7 + /** 8 + * Interface for building Paper Mario decomp projects. 9 + * Implementations handle different build environments (native Nix, WSL+NixOS). 10 + */ 11 + public interface BuildEnvironment 12 + { 13 + // --- Engine storage and git operations --- 14 + 15 + /** 16 + * Returns the base directory for engine storage (bare repo + worktrees), 17 + * as a host-side File that Java can use for file operations. 18 + */ 19 + File getEngineBaseDir(); 20 + 21 + /** 22 + * Clones a bare git repository. 23 + * @param url The repository URL 24 + * @param targetDir Host-side File for the bare repo destination 25 + * @param listener Callback for output 26 + * @throws IOException If the command fails 27 + */ 28 + void gitCloneBare(String url, File targetDir, BuildOutputListener listener) throws IOException; 29 + 30 + /** 31 + * Creates a git worktree from a bare repository. 32 + * @param bareRepo Host-side File for the bare repo 33 + * @param worktreeDir Host-side File for the new worktree 34 + * @param ref Git ref to check out 35 + * @param listener Callback for output 36 + * @throws IOException If the command fails 37 + */ 38 + void gitWorktreeAdd(File bareRepo, File worktreeDir, String ref, BuildOutputListener listener) throws IOException; 39 + 40 + /** 41 + * Fetches all remotes in a git repository. 42 + * @param repo Host-side File for the repo/worktree 43 + * @param listener Callback for output 44 + * @throws IOException If the command fails 45 + */ 46 + void gitFetchAll(File repo, BuildOutputListener listener) throws IOException; 47 + 48 + /** 49 + * Checks out a git ref in a repository. 50 + * @param dir Host-side File for the repo/worktree 51 + * @param ref Git ref to check out 52 + * @param listener Callback for output 53 + * @throws IOException If the command fails 54 + */ 55 + void gitCheckout(File dir, String ref, BuildOutputListener listener) throws IOException; 56 + 57 + // --- Build operations --- 58 + 59 + /** 60 + * Returns a human-readable name for this environment type. 61 + */ 62 + String getName(); 63 + 64 + /** 65 + * Runs configure (./configure). 66 + * @param projectDir The project directory to build in 67 + * @param listener Callback for real-time build output 68 + * @return The result of the configure operation 69 + * @throws BuildException If the build environment is not properly set up 70 + * @throws IOException If an I/O error occurs 71 + */ 72 + BuildResult configure(File projectDir, BuildOutputListener listener) throws BuildException, IOException; 73 + 74 + /** 75 + * Builds the project (ninja). 76 + * @param projectDir The project directory to build in 77 + * @param listener Callback for real-time build output 78 + * @return The result of the build operation 79 + * @throws BuildException If the build environment is not properly set up 80 + * @throws IOException If an I/O error occurs 81 + */ 82 + BuildResult build(File projectDir, BuildOutputListener listener) throws BuildException, IOException; 83 + 84 + /** 85 + * Cleans the build directory (./configure --clean). 86 + * @param projectDir The project directory to clean 87 + * @param listener Callback for real-time build output 88 + * @return The result of the clean operation 89 + * @throws BuildException If the build environment is not properly set up 90 + * @throws IOException If an I/O error occurs 91 + */ 92 + BuildResult clean(File projectDir, BuildOutputListener listener) throws BuildException, IOException; 93 + 94 + /** 95 + * Builds the project asynchronously. 96 + * @param projectDir The project directory to build in 97 + * @param listener Callback for real-time build output 98 + * @return A CompletableFuture that completes with the build result 99 + */ 100 + CompletableFuture<BuildResult> buildAsync(File projectDir, BuildOutputListener listener); 101 + 102 + /** 103 + * Cancels any running build operation. 104 + * @return True if a build was cancelled, false if no build was running 105 + */ 106 + boolean cancel(); 107 + 108 + /** 109 + * Returns whether a build is currently in progress. 110 + */ 111 + boolean isBuilding(); 112 + }
+142
src/main/java/project/engine/Engine.java
··· 1 + package project.engine; 2 + 3 + import java.io.File; 4 + import java.io.IOException; 5 + 6 + import app.Environment; 7 + import project.Project; 8 + import util.Logger; 9 + 10 + /** 11 + * A checkout of papermario-dx that has been built, ready for modding. 12 + * 13 + * Also maintains a bare clone, so that each Engine can be a worktree of the bare repo. 14 + * Engines may also be a submodule inside a project for users who want to modify the engine. 15 + */ 16 + public class Engine 17 + { 18 + private static final String BARE_REPO_NAME = "papermario-dx.git"; 19 + private static final String REPO_URL = "https://github.com/bates64/papermario-dx.git"; 20 + private static final String BASEROM_PATH = "ver/us/baserom.z64"; 21 + private static final String DUMP_PATH = "ver/us/build/star-rod-dump"; 22 + 23 + private final File directory; // the worktree or submodule directory 24 + private final String ref; 25 + private final boolean isSubmodule; 26 + private final BuildEnvironment buildEnv; 27 + 28 + private Engine(File directory, String ref, boolean isSubmodule, BuildEnvironment buildEnv) throws IOException, BuildException 29 + { 30 + this.directory = directory; 31 + this.ref = ref; 32 + this.isSubmodule = isSubmodule; 33 + this.buildEnv = buildEnv; 34 + 35 + if (!isSubmodule) { 36 + File bareRepo = getBareRepoDir(); 37 + if (!bareRepo.exists()) 38 + cloneBareRepo(); 39 + 40 + if (!directory.exists() || !isGitRepo(directory)) 41 + createWorktree(); 42 + else 43 + checkoutRef(); 44 + } 45 + } 46 + 47 + // --- Factory --- 48 + 49 + /** 50 + * Resolves and sets up the engine for a project. 51 + * If the engine uses a worktree, it will be cloned/checked out synchronously. 52 + */ 53 + public static Engine forProject(Project project) throws BuildException, IOException 54 + { 55 + BuildEnvironment buildEnv = createBuildEnvironment(); 56 + String ref = project.getManifest().getEngineRef(); 57 + 58 + // Check for submodule first 59 + File submoduleDir = new File(project.getDirectory(), "papermario-dx"); 60 + if (isGitRepo(submoduleDir)) 61 + return new Engine(submoduleDir, ref, true, buildEnv); 62 + 63 + // Worktree-based engine 64 + File engineBase = buildEnv.getEngineBaseDir(); 65 + 66 + String projectId = project.getManifest().getId(); 67 + if (projectId == null || projectId.isEmpty()) 68 + projectId = project.getDirectory().getName(); 69 + File worktreeDir = new File(engineBase, "worktrees/" + projectId); 70 + 71 + return new Engine(worktreeDir, ref, false, buildEnv); 72 + } 73 + 74 + private static BuildEnvironment createBuildEnvironment() throws BuildException 75 + { 76 + if (Environment.isWindows()) 77 + return new WslNixOsEnvironment(); 78 + return new NixEnvironment(); 79 + } 80 + 81 + private File getBareRepoDir() 82 + { 83 + return new File(buildEnv.getEngineBaseDir(), BARE_REPO_NAME); 84 + } 85 + 86 + public File getDirectory() 87 + { 88 + return directory; 89 + } 90 + 91 + public File getBaseRom() 92 + { 93 + return new File(directory, BASEROM_PATH); 94 + } 95 + 96 + public File getDumpDir() 97 + { 98 + return new File(directory, DUMP_PATH); 99 + } 100 + 101 + public String getRef() 102 + { 103 + return ref; 104 + } 105 + 106 + public boolean isSubmodule() 107 + { 108 + return isSubmodule; 109 + } 110 + 111 + public BuildEnvironment getBuildEnvironment() 112 + { 113 + return buildEnv; 114 + } 115 + 116 + private void cloneBareRepo() throws IOException 117 + { 118 + buildEnv.gitCloneBare(REPO_URL, getBareRepoDir(), BuildOutputListener.toLogger()); 119 + } 120 + 121 + private void createWorktree() throws IOException 122 + { 123 + buildEnv.gitWorktreeAdd(getBareRepoDir(), directory, ref, BuildOutputListener.toLogger()); 124 + } 125 + 126 + private void checkoutRef() throws IOException 127 + { 128 + buildEnv.gitFetchAll(getBareRepoDir(), BuildOutputListener.toLogger()); 129 + buildEnv.gitCheckout(directory, ref, BuildOutputListener.toLogger()); 130 + } 131 + 132 + // TODO: call this somewhere! 133 + public BuildResult splitAssets() throws BuildException, IOException 134 + { 135 + return buildEnv.configure(directory, BuildOutputListener.toLogger()); 136 + } 137 + 138 + private static boolean isGitRepo(File dir) 139 + { 140 + return dir.isDirectory() && (new File(dir, ".git").exists() || new File(dir, "HEAD").exists()); 141 + } 142 + }
+184
src/main/java/project/engine/NixEnvironment.java
··· 1 + package project.engine; 2 + 3 + import java.io.File; 4 + import java.io.IOException; 5 + import java.util.concurrent.CompletableFuture; 6 + 7 + import app.BuildOutputDialog; 8 + import app.Environment; 9 + 10 + /** 11 + * Build environment implementation for native Nix (Linux/macOS). 12 + * Runs commands via `nix develop -c bash -c "<command>"`. 13 + */ 14 + public class NixEnvironment implements BuildEnvironment 15 + { 16 + private static final String ROM_PATH = "ver/us/build/papermario.z64"; 17 + 18 + private final ProcessRunner runner = new ProcessRunner(); 19 + 20 + // --- Engine storage and git operations --- 21 + 22 + @Override 23 + public File getEngineBaseDir() 24 + { 25 + return new File(Environment.getUserStateDir(), "engine"); 26 + } 27 + 28 + @Override 29 + public void gitCloneBare(String url, File targetDir, BuildOutputListener listener) throws IOException 30 + { 31 + targetDir.getParentFile().mkdirs(); 32 + runGitCommand(null, listener, "clone", "--bare", url, targetDir.getAbsolutePath()); 33 + } 34 + 35 + @Override 36 + public void gitWorktreeAdd(File bareRepo, File worktreeDir, String ref, BuildOutputListener listener) throws IOException 37 + { 38 + worktreeDir.getParentFile().mkdirs(); 39 + runGitCommand(bareRepo, listener, "worktree", "add", worktreeDir.getAbsolutePath(), ref); 40 + } 41 + 42 + @Override 43 + public void gitFetchAll(File repo, BuildOutputListener listener) throws IOException 44 + { 45 + runGitCommand(repo, listener, "fetch", "--all"); 46 + } 47 + 48 + @Override 49 + public void gitCheckout(File dir, String ref, BuildOutputListener listener) throws IOException 50 + { 51 + runGitCommand(dir, listener, "checkout", ref); 52 + } 53 + 54 + private void runGitCommand(File workingDir, BuildOutputListener listener, String... args) throws IOException 55 + { 56 + String[] cmd = new String[args.length + 1]; 57 + cmd[0] = "git"; 58 + System.arraycopy(args, 0, cmd, 1, args.length); 59 + ProcessRunner.ProcessResult result = runner.run(cmd, workingDir, listener); 60 + if (!result.isSuccess()) { 61 + throw new IOException("Git command failed with exit code " + result.getExitCode()); 62 + } 63 + } 64 + 65 + // --- Build operations --- 66 + 67 + @Override 68 + public String getName() 69 + { 70 + return "Nix"; 71 + } 72 + 73 + @Override 74 + public BuildResult configure(File projectDir, BuildOutputListener listener) throws BuildException, IOException 75 + { 76 + validateEnvironment(projectDir); 77 + return runNixCommand(projectDir, "./configure", listener); 78 + } 79 + 80 + @Override 81 + public BuildResult build(File projectDir, BuildOutputListener listener) throws BuildException, IOException 82 + { 83 + validateEnvironment(projectDir); 84 + ProcessRunner.ProcessResult result = runNixCommandRaw(projectDir, "NINJA_STATUS='" + BuildOutputDialog.NINJA_STATUS + "' ninja", listener); 85 + 86 + if (result.wasCancelled()) { 87 + return BuildResult.cancelled(result.getDuration()); 88 + } 89 + 90 + File rom = new File(projectDir, ROM_PATH); 91 + if (result.isSuccess() && rom.exists()) { 92 + return BuildResult.success(result.getExitCode(), result.getDuration(), rom); 93 + } 94 + else { 95 + String error = result.getExitCode() == 0 ? "ROM file not found" : "Build failed with exit code " + result.getExitCode(); 96 + return BuildResult.failure(result.getExitCode(), result.getDuration(), error); 97 + } 98 + } 99 + 100 + @Override 101 + public BuildResult clean(File projectDir, BuildOutputListener listener) throws BuildException, IOException 102 + { 103 + validateEnvironment(projectDir); 104 + return runNixCommand(projectDir, "./configure --clean", listener); 105 + } 106 + 107 + @Override 108 + public CompletableFuture<BuildResult> buildAsync(File projectDir, BuildOutputListener listener) 109 + { 110 + return CompletableFuture.supplyAsync(() -> { 111 + try { 112 + return build(projectDir, listener); 113 + } 114 + catch (BuildException | IOException e) { 115 + return BuildResult.failure(-1, java.time.Duration.ZERO, e.getMessage()); 116 + } 117 + }, Environment.getExecutor()); 118 + } 119 + 120 + @Override 121 + public boolean cancel() 122 + { 123 + return runner.cancel(); 124 + } 125 + 126 + @Override 127 + public boolean isBuilding() 128 + { 129 + return runner.isRunning(); 130 + } 131 + 132 + private void validateEnvironment(File projectDir) throws BuildException 133 + { 134 + // Check for nix binary 135 + if (!isNixInstalled()) { 136 + throw new BuildException( 137 + "Nix is not installed. Please install Nix from https://nixos.org/download.html\n" + 138 + "Run: curl -L https://nixos.org/nix/install | sh -s -- --daemon"); 139 + } 140 + 141 + File flakeFile = new File(projectDir, "flake.nix"); 142 + if (!flakeFile.exists()) { 143 + throw new BuildException("No flake.nix found in project directory: " + projectDir.getAbsolutePath()); 144 + } 145 + } 146 + 147 + private boolean isNixInstalled() 148 + { 149 + try { 150 + ProcessBuilder pb = new ProcessBuilder("which", "nix"); 151 + Process process = pb.start(); 152 + int exitCode = process.waitFor(); 153 + return exitCode == 0; 154 + } 155 + catch (IOException | InterruptedException e) { 156 + return false; 157 + } 158 + } 159 + 160 + private BuildResult runNixCommand(File projectDir, String command, BuildOutputListener listener) throws IOException 161 + { 162 + ProcessRunner.ProcessResult result = runNixCommandRaw(projectDir, command, listener); 163 + 164 + if (result.wasCancelled()) { 165 + return BuildResult.cancelled(result.getDuration()); 166 + } 167 + else if (result.isSuccess()) { 168 + return BuildResult.success(result.getExitCode(), result.getDuration(), null); 169 + } 170 + else { 171 + return BuildResult.failure(result.getExitCode(), result.getDuration(), 172 + "Command failed with exit code " + result.getExitCode()); 173 + } 174 + } 175 + 176 + private ProcessRunner.ProcessResult runNixCommandRaw(File projectDir, String command, BuildOutputListener listener) throws IOException 177 + { 178 + String[] cmd = new String[] { 179 + "nix", "develop", "-c", "bash", "-c", command 180 + }; 181 + 182 + return runner.run(cmd, projectDir, listener); 183 + } 184 + }
+4 -3
src/main/java/project/ui/CreateProjectDialog.java
··· 23 23 import game.map.editor.ui.dialogs.DirChooser; 24 24 import net.miginfocom.swing.MigLayout; 25 25 import project.Project; 26 + import project.ProjectListing; 26 27 import util.Logger; 27 28 28 29 public class CreateProjectDialog extends JDialog 29 30 { 30 - private Project result = null; 31 + private ProjectListing result = null; 31 32 32 33 private JTextField nameField; 33 34 private JTextField idField; ··· 40 41 /** 41 42 * Shows the dialog and returns the created project, or null if cancelled. 42 43 */ 43 - public static Project showDialog(JFrame parent) 44 + public static ProjectListing showDialog(JFrame parent) 44 45 { 45 46 CreateProjectDialog dialog = new CreateProjectDialog(parent); 46 47 dialog.setVisible(true); ··· 134 135 add(nameField, "growx"); 135 136 136 137 JLabel idLabel = new JLabel("ID"); 137 - JLabel idDesc = new JLabel("The internal name of your mod. Must be unique between all mods."); 138 + JLabel idDesc = new JLabel("Unique internal identifier"); 138 139 idDesc.setForeground(SwingUtils.getGrayTextColor()); 139 140 SwingUtils.setFontSize(idDesc, 11); 140 141 add(idLabel, "split 2, gaptop 8");
+4 -4
src/main/java/project/ui/ProjectCellRenderer.java
··· 14 14 import javax.swing.SwingConstants; 15 15 16 16 import app.SwingUtils; 17 - import project.Project; 17 + import project.ProjectListing; 18 18 import net.miginfocom.swing.MigLayout; 19 19 20 20 /** 21 21 * Cell renderer for projects in the project list. 22 22 * Displays project name (bold), path, and last opened time. 23 23 */ 24 - public class ProjectCellRenderer extends JPanel implements ListCellRenderer<Project> 24 + public class ProjectCellRenderer extends JPanel implements ListCellRenderer<ProjectListing> 25 25 { 26 26 private final JLabel nameLabel; 27 27 private final JLabel pathLabel; ··· 49 49 50 50 @Override 51 51 public Component getListCellRendererComponent( 52 - JList<? extends Project> list, 53 - Project project, 52 + JList<? extends ProjectListing> list, 53 + ProjectListing project, 54 54 int index, 55 55 boolean isSelected, 56 56 boolean cellHasFocus)
+18 -18
src/main/java/project/ui/ProjectSwitcherDialog.java
··· 52 52 import app.Themes.Theme; 53 53 import app.config.Options; 54 54 import dev.kdl.parse.KdlParseException; 55 - import project.Project; 55 + import project.ProjectListing; 56 56 import project.ProjectManager; 57 57 import game.map.editor.ui.dialogs.ChooseDialogResult; 58 58 import game.map.editor.ui.dialogs.DirChooser; ··· 68 68 { 69 69 private final String TAB_PROJECTS = "Projects"; 70 70 71 - private JList<Project> list; 72 - private DefaultListModel<Project> listModel; 73 - private FilteredListModel<Project> filteredListModel; 71 + private JList<ProjectListing> list; 72 + private DefaultListModel<ProjectListing> listModel; 73 + private FilteredListModel<ProjectListing> filteredListModel; 74 74 private JTextField filterTextField; 75 75 private JPopupMenu contextMenu; 76 76 private CardLayout cardLayout; ··· 80 80 private final DirChooser dirChooser; 81 81 82 82 private final CountDownLatch latch = new CountDownLatch(1); 83 - private Project selectedProject = null; 83 + private ProjectListing selectedProject = null; 84 84 85 85 /** 86 - * Shows the project switcher and returns the selected project. 86 + * Shows the project switcher and returns the selected project listing. 87 87 * Blocks until the user makes a selection or closes the window. 88 - * @return The selected project, or null if cancelled 88 + * @return The selected project listing, or null if cancelled 89 89 */ 90 - public static Project showPrompt() 90 + public static ProjectListing showPrompt() 91 91 { 92 92 ProjectSwitcherDialog window = new ProjectSwitcherDialog(); 93 93 window.setVisible(true); ··· 229 229 JPanel panel = new JPanel(new MigLayout("ins 16, fill, wrap")); 230 230 231 231 // Load projects 232 - listModel = new DefaultListModel<>(); 232 + listModel = new DefaultListModel<ProjectListing>(); 233 233 refreshProjectList(); 234 234 235 235 // Create list ··· 332 332 // Buttons 333 333 JButton createButton = new JButton("New Project"); 334 334 createButton.addActionListener(e -> { 335 - Project newProject = CreateProjectDialog.showDialog(this); 335 + ProjectListing newProject = CreateProjectDialog.showDialog(this); 336 336 if (newProject != null) { 337 337 projectManager.recordProjectOpened(newProject); 338 338 refreshProjectList(); ··· 371 371 private void refreshProjectList() 372 372 { 373 373 listModel.clear(); 374 - List<Project> projects = projectManager.getRecentProjects(); 375 - for (Project project : projects) { 374 + List<ProjectListing> projects = projectManager.getRecentProjects(); 375 + for (ProjectListing project : projects) { 376 376 listModel.addElement(project); 377 377 } 378 378 } ··· 380 380 private void updateListFilter() 381 381 { 382 382 filteredListModel.setFilter(element -> { 383 - Project project = (Project) element; 383 + ProjectListing project = (ProjectListing) element; 384 384 String filterText = filterTextField.getText().toUpperCase(); 385 385 String name = project.getName().toUpperCase(); 386 386 String path = project.getPath().toUpperCase(); ··· 390 390 391 391 private void openSelectedProject() 392 392 { 393 - Project selected = list.getSelectedValue(); 393 + ProjectListing selected = list.getSelectedValue(); 394 394 if (selected == null) { 395 395 return; 396 396 } ··· 404 404 { 405 405 if (dirChooser.prompt() == ChooseDialogResult.APPROVE) { 406 406 File selectedDir = dirChooser.getSelectedFile(); 407 - Project newProject; 407 + ProjectListing newProject; 408 408 try { 409 - newProject = new Project(selectedDir); 409 + newProject = new ProjectListing(selectedDir); 410 410 } catch (IOException | KdlParseException e) { 411 411 Environment.showErrorMessage("Failed to open project", "The folder you selected is not a valid project: %s", e.getMessage()); 412 412 return; ··· 425 425 426 426 private void removeSelectedProject() 427 427 { 428 - Project selected = list.getSelectedValue(); 428 + ProjectListing selected = list.getSelectedValue(); 429 429 if (selected == null) { 430 430 return; 431 431 } ··· 450 450 451 451 private void deleteSelectedProjectFromDisk() 452 452 { 453 - Project selected = list.getSelectedValue(); 453 + ProjectListing selected = list.getSelectedValue(); 454 454 if (selected == null) { 455 455 return; 456 456 }
+1 -1
src/main/resources/database/templates/blank/project.kdl
··· 3 3 description $PROJECT_DESCRIPTION 4 4 license "CC0" 5 5 6 - engine version="0.0.0" 6 + engine ref="main"