Skip to content

Commit b8c4b06

Browse files
committed
Support Reforged map-folder project generation
1 parent c262807 commit b8c4b06

8 files changed

Lines changed: 108 additions & 23 deletions

File tree

src/main/kotlin/config/WurstProjectConfig.kt

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ object WurstProjectConfig {
2727
private val schema by lazy { javaClass.classLoader.getResource("wbschema.json") }
2828
private val log = KotlinLogging.logger {}
2929

30-
fun handleCreate(projectRoot: Path, gameRoot: Path?, projectConfig: WurstProjectConfigData) {
30+
fun handleCreate(projectRoot: Path, gameRoot: Path?, projectConfig: WurstProjectConfigData, templateBranch: String = "master") {
3131
try {
32-
createProject(projectRoot, gameRoot, projectConfig)
32+
createProject(projectRoot, gameRoot, projectConfig, templateBranch)
3333
} catch (e: Exception) {
3434
if (DependencyManager.debug) {
3535
e.printStackTrace()
@@ -60,7 +60,7 @@ object WurstProjectConfig {
6060
}
6161

6262
@Throws(Exception::class)
63-
private fun createProject(projectRoot: Path, gameRoot: Path?, projectConfig: WurstProjectConfigData) {
63+
private fun createProject(projectRoot: Path, gameRoot: Path?, projectConfig: WurstProjectConfigData, templateBranch: String) {
6464
Log.print("Creating project root..")
6565
if (Files.exists(projectRoot) && Files.list(projectRoot).filter { !Files.isDirectory(it) }.findAny().isPresent) {
6666
log.error("Project root already exists and contains files")
@@ -71,21 +71,21 @@ object WurstProjectConfig {
7171

7272
Log.print("Download template..")
7373
log.info("⏬ Downloading template..")
74-
Download.downloadBareboneProject {
75-
extractDownload(it, projectRoot, gameRoot, projectConfig)
74+
Download.downloadBareboneProject(templateBranch) {
75+
extractDownload(it, projectRoot, gameRoot, projectConfig, templateBranch)
7676
}
7777
}
7878
}
7979

80-
private fun extractDownload(it: Path, projectRoot: Path, gameRoot: Path?, projectConfig: WurstProjectConfigData) {
80+
private fun extractDownload(it: Path, projectRoot: Path, gameRoot: Path?, projectConfig: WurstProjectConfigData, templateBranch: String) {
8181
Log.println(" done.")
8282

8383
Log.print("Extracting template..")
8484
val extractSuccess = ZipArchiveExtractor.extractArchive(it, projectRoot)
8585
Files.delete(it)
8686
if (extractSuccess) {
8787
Log.print("done\n")
88-
cleanupDownload(projectRoot)
88+
cleanupDownload(projectRoot, templateBranch)
8989
normalizeGeneratedTemplate(projectRoot)
9090
} else {
9191
Log.print("error\n")
@@ -97,9 +97,9 @@ object WurstProjectConfig {
9797
log.info("✔ Project generated.")
9898
}
9999

100-
private fun cleanupDownload(projectRoot: Path) {
100+
private fun cleanupDownload(projectRoot: Path, templateBranch: String) {
101101
Log.print("Clean up..")
102-
val folder = projectRoot.resolve("wurst-project-template-master")
102+
val folder = projectRoot.resolve("wurst-project-template-$templateBranch")
103103
copyFolder(folder, projectRoot)
104104
Files.walk(folder).sorted { a, b -> b.compareTo(a) }.forEach { p ->
105105
try {

src/main/kotlin/file/CLICommand.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,18 @@ enum class GlobalOptions(val optionName: String = "", val argCount: Int = 0) {
8585
setupMain.wc3Patch = CoreJassProvider.normalizePatchInput(args[0])
8686
}
8787
},
88+
MAP_FORMAT("--map-format", 1) {
89+
override fun runOption(setupMain: SetupMain, args: List<String>) {
90+
val format = MapFormat.parse(args[0])
91+
if (format == null) {
92+
log.error("❌ Unknown map format: ${args[0]}. Use archive or folder.")
93+
ExitHandler.exit(1)
94+
} else {
95+
setupMain.mapFormat = format
96+
setupMain.mapFormatExplicit = true
97+
}
98+
}
99+
},
88100
WC3_PATH("--wc3-path", 1) {
89101
override fun runOption(setupMain: SetupMain, args: List<String>) {
90102
setupMain.gamePath = java.nio.file.Paths.get(args[0])

src/main/kotlin/file/CoreJassProvider.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,13 @@ object CoreJassProvider {
171171
.orElse(false)
172172
}
173173

174+
fun isReforgedPatch(input: String?): Boolean {
175+
val patch = normalizePatchInput(input)
176+
return Wc3PatchTarget.parse(patch)
177+
.map { it.kind() == Wc3PatchTarget.Kind.REFORGED }
178+
.orElse(false)
179+
}
180+
174181
fun ensureFiles(projectRoot: Path, wc3Patch: String?): List<Path> {
175182
val buildFolder = projectRoot.resolve("_build")
176183
Files.createDirectories(buildFolder)

src/main/kotlin/file/Download.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ object Download {
1515
private val log = KotlinLogging.logger {}
1616

1717
private const val compilerReleaseBaseUrl = "https://github.com/wurstscript/WurstScript/releases/download/nightly/"
18-
private const val bareboneUrl = "github.com/wurstscript/wurst-project-template/archive/master.zip"
18+
private const val bareboneBaseUrl = "github.com/wurstscript/wurst-project-template/archive"
1919

2020
@Throws(IOException::class)
2121
fun downloadSetup(callback: (Path) -> Unit) {
@@ -28,13 +28,13 @@ object Download {
2828
}
2929

3030
@Throws(IOException::class)
31-
fun downloadBareboneProject(callback: (Path) -> Unit) {
31+
fun downloadBareboneProject(templateBranch: String = "master", callback: (Path) -> Unit) {
3232
try {
33-
downloadDirect("https://$bareboneUrl", callback)
33+
downloadDirect("https://${bareboneBaseUrl}/${templateBranch}.zip", callback)
3434
} catch (e: Exception) {
3535
log.warn("downloadBareboneProject Exception caught", e)
3636
Log.println("Https error, falling back to unsafe http.")
37-
downloadDirect("http://$bareboneUrl", callback)
37+
downloadDirect("http://${bareboneBaseUrl}/${templateBranch}.zip", callback)
3838
}
3939
}
4040

src/main/kotlin/file/MapFormat.kt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package file
2+
3+
/** The storage format of the starter map included in a generated project. */
4+
enum class MapFormat(
5+
val cliName: String,
6+
val templateBranch: String,
7+
val label: String,
8+
) {
9+
ARCHIVE("archive", "master", "map archive (.w3x file)"),
10+
FOLDER("folder", "map-folder", "map folder (.w3x directory)"),
11+
;
12+
13+
companion object {
14+
fun parse(value: String): MapFormat? = values().firstOrNull {
15+
it.cliName == value.trim().lowercase() || it.name.lowercase() == value.trim().lowercase()
16+
}
17+
}
18+
}

src/main/kotlin/file/SetupApp.kt

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ object SetupApp {
149149
| test [filter] Run unit tests, optionally filtered by package/function name
150150
| typecheck Typecheck the project without building a map
151151
| outdated Check whether project dependencies are up to date
152-
| build <mapfile> Build the project using the given input map
152+
| build <mapfile|map-folder> Build the project using the given map archive or folder
153153
| exportobjects <mapfile|folder> Export object editor data to Wurst source
154154
|
155155
|Global options:
@@ -162,6 +162,7 @@ object SetupApp {
162162
|Generate options:
163163
| --script-mode lua|jass Script mode (default: lua)
164164
| --wc3-patch <patch> WC3 patch target: reforged, pre1.29, or jass-history version
165+
| --map-format archive|folder Starter map storage (default: folder for Reforged)
165166
| --wc3-path <dir> Warcraft III install folder for VS Code/run
166167
| --with-agents / --no-agents Include AGENTS.md (default: no)
167168
| --with-ci / --no-ci Include GitHub Actions workflow (default: no)
@@ -220,12 +221,12 @@ object SetupApp {
220221
wc3Patch = setup.wc3Patch
221222
)
222223
val gameRoot = resolveGenerateGamePath(setup, projectConfig.wc3Patch)
223-
WurstProjectConfig.handleCreate(projectDir, gameRoot, projectConfig)
224+
WurstProjectConfig.handleCreate(projectDir, gameRoot, projectConfig, setup.mapFormat.templateBranch)
224225
ensureCoreJassFiles(projectDir, projectConfig.wc3Patch)
225226
if (Files.exists(projectDir)) {
226227
if (setup.addAgents) downloadAgentsMd(projectDir)
227228
if (setup.addGithubWorkflow) writeCiWorkflow(projectDir)
228-
printGenerateNextSteps(projectDir, projectConfig, setup.addAgents, setup.addGithubWorkflow, gameRoot)
229+
printGenerateNextSteps(projectDir, projectConfig, setup.mapFormat, setup.addAgents, setup.addGithubWorkflow, gameRoot)
229230
}
230231
}
231232
setup.command == CLICommand.TEST -> {
@@ -315,7 +316,7 @@ object SetupApp {
315316

316317
private fun missingMap(requestedMap: String? = null): Nothing {
317318
if (requestedMap == null) {
318-
log.error("❌ No input map specified and no .w3x/.w3m file was found in the project root.")
319+
log.error("❌ No input map specified and no .w3x/.w3m archive or map folder was found in the project root.")
319320
log.info("Try: put a map in the project root, or run `grill build YourMap.w3x`.")
320321
} else {
321322
log.error("❌ Map not found: $requestedMap")
@@ -343,6 +344,7 @@ object SetupApp {
343344
private fun printGenerateNextSteps(
344345
projectDir: Path,
345346
projectConfig: WurstProjectConfigData,
347+
mapFormat: MapFormat,
346348
addAgents: Boolean,
347349
addGithubWorkflow: Boolean,
348350
gameRoot: Path?
@@ -355,6 +357,7 @@ object SetupApp {
355357
|Choices:
356358
| Script mode: ${(projectConfig.scriptMode ?: ScriptMode.LUA).name.lowercase()}
357359
| WC3 patch: ${CoreJassProvider.describePatch(projectConfig.wc3Patch ?: CoreJassProvider.DEFAULT_PATCH)}
360+
| Map storage: ${if (mapFormat == MapFormat.FOLDER) "folder (.w3x directory)" else "archive (.w3x file)"}
358361
| Warcraft III: ${gameRoot?.toAbsolutePath()?.normalize() ?: "not configured"}
359362
| Stdlib: ${if (projectConfig.dependencies.any { it.endsWith(":pre1.29") }) "pre1.29" else "current"}
360363
| Curated dependencies: $curatedSummary
@@ -366,6 +369,7 @@ object SetupApp {
366369
""".trimMargin())
367370
}
368371

372+
369373
private fun resolveGenerateGamePath(setup: SetupMain, wc3Patch: String?): Path? {
370374
if (setup.gamePathOptedOut) {
371375
log.info("Warcraft III path: not configured by choice.")
@@ -595,6 +599,10 @@ object SetupApp {
595599
useInteractiveMenus = useInteractiveMenus,
596600
currentPatch = setup.wc3Patch
597601
)
602+
if (!setup.mapFormatExplicit) {
603+
setup.mapFormat = recommendedMapFormat(setup.wc3Patch)
604+
}
605+
setup.mapFormat = selectMapFormat(prompt, setup.mapFormat, useInteractiveMenus)
598606
setup.gamePathOptedOut = false
599607
setup.gamePath = selectGamePath(setup, prompt, setup.wc3Patch, setup.gamePath)
600608

@@ -608,6 +616,24 @@ object SetupApp {
608616

609617
setup.curatedDependencyIds = selectCuratedDependencies(prompt, setup.curatedDependencyIds).toMutableList()
610618
}
619+
internal fun recommendedMapFormat(wc3Patch: String?): MapFormat =
620+
if (CoreJassProvider.isReforgedPatch(wc3Patch)) MapFormat.FOLDER else MapFormat.ARCHIVE
621+
622+
private fun selectMapFormat(
623+
prompt: (String, String?) -> String?,
624+
current: MapFormat,
625+
useInteractiveMenus: Boolean,
626+
): MapFormat {
627+
val choices = listOf(
628+
TerminalMenu.Choice(MapFormat.FOLDER, "${MapFormat.FOLDER.label} (recommended for Reforged)"),
629+
TerminalMenu.Choice(MapFormat.ARCHIVE, "${MapFormat.ARCHIVE.label} (recommended for classic/legacy)"),
630+
)
631+
if (useInteractiveMenus) {
632+
TerminalMenu.choose("Map storage format:", choices, choices.indexOfFirst { it.value == current })?.let { return it }
633+
}
634+
val answer = prompt("Map storage (archive/folder)", current.cliName)?.lowercase()
635+
return MapFormat.parse(answer.orEmpty()) ?: current
636+
}
611637

612638
private fun selectCuratedDependencies(
613639
prompt: (String, String?) -> String?,

src/main/kotlin/file/SetupMain.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,13 @@ class SetupMain {
3131

3232
var debug = false
3333

34-
// Generate wizard options (defaults: non-interactive, Lua, Reforged, no extras)
34+
// Generate wizard options (defaults: non-interactive, Lua, Reforged, Reforged folder, no extras)
3535
var addAgents: Boolean = false
3636
var addGithubWorkflow: Boolean = false
3737
var scriptMode: ScriptMode = ScriptMode.LUA
3838
var wc3Patch: String = CoreJassProvider.DEFAULT_PATCH
39+
var mapFormat: MapFormat = MapFormat.FOLDER
40+
var mapFormatExplicit: Boolean = false
3941

4042
/** Ids of curated dependencies (see [CuratedDependencies]) to seed into the generated project. */
4143
var curatedDependencyIds: MutableList<String> = mutableListOf()

src/test/kotlin/GenerateTests.kt

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import file.CLICommand
33
import file.CoreJassProvider
44
import file.CuratedDependencies
55
import file.ExitHandler
6+
import file.MapFormat
67
import file.SetupApp
78
import file.SetupMain
89
import org.testng.Assert
@@ -44,6 +45,7 @@ class GenerateTests {
4445
Assert.assertEquals(setup.commandArg, "myproject")
4546
Assert.assertEquals(setup.scriptMode, ScriptMode.LUA)
4647
Assert.assertEquals(setup.wc3Patch, CoreJassProvider.DEFAULT_PATCH)
48+
Assert.assertEquals(setup.mapFormat, MapFormat.FOLDER)
4749
Assert.assertFalse(setup.addAgents)
4850
Assert.assertFalse(setup.addGithubWorkflow)
4951
}
@@ -64,6 +66,16 @@ class GenerateTests {
6466
Assert.assertEquals(setup.wc3Patch, CoreJassProvider.PRE_129_PATCH)
6567
}
6668

69+
@Test(priority = 10)
70+
fun testMapFormatFlagAndPatchRecommendations() {
71+
val archive = SetupMain()
72+
archive.parseArgs(listOf("generate", "myproject", "--map-format", "archive"))
73+
Assert.assertEquals(archive.mapFormat, MapFormat.ARCHIVE)
74+
Assert.assertTrue(archive.mapFormatExplicit)
75+
Assert.assertEquals(SetupApp.recommendedMapFormat("v1.31"), MapFormat.ARCHIVE)
76+
Assert.assertEquals(SetupApp.recommendedMapFormat("v1.36"), MapFormat.FOLDER)
77+
}
78+
6779
@Test(priority = 10)
6880
fun testWc3PatchJassHistoryVersionFlag() {
6981
val setup = SetupMain()
@@ -375,11 +387,19 @@ class GenerateTests {
375387
Assert.assertTrue(setup.devBuild)
376388
}
377389

390+
@Test(priority = 10)
391+
fun testExportObjectsCommandParsesMapOrFolderArg() {
392+
val setup = SetupMain()
393+
setup.parseArgs(listOf("exportobjects", "ExampleMap.w3x"))
394+
Assert.assertEquals(setup.command, CLICommand.EXPORTOBJECTS)
395+
Assert.assertEquals(setup.commandArg, "ExampleMap.w3x")
396+
}
397+
378398
@Test(priority = 10)
379399
fun testGenerateWithoutNameUsesWizardPrompt() {
380400
val setup = SetupMain()
381401
setup.parseArgs(listOf("generate"))
382-
val answers = java.util.ArrayDeque(listOf("wizardproject", "jass", "pre1.29", "none", "y", "y", "n"))
402+
val answers = java.util.ArrayDeque(listOf("wizardproject", "jass", "pre1.29", "archive", "none", "y", "y", "n"))
383403
val prevPrompt = SetupApp.generatePrompt
384404
try {
385405
SetupApp.generatePrompt = { _, _ -> answers.removeFirst() }
@@ -400,7 +420,7 @@ class GenerateTests {
400420
fun testGenerateWithNameUsesWizardPrompt() {
401421
val setup = SetupMain()
402422
setup.parseArgs(listOf("generate", "wizardproject"))
403-
val answers = java.util.ArrayDeque(listOf("jass", "pre1.29", "none", "y", "y", "n"))
423+
val answers = java.util.ArrayDeque(listOf("jass", "pre1.29", "archive", "none", "y", "y", "n"))
404424
val prevPrompt = SetupApp.generatePrompt
405425
try {
406426
SetupApp.generatePrompt = { _, _ -> answers.removeFirst() }
@@ -448,7 +468,7 @@ class GenerateTests {
448468
fun testGenerateWizardRejectsUnsupportedScriptModeAndPatchInput() {
449469
val setup = SetupMain()
450470
setup.parseArgs(listOf("generate"))
451-
val answers = java.util.ArrayDeque(listOf("wizardproject", "t", "jass", "t", "2", "none", "n", "n", "n"))
471+
val answers = java.util.ArrayDeque(listOf("wizardproject", "t", "jass", "t", "2", "archive", "none", "n", "n", "n"))
452472
val prevPrompt = SetupApp.generatePrompt
453473
try {
454474
SetupApp.generatePrompt = { _, _ -> answers.removeFirst() }
@@ -468,7 +488,7 @@ class GenerateTests {
468488
fun testGenerateWizardPreservesCliPatchDefault() {
469489
val setup = SetupMain()
470490
setup.parseArgs(listOf("generate", "--wc3-patch", "pre1.29"))
471-
val answers = java.util.ArrayDeque(listOf("wizardproject", "", "", "none", "n", "n", "n"))
491+
val answers = java.util.ArrayDeque(listOf("wizardproject", "", "", "archive", "none", "n", "n", "n"))
472492
val prevPrompt = SetupApp.generatePrompt
473493
try {
474494
SetupApp.generatePrompt = { _, _ -> answers.removeFirst() }
@@ -539,7 +559,7 @@ class GenerateTests {
539559
fun testGenerateWizardSelectsCuratedDependency() {
540560
val setup = SetupMain()
541561
setup.parseArgs(listOf("generate"))
542-
val answers = java.util.ArrayDeque(listOf("wizardproject", "lua", "", "none", "n", "n", "y"))
562+
val answers = java.util.ArrayDeque(listOf("wizardproject", "lua", "", "archive", "none", "n", "n", "y"))
543563
val prevPrompt = SetupApp.generatePrompt
544564
try {
545565
SetupApp.generatePrompt = { _, _ -> answers.removeFirst() }

0 commit comments

Comments
 (0)