Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
f0098e4
chore: cake-model-viewer -> cake-viewer
demoth Oct 21, 2026
3bcf53e
chore: small cleanup and todos
demoth Oct 21, 2026
5835efa
chore: update readme and cleanup planning notes
demoth Oct 21, 2026
acbc232
chore: move docs to info/
demoth Oct 21, 2026
499b169
Fix Cake Viewer native build on current GraalVM
demoth Oct 21, 2026
2a7377e
Share native reflection configuration across Cake launchers
demoth Oct 21, 2026
5b271ad
Support TGA images in Cake Viewer
demoth Oct 21, 2026
2257891
Add WAV playback to Cake Viewer
demoth Oct 21, 2026
d374f51
Add SP2 sprite preview and animation to Cake Viewer
demoth Oct 21, 2026
d166f0f
Use engine skin for Cake Viewer information panel
demoth Oct 21, 2026
a0a66a4
Show MD2 frame and skin texture counts in Cake Viewer
demoth Oct 21, 2026
f3c3102
Show sound duration in Cake Viewer information panel
demoth Oct 21, 2026
0584ff8
Restore BSP world rendering and show map information in Cake Viewer
demoth Oct 21, 2026
a12360b
Allow Cake Viewer resolver context changes
demoth Oct 21, 2026
2384851
Replace Cake Viewer previews through file dropping
demoth Oct 21, 2026
2e8a5c0
Expose typed snapshots of resolved VFS entries
demoth Oct 21, 2026
40b36e2
Add isolated archive contexts to Cake Viewer resolver
demoth Oct 21, 2026
97f45b9
Add PAK folder browsing to Cake Viewer
demoth Oct 21, 2026
4ff623b
Keep Cake Viewer UI pixel size stable on window resize
demoth Oct 22, 2026
d21d4e4
Preview archive images in Cake Viewer
demoth Oct 22, 2026
b445472
Show image thumbnails in Cake Viewer folder grid
demoth Oct 22, 2026
30b0f0f
Simplify viewer tiles and retain folder thumbnails
demoth Oct 22, 2026
63795c2
Add archive sound previews with audition and looping
demoth Oct 22, 2026
6d6c902
Consolidate viewer preview resource ownership
demoth Oct 22, 2026
b4e089c
Preview and animate archive sprites in Cake Viewer
demoth Oct 22, 2026
e200aa1
Preview archive MD2 models with orbit camera
demoth Oct 22, 2026
48255d3
Share frame playback controls between models and sprites
demoth Oct 22, 2026
7306a37
Preview archive BSP maps with focused fly controls
demoth Oct 22, 2026
8e65d80
Add image fit, zoom and pan to archive previews
demoth Oct 22, 2026
e655086
Share image navigation with sprite previews
demoth Oct 22, 2026
7d4cf18
Share sound transport with pause and resume controls
demoth Oct 22, 2026
4e4ff1f
Select loaded MD2 skins in archive previews
demoth Oct 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ build from source:
- `./gradlew :cake:cake-fullgame:run` - to run Cake with singleplayer and hosting support
- `./gradlew :cake:cake-client:distZip` - to build the desktop Java distribution
- `./gradlew :cake:cake-fullgame:distZip` - to build the full game desktop Java distribution
- `./gradlew :cake:cake-modelviewer:distZip` - to build the model viewer desktop Java distribution
- `./gradlew :cake:cake-viewer:distZip` - to build the viewer desktop Java distribution

If you run jake from an IDE:
1. to run the server run `Jake2Dedicated.main` with the desired args (`+set coop 1 +map base1`)
Expand Down
30 changes: 18 additions & 12 deletions cake/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,16 @@ and Host Game menus. Core accesses hosting through `CakeHost`; only
### `desktop-common`

Shared game window configuration, video platform, JVM restart helper and desktop
icons. Both game launchers use `CakeDesktopLauncher`; the model viewer shares
icons. Both game launchers use `CakeDesktopLauncher`; the viewer shares
the restart helper and icons while retaining its own window configuration.

### `cake-modelviewer`
The icons in `desktop-common/icons` and `desktop-common/src/main/resources` are
the default libGDX Liftoff placeholders, pending Cake branding.

Standalone desktop launcher for the Cake model viewer.
### `cake-viewer`

Standalone desktop launcher for the Cake Viewer.
Opens MD2 models, BSP maps, and PCX/WAL images; see the [viewer guide](../info/cake-viewer.md).

### [`engine-tools`](engine-tools/README.md)

Expand All @@ -40,7 +44,7 @@ From repository root:

- `./gradlew :cake:cake-client:run` - runs the remote-only client.
- `./gradlew :cake:cake-fullgame:run` - runs the client with singleplayer and hosting support.
- `./gradlew :cake:cake-modelviewer:run --args="/absolute/path/to/file.md2"` - runs the model viewer launcher directly.
- `./gradlew :cake:cake-viewer:run --args="/absolute/path/to/file.md2"` - runs the viewer launcher directly.

Main desktop entrypoint:

Expand All @@ -51,17 +55,17 @@ Main desktop entrypoint:
Build portable ZIPs with Java 25:

```sh
./gradlew :cake:cake-client:distZip :cake:cake-modelviewer:distZip
./gradlew :cake:cake-client:distZip :cake:cake-viewer:distZip
./gradlew :cake:cake-fullgame:distZip
```

Each launcher's `build/distributions/` contains a ZIP with `bin/` launch scripts
and `lib/` application/dependency JARs. Extract the entire ZIP and run
`bin/cake-client`, `bin/cake-fullgame`, or `bin/cake-modelviewer` (the `.bat` script on Windows).
Pass an asset path to the model viewer; Quake 2 game data remains external.
`bin/cake-client`, `bin/cake-fullgame`, or `bin/cake-viewer` (the `.bat` script on Windows).
Pass an asset path to the viewer; Quake 2 game data remains external.

For a local installation, use `:cake:cake-client:installDist` or
`:cake:cake-modelviewer:installDist`; scripts are under the corresponding
`:cake:cake-viewer:installDist`; scripts are under the corresponding
`build/install/<application>/bin/` directory. JARs under `build/libs/` are normal
project JARs and need the distribution classpath. Use `distZip` instead of the
former custom `dist` task; standalone fat JARs are no longer produced.
Expand All @@ -80,7 +84,7 @@ tasks. Beryx 2.0.1 still requires `--no-configuration-cache` for
```

`runtimeZip` writes `build/image.zip`; `jpackageImage` writes an application
under `build/jpackage/`. Use the corresponding model-viewer tasks for that app.
under `build/jpackage/`. Use the corresponding viewer tasks for that app.
These outputs bundle the project Java toolchain and target the build machine's
OS and architecture. Installers remain disabled.

Expand All @@ -91,15 +95,17 @@ experimental native executable:

```sh
./gradlew -PenableGraalNative=true :cake:cake-client:nativeCompile
./gradlew -PenableGraalNative=true :cake:cake-modelviewer:nativeCompile
./gradlew -PenableGraalNative=true :cake:cake-viewer:nativeCompile
```

These tasks require GraalVM 25 or newer with Native Image. Set `GRAALVM_HOME` to
that installation; Gradle can continue running on the regular project JDK.
Native resources are detected from the runtime classpath, including repository
assets and dependency libraries. Ordinary JVM builds and ZIPs omit the Graal helpers.
Fullgame native-image support is not verified; embedded game reflection and save
serialization need additional reachability coverage before that is supported.
All three launchers use the shared native configuration and explicit reflection
metadata for audio and Scene2D skin/pool classes. Native compilation and basic
startup are verified on Linux ARM64; fullgame hosting and save/load still need
runtime validation.

## Playing and hosting

Expand Down
1 change: 0 additions & 1 deletion cake/cake-fullgame/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ apply(from = rootProject.file("cake/desktop.gradle"))

dependencies {
implementation(project(":cake:core"))
implementation(project(":cake:desktop-common"))
implementation(project(":server"))
implementation(project(":qcommon"))
runtimeOnly(project(":game"))
Expand Down
Binary file removed cake/cake-lwjgl3/icons/logo.icns
Binary file not shown.
Binary file removed cake/cake-lwjgl3/icons/logo.ico
Binary file not shown.
Binary file removed cake/cake-lwjgl3/icons/logo.png
Binary file not shown.
Binary file removed cake/cake-lwjgl3/src/main/resources/libgdx128.png
Binary file not shown.
Binary file removed cake/cake-lwjgl3/src/main/resources/libgdx16.png
Binary file not shown.
Binary file removed cake/cake-lwjgl3/src/main/resources/libgdx32.png
Binary file not shown.
Binary file removed cake/cake-lwjgl3/src/main/resources/libgdx64.png
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ plugins {
}

application {
mainClass.set('org.demoth.cake.lwjgl3.Lwjgl3ModelViewerLauncher')
mainClass.set('org.demoth.cake.lwjgl3.Lwjgl3ViewerLauncher')
}

apply from: rootProject.file('cake/desktop.gradle')
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package org.demoth.cake.lwjgl3

import com.badlogic.gdx.Gdx
import com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application
import com.badlogic.gdx.backends.lwjgl3.Lwjgl3ApplicationConfiguration
import com.badlogic.gdx.backends.lwjgl3.Lwjgl3WindowAdapter
import com.badlogic.gdx.graphics.glutils.HdpiMode
import org.demoth.cake.modelviewer.CakeModelViewer
import org.demoth.cake.viewer.CakeViewer

object Lwjgl3ModelViewerLauncher {
object Lwjgl3ViewerLauncher {
@JvmStatic
fun main(args: Array<String>) {
if (startNewJvmIfRequired(args)) return // This handles macOS support and helps on Windows.
Expand All @@ -15,11 +17,19 @@ object Lwjgl3ModelViewerLauncher {
}

private fun createApplication(args: Array<String>): Lwjgl3Application {
return Lwjgl3Application(CakeModelViewer(args), createConfiguration())
val viewer = CakeViewer(args)
val configuration = createConfiguration().apply {
setWindowListener(object : Lwjgl3WindowAdapter() {
override fun filesDropped(files: Array<String>) {
files.firstOrNull()?.let { path -> Gdx.app.postRunnable { viewer.openFile(path) } }
}
})
}
return Lwjgl3Application(viewer, configuration)
}

private fun createConfiguration() = Lwjgl3ApplicationConfiguration().apply {
setTitle("Cake Model Viewer 1.2.2")
setTitle("Cake Viewer 1.2.2")
setOpenGLEmulation(Lwjgl3ApplicationConfiguration.GLEmulation.GL32, 3, 2)
useVsync(true)
//// Limits FPS to the refresh rate of the currently active monitor.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ object CakeStartupBootstrap {
private fun loadProfileConfig(profileId: String, profileConfigStore: CakeProfileConfigStore) {
try {
val configText = profileConfigStore.readConfig(profileId) ?: return
// TODO: cleanup: avoid "after restart" messages for saved video settings applied before window creation.
Cbuf.AddAndExecuteScript(configText)
Cvar.getInstance().updateLatchedVars()
Com.Printf("Loaded Cake profile config for '$profileId'\n")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import com.badlogic.gdx.graphics.glutils.ShaderProgram

/**
* Configures LibGDX shader source prefixes for the GL3 pipeline used by Cake.
* Shared between the game client and the standalone model viewer.
* Shared between the game client and the standalone viewer.
*/
fun initializeShaderCompatibility() {
ShaderProgram.prependVertexCode = """
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package org.demoth.cake.assets
import com.badlogic.gdx.files.FileHandle
import jake2.qcommon.vfs.DefaultVirtualFileSystem
import jake2.qcommon.vfs.VfsConfig
import jake2.qcommon.vfs.VfsEntry
import jake2.qcommon.vfs.VfsSourceType
import java.nio.file.Files
import java.nio.file.Path
Expand All @@ -14,20 +15,22 @@ import java.nio.file.Path
* to temporary files so libGDX loaders can consume standard [FileHandle]s.
*/
internal class CakeVfsAssetSource {
private val vfs = DefaultVirtualFileSystem()
private var vfs = DefaultVirtualFileSystem()
private val extractedPackageFiles = mutableMapOf<String, Path>()

private var configuredBasedir: String? = null
private var configuredGameMod: String? = null
private var configuredCaseSensitive: Boolean = false
private var initialized = false
private var archiveContext = false

@Synchronized
fun configure(basedir: String?, gameMod: String?, caseSensitive: Boolean) {
val normalizedBasedir = basedir?.takeIf { it.isNotBlank() }
val normalizedGameMod = gameMod?.takeIf { it.isNotBlank() }
if (
initialized &&
!archiveContext &&
configuredBasedir == normalizedBasedir &&
configuredGameMod == normalizedGameMod &&
configuredCaseSensitive == caseSensitive
Expand All @@ -38,6 +41,7 @@ internal class CakeVfsAssetSource {
configuredBasedir = normalizedBasedir
configuredGameMod = normalizedGameMod
configuredCaseSensitive = caseSensitive
archiveContext = false

clearExtractedPackageFiles()

Expand All @@ -57,6 +61,25 @@ internal class CakeVfsAssetSource {
initialized = true
}

/** Mounts only the selected archive, retaining the previous context if mounting fails. */
@Synchronized
fun configureArchive(archivePath: String): List<VfsEntry> {
val archiveVfs = DefaultVirtualFileSystem()
archiveVfs.configure(
VfsConfig(
null, "baseq2", null, false, false, false, emptyList(),
setOf("pak", "pk2", "pk3", "pkz", "zip"),
),
)
val mounted = archiveVfs.mountPackage(archivePath)
require(mounted.success) { mounted.error ?: "Could not mount archive: $archivePath" }
clearExtractedPackageFiles()
vfs = archiveVfs
archiveContext = true
initialized = true
return vfs.listEntries()
}

@Synchronized
fun resolve(path: String): FileHandle? {
val lookup = vfs.resolve(path)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import com.badlogic.gdx.graphics.g3d.Material
import com.badlogic.gdx.graphics.g3d.Model
import com.badlogic.gdx.graphics.g3d.attributes.TextureAttribute
import com.badlogic.gdx.graphics.g3d.utils.ModelBuilder
import com.badlogic.gdx.math.Vector3
import com.badlogic.gdx.math.collision.BoundingBox
import com.badlogic.gdx.utils.Array
import com.badlogic.gdx.utils.Disposable
import jake2.qcommon.Com
Expand All @@ -36,6 +38,7 @@ import kotlin.math.floor
* [inlineRenderData] stores stable per-face part metadata for inline brush models (`*1`, `*2`, ...).
* [lightmapAtlas] describes packed BSP lightmap pages generated at load time.
* [generatedTextures] are runtime-generated lightmap atlas page textures (4 style-slot textures per page).
* [entities] and world [bounds] retain decoded map metadata for consumers such as resource previews.
* BSP textures are loaded as independent AssetManager dependencies and are not disposed here.
*/
class BspMapAsset(
Expand All @@ -47,6 +50,8 @@ class BspMapAsset(
val lightmapAtlas: BspLightmapAtlasMetadata? = null,
val lightmapAtlasPages: List<BspLightmapAtlasPageTextures> = emptyList(),
val generatedTextures: List<Texture> = emptyList(),
val entities: List<Map<String, String>> = emptyList(),
val bounds: BoundingBox = BoundingBox(),
) : Disposable {
override fun dispose() {
generatedTextures.forEach { it.dispose() }
Expand Down Expand Up @@ -284,6 +289,10 @@ class BspLoader(resolver: FileHandleResolver) : SynchronousAssetLoader<BspMapAss
BspLightmapAtlasPageTextures(textures = textures)
},
generatedTextures = generatedTextures,
entities = bsp.entities,
bounds = bsp.models.firstOrNull()?.let {
BoundingBox(Vector3(it.mins.x, it.mins.y, it.mins.z), Vector3(it.maxs.x, it.maxs.y, it.maxs.z))
} ?: BoundingBox(),
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ etc.) use the remaining slots, with part ids `inline_<modelIndex>_face_<faceInde
`BspWorldRenderData` retains stable surfaces, leaf-to-surface mappings, and texinfo
chains independently of draw grouping. Animated WAL dependencies include chain
frames even when no face references them directly.
`BspMapAsset.entities` and `bounds` retain the already-decoded entity dictionaries
and world model bounds, allowing preview camera placement without reparsing the map.

## World and Inline Rendering

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import com.badlogic.gdx.graphics.VertexAttributes.Usage.Generic
import com.badlogic.gdx.graphics.g3d.Material
import com.badlogic.gdx.graphics.g3d.Model
import com.badlogic.gdx.graphics.g3d.utils.ModelBuilder
import com.badlogic.gdx.math.collision.BoundingBox
import com.badlogic.gdx.utils.Array
import com.badlogic.gdx.utils.Disposable
import com.badlogic.gdx.utils.GdxRuntimeException
Expand All @@ -41,7 +42,10 @@ import org.demoth.cake.assets.ResourceKind
* [model] is the renderable geometry using the VAT shader attributes.
* Geometry is pre-normalized to Cake winding expectations, so default backface culling works as-is.
* [frames] is frame count metadata used by animation code.
* [frameNames] retains stored names in the same order as the animation frames.
* [skins] contains resolved skin textures in MD2 skin index order when available.
* [skinNames] retains their resolved paths; it is empty for the generated fallback skin.
* [bounds] encloses decoded positions across all frames; the VAT mesh has no position attribute.
*
* This allows future runtime skin switching (for example normal/injured monster skin)
* without replacing model geometry.
Expand All @@ -50,6 +54,9 @@ class Md2Asset(
val model: Model,
val frames: Int,
val skins: List<Texture>,
val bounds: BoundingBox,
val frameNames: List<String>,
val skinNames: List<String>,
) : Disposable {
override fun dispose() {
model.dispose()
Expand Down Expand Up @@ -87,7 +94,7 @@ class Md2Loader(resolver: FileHandleResolver) : SynchronousAssetLoader<Md2Asset,
*
* Typical usage:
* - Game client players: synthetic key `<skin>|<model>`, keep defaults.
* - Model viewer: keep embedded skins enabled and enable default fallback.
* - Cake Viewer: keep embedded skins enabled and enable default fallback.
*/
data class Parameters(
val loadEmbeddedSkins: Boolean = true,
Expand Down Expand Up @@ -159,7 +166,14 @@ class Md2Loader(resolver: FileHandleResolver) : SynchronousAssetLoader<Md2Asset,
return Md2Asset(
model = model,
frames = md2.frames.size,
frameNames = md2.frames.map { it.name },
skins = skins,
skinNames = skinPaths,
bounds = BoundingBox().apply {
inf()
val positions = vertexData.vertexPositions
for (i in positions.indices step 3) ext(positions[i], positions[i + 1], positions[i + 2])
},
)
}

Expand Down
10 changes: 8 additions & 2 deletions cake/core/src/main/kotlin/org/demoth/cake/assets/md2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ AssetManager request (model path or <skinPath>|<modelPath>)
-> Md2Shader: sample animation textures, interpolate positions, draw
```

`Md2Asset` contains the model, frame count, and ordered skin textures. The model
`Md2Asset` contains the model, frame count and stored names, ordered skin textures, and bounds
covering decoded vertex positions across all frames. The viewer uses these bounds
for camera framing; ordinary mesh bounds cannot represent the VAT geometry,
whose mesh contains animation indices rather than position attributes. The model
viewer and transient model effects use the same loader/shader pipeline as gameplay
entities. The shared `CakeShaderProvider` dispatches MD2, BSP, and default shaders;
its cross-pipeline routing remains outside this package.
Expand All @@ -26,8 +29,11 @@ its cross-pipeline routing remains outside this package.

`Md2Loader` chooses skins in this order: a synthetic key's prefix; otherwise embedded
MD2 skin paths if enabled; otherwise an optional generated white 1-by-1 skin.
The model viewer enables `useDefaultSkinIfMissing` for models with no selected skin
The viewer enables `useDefaultSkinIfMissing` for models with no selected skin
paths. That option does not recover arbitrary failures to load a referenced file.
`Md2Asset.skinNames` retains the resolved paths in texture order, or an empty list
for the generated fallback. The viewer uses these names in its skin selector and
changes the preview instance's `skinIndex` without reloading assets.

Player variants use keys such as
`players/male/grunt.pcx|players/male/tris.md2`. `PlayerConfiguration` selects the
Expand Down
Loading
Loading