|
| 1 | +package file |
| 2 | + |
| 3 | +import logging.KotlinLogging |
| 4 | +import java.net.URI |
| 5 | +import java.nio.file.AtomicMoveNotSupportedException |
| 6 | +import java.nio.file.Files |
| 7 | +import java.nio.file.Path |
| 8 | +import java.nio.file.StandardCopyOption |
| 9 | +import java.util.jar.JarFile |
| 10 | + |
| 11 | +object CoreJassProvider { |
| 12 | + const val DEFAULT_PATCH = "v1.36" |
| 13 | + const val PRE_129_PATCH = "v1.28" |
| 14 | + |
| 15 | + private const val JASS_HISTORY_RAW = "https://raw.githubusercontent.com/Luashine/jass-history" |
| 16 | + private const val JASS_HISTORY_REF = "master" |
| 17 | + private val log = KotlinLogging.logger {} |
| 18 | + |
| 19 | + private val PATCH_TO_JASS_HISTORY_FOLDER = linkedMapOf( |
| 20 | + "v1.36" to "Reforged-v1.36.1.20719-w3-51d40ee", |
| 21 | + "v1.35" to "Reforged-v1.35.0.20093-w3-5ec1b77", |
| 22 | + "v1.34" to "Reforged-v1.34.0.19632-w3-31590bf", |
| 23 | + "v1.33" to "Reforged-v1.33.0.19378-w3-e94d62c", |
| 24 | + "v1.32" to "Reforged-v1.32.10.19202", |
| 25 | + "v1.31" to "TFT-v1.31.1.12173", |
| 26 | + "v1.30" to "TFT-v1.30.4.11274", |
| 27 | + "v1.29" to "TFT-v1.29.2.9231", |
| 28 | + "v1.28" to "TFT-v1.28.2.7395", |
| 29 | + "v1.27b" to "TFT-v1.27b-ru", |
| 30 | + "v1.27a" to "TFT-v1.27a-ru", |
| 31 | + "v1.26a" to "TFT-v1.26a-ru", |
| 32 | + "v1.25b" to "TFT-v1.25b-ru", |
| 33 | + "v1.24e" to "TFT-v1.24e-ru", |
| 34 | + "v1.24d" to "TFT-v1.24d-ru", |
| 35 | + "v1.24c" to "TFT-v1.24c-ru", |
| 36 | + "v1.24b" to "TFT-v1.24b-ru", |
| 37 | + "v1.24a" to "TFT-v1.24a-ru", |
| 38 | + "v1.23a" to "TFT-v1.23a-ru", |
| 39 | + "v1.22a" to "TFT-v1.22a-ru", |
| 40 | + "v1.21b" to "TFT-v1.21b-ru", |
| 41 | + "v1.21a" to "TFT-v1.21a-ru", |
| 42 | + "v1.21" to "Beta-ROC-v1.21", |
| 43 | + "v1.20e" to "TFT-v1.20e-ru", |
| 44 | + "v1.20d" to "TFT-v1.20d-ru", |
| 45 | + "v1.20c" to "TFT-v1.20c-ru", |
| 46 | + "v1.20b" to "TFT-v1.20b-ru", |
| 47 | + "v1.20a" to "TFT-v1.20a-ru", |
| 48 | + "v1.20" to "Beta-ROC-v1.20", |
| 49 | + "v1.19b" to "TFT-v1.19b-ru", |
| 50 | + "v1.19a" to "TFT-v1.19a-ru", |
| 51 | + "v1.18a" to "TFT-v1.18a-ru", |
| 52 | + "v1.17a" to "TFT-v1.17a-ru", |
| 53 | + "v1.16a" to "TFT-v1.16a-ru", |
| 54 | + "v1.15" to "TFT-v1.15-ru", |
| 55 | + "v1.14b" to "TFT-v1.14b-ru", |
| 56 | + "v1.14" to "TFT-v1.14-ru", |
| 57 | + "v1.13b" to "TFT-v1.13b-ru", |
| 58 | + "v1.13" to "TFT-v1.13-ru", |
| 59 | + "v1.12" to "TFT-v1.12-ru", |
| 60 | + "v1.11" to "TFT-v1.11-ru", |
| 61 | + "v1.10" to "TFT-v1.10-ru", |
| 62 | + "v1.07" to "TFT-v1.07-ru", |
| 63 | + "v1.06" to "ROC-v1.06-ru", |
| 64 | + "v1.05" to "ROC-v1.05-ru", |
| 65 | + "v1.04" to "ROC-v1.04-ru", |
| 66 | + "v1.03" to "ROC-v1.03-ru", |
| 67 | + "v1.02a" to "ROC-v1.02a-ru", |
| 68 | + "v1.02" to "ROC-v1.02-ru", |
| 69 | + "v1.01b" to "ROC-v1.01b-ru", |
| 70 | + "v1.01" to "ROC-v1.01-ru", |
| 71 | + "v1.00" to "ROC-v1.00-ru" |
| 72 | + ) |
| 73 | + |
| 74 | + val supportedPatches: List<String> = PATCH_TO_JASS_HISTORY_FOLDER.keys.toList() |
| 75 | + |
| 76 | + fun describePatch(patch: String): String { |
| 77 | + val normalizedPatch = normalizePatchInput(patch) |
| 78 | + val label = when (normalizedPatch) { |
| 79 | + DEFAULT_PATCH -> "latest Reforged / WC3 2.x core JASS" |
| 80 | + "v1.31" -> "latest classic TFT" |
| 81 | + PRE_129_PATCH -> "legacy pre-1.29" |
| 82 | + else -> { |
| 83 | + val minor = Regex("""v1\.(\d+)""").find(normalizedPatch)?.groupValues?.get(1)?.toIntOrNull() |
| 84 | + when { |
| 85 | + minor != null && minor >= 32 -> "Reforged" |
| 86 | + minor != null && minor >= 7 -> "classic TFT" |
| 87 | + minor != null -> "classic ROC" |
| 88 | + else -> null |
| 89 | + } |
| 90 | + } |
| 91 | + } |
| 92 | + return if (label == null) normalizedPatch else "$normalizedPatch ($label)" |
| 93 | + } |
| 94 | + |
| 95 | + fun jassHistoryFolderForPatch(patch: String): String? { |
| 96 | + return PATCH_TO_JASS_HISTORY_FOLDER[normalizePatchInput(patch)] |
| 97 | + } |
| 98 | + |
| 99 | + fun isSupportedPatch(patch: String): Boolean { |
| 100 | + return PATCH_TO_JASS_HISTORY_FOLDER.containsKey(normalizePatchInput(patch)) |
| 101 | + } |
| 102 | + |
| 103 | + fun normalizePatchInput(input: String?): String { |
| 104 | + val patch = input?.trim().orEmpty() |
| 105 | + val normalizedAlias = when (patch.lowercase()) { |
| 106 | + "", "reforged", "latest" -> DEFAULT_PATCH |
| 107 | + "pre1.29", "pre-1.29", "pre_129", "pre-129" -> PRE_129_PATCH |
| 108 | + else -> patch |
| 109 | + } |
| 110 | + val withPrefix = if (normalizedAlias.matches(Regex("""\d+\.\d+.*"""))) "v$normalizedAlias" else normalizedAlias |
| 111 | + val canonicalCase = PATCH_TO_JASS_HISTORY_FOLDER.keys.firstOrNull { it.equals(withPrefix, ignoreCase = true) } |
| 112 | + if (canonicalCase != null) { |
| 113 | + return canonicalCase |
| 114 | + } |
| 115 | + return PATCH_TO_JASS_HISTORY_FOLDER.entries.firstOrNull { it.value.equals(withPrefix, ignoreCase = true) }?.key |
| 116 | + ?: withPrefix |
| 117 | + } |
| 118 | + |
| 119 | + fun isPre129Patch(input: String?): Boolean { |
| 120 | + val patch = normalizePatchInput(input) |
| 121 | + if (patch == PRE_129_PATCH) { |
| 122 | + return true |
| 123 | + } |
| 124 | + val version = Regex("""v(\d+)\.(\d+)""").find(patch)?.groupValues ?: return false |
| 125 | + return version[1].toIntOrNull() == 1 && (version[2].toIntOrNull() ?: 29) < 29 |
| 126 | + } |
| 127 | + |
| 128 | + fun ensureFiles(projectRoot: Path, wc3Patch: String?): List<Path> { |
| 129 | + val buildFolder = projectRoot.resolve("_build") |
| 130 | + Files.createDirectories(buildFolder) |
| 131 | + val patch = normalizePatchInput(wc3Patch) |
| 132 | + val previousPatch = readProvenance(buildFolder) |
| 133 | + val materializedFiles = listOf( |
| 134 | + materializeFile(buildFolder, "common.j", patch, previousPatch), |
| 135 | + materializeFile(buildFolder, "blizzard.j", patch, previousPatch) |
| 136 | + ) |
| 137 | + if (materializedFiles.all { it.managedByGrill }) { |
| 138 | + Files.writeString( |
| 139 | + buildFolder.resolve("core-jass.provenance"), |
| 140 | + "wc3Patch: $patch\njassHistoryFolder: ${jassHistoryFolderForPatch(patch).orEmpty()}\n" |
| 141 | + ) |
| 142 | + } else { |
| 143 | + log.warn( |
| 144 | + "Existing _build core JASS files have no Grill provenance; leaving them project-owned. " + |
| 145 | + "Delete _build/common.j and _build/blizzard.j to let Grill regenerate them for $patch." |
| 146 | + ) |
| 147 | + } |
| 148 | + return materializedFiles.map { it.path } |
| 149 | + } |
| 150 | + |
| 151 | + fun fetchJassHistoryVersions(): List<String> { |
| 152 | + return supportedPatches |
| 153 | + } |
| 154 | + |
| 155 | + fun recommendedPatchOptions(versions: List<String>): List<String> { |
| 156 | + return (listOf(DEFAULT_PATCH, "v1.31", PRE_129_PATCH) + versions.take(1)).distinct() |
| 157 | + } |
| 158 | + |
| 159 | + private data class MaterializedFile(val path: Path, val managedByGrill: Boolean) |
| 160 | + |
| 161 | + private fun materializeFile(buildFolder: Path, fileName: String, patch: String, previousPatch: String?): MaterializedFile { |
| 162 | + val target = buildFolder.resolve(fileName) |
| 163 | + val jassHistoryFolder = PATCH_TO_JASS_HISTORY_FOLDER[patch] |
| 164 | + if (jassHistoryFolder == null) { |
| 165 | + throw IllegalArgumentException("Unsupported WC3 patch <$patch>. Supported values: ${supportedPatches.joinToString()}") |
| 166 | + } |
| 167 | + |
| 168 | + if (previousPatch == null && Files.exists(target)) { |
| 169 | + log.info("Keeping existing _build/$fileName because it has no Grill provenance.") |
| 170 | + return MaterializedFile(target, managedByGrill = false) |
| 171 | + } |
| 172 | + |
| 173 | + val canKeepExisting = previousPatch == patch |
| 174 | + if (canKeepExisting && isValidCoreJassFile(target)) { |
| 175 | + log.info("Using cached _build/$fileName for $patch.") |
| 176 | + return MaterializedFile(target, managedByGrill = true) |
| 177 | + } |
| 178 | + |
| 179 | + try { |
| 180 | + downloadJassHistoryFile(fileName, patch, jassHistoryFolder, target) |
| 181 | + return MaterializedFile(target, managedByGrill = true) |
| 182 | + } catch (e: Exception) { |
| 183 | + if (canKeepExisting && isValidCoreJassFile(target)) { |
| 184 | + log.warn("Could not refresh $fileName for $patch; keeping existing _build copy. Reason: ${e.message}") |
| 185 | + return MaterializedFile(target, managedByGrill = true) |
| 186 | + } |
| 187 | + if (patch == DEFAULT_PATCH || patch == PRE_129_PATCH) { |
| 188 | + log.warn("Could not download $fileName for $patch; falling back to bundled core JASS. Reason: ${e.message}") |
| 189 | + copyBundledCoreJass(fileName, patch, target) |
| 190 | + return MaterializedFile(target, managedByGrill = true) |
| 191 | + } |
| 192 | + throw RuntimeException("Could not download $fileName for WC3 patch <$patch> from jass-history.", e) |
| 193 | + } |
| 194 | + } |
| 195 | + |
| 196 | + private fun readProvenance(buildFolder: Path): String? { |
| 197 | + val provenanceFile = buildFolder.resolve("core-jass.provenance") |
| 198 | + if (!Files.exists(provenanceFile)) { |
| 199 | + return null |
| 200 | + } |
| 201 | + return Files.readString(provenanceFile) |
| 202 | + .lineSequence() |
| 203 | + .firstOrNull { it.startsWith("wc3Patch:") } |
| 204 | + ?.substringAfter(":") |
| 205 | + ?.trim() |
| 206 | + ?.let(::normalizePatchInput) |
| 207 | + } |
| 208 | + |
| 209 | + private fun copyBundledCoreJass(fileName: String, patch: String, target: Path) { |
| 210 | + val patchFolder = when (patch) { |
| 211 | + PRE_129_PATCH -> "pre1.29" |
| 212 | + else -> "reforged" |
| 213 | + } |
| 214 | + |
| 215 | + Files.createDirectories(target.parent) |
| 216 | + val resourcePath = "core-jass/$patchFolder/$fileName" |
| 217 | + javaClass.classLoader.getResourceAsStream(resourcePath)?.use { input -> |
| 218 | + Files.copy(input, target, StandardCopyOption.REPLACE_EXISTING) |
| 219 | + return |
| 220 | + } |
| 221 | + |
| 222 | + JarFile(global.InstallationManager.getCompilerPath()).use { jar -> |
| 223 | + val entry = jar.getEntry(fileName) |
| 224 | + ?: throw IllegalStateException("Bundled $fileName was not found for $patch.") |
| 225 | + jar.getInputStream(entry).use { input -> |
| 226 | + Files.copy(input, target, StandardCopyOption.REPLACE_EXISTING) |
| 227 | + } |
| 228 | + } |
| 229 | + } |
| 230 | + |
| 231 | + private fun downloadJassHistoryFile(fileName: String, patch: String, jassHistoryFolder: String, target: Path) { |
| 232 | + Files.createDirectories(target.parent) |
| 233 | + val rawUrl = "$JASS_HISTORY_RAW/$JASS_HISTORY_REF/war3extract/$jassHistoryFolder/scripts/$fileName" |
| 234 | + val tempFile = Files.createTempFile(target.parent, "$fileName.", ".download") |
| 235 | + var replacedTarget = false |
| 236 | + try { |
| 237 | + URI(rawUrl).toURL().openStream().use { input -> |
| 238 | + Files.copy(input, tempFile, StandardCopyOption.REPLACE_EXISTING) |
| 239 | + } |
| 240 | + if (!isValidCoreJassFile(tempFile)) { |
| 241 | + throw IllegalStateException("Downloaded $fileName from jass-history did not look valid.") |
| 242 | + } |
| 243 | + moveValidatedDownload(tempFile, target) |
| 244 | + replacedTarget = true |
| 245 | + } finally { |
| 246 | + if (!replacedTarget) { |
| 247 | + Files.deleteIfExists(tempFile) |
| 248 | + } |
| 249 | + } |
| 250 | + } |
| 251 | + |
| 252 | + private fun isValidCoreJassFile(path: Path): Boolean { |
| 253 | + return Files.exists(path) && Files.size(path) >= 1024L |
| 254 | + } |
| 255 | + |
| 256 | + private fun moveValidatedDownload(source: Path, target: Path) { |
| 257 | + try { |
| 258 | + Files.move(source, target, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE) |
| 259 | + } catch (_: AtomicMoveNotSupportedException) { |
| 260 | + Files.move(source, target, StandardCopyOption.REPLACE_EXISTING) |
| 261 | + } |
| 262 | + } |
| 263 | + |
| 264 | +} |
0 commit comments