Skip to content

Commit cebde52

Browse files
authored
Slim agent template and support Reforged map folders (#77)
1 parent 5784151 commit cebde52

13 files changed

Lines changed: 269 additions & 247 deletions

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ This repo builds the Grill CLI and project setup tooling. The generated map-proj
1515
- Local helpers in `config/ProjectConfigModels.kt` should stay thin: typealiases plus immutable copy helpers for shared records.
1616
- `YamlHelper.dumpProjectConfig` intentionally serializes a pruned YAML map instead of the shared records directly. This preserves the old user-facing `wurst.build` behavior by omitting null/default nested fields.
1717
- `wbschema.json` should stay lenient and aligned with the shared config parser, especially for `scriptMode`, `wc3Patch`, and nullable legacy fields.
18+
- Compiler-owned agent references belong in `~/.wurst/wurst-compiler/agent-docs/`. Generated project notes should prefer those version-matched local files when present and retain an online fallback until compiler releases ship them.
1819

1920
## WC3 Patch And Core JASS
2021

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: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ object SetupApp {
3232

3333
private data class WurstProcessResult(val exitCode: Int, val output: List<String>)
3434

35-
internal const val AGENTS_TEMPLATE_VERSION = "2026-08-05"
35+
internal const val AGENTS_TEMPLATE_VERSION = "2026-08-08"
3636
private const val AGENTS_TEMPLATE_MARKER_PREFIX = "<!-- WURST_AGENTS_TEMPLATE_VERSION:"
3737
private const val AGENTS_TEMPLATE_MARKER = "<!-- WURST_AGENTS_TEMPLATE_VERSION: $AGENTS_TEMPLATE_VERSION -->"
3838
private const val AGENTS_TEMPLATE_SOURCE_HINT = "WurstScript Warcraft III map project notes"
@@ -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?,
@@ -936,6 +962,17 @@ object SetupApp {
936962
if (markerLine == AGENTS_TEMPLATE_MARKER) {
937963
return null
938964
}
965+
if (markerLine != null) {
966+
val markerVersion = markerLine
967+
.removePrefix(AGENTS_TEMPLATE_MARKER_PREFIX)
968+
.removeSuffix("-->")
969+
.trim()
970+
// Template versions are ISO dates, so lexical ordering is chronological. A newer
971+
// downloaded template is valid even when this older Grill binary cannot recognize it.
972+
if (markerVersion > AGENTS_TEMPLATE_VERSION) {
973+
return null
974+
}
975+
}
939976
if (markerLine != null) {
940977
return "AGENTS.md was generated from an older WurstSetup template ($markerLine). Consider refreshing it from templates/AGENTS.md and re-applying project-local notes."
941978
}

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/main/kotlin/global/InstallationManager.kt

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import net.NetStatus
99
import java.nio.file.Files
1010
import java.nio.file.Path
1111
import java.nio.file.Paths
12+
import java.nio.file.StandardCopyOption
13+
import java.util.jar.JarFile
1214
import java.util.regex.Pattern
1315

1416

@@ -19,6 +21,7 @@ object InstallationManager {
1921
private val log = KotlinLogging.logger {}
2022
private const val FOLDER_PATH = ".wurst"
2123
private const val COMPILER_FILE_NAME = "wurstscript.jar"
24+
private const val LANGUAGE_AGENT_DOC_ENTRY = "agent-docs/WURST_LANGUAGE.md"
2225
private const val GRILL_JAR_NAME = "grill.jar"
2326
private const val LEGACY_GRILL_JAR_NAME = "WurstSetup.jar"
2427

@@ -49,6 +52,7 @@ object InstallationManager {
4952
log.info("verifyInstallation: detectedCompilerJar=$detectedCompilerJar exists=${detectedCompilerJar?.let { Files.exists(it) }}")
5053
if (detectedCompilerJar != null) {
5154
log.info("Found installation at $detectedCompilerJar")
55+
ensureCompilerAgentDocs(detectedCompilerJar)
5256
status = InstallationStatus.INSTALLED_UNKNOWN
5357
try {
5458
if (!Files.isWritable(detectedCompilerJar)) {
@@ -94,9 +98,11 @@ object InstallationManager {
9498
log.info("\t📦 Extracting..")
9599
ZipArchiveExtractor.extractArchive(it, installDir)
96100
Files.delete(it)
97-
if (detectCompilerJar() == null) {
101+
val compilerJar = detectCompilerJar()
102+
if (compilerJar == null) {
98103
log.error("❌ Compiler not found after extraction.")
99104
} else {
105+
ensureCompilerAgentDocs(compilerJar)
100106
if (isFreshInstall) { wurstConfig = WurstConfigData() }
101107
ensureGrillJarInstalled()
102108
setLaunchersExecutable()
@@ -193,6 +199,32 @@ object InstallationManager {
193199
}
194200
}
195201

202+
private fun ensureCompilerAgentDocs(compilerJar: Path) {
203+
try {
204+
JarFile(compilerJar.toFile()).use { jar ->
205+
val docsDir = compilerDir.resolve("agent-docs")
206+
val docsFile = docsDir.resolve("WURST_LANGUAGE.md")
207+
val entry = jar.getJarEntry(LANGUAGE_AGENT_DOC_ENTRY)
208+
if (entry == null) {
209+
// A compiler downgrade may remove the resource. Do not leave docs from the old compiler
210+
// in place, or generated projects could use language guidance for the wrong version.
211+
Files.deleteIfExists(docsFile)
212+
return
213+
}
214+
Files.createDirectories(docsDir)
215+
jar.getInputStream(entry).use { input ->
216+
Files.copy(
217+
input,
218+
docsFile,
219+
StandardCopyOption.REPLACE_EXISTING
220+
)
221+
}
222+
}
223+
} catch (e: Exception) {
224+
log.warn("Could not extract compiler agent docs: ${e.message}")
225+
}
226+
}
227+
196228
private fun resolveOwnJar(): Path? {
197229
return try {
198230
val url = InstallationManager::class.java.protectionDomain.codeSource.location
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import file.SetupApp
2+
import org.testng.Assert
3+
import org.testng.annotations.Test
4+
import java.nio.file.Files
5+
import java.nio.file.Paths
6+
7+
class AgentsTemplateTests {
8+
private val templatePath = Paths.get("templates", "AGENTS.md")
9+
10+
@Test
11+
fun testTemplateStaysTokenLean() {
12+
val content = Files.readString(templatePath)
13+
val wordCount = Regex("""\S+""").findAll(content).count()
14+
15+
Assert.assertTrue(wordCount <= 900, "AGENTS template grew to $wordCount words (limit: 900)")
16+
Assert.assertTrue(content.length <= 7000, "AGENTS template grew to ${content.length} characters (limit: 7000)")
17+
}
18+
19+
@Test
20+
fun testLanguageDocsPreferCompilerMatchedLocalReference() {
21+
val content = Files.readString(templatePath)
22+
val localReference = "~/.wurst/wurst-compiler/agent-docs/WURST_LANGUAGE.md"
23+
val onlineFallback = "https://wurstlang.org/manual.html"
24+
val localIndex = content.indexOf(localReference)
25+
val onlineIndex = content.indexOf(onlineFallback)
26+
27+
Assert.assertTrue(localIndex >= 0, "Missing compiler-matched local language reference")
28+
Assert.assertTrue(onlineIndex > localIndex, "Online manual must remain a fallback after the local reference")
29+
}
30+
31+
@Test
32+
fun testNewerTemplateDoesNotLookStaleToOlderGrill() {
33+
val newerMarked = "<!-- WURST_AGENTS_TEMPLATE_VERSION: 2099-01-01 -->\n# AGENTS.md\n"
34+
35+
Assert.assertNull(SetupApp.agentsTemplateWarning(newerMarked))
36+
}
37+
}

0 commit comments

Comments
 (0)