initial commit: llamamod 0.2.0 – reverse-engineered level formats (wad, map, trak, text, stpc), python tools and notes, java mod manager (iso install, mod pipeline with exe patching, save editor, wine/windows launcher) with mods (test world, debug wireframe, mirror, randomizer, fog, cheats, intro skip, practice, free camera, boot to level, level projects) and the 3d level editor (geometry, objects, cross-world import, environment, textures, play-test, manual)
This commit is contained in:
commit
b36a366240
826 files changed
+105304
No files matched your search
+289
@@ -0,0 +1,289 @@
|
||||
plugins {
|
||||
id 'application'
|
||||
id 'com.diffplug.spotless' version '7.0.2'
|
||||
}
|
||||
|
||||
group = 'com.eliasmueller.llamamod'
|
||||
version = '0.2.0'
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
def lwjglVersion = '3.4.2'
|
||||
def lwjglNatives = ['natives-linux', 'natives-windows']
|
||||
|
||||
dependencies {
|
||||
implementation 'com.formdev:flatlaf:3.5.4'
|
||||
implementation 'com.formdev:flatlaf-fonts-inter:4.1'
|
||||
// level editor viewport: LWJGL (OpenGL) inside Swing via lwjgl3-awt; natives for both packages
|
||||
implementation platform("org.lwjgl:lwjgl-bom:${lwjglVersion}")
|
||||
implementation('org.lwjglx:lwjgl3-awt:0.2.5') { transitive = false }
|
||||
implementation 'org.joml:joml:1.10.8'
|
||||
['lwjgl', 'lwjgl-opengl', 'lwjgl-jawt', 'lwjgl-egl'].each { m ->
|
||||
implementation "org.lwjgl:${m}"
|
||||
if (m in ['lwjgl', 'lwjgl-opengl']) {
|
||||
lwjglNatives.each { n -> runtimeOnly "org.lwjgl:${m}::${n}" }
|
||||
}
|
||||
}
|
||||
testImplementation platform('org.junit:junit-bom:5.11.4')
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter'
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
def targetJavaVersion = 25
|
||||
java {
|
||||
toolchain.languageVersion = JavaLanguageVersion.of(targetJavaVersion)
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile).configureEach {
|
||||
options.encoding = 'UTF-8'
|
||||
options.release.set(targetJavaVersion)
|
||||
}
|
||||
|
||||
application {
|
||||
mainClass = 'com.eliasmueller.llamamod.Main'
|
||||
}
|
||||
|
||||
// build-info.properties: version and a build id (hash of the compiled classes and resources, without
|
||||
// the edition switch, so an unchanged rebuild keeps the id and the mod cache stays valid)
|
||||
def buildInfoDir = layout.buildDirectory.dir('generated/buildinfo')
|
||||
tasks.register('buildInfo') {
|
||||
def classes = sourceSets.main.output.classesDirs
|
||||
def resources = fileTree('src/main/resources') { exclude 'com/eliasmueller/llamamod/edition.properties' }
|
||||
def ver = project.version.toString()
|
||||
def outDir = buildInfoDir
|
||||
inputs.files(classes).withPathSensitivity(PathSensitivity.RELATIVE)
|
||||
inputs.files(resources).withPathSensitivity(PathSensitivity.RELATIVE)
|
||||
inputs.property('version', ver)
|
||||
outputs.dir(outDir)
|
||||
doLast {
|
||||
def md = java.security.MessageDigest.getInstance('SHA-256')
|
||||
def add = { File root ->
|
||||
if (!root.exists()) {
|
||||
return
|
||||
}
|
||||
def files = []
|
||||
root.eachFileRecurse(groovy.io.FileType.FILES) { files << it }
|
||||
files.sort { root.toPath().relativize(it.toPath()).toString().replace('\\', '/') }.each { f ->
|
||||
if (f.name == 'edition.properties') {
|
||||
return
|
||||
}
|
||||
md.update(root.toPath().relativize(f.toPath()).toString().replace('\\', '/').getBytes('UTF-8'))
|
||||
md.update(f.bytes)
|
||||
}
|
||||
}
|
||||
classes.files.each(add)
|
||||
add(file('src/main/resources'))
|
||||
def id = md.digest().encodeHex().toString().substring(0, 10)
|
||||
def f = outDir.get().file('com/eliasmueller/llamamod/build-info.properties').asFile
|
||||
f.parentFile.mkdirs()
|
||||
f.text = "version=${ver}\nbuild=${id}\n"
|
||||
}
|
||||
}
|
||||
|
||||
// user manual (docs/manual, Markdown + images) bundled for the editor's Help → Manual window
|
||||
processResources {
|
||||
from('docs/manual') {
|
||||
into 'com/eliasmueller/llamamod/editor/tools/help/manual'
|
||||
}
|
||||
from(tasks.named('buildInfo'))
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
// golden tests: -Dllamamod.cd=<cd dir> -Dllamamod.ref=<python reference dir>
|
||||
systemProperty 'llamamod.cd', System.getProperty('llamamod.cd', "${System.getProperty('user.home')}/.wine-lama/iso-dk/Setup/Eng/Cd")
|
||||
systemProperty 'llamamod.ref', System.getProperty('llamamod.ref', '')
|
||||
// level builder references: python3 tools/levelbuild.py levels/testwelt.py <lbref>/testwelt.wad etc.
|
||||
systemProperty 'llamamod.lbref', System.getProperty('llamamod.lbref', '')
|
||||
maxHeapSize = '2g'
|
||||
}
|
||||
|
||||
// fat jars with all dependencies: full edition (fatJar) and launcher edition without the editor UI
|
||||
// (fatJarLauncher, edition.properties editor=false; the editor classes stay, mods need them)
|
||||
def editions = ['full', 'launcher']
|
||||
def launcherEditionDir = layout.buildDirectory.dir('generated/edition-launcher')
|
||||
tasks.register('launcherEdition') {
|
||||
def outDir = launcherEditionDir
|
||||
outputs.dir(outDir)
|
||||
doLast {
|
||||
def f = outDir.get().file('com/eliasmueller/llamamod/edition.properties').asFile
|
||||
f.parentFile.mkdirs()
|
||||
f.text = 'editor=false\n'
|
||||
}
|
||||
}
|
||||
editions.each { ed ->
|
||||
tasks.register(ed == 'full' ? 'fatJar' : 'fatJarLauncher', Jar) {
|
||||
archiveClassifier = ed == 'full' ? 'all' : 'launcher'
|
||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||
manifest {
|
||||
attributes 'Main-Class': application.mainClass, 'Implementation-Version': project.version, 'Multi-Release': 'true'
|
||||
}
|
||||
if (ed == 'full') {
|
||||
from sourceSets.main.output
|
||||
} else {
|
||||
from(tasks.named('launcherEdition'))
|
||||
from(sourceSets.main.output) { exclude 'com/eliasmueller/llamamod/edition.properties' }
|
||||
}
|
||||
dependsOn configurations.runtimeClasspath
|
||||
from { configurations.runtimeClasspath.findAll { it.name.endsWith('jar') }.collect { zipTree(it) } }
|
||||
exclude 'META-INF/*.SF', 'META-INF/*.DSA', 'META-INF/*.RSA', 'module-info.class', 'META-INF/versions/*/module-info.class'
|
||||
}
|
||||
}
|
||||
|
||||
spotless {
|
||||
java {
|
||||
target 'src/**/*.java'
|
||||
palantirJavaFormat()
|
||||
removeUnusedImports()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- packaging
|
||||
// Self-contained Linux folder (+ .tar.gz) and Windows zip (LlamaMod.exe + runtime), both built on
|
||||
// Linux. jlink cross-links using the Temurin jmods of the target platform; the jlink binary must
|
||||
// be the exact same JDK version (JDK 25 ships no jmods, so they are downloaded separately).
|
||||
// Downloads are cached in ~/opt (override: -Pllamamod.opt=<dir>).
|
||||
def runtimeVersion = '25.0.5+8'
|
||||
def optDir = file(findProperty('llamamod.opt') ?: "${System.getProperty('user.home')}/opt")
|
||||
def distDir = layout.buildDirectory.dir('dist').get().asFile
|
||||
// jdk.unsupported: sun.misc.Unsafe for LWJGL
|
||||
def runtimeModules = 'java.base,java.desktop,java.prefs,java.logging,jdk.charsets,jdk.zipfs,jdk.unsupported'
|
||||
def extraModules = [linux: '', windows: ',jdk.crypto.mscapi']
|
||||
|
||||
def adoptium = { String os, String imageType ->
|
||||
"https://api.adoptium.net/v3/binary/version/jdk-${runtimeVersion.replace('+', '%2B')}/${os}/x64/${imageType}/hotspot/normal/eclipse"
|
||||
}
|
||||
def sh = { List cmd ->
|
||||
def p = new ProcessBuilder(cmd.collect { it.toString() }).inheritIO().start()
|
||||
if (p.waitFor() != 0) {
|
||||
throw new GradleException("Command failed: ${cmd.join(' ')}")
|
||||
}
|
||||
}
|
||||
def download = { String url, File target ->
|
||||
if (!target.exists()) {
|
||||
target.parentFile.mkdirs()
|
||||
logger.lifecycle("Downloading ${url}")
|
||||
def tmp = new File(target.path + '.part')
|
||||
new URI(url).toURL().withInputStream { i -> tmp.withOutputStream { it << i } }
|
||||
tmp.renameTo(target)
|
||||
}
|
||||
target
|
||||
}
|
||||
|
||||
tasks.register('fetchJdk') {
|
||||
description = 'Downloads the Linux JDK (for jlink) and the jmods of both target platforms.'
|
||||
doLast {
|
||||
def jdk = new File(optDir, "jdk-${runtimeVersion}")
|
||||
if (!new File(jdk, 'bin/jlink').exists()) {
|
||||
def tgz = download(adoptium('linux', 'jdk'), new File(optDir, "dl/jdk-${runtimeVersion}-linux.tar.gz"))
|
||||
sh(['tar', 'xzf', tgz, '-C', optDir])
|
||||
}
|
||||
['linux': 'tar.gz', 'windows': 'zip'].each { os, ext ->
|
||||
def dir = new File(optDir, "jmods-${runtimeVersion}-${os}")
|
||||
if (!dir.exists()) {
|
||||
def arc = download(adoptium(os, 'jmods'), new File(optDir, "dl/jmods-${runtimeVersion}-${os}.${ext}"))
|
||||
def tmp = new File(optDir, "jmods-${runtimeVersion}-${os}.tmp")
|
||||
project.delete(tmp)
|
||||
tmp.mkdirs()
|
||||
if (ext == 'zip') {
|
||||
project.copy { from project.zipTree(arc); into tmp }
|
||||
} else {
|
||||
sh(['tar', 'xzf', arc, '-C', tmp])
|
||||
}
|
||||
// archives contain one top-level folder holding the *.jmod files
|
||||
def jmodDir = fileTree(tmp).matching { include '**/java.base.jmod' }.singleFile.parentFile
|
||||
jmodDir.renameTo(dir)
|
||||
project.delete(tmp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def jlinkTask = { String name, String os ->
|
||||
tasks.register(name) {
|
||||
dependsOn 'fetchJdk'
|
||||
def out = new File(layout.buildDirectory.get().asFile, "runtime-${os}")
|
||||
outputs.dir(out)
|
||||
doLast {
|
||||
project.delete(out)
|
||||
sh([new File(optDir, "jdk-${runtimeVersion}/bin/jlink").path,
|
||||
'--module-path', new File(optDir, "jmods-${runtimeVersion}-${os}").path,
|
||||
'--add-modules', runtimeModules + extraModules[os],
|
||||
'--strip-debug', '--no-header-files', '--no-man-pages',
|
||||
'--compress', 'zip-9',
|
||||
'--output', out.path])
|
||||
}
|
||||
}
|
||||
}
|
||||
jlinkTask('jlinkLinux', 'linux')
|
||||
jlinkTask('jlinkWindows', 'windows')
|
||||
|
||||
def jarTask = { String ed -> ed == 'full' ? 'fatJar' : 'fatJarLauncher' }
|
||||
def suffix = { String ed -> ed == 'full' ? '' : 'Launcher' }
|
||||
def packageName = { String ed, String os -> "LlamaMod-${project.version}-${ed}-${os}-x64" }
|
||||
|
||||
editions.each { ed ->
|
||||
def stage = new File(distDir, "${ed}/LlamaMod-linux-x64")
|
||||
tasks.register("linuxImage${suffix(ed)}", Sync) {
|
||||
dependsOn jarTask(ed), 'jlinkLinux'
|
||||
from(layout.buildDirectory.dir('runtime-linux')) { into 'runtime' }
|
||||
from(tasks.named(jarTask(ed))) { into 'lib'; rename { 'llamamod.jar' } }
|
||||
from('packaging/linux') { filePermissions { unix('rwxr-xr-x') }; include 'LlamaMod', '*.sh' }
|
||||
from('packaging/linux') { exclude 'LlamaMod', '*.sh' }
|
||||
from('src/main/resources/com/eliasmueller/llamamod/ui/icon-256.png') { rename { 'llamamod.png' } }
|
||||
into stage
|
||||
// jlink marks legal/ files read-only, so Sync cannot overwrite them
|
||||
doFirst { project.delete(stage) }
|
||||
}
|
||||
|
||||
tasks.register("packageLinux${suffix(ed)}") {
|
||||
description = "Self-contained Linux build (${ed} edition): build/dist/${packageName(ed, 'linux')}.tar.gz."
|
||||
dependsOn "linuxImage${suffix(ed)}"
|
||||
doLast {
|
||||
// system tar keeps the executable bits of the runtime (Gradle's Tar normalizes them)
|
||||
sh(['tar', 'czf', new File(distDir, "${packageName(ed, 'linux')}.tar.gz"), '-C', stage.parentFile, stage.name])
|
||||
}
|
||||
}
|
||||
|
||||
def exe = new File(layout.buildDirectory.get().asFile, "exe/${ed}/LlamaMod.exe")
|
||||
tasks.register("exeWindows${suffix(ed)}") {
|
||||
description = 'Wraps the fat jar into LlamaMod.exe with launch4j (Linux binary, downloaded to ~/opt).'
|
||||
dependsOn jarTask(ed)
|
||||
inputs.files(tasks.named(jarTask(ed)))
|
||||
inputs.dir('packaging/windows')
|
||||
outputs.file(exe)
|
||||
doLast {
|
||||
def l4j = new File(optDir, 'launch4j')
|
||||
if (!new File(l4j, 'launch4j.jar').exists()) {
|
||||
def tgz = download('https://sourceforge.net/projects/launch4j/files/launch4j-3/3.50/launch4j-3.50-linux-x64.tgz/download',
|
||||
new File(optDir, 'dl/launch4j-3.50-linux-x64.tgz'))
|
||||
sh(['tar', 'xzf', tgz, '-C', optDir])
|
||||
}
|
||||
exe.parentFile.mkdirs()
|
||||
def cfg = new File(exe.parentFile, 'launch4j.xml')
|
||||
cfg.text = file('packaging/windows/launch4j.xml.in').text
|
||||
.replace('@JAR@', tasks.named(jarTask(ed)).get().archiveFile.get().asFile.path)
|
||||
.replace('@OUT@', exe.path)
|
||||
.replace('@ICON@', file('packaging/icon.ico').path)
|
||||
.replace('@VERSION@', project.version.toString())
|
||||
sh([new File(optDir, "jdk-${runtimeVersion}/bin/java").path, '-jar', new File(l4j, 'launch4j.jar').path, cfg.path])
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("packageWindows${suffix(ed)}", Zip) {
|
||||
description = "Windows build (made on Linux, ${ed} edition): build/dist/${packageName(ed, 'windows')}.zip."
|
||||
dependsOn "exeWindows${suffix(ed)}", 'jlinkWindows'
|
||||
from(layout.buildDirectory.dir('runtime-windows')) { into 'LlamaMod/runtime' }
|
||||
from(exe) { into 'LlamaMod' }
|
||||
from('packaging/windows') { exclude '*.in'; into 'LlamaMod' }
|
||||
destinationDirectory = distDir
|
||||
archiveFileName = "${packageName(ed, 'windows')}.zip"
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register('packageAll') {
|
||||
description = 'Linux and Windows packages of both editions (full, launcher) in build/dist.'
|
||||
dependsOn editions.collectMany { ["packageLinux${suffix(it)}", "packageWindows${suffix(it)}"] }
|
||||
}
|
||||
Reference in new issue
Block a user