this repo has no description
1
fork

Configure Feed

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

add CreateProjectDialog and project Manifests

+711 -289
+1
build.gradle.kts
··· 88 88 89 89 implementation("com.google.code.gson:gson:2.10.1") 90 90 implementation("org.yaml:snakeyaml:2.2") 91 + implementation("com.github.kdl-org:kdl4j:v1.0.1") 91 92 92 93 implementation("com.formdev:flatlaf:3.4.1") 93 94 implementation("com.formdev:flatlaf-intellij-themes:3.4.1")
+118
dev/kdl/KdlDocument.java
··· 1 + package dev.kdl; 2 + 3 + import jakarta.annotation.Nonnull; 4 + 5 + import java.util.ArrayList; 6 + import java.util.Collections; 7 + import java.util.List; 8 + 9 + /** 10 + * A KDL document. 11 + * 12 + * @param nodes the nodes in the document 13 + */ 14 + public record KdlDocument(@Nonnull List<KdlNode> nodes) { 15 + /** 16 + * Creates a new document with the provided nodes. 17 + * 18 + * @param nodes the nodes in the document 19 + */ 20 + public KdlDocument(@Nonnull List<KdlNode> nodes) { 21 + this.nodes = Collections.unmodifiableList(nodes); 22 + } 23 + 24 + /** 25 + * Creates a new builder to create a new document from the current one. 26 + * 27 + * @return a new builder with the nodes of this document 28 + */ 29 + @Nonnull 30 + public Builder mutate() { 31 + return new Builder(nodes); 32 + } 33 + 34 + /** 35 + * @return a new document builder 36 + */ 37 + @Nonnull 38 + public static Builder builder() { 39 + return new Builder(); 40 + } 41 + 42 + /** 43 + * A {@link KdlDocument} builder. 44 + */ 45 + public static final class Builder { 46 + 47 + private Builder() { 48 + this.nodes = new ArrayList<>(); 49 + } 50 + 51 + private Builder(List<KdlNode> nodes) { 52 + this.nodes = new ArrayList<>(nodes); 53 + } 54 + 55 + /** 56 + * Adds a node to the document being built. 57 + * 58 + * @param node a node to add 59 + * @return this builder 60 + */ 61 + @Nonnull 62 + public Builder node(@Nonnull KdlNode node) { 63 + nodes.add(node); 64 + return this; 65 + } 66 + 67 + /** 68 + * Adds a node to the document being built using a node builder. 69 + * 70 + * @param node a node builder to add 71 + * @return this builder 72 + */ 73 + @Nonnull 74 + public Builder node(@Nonnull KdlNode.Builder node) { 75 + nodes.add(node.build()); 76 + return this; 77 + } 78 + 79 + /** 80 + * Adds nodes to the document being built. 81 + * 82 + * @param nodes nodes to add 83 + * @return this builder 84 + */ 85 + @Nonnull 86 + public Builder nodes(@Nonnull KdlNode... nodes) { 87 + Collections.addAll(this.nodes, nodes); 88 + return this; 89 + } 90 + 91 + /** 92 + * Adds nodes to the document being built using node builders. 93 + * 94 + * @param nodes node builders to add 95 + * @return this builder 96 + */ 97 + @Nonnull 98 + public Builder nodes(@Nonnull KdlNode.Builder... nodes) { 99 + for (var node : nodes) { 100 + this.nodes.add(node.build()); 101 + } 102 + return this; 103 + } 104 + 105 + /** 106 + * Creates a new document. 107 + * 108 + * @return a new KDL document 109 + */ 110 + @Nonnull 111 + public KdlDocument build() { 112 + return new KdlDocument(nodes); 113 + } 114 + 115 + @Nonnull 116 + private final List<KdlNode> nodes; 117 + } 118 + }
+21
gradle/verification-metadata.xml
··· 107 107 <sha256 value="3cb3886b97df6e066f108c316b219f262c97c3cb2df6da78927e645deb643cb0" origin="Generated by Gradle"/> 108 108 </artifact> 109 109 </component> 110 + <component group="com.github.kdl-org" name="kdl4j" version="v1.0.1"> 111 + <artifact name="kdl4j-v1.0.1.jar"> 112 + <sha256 value="21c45216d7cac4d673f43aad728860d2e37d0daccdc33d2655fbcb87ab239b54" origin="Generated by Gradle"/> 113 + </artifact> 114 + <artifact name="kdl4j-v1.0.1.module"> 115 + <sha256 value="6477f6e682463be5bd48baec84761ce06547a11a3460ba0bcb1a8dcd59520bba" origin="Generated by Gradle"/> 116 + </artifact> 117 + </component> 110 118 <component group="com.github.oshi" name="oshi-core" version="5.7.1"> 111 119 <artifact name="oshi-core-5.7.1.jar"> 112 120 <sha256 value="e7303b63a03d71c8f5acc087425b60fcb9ede170f45f844d92071fd489725cdc" origin="Generated by Gradle"/> ··· 332 340 <component group="io.netty" name="netty-bom" version="4.1.86.Final"> 333 341 <artifact name="netty-bom-4.1.86.Final.pom"> 334 342 <sha256 value="12716c1fe64cf5bdaa7044d37d13aae3a88821b91d4798420c46a74769178afd" origin="Generated by Gradle"/> 343 + </artifact> 344 + </component> 345 + <component group="jakarta.annotation" name="jakarta.annotation-api" version="3.0.0"> 346 + <artifact name="jakarta.annotation-api-3.0.0.jar"> 347 + <sha256 value="b01f55552284cfb149411e64eabca75e942d26d2e1786b32914250e4330afaa2" origin="Generated by Gradle"/> 348 + </artifact> 349 + <artifact name="jakarta.annotation-api-3.0.0.pom"> 350 + <sha256 value="9fc66a87375df8443aba6bdcc1d4ff07f126542583785a482a9262a19bfe8eae" origin="Generated by Gradle"/> 335 351 </artifact> 336 352 </component> 337 353 <component group="jakarta.platform" name="jakarta.jakartaee-bom" version="9.0.0"> ··· 633 649 <component group="org.eclipse.ee4j" name="project" version="1.0.6"> 634 650 <artifact name="project-1.0.6.pom"> 635 651 <sha256 value="4e7d8329d8da7dcf30779d824241be145f27108932f5a5a24eb907677bc8d72d" origin="Generated by Gradle"/> 652 + </artifact> 653 + </component> 654 + <component group="org.eclipse.ee4j" name="project" version="1.0.9"> 655 + <artifact name="project-1.0.9.pom"> 656 + <sha256 value="825379934a1cf29249f349f2d18adaf79a7b2cb2dbe231518974e1ee70941c17" origin="Generated by Gradle"/> 636 657 </artifact> 637 658 </component> 638 659 <component group="org.eclipse.jetty" name="jetty-bom" version="9.4.50.v20221201">
+1
src/main/java/app/Directories.java
··· 16 16 DATABASE (Root.CONFIG, "/database/"), 17 17 DATABASE_EDITOR (Root.CONFIG, DATABASE, "/editor/"), 18 18 DATABASE_THEMES (Root.CONFIG, DATABASE, "/themes/"), 19 + DATABASE_TEMPLATES (Root.CONFIG, DATABASE, "/templates/"), 19 20 20 21 LOGS (Root.STATE, "/logs/"), 21 22 TEMP (Root.STATE, "/temp/"),
+73 -115
src/main/java/app/Environment.java
··· 21 21 import java.util.concurrent.ExecutorService; 22 22 import java.util.concurrent.Executors; 23 23 import java.util.jar.Attributes; 24 - import java.util.jar.Manifest; 25 24 import java.util.regex.Matcher; 26 25 import java.util.regex.Pattern; 27 26 ··· 45 44 import app.input.IOUtils; 46 45 import project.Project; 47 46 import project.ProjectManager; 48 - import project.ProjectValidator; 47 + import project.Manifest; 49 48 import project.ui.ProjectSwitcherDialog; 50 49 import assets.AssetExtractor; 51 50 import assets.ExpectedAsset; 51 + import dev.kdl.parse.KdlParseException; 52 52 import game.ProjectDatabase; 53 53 import game.entity.EntityExtractor; 54 54 import game.map.editor.ui.dialogs.ChooseDialogResult; ··· 92 92 public static Config mainConfig = null; 93 93 public static Config projectConfig = null; 94 94 95 - private static DirChooser projectChooser; 96 - private static File projectDirectory = null; 95 + private static Project project = null; 97 96 98 97 private static String gameVersion = ""; 99 98 ··· 192 191 if (fromJar) { 193 192 ClassLoader cl = Environment.class.getClassLoader(); 194 193 try { 195 - Manifest manifest = new Manifest(cl.getResourceAsStream("META-INF/MANIFEST.MF")); 194 + var manifest = new java.util.jar.Manifest(cl.getResourceAsStream("META-INF/MANIFEST.MF")); 196 195 Attributes attr = manifest.getMainAttributes(); 197 196 198 197 versionString = attr.getValue("App-Version"); ··· 227 226 } 228 227 } 229 228 230 - projectChooser = new DirChooser(codeSource.getParentFile(), "Select Project Directory"); 231 - 232 229 // Create user directories 233 230 getUserConfigDir().mkdirs(); 234 231 getUserStateDir().mkdirs(); ··· 260 257 checkForUpdate(); 261 258 } 262 259 263 - File projDir = chooseProjectDir(); 264 - if (projDir == null) 265 - exit(); 266 - 267 - LoadingBar.show("Loading Project", true); 268 - boolean validProject = loadProject(projDir); 269 - if (!validProject) 270 - exit(); 260 + try { 261 + Project project = chooseProject(); 262 + if (project == null) 263 + exit(); 264 + LoadingBar.show("Loading " + project.getName(), true); 265 + boolean validProject = loadProject(project); 266 + if (!validProject) 267 + exit(1); 268 + } catch (IOException | KdlParseException e) { 269 + showErrorMessage("Failed To Load Project", e.getMessage()); 270 + exit(1); 271 + } 271 272 } 272 273 catch (Throwable t) { 273 274 StarRodMain.handleEarlyCrash(t); ··· 328 329 return new File("."); 329 330 } 330 331 332 + public static Project getProject() 333 + { 334 + return project; 335 + } 336 + 337 + @Deprecated 331 338 public static File getProjectDirectory() 332 339 { 333 - return projectDirectory; 340 + return project.getDirectory(); 334 341 } 335 342 343 + @Deprecated 336 344 public static File getSourceDirectory() 337 345 { 338 - return new File(projectDirectory, "/src/"); 346 + return new File(project.getDirectory(), "/src/"); 339 347 } 340 348 349 + @Deprecated 341 350 public static File getProjectFile(String relativePath) 342 351 { 343 - return new File(projectDirectory, relativePath); 352 + return new File(project.getDirectory(), relativePath); 344 353 } 345 354 346 355 public static void checkForUpdate() ··· 424 433 return new File(dotConfig, "/star-rod/"); 425 434 } 426 435 436 + public static final File getUserDocumentsDir() 437 + { 438 + String userHome = System.getProperty("user.home"); 439 + 440 + if (isWindows()) 441 + return new File(System.getenv("USERPROFILE"), "Documents"); 442 + 443 + if (isMacOS()) 444 + return new File(userHome, "Documents"); 445 + 446 + // Linux: XDG_DOCUMENTS_DIR, fallback to ~/Documents 447 + String xdgDocsDir = System.getenv("XDG_DOCUMENTS_DIR"); 448 + if (xdgDocsDir != null && !xdgDocsDir.isEmpty()) 449 + return new File(xdgDocsDir); 450 + return new File(userHome, "Documents"); 451 + } 452 + 427 453 public static final File getUserStateDir() 428 454 { 429 455 String userHome = System.getProperty("user.home"); ··· 466 492 } 467 493 } 468 494 469 - private static File chooseProjectDir() throws IOException 495 + private static Project chooseProject() throws IOException, KdlParseException 470 496 { 471 - // if current directory seems to be a decomp project, use it 472 - if (ProjectValidator.isCurrentDirectoryProject()) { 473 - return new File("."); 497 + // Search current directory and its parents for a project manifest 498 + File currentDir = new File("."); 499 + while (currentDir != null) { 500 + File projectDir = new File(currentDir, Manifest.FILENAME); 501 + if (projectDir.isFile()) { 502 + return new Project(projectDir); 503 + } 504 + currentDir = currentDir.getParentFile(); 474 505 } 475 506 476 - // show project switcher to select a project 507 + // Show project switcher to select a project 477 508 if (commandLine) { 478 509 Logger.logError("CWD is not a valid project. Please run Star Rod from a project."); 479 510 return null; 480 511 } 481 - Project selected = ProjectSwitcherDialog.showPrompt(); 482 - if (selected != null) { 483 - return selected.getPath(); 484 - } 485 - return null; 486 - } 487 - 488 - public static void promptChangeProject() throws IOException 489 - { 490 - if (projectChooser.prompt() == ChooseDialogResult.APPROVE) { 491 - File dirChoice = projectChooser.getSelectedFile(); 492 - loadProject(dirChoice); 493 - } 494 - } 495 - 496 - private static File promptSelectProject() 497 - { 498 - if (projectChooser.prompt() == ChooseDialogResult.APPROVE) 499 - return projectChooser.getSelectedFile(); 500 - else 501 - return null; 502 - } 503 - 504 - private static void showErrorMessage(String title, String fmt, Object ... args) 505 - { 506 - String message = String.format(fmt, args); 507 - if (isCommandLine()) 508 - Logger.logError(message); 509 - else 510 - SwingUtils.getErrorDialog() 511 - .setTitle(title) 512 - .setMessage(message) 513 - .show(); 512 + return ProjectSwitcherDialog.showPrompt(); 514 513 } 515 514 516 - public static boolean loadProject(File projectDir) throws IOException 515 + public static boolean loadProject(Project newProject) throws IOException 517 516 { 518 - if (projectDir == null) { 519 - showErrorMessage("Invalid Decomp Project", "No project directory is set."); 520 - return false; 521 - } 522 - 523 - if (!projectDir.exists() || !projectDir.isDirectory()) { 524 - showErrorMessage("Invalid Decomp Project", "Not a valid directory: %n%s", projectDir.getAbsolutePath()); 525 - return false; 526 - } 527 - 528 - // check version to get appropriate splat 529 - gameVersion = mainConfig.getString(Options.GameVersion); 530 - File versionDir = new File(projectDir, "ver/" + gameVersion); 531 - if (!versionDir.exists()) { 532 - showErrorMessage("Invalid Decomp Project", 533 - "Project does not have game version: %s", gameVersion); 534 - return false; 535 - } 536 - 537 - // get splat config 538 - File decompCfg = new File(versionDir, FN_SPLAT); 539 - if (!decompCfg.exists()) { 540 - showErrorMessage("Invalid Decomp Project", 541 - "Could not find splat file for directory: %n%s", decompCfg.getAbsolutePath()); 542 - return false; 543 - } 544 - 545 - // resolve asset dirs 546 - try { 547 - assetDirectories = getAssetDirs(projectDir, decompCfg); 548 - } 549 - catch (IOException e) { 550 - Logger.printStackTrace(e); 551 - showErrorMessage("Splat Read Exception", 552 - "IOException while attempting to read splat file: %n%s %n%s", decompCfg.getAbsolutePath(), 553 - e.getMessage()); 554 - return false; 555 - } 517 + project = newProject; 556 518 519 + // TODO: get similar to classic 520 + /* 557 521 // get US baserom 558 - usBaseRom = new File(projectDir, FN_BASEROM); 522 + usBaseRom = new File(project.getDirectory(), FN_BASEROM); 559 523 if (!usBaseRom.exists()) { 560 524 showErrorMessage("Missing US Base ROM", 561 525 "Could not find US baserom for project. %n" + 562 526 "Star Rod requries one for asset extraction."); 563 527 return false; 564 528 } 529 + */ 565 530 566 531 // save project dir 567 - projectDirectory = projectDir; 568 - SwingUtilities.invokeLater(() -> { 569 - projectChooser.setCurrentDirectory(projectDir); 570 - }); 571 - Directories.setProjectDirectory(projectDirectory.getAbsolutePath()); 532 + Directories.setProjectDirectory(project.getPath()); 572 533 573 - readProjectConfig(); 574 534 reloadIcons(); 575 535 576 536 ProjectDatabase.initialize(); ··· 591 551 AssetExtractor.extractAll(); 592 552 593 553 // Record that this project was opened 594 - ProjectManager.getInstance().recordProjectOpened(projectDirectory); 554 + ProjectManager.getInstance().recordProjectOpened(project); 595 555 596 556 return true; 597 557 } 598 558 599 - private static void readProjectConfig() throws IOException 559 + public static void showErrorMessage(String title, String fmt, Object ... args) 600 560 { 601 - File configFile = new File(projectDirectory, FN_PROJ_CONFIG); 602 - 603 - if (!configFile.exists()) { 604 - projectConfig = makeConfig(configFile, Scope.Project); 605 - projectConfig.saveConfigFile(); 606 - } 607 - else { 608 - // config exists, read it 609 - projectConfig = new Config(configFile, Scope.Project); 610 - projectConfig.readConfig(); 611 - } 561 + String message = String.format(fmt, args); 562 + if (isCommandLine()) 563 + Logger.logError(message); 564 + else 565 + SwingUtils.getErrorDialog() 566 + .setTitle(title) 567 + .setMessage(message) 568 + .setOptions("OK") 569 + .show(); 612 570 } 613 571 614 572 private static Config makeConfig(File configFile, Scope scope) throws IOException
+2 -1
src/main/java/app/config/Options.java
··· 20 20 GameVersion (true, Scope.Main, Type.String, "GameVersion", "us"), 21 21 22 22 LogDetails (true, Scope.Main, Type.Boolean, "LogDetails", "false"), 23 - Theme (true, Scope.Main, Type.String, "Theme", "FlatDark"), 23 + Theme (false, Scope.Main, Type.String, "Theme", ""), 24 24 ExitToMenu (true, Scope.Main, Type.Boolean, "ExitToMenu", "true"), 25 25 CheckForUpdates (true, Scope.Main, Type.Boolean, "CheckForUpdates", "true"), 26 + ProjectsDir (false, Scope.Main, Type.String, "ProjectsDir", ""), 26 27 27 28 ExtractedMapData (true, Scope.Project, Type.Boolean, "ExtractedMapData", "false"), 28 29
+10 -10
src/main/java/project/JsonProjectRepository.java
··· 15 15 import com.google.gson.reflect.TypeToken; 16 16 17 17 import app.Environment; 18 + import dev.kdl.parse.KdlParseException; 18 19 import util.Logger; 19 20 20 21 /** ··· 48 49 49 50 while (iter.hasNext()) { 50 51 ProjectData data = iter.next(); 51 - File path = new File(data.path); 52 52 53 - // Remove invalid entries 54 - if (!path.exists()) { 53 + try { 54 + projects.add(new Project(new File(data.path), data.lastOpened)); 55 + } catch (IOException | KdlParseException e) { 56 + Logger.logWarning("Ignoring invalid project: " + data.path); 55 57 iter.remove(); 56 58 modified = true; 57 59 continue; 58 60 } 59 - 60 - projects.add(new Project(path, data.lastOpened)); 61 61 } 62 62 63 63 // Save if we removed any invalid entries ··· 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().getAbsolutePath(); 79 + String absolutePath = project.getPath(); 80 80 dataList.removeIf(data -> data.path.equals(absolutePath)); 81 81 82 82 // Add new entry ··· 89 89 } 90 90 91 91 @Override 92 - public synchronized void removeProject(File projectPath) 92 + public synchronized void removeProject(Project project) 93 93 { 94 94 List<ProjectData> dataList = loadProjectData(); 95 - String absolutePath = projectPath.getAbsolutePath(); 95 + String absolutePath = project.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(File projectPath) 101 + public synchronized void updateLastOpened(Project project) 102 102 { 103 103 List<ProjectData> dataList = loadProjectData(); 104 - String absolutePath = projectPath.getAbsolutePath(); 104 + String absolutePath = project.getPath(); 105 105 106 106 for (ProjectData data : dataList) { 107 107 if (data.path.equals(absolutePath)) {
+39
src/main/java/project/Manifest.java
··· 1 + package project; 2 + 3 + import java.io.File; 4 + import java.io.IOException; 5 + import java.nio.file.Path; 6 + 7 + import dev.kdl.KdlDocument; 8 + import dev.kdl.parse.KdlParseException; 9 + import dev.kdl.parse.KdlParser; 10 + 11 + /** A project.kdl file. Mutations are automatically saved to disk. */ 12 + public class Manifest { 13 + public static final String FILENAME = "project.kdl"; 14 + 15 + private final File file; 16 + private final KdlDocument doc; 17 + 18 + public Manifest(Project project) throws IOException, KdlParseException { 19 + file = new File(project.getPath(), FILENAME); 20 + 21 + if (!file.exists()) { 22 + throw new IOException(FILENAME + " does not exist"); 23 + } 24 + 25 + var parser = KdlParser.v2(); 26 + doc = parser.parse(Path.of(file.getAbsolutePath())); 27 + } 28 + public String toString() { 29 + return "Manifest(" + file.getPath() + ")"; 30 + } 31 + 32 + public String getName() { 33 + return doc.nodes().stream() 34 + .filter(n -> n.name().equals("name")) 35 + .findFirst() 36 + .map(n -> n.arguments().get(0).value().toString()) 37 + .orElse(file.getParentFile().getName()); 38 + } 39 + }
+59 -23
src/main/java/project/Project.java
··· 1 1 package project; 2 2 3 + import static app.Directories.DATABASE_TEMPLATES; 4 + 3 5 import java.io.File; 4 - import java.util.Objects; 6 + import java.io.IOException; 7 + 8 + import org.apache.commons.io.FileUtils; 9 + 10 + import dev.kdl.parse.KdlParseException; 5 11 6 - /** 7 - * Immutable data class representing a Star Rod project. 8 - * Stores the project path and last opened timestamp. 9 - */ 10 12 public class Project implements Comparable<Project> 11 13 { 12 - private final File path; 13 - private final long lastOpened; 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; 14 17 15 - public Project(File path, long lastOpened) 18 + /** Loads a project from a directory. */ 19 + public Project(File path, long lastOpened) throws IOException, KdlParseException 16 20 { 17 - Objects.requireNonNull(path, "Project path cannot be null"); 18 - this.path = path.getAbsoluteFile(); 21 + if (!path.isDirectory()) 22 + throw new IllegalArgumentException("Project path must be a directory: " + path); 23 + this.directory = path.getAbsoluteFile(); 19 24 this.lastOpened = lastOpened; 25 + this.manifest = new Manifest(this); 20 26 } 21 27 22 - public Project(File path) 28 + /** Loads a project from a directory. */ 29 + public Project(File path) throws IOException, KdlParseException 23 30 { 24 31 this(path, System.currentTimeMillis()); 25 32 } 26 33 27 - public File getPath() 34 + public String getPath() 28 35 { 29 - return path; 36 + return directory.getPath(); 30 37 } 31 38 32 - public String getName() 39 + public File getDirectory() 33 40 { 34 - return path.getName(); 41 + return directory; 35 42 } 36 43 37 44 public long getLastOpened() ··· 39 46 return lastOpened; 40 47 } 41 48 42 - /** 43 - * Creates a new Project instance with updated lastOpened timestamp. 44 - */ 45 - public Project withLastOpened(long timestamp) 49 + public String getName() 46 50 { 47 - return new Project(path, timestamp); 51 + return manifest.getName(); 52 + } 53 + 54 + public Manifest getManifest() 55 + { 56 + return manifest; 57 + } 58 + 59 + /** Creates a new project from a template. */ 60 + public static Project create(File path, String template, String id, String name) throws IOException, KdlParseException 61 + { 62 + if (!path.exists()) 63 + path.mkdirs(); 64 + if (!path.isDirectory()) 65 + throw new IllegalArgumentException("Project path must be a directory: " + path); 66 + 67 + // Copy entire template directory here 68 + File templateDir = DATABASE_TEMPLATES.file(template); 69 + if (!templateDir.exists()) 70 + throw new IllegalArgumentException("Missing template: " + templateDir.getPath()); 71 + FileUtils.copyDirectory(templateDir, path); 72 + 73 + // Substitute placeholders in project.kdl 74 + File manifestFile = new File(path, Manifest.FILENAME); 75 + if (manifestFile.exists()) { 76 + 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", ""); 80 + FileUtils.writeStringToFile(manifestFile, content, "UTF-8"); 81 + } 82 + 83 + return new Project(path); 48 84 } 49 85 50 86 @Override ··· 62 98 if (obj == null || getClass() != obj.getClass()) 63 99 return false; 64 100 Project other = (Project) obj; 65 - return path.equals(other.path); 101 + return directory.equals(other.directory); 66 102 } 67 103 68 104 @Override 69 105 public int hashCode() 70 106 { 71 - return path.hashCode(); 107 + return directory.hashCode(); 72 108 } 73 109 74 110 @Override 75 111 public String toString() 76 112 { 77 - return getName() + " (" + path.getAbsolutePath() + ")"; 113 + return getName() + " (" + directory.getAbsolutePath() + ")"; 78 114 } 79 115 }
+11 -41
src/main/java/project/ProjectManager.java
··· 6 6 7 7 import org.apache.commons.io.FileUtils; 8 8 9 - import app.Environment; 10 9 import util.Logger; 11 10 12 11 /** ··· 46 45 47 46 /** 48 47 * Records that a project was opened (adds or updates its timestamp). 49 - * @param projectPath The path to the project 50 48 */ 51 - public void recordProjectOpened(File projectPath) 49 + public void recordProjectOpened(Project project) 52 50 { 53 - repository.updateLastOpened(projectPath); 51 + repository.updateLastOpened(project); 54 52 } 55 53 56 54 /** ··· 60 58 */ 61 59 public void removeFromHistory(Project project) 62 60 { 63 - repository.removeProject(project.getPath()); 61 + repository.removeProject(project); 64 62 } 65 63 66 64 /** ··· 70 68 */ 71 69 public boolean deleteFromDisk(Project project) 72 70 { 73 - File projectDir = project.getPath(); 71 + File projectDir = new File(project.getPath()); 74 72 75 - // First remove from history 76 - repository.removeProject(projectDir); 73 + repository.removeProject(project); 77 74 78 - // Then delete from disk 79 - if (projectDir.exists()) { 80 - try { 81 - FileUtils.deleteDirectory(projectDir); 82 - Logger.log("Deleted project directory: " + projectDir.getAbsolutePath()); 83 - return true; 84 - } 85 - catch (IOException e) { 86 - Logger.logError("Failed to delete project: " + e.getMessage()); 87 - return false; 88 - } 75 + try { 76 + FileUtils.deleteDirectory(projectDir); 77 + return true; 89 78 } 90 - return true; // Already doesn't exist 91 - } 92 - 93 - /** 94 - * Checks if a directory is a valid Star Rod project. 95 - */ 96 - public boolean isValidProject(File dir) 97 - { 98 - return ProjectValidator.isValidProject(dir); 99 - } 100 - 101 - /** 102 - * Loads a project using Environment.loadProject(). 103 - * @param projectPath The path to the project 104 - * @return true if the project was loaded successfully 105 - */ 106 - public boolean openProject(File projectPath) throws IOException 107 - { 108 - boolean success = Environment.loadProject(projectPath); 109 - if (success) { 110 - recordProjectOpened(projectPath); 79 + catch (IOException e) { 80 + Logger.logError("Failed to delete project: " + e.getMessage()); 81 + return false; 111 82 } 112 - return success; 113 83 } 114 84 }
+3 -5
src/main/java/project/ProjectRepository.java
··· 22 22 23 23 /** 24 24 * Removes a project from the repository. 25 - * @param projectPath The path of the project to remove 26 25 */ 27 - void removeProject(File projectPath); 26 + void removeProject(Project project); 28 27 29 28 /** 30 29 * Updates the last opened timestamp for a project. 31 - * @param projectPath The path of the project to update 32 - */ 33 - void updateLastOpened(File projectPath); 30 + */ 31 + void updateLastOpened(Project project); 34 32 }
-54
src/main/java/project/ProjectValidator.java
··· 1 - package project; 2 - 3 - import java.io.File; 4 - 5 - import app.Environment; 6 - import app.config.Options; 7 - 8 - /** 9 - * Validates whether a directory is a valid Star Rod project. 10 - * A valid project must have a splat.yaml file in ver/{gameVersion}/. 11 - */ 12 - public class ProjectValidator 13 - { 14 - private static final String FN_SPLAT = "splat.yaml"; 15 - 16 - /** 17 - * Checks if a directory is a valid Star Rod project. 18 - * @param dir The directory to check 19 - * @return true if the directory contains a valid project structure 20 - */ 21 - public static boolean isValidProject(File dir) 22 - { 23 - if (dir == null || !dir.exists() || !dir.isDirectory()) { 24 - return false; 25 - } 26 - 27 - // Get game version from config (default "us") 28 - String gameVersion = "us"; 29 - if (Environment.mainConfig != null) { 30 - String configVersion = Environment.mainConfig.getString(Options.GameVersion); 31 - if (configVersion != null && !configVersion.isEmpty()) { 32 - gameVersion = configVersion; 33 - } 34 - } 35 - 36 - // Check for splat.yaml in ver/{gameVersion}/ 37 - File versionDir = new File(dir, "ver/" + gameVersion); 38 - if (!versionDir.exists() || !versionDir.isDirectory()) { 39 - return false; 40 - } 41 - 42 - File splatFile = new File(versionDir, FN_SPLAT); 43 - return splatFile.exists(); 44 - } 45 - 46 - /** 47 - * Checks if the current working directory is a valid Star Rod project. 48 - * @return true if cwd contains a valid project structure 49 - */ 50 - public static boolean isCurrentDirectoryProject() 51 - { 52 - return isValidProject(new File(".")); 53 - } 54 - }
+327
src/main/java/project/ui/CreateProjectDialog.java
··· 1 + package project.ui; 2 + 3 + import java.awt.event.WindowEvent; 4 + import java.io.File; 5 + import java.io.IOException; 6 + 7 + import javax.swing.JButton; 8 + import javax.swing.JDialog; 9 + import javax.swing.JFrame; 10 + import javax.swing.JLabel; 11 + import javax.swing.JPanel; 12 + import javax.swing.JTextField; 13 + import javax.swing.SwingUtilities; 14 + import javax.swing.WindowConstants; 15 + import javax.swing.event.DocumentEvent; 16 + import javax.swing.event.DocumentListener; 17 + 18 + import app.Environment; 19 + import app.SwingUtils; 20 + import app.config.Options; 21 + import dev.kdl.parse.KdlParseException; 22 + import game.map.editor.ui.dialogs.ChooseDialogResult; 23 + import game.map.editor.ui.dialogs.DirChooser; 24 + import net.miginfocom.swing.MigLayout; 25 + import project.Project; 26 + import util.Logger; 27 + 28 + public class CreateProjectDialog extends JDialog 29 + { 30 + private Project result = null; 31 + 32 + private JTextField nameField; 33 + private JTextField idField; 34 + private JTextField pathField; 35 + private JButton createButton; 36 + 37 + private boolean idManuallyEdited = false; 38 + private File browsedDir = null; 39 + 40 + /** 41 + * Shows the dialog and returns the created project, or null if cancelled. 42 + */ 43 + public static Project showDialog(JFrame parent) 44 + { 45 + CreateProjectDialog dialog = new CreateProjectDialog(parent); 46 + dialog.setVisible(true); 47 + return dialog.result; 48 + } 49 + 50 + private CreateProjectDialog(JFrame parent) 51 + { 52 + super(parent); 53 + 54 + nameField = new JTextField(); 55 + nameField.setMargin(SwingUtils.TEXTBOX_INSETS); 56 + 57 + idField = new JTextField(); 58 + idField.setMargin(SwingUtils.TEXTBOX_INSETS); 59 + 60 + pathField = new JTextField(); 61 + pathField.setMargin(SwingUtils.TEXTBOX_INSETS); 62 + pathField.setEditable(false); 63 + 64 + // Auto-generate ID from Name 65 + nameField.getDocument().addDocumentListener(new DocumentListener() { 66 + @Override 67 + public void changedUpdate(DocumentEvent e) 68 + { 69 + onNameChanged(); 70 + } 71 + 72 + @Override 73 + public void insertUpdate(DocumentEvent e) 74 + { 75 + onNameChanged(); 76 + } 77 + 78 + @Override 79 + public void removeUpdate(DocumentEvent e) 80 + { 81 + onNameChanged(); 82 + } 83 + }); 84 + 85 + // Track manual edits to ID 86 + idField.getDocument().addDocumentListener(new DocumentListener() { 87 + @Override 88 + public void changedUpdate(DocumentEvent e) 89 + { 90 + onIdChanged(); 91 + } 92 + 93 + @Override 94 + public void insertUpdate(DocumentEvent e) 95 + { 96 + onIdChanged(); 97 + } 98 + 99 + @Override 100 + public void removeUpdate(DocumentEvent e) 101 + { 102 + onIdChanged(); 103 + } 104 + }); 105 + 106 + // Browse button 107 + JButton browseButton = new JButton("Browse..."); 108 + SwingUtils.addBorderPadding(browseButton); 109 + browseButton.addActionListener(e -> browseForPath()); 110 + 111 + // Create and Cancel buttons 112 + createButton = new JButton("Create"); 113 + SwingUtils.addBorderPadding(createButton); 114 + createButton.setEnabled(false); 115 + createButton.addActionListener(e -> createProject()); 116 + 117 + JButton cancelButton = new JButton("Cancel"); 118 + SwingUtils.addBorderPadding(cancelButton); 119 + cancelButton.addActionListener(e -> setVisible(false)); 120 + 121 + setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); 122 + addWindowListener(new java.awt.event.WindowAdapter() { 123 + @Override 124 + public void windowClosing(WindowEvent e) 125 + { 126 + setVisible(false); 127 + } 128 + }); 129 + 130 + setLayout(new MigLayout("ins 16, wrap", "[grow]")); 131 + 132 + JLabel nameLabel = new JLabel("Name"); 133 + add(nameLabel, ""); 134 + add(nameField, "growx"); 135 + 136 + JLabel idLabel = new JLabel("ID"); 137 + JLabel idDesc = new JLabel("The internal name of your mod. Must be unique between all mods."); 138 + idDesc.setForeground(SwingUtils.getGrayTextColor()); 139 + SwingUtils.setFontSize(idDesc, 11); 140 + add(idLabel, "split 2, gaptop 8"); 141 + add(idDesc, "ax right, pushx"); 142 + add(idField, "growx"); 143 + 144 + JLabel pathLabel = new JLabel("Path"); 145 + add(pathLabel, "gaptop 8"); 146 + add(pathField, "split 2, growx"); 147 + add(browseButton, ""); 148 + 149 + add(new JPanel(), "growx, sg but, split 3, gaptop 12"); 150 + add(createButton, "growx, sg but"); 151 + add(cancelButton, "growx, sg but"); 152 + 153 + updatePath(); 154 + validate_(); 155 + 156 + pack(); 157 + setResizable(false); 158 + 159 + setTitle("New Project"); 160 + setIconImage(Environment.getDefaultIconImage()); 161 + setLocationRelativeTo(parent); 162 + setModal(true); 163 + nameField.requestFocusInWindow(); 164 + 165 + SwingUtilities.invokeLater(() -> { 166 + getRootPane().setDefaultButton(createButton); 167 + nameField.requestFocusInWindow(); 168 + }); 169 + } 170 + 171 + private boolean updatingId = false; 172 + 173 + private void onNameChanged() 174 + { 175 + if (!idManuallyEdited) { 176 + updatingId = true; 177 + idField.setText(toSnakeCase(nameField.getText())); 178 + updatingId = false; 179 + } 180 + updatePath(); 181 + validate_(); 182 + } 183 + 184 + private void onIdChanged() 185 + { 186 + if (!updatingId) { 187 + idManuallyEdited = !idField.getText().isEmpty(); 188 + } 189 + updatePath(); 190 + validate_(); 191 + } 192 + 193 + private void updatePath() 194 + { 195 + String id = getEffectiveId(); 196 + File dir; 197 + 198 + if (browsedDir != null) { 199 + dir = id.isEmpty() ? browsedDir : new File(browsedDir, id); 200 + } 201 + else { 202 + File projectsDir = getDefaultProjectsDir(); 203 + dir = id.isEmpty() ? projectsDir : new File(projectsDir, id); 204 + } 205 + 206 + pathField.setText(abbreviateHome(dir.getAbsolutePath())); 207 + } 208 + 209 + private String getEffectiveId() 210 + { 211 + String id = idField.getText().trim(); 212 + if (id.isEmpty()) 213 + return toSnakeCase(nameField.getText()); 214 + return id; 215 + } 216 + 217 + private File getProjectPath() 218 + { 219 + String id = getEffectiveId(); 220 + if (browsedDir != null) { 221 + return id.isEmpty() ? browsedDir : new File(browsedDir, id); 222 + } 223 + File projectsDir = getDefaultProjectsDir(); 224 + return id.isEmpty() ? projectsDir : new File(projectsDir, id); 225 + } 226 + 227 + private void browseForPath() 228 + { 229 + DirChooser dirChooser = new DirChooser(getDefaultProjectsDir(), "Select Project Location"); 230 + if (dirChooser.prompt() == ChooseDialogResult.APPROVE) { 231 + File selected = dirChooser.getSelectedFile(); 232 + String[] contents = selected.list(); 233 + if (contents != null && contents.length > 0) { 234 + // Directory has files, use it as parent 235 + browsedDir = selected; 236 + } 237 + else { 238 + // Empty directory, use it directly 239 + browsedDir = selected.getParentFile(); 240 + // If the selected dir name matches the id, just use parent as browsedDir 241 + String id = getEffectiveId(); 242 + if (!selected.getName().equals(id)) { 243 + browsedDir = selected; 244 + } 245 + } 246 + updatePath(); 247 + validate_(); 248 + } 249 + } 250 + 251 + private void validate_() 252 + { 253 + String name = nameField.getText().trim(); 254 + String id = getEffectiveId(); 255 + File path = getProjectPath(); 256 + 257 + String error = null; 258 + 259 + if (name.isEmpty()) { 260 + error = "Enter a project name"; 261 + } 262 + else if (id.isEmpty()) { 263 + error = "Enter a project ID"; 264 + } 265 + else if (!id.matches("[a-z]*[a-z0-9_]*")) { 266 + error = "ID must contain only lowercase letters, digits, and underscores, and must start with a letter"; 267 + } 268 + else if (new File(path, "project.kdl").exists()) { 269 + error = "A project already exists at this location"; 270 + } 271 + 272 + if (error != null) { 273 + createButton.setToolTipText(error); 274 + createButton.setEnabled(false); 275 + } 276 + else { 277 + createButton.setToolTipText(null); 278 + createButton.setEnabled(true); 279 + } 280 + } 281 + 282 + private void createProject() 283 + { 284 + File path = getProjectPath(); 285 + String id = getEffectiveId(); 286 + String name = nameField.getText().trim(); 287 + 288 + try { 289 + result = Project.create(path, "blank", id, name); 290 + setVisible(false); 291 + } 292 + catch (IOException | KdlParseException e) { 293 + Logger.logError("Failed to create project: " + e.getMessage()); 294 + Environment.showErrorMessage("Failed to create project", "%s", e.getMessage()); 295 + } 296 + } 297 + 298 + private static File getDefaultProjectsDir() 299 + { 300 + String configured = Environment.mainConfig.getString(Options.ProjectsDir); 301 + if (configured != null && !configured.isEmpty()) { 302 + return new File(configured); 303 + } 304 + 305 + File docs = Environment.getUserDocumentsDir(); 306 + String subdir = Environment.isLinux() ? "starrod" : "Star Rod"; 307 + return new File(docs, subdir); 308 + } 309 + 310 + static String toSnakeCase(String input) 311 + { 312 + return input.trim() 313 + .toLowerCase() 314 + .replaceAll("[^a-z0-9]+", "_") 315 + .replaceAll("_+", "_") 316 + .replaceAll("^_|_$", ""); 317 + } 318 + 319 + private static String abbreviateHome(String path) 320 + { 321 + String home = System.getProperty("user.home"); 322 + if (home != null && path.startsWith(home)) { 323 + return "~" + path.substring(home.length()); 324 + } 325 + return path; 326 + } 327 + }
+1 -1
src/main/java/project/ui/ProjectCellRenderer.java
··· 72 72 73 73 if (project != null) { 74 74 nameLabel.setText(project.getName()); 75 - pathLabel.setText(project.getPath().getAbsolutePath()); 75 + pathLabel.setText(project.getPath()); 76 76 timeLabel.setText(formatRelativeTime(project.getLastOpened())); 77 77 } 78 78 else {
+19 -39
src/main/java/project/ui/ProjectSwitcherDialog.java
··· 14 14 import java.awt.event.WindowAdapter; 15 15 import java.awt.event.WindowEvent; 16 16 import java.io.File; 17 + import java.io.IOException; 17 18 import java.net.URI; 18 19 import java.util.List; 19 20 import java.util.concurrent.CountDownLatch; ··· 50 51 import app.Themes; 51 52 import app.Themes.Theme; 52 53 import app.config.Options; 54 + import dev.kdl.parse.KdlParseException; 53 55 import project.Project; 54 56 import project.ProjectManager; 55 - import project.ProjectValidator; 56 57 import game.map.editor.ui.dialogs.ChooseDialogResult; 57 58 import game.map.editor.ui.dialogs.DirChooser; 58 59 import net.miginfocom.swing.MigLayout; ··· 331 332 // Buttons 332 333 JButton createButton = new JButton("New Project"); 333 334 createButton.addActionListener(e -> { 334 - // TODO 335 - SwingUtils.getMessageDialog() 336 - .setTitle("Coming Soon") 337 - .setMessage("Project creation is not yet implemented.", 338 - "For now, please clone the papermario or papermario-dx repository manually.") 339 - .setMessageType(JOptionPane.INFORMATION_MESSAGE) 340 - .show(); 335 + Project newProject = CreateProjectDialog.showDialog(this); 336 + if (newProject != null) { 337 + projectManager.recordProjectOpened(newProject); 338 + refreshProjectList(); 339 + updateListFilter(); 340 + list.setSelectedValue(newProject, true); 341 + openSelectedProject(); 342 + } 341 343 }); 342 344 343 345 JButton browseButton = new JButton("Browse..."); ··· 381 383 Project project = (Project) element; 382 384 String filterText = filterTextField.getText().toUpperCase(); 383 385 String name = project.getName().toUpperCase(); 384 - String path = project.getPath().getAbsolutePath().toUpperCase(); 386 + String path = project.getPath().toUpperCase(); 385 387 return name.contains(filterText) || path.contains(filterText); 386 388 }); 387 389 } ··· 393 395 return; 394 396 } 395 397 396 - // Validate project 397 - if (!ProjectValidator.isValidProject(selected.getPath())) { 398 - int choice = SwingUtils.getConfirmDialog() 399 - .setTitle("Invalid Project") 400 - .setMessage("This directory is no longer a valid Star Rod project.", 401 - "Would you like to remove it from the list?") 402 - .setOptionsType(JOptionPane.YES_NO_OPTION) 403 - .choose(); 404 - 405 - if (choice == JOptionPane.YES_OPTION) { 406 - projectManager.removeFromHistory(selected); 407 - refreshProjectList(); 408 - updateListFilter(); 409 - } 410 - return; 411 - } 412 - 413 398 selectedProject = selected; 414 399 latch.countDown(); 415 400 dispose(); ··· 419 404 { 420 405 if (dirChooser.prompt() == ChooseDialogResult.APPROVE) { 421 406 File selectedDir = dirChooser.getSelectedFile(); 422 - 423 - if (!ProjectValidator.isValidProject(selectedDir)) { 424 - SwingUtils.getErrorDialog() 425 - .setTitle("Invalid Project") 426 - .setMessage("The selected directory is not a valid Star Rod project.", 427 - "A valid project must have ver/us/splat.yaml") 428 - .show(); 407 + Project newProject; 408 + try { 409 + newProject = new Project(selectedDir); 410 + } catch (IOException | KdlParseException e) { 411 + Environment.showErrorMessage("Failed to open project", "The folder you selected is not a valid project: %s", e.getMessage()); 429 412 return; 430 413 } 431 - 432 - // Add to history and select 433 - Project newProject = new Project(selectedDir); 434 - projectManager.recordProjectOpened(selectedDir); 414 + projectManager.recordProjectOpened(newProject); 435 415 436 416 // Refresh and select the new project 437 417 refreshProjectList(); ··· 451 431 } 452 432 453 433 int choice = SwingUtils.getConfirmDialog() 454 - .setTitle("Remove Project") 434 + .setTitle("Remove project") 455 435 .setMessage("Remove \"" + selected.getName() + "\" from the project list?", 456 436 "The project files will not be deleted.") 457 437 .setOptionsType(JOptionPane.YES_NO_OPTION) ··· 481 461 482 462 Object[] message = { 483 463 "WARNING: This will permanently delete all files in:", 484 - selected.getPath().getAbsolutePath(), 464 + selected.getPath(), 485 465 "", 486 466 confirmCheck 487 467 };
+20
src/main/resources/database/templates/blank/.gitignore
··· 1 + # Build artifacts 2 + /build 3 + compile_commands.json 4 + *.z64 5 + *.n64 6 + 7 + # IDE settings 8 + .vscode 9 + .zed 10 + .starrod 11 + .claude 12 + .cache 13 + 14 + # Miscellaneous 15 + .DS_Store 16 + Thumbs.db 17 + Desktop.ini 18 + *.lnk 19 + *.log 20 + *.tmp
+6
src/main/resources/database/templates/blank/project.kdl
··· 1 + id $PROJECT_ID 2 + name $PROJECT_NAME 3 + description $PROJECT_DESCRIPTION 4 + license "CC0" 5 + 6 + engine version="0.0.0"