Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
618d575
Move level restoration into the owning game instance
demoth Oct 26, 2026
0296074
Keep JVM diagnostics with host command registration
demoth Oct 26, 2026
4eb84e7
Scope save file operations to an explicit root and remove SV_CCMDS
demoth Oct 26, 2026
ea2095b
Consolidate per-map server state into the game instance
demoth Oct 26, 2026
b203968
Consolidate game import behavior and remove SV_GAME
demoth Oct 26, 2026
7abcc6b
Consolidate spatial state and operations into the game instance
demoth Oct 26, 2026
06bccc7
Consolidate client snapshot construction into the game instance
demoth Oct 26, 2026
397449b
Split server sending behavior by Game and Host ownership
demoth Oct 26, 2026
c9220ac
Move game initialization and entity event cleanup into Game
demoth Oct 26, 2026
4b15825
Consolidate client protocol and mutable server state under Host owner…
demoth Oct 26, 2026
a5a8551
Consolidate Host operator commands and remove legacy sv commands
demoth Oct 26, 2026
12e5a08
Replace SV_MAIN with Kotlin ServerHost
demoth Oct 26, 2026
fdfd405
Clean up ServerHost Kotlin style and deprecated formatting
demoth Oct 26, 2026
4b741b5
chore: cleanup ServerHost
demoth Oct 26, 2026
ab8108b
Group ServerHost members by responsibility
demoth Oct 26, 2026
da81095
Move safe transition archival into Game
demoth Oct 26, 2026
0856ada
chore: remove suppress deprecation
demoth Oct 26, 2026
9ad23e3
Use Host time and frames for client accounting
demoth Oct 26, 2026
d6652cb
Own cinematic continuation per Game and target Host transitions
demoth Oct 26, 2026
2de9df4
Isolate Game coop and deathmatch cvars
demoth Oct 26, 2026
0c15a8c
Own configured air acceleration in Game cvars
demoth Oct 26, 2026
4a6d636
Isolate live Game rules
demoth Oct 26, 2026
c4446ec
Share Host session preparation across map and load
demoth Oct 26, 2026
22a37e8
Validate saves before replacement and propagate persistence failures
demoth Oct 26, 2026
fe2a11c
Replace precache entities during restoration and remove sv_noreload
demoth Oct 26, 2026
8103ab6
Persist entity components when restoring saved levels
demoth Oct 26, 2026
88d105e
Recover failed loads to a reusable idle Host
demoth Oct 26, 2026
588289f
Share map and load transition outcomes and recovery
demoth Oct 26, 2026
505f44c
Recover automatic map continuation failures
demoth Oct 26, 2026
5e667f7
Consolidate server session cleanup in Host stop
demoth Oct 26, 2026
e88a059
Document deferred multi-Game persistence isolation
demoth Oct 26, 2026
5d6d47e
Remove deferred multi-Game save targeting
demoth Oct 26, 2026
a8b8b21
Give saving explicit outcomes at the Host boundary
demoth Oct 26, 2026
4d7c42c
Keep missing map lists from stopping the Host
demoth Oct 26, 2026
7b11ede
Centralize targeted Game shutdown in Host
demoth Oct 26, 2026
000bfc3
Contain Game import failures at Host execution boundaries
demoth Oct 26, 2026
b7c6459
Guarantee connection cleanup when disconnect callbacks fail
demoth Oct 27, 2026
e052905
Close dedicated Host on startup and loop exit
demoth Oct 27, 2026
8ed6a8e
Route collision failures through their owning Game
demoth Oct 27, 2026
2e68aba
Document query-local collision scratch follow-up
demoth Oct 27, 2026
5274df3
Propagate low-level output failures without global teardown
demoth Oct 27, 2026
92c25d4
Order Host challenges independently of the global clock
demoth Oct 27, 2026
c9d9412
Remove unused Game realtime and frame timestamp plumbing
demoth Oct 27, 2026
759ce0c
Share Game pause policy and report Game-owned map identity
demoth Oct 27, 2026
497f48b
Avoid inherited map names when Game server-info replacement fails
demoth Oct 27, 2026
1bf70a9
Isolate opt-in rendered game tests by application process
demoth Oct 27, 2026
b52ce6b
Bump version to 1.2.5 after Host/Game cleanup
demoth Oct 27, 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 build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ subprojects {
apply plugin: 'jacoco'

group = 'org.demoth'
version = '1.2.4'
version = '1.2.5'
ext.appName = 'cake'

kotlin {
Expand Down
8 changes: 8 additions & 0 deletions cake/cake-fullgame/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,11 @@ dependencies {
runtimeOnly(project(":game"))
testImplementation(project(":game"))
}

// Real-application tests bootstrap process-wide engine state and replace user.home.
// Keep each opt-in test class in its own application process.
tasks.test {
if (providers.environmentVariable("CAKE_HOST_GAME_TESTS").orNull == "1") {
forkEvery = 1
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import jake2.qcommon.network.NET
import jake2.qcommon.vfs.EngineVfs
import jake2.qcommon.vfs.EngineWriteRoot
import jake2.qcommon.vfs.VfsConfig
import jake2.server.SV_MAIN
import jake2.server.ServerHost
import org.demoth.cake.hosting.*
import org.demoth.cake.profile.CakeGameProfile
import java.io.IOException
Expand All @@ -21,7 +21,8 @@ import java.nio.file.Path
class EmbeddedCakeHost(
private val profilesRoot: Path = Path.of(System.getProperty("user.home"), ".cake"),
) : CakeHost {
private var server: SV_MAIN? = null
internal var server: ServerHost? = null
private set
private var starting = false
private var ownerThread: Thread? = null

Expand Down Expand Up @@ -62,7 +63,7 @@ class EmbeddedCakeHost(
return HostStartResult.Rejected("Map not found: ${request.map}")
}
val cvars = Cvar.getInstance()
val manager = SV_MAIN()
val manager = ServerHost()
server = manager
cvars.find<Boolean>("deathmatch").forceSet(request.mode == HostMode.DEATHMATCH)
cvars.find<Boolean>("coop").forceSet(request.mode == HostMode.COOP)
Expand Down Expand Up @@ -128,18 +129,14 @@ class EmbeddedCakeHost(
server = null
starting = false
try {
manager?.SV_Shutdown("$reason\n", false)
manager?.stop("$reason\n", false)
} finally {
try {
manager?.close()
} finally {
if (ownedSession) {
// Shutdown notification is sent before sockets close.
NET.configureServer(false)
// This adapter also owns disposal of the local client's queued traffic.
NET.clearLoopbacks()
Cbuf.ClearDeferred()
val cvars = Cvar.getInstance()
if (cvars.isRegistered("paused")) cvars.forceSetResult("paused", "0")
}
previousWriteRoot?.let(EngineWriteRoot::setRoot)
previousWriteRoot = null
Expand All @@ -151,8 +148,8 @@ class EmbeddedCakeHost(
checkThread()
if (server?.isRunning != true) return
val cvars = Cvar.getInstance()
val singlePlayer = !cvars.find<Boolean>("deathmatch").typedValue &&
!cvars.find<Boolean>("coop").typedValue && cvars.find<Int>("maxclients").typedValue == 1
val game = server?.games?.get("default") ?: return
val singlePlayer = !game.isDeathmatch && !game.isCoop && server?.clients?.size == 1
cvars.find<Boolean>("paused").forceSet(paused && singlePlayer)
}

Expand Down
371 changes: 366 additions & 5 deletions cake/cake-fullgame/src/test/java/jake2/server/GameImportsTest.java

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ class CakeHostApplicationTest {
Cbuf.AddText("host_start testbox coop")
cake!!.render()
assertEquals(HostState.RUNNING, host.state)
frame = gameFrame()
frame = gameFrame(host)
}
1 -> {
val graphics = Gdx.graphics
Expand All @@ -84,7 +84,7 @@ class CakeHostApplicationTest {
override fun getBackBufferHeight() = 0
}
try { cake!!.render() } finally { Gdx.graphics = graphics }
assertEquals(frame + 1, gameFrame(), "Menus and an undrawable window must not stop coop")
assertEquals(frame + 1, gameFrame(host), "Menus and an undrawable window must not stop coop")
}
2 -> {
Cbuf.AddText("connect 127.0.0.1:9")
Expand All @@ -108,11 +108,11 @@ class CakeHostApplicationTest {
Cbuf.AddText("host_start testbox")
cake!!.render()
assertEquals(HostState.RUNNING, host.state)
frame = gameFrame()
frame = gameFrame(host)
}
5 -> {
cake!!.render()
assertEquals(frame, gameFrame(), "The main menu pauses singleplayer")
assertEquals(frame, gameFrame(host), "The main menu pauses singleplayer")
Gdx.app.exit()
}
}
Expand Down Expand Up @@ -142,7 +142,7 @@ class CakeHostApplicationTest {
}
}

private fun gameFrame() = (SV_MAIN.games.getValue("default").gameExports as GameExportsImpl).level.framenum
private fun gameFrame(host: EmbeddedCakeHost) = (host.server!!.games.getValue("default").gameExports as GameExportsImpl).level.framenum
private fun clientState(cake: Cake) = Cake::class.java.getDeclaredField("networkState").apply {
isAccessible = true
}.get(cake).toString()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,12 @@ class CakeLocalSessionGlTest {
when (step) {
0 -> if (state(cake) == "ACTIVE") {
assertTrue(NET.ip_sockets.all { it == null })
origin = playerOrigin()
origin = playerOrigin(host)
next("+forward")
}
1 -> if (ticks >= 45) {
Cmd.ExecuteString("-forward")
val moved = playerOrigin()
val moved = playerOrigin(host)
assertTrue(kotlin.math.abs(moved[0] - origin[0]) + kotlin.math.abs(moved[1] - origin[1]) > 1f,
"Local movement must reach the server")
next("gamemap base2")
Expand Down Expand Up @@ -128,9 +128,9 @@ class CakeLocalSessionGlTest {
}
9 -> {
assertTrue(clientPeer.isAlive, "Thin-client peer exited; see ${temp.resolve("client.log")}")
if (Files.exists(clientReady) && spawnedClients() == 2) next("gamemap base2")
if (Files.exists(clientReady) && spawnedClients(host) == 2) next("gamemap base2")
}
10 -> if (ticks > 2 && state(cake) == "ACTIVE" && mapName() == "base2" && spawnedClients() == 2) {
10 -> if (ticks > 2 && state(cake) == "ACTIVE" && mapName() == "base2" && spawnedClients(host) == 2) {
assertSame(serverSocket, NET.ip_sockets[Defines.NS_SERVER])
clientPeer.destroy()
next("host_start base1 deathmatch")
Expand All @@ -144,7 +144,7 @@ class CakeLocalSessionGlTest {
}
12 -> {
assertTrue(clientPeer.isAlive)
if (Files.exists(clientReady) && spawnedClients() == 2) {
if (Files.exists(clientReady) && spawnedClients(host) == 2) {
clientPeer.destroy()
serverPeer = launchPeer("server", basedir, serverReady)
peers.add(serverPeer)
Expand Down Expand Up @@ -205,8 +205,8 @@ class CakeLocalSessionGlTest {
}.get(cake).toString()

private fun mapName() = Cvar.getInstance().find<String>("mapname").typedValue
private fun playerOrigin() = (SV_MAIN.games.getValue("default").gameExports as GameExportsImpl).getEdict(1).s.origin.copyOf()
private fun spawnedClients() = SV_MAIN.games.getValue("default").serverMain.clients.count {
private fun playerOrigin(host: EmbeddedCakeHost) = (host.server!!.games.getValue("default").gameExports as GameExportsImpl).getEdict(1).s.origin.copyOf()
private fun spawnedClients(host: EmbeddedCakeHost) = host.server!!.games.getValue("default").serverMain.clients.count {
it.state == ClientStates.CS_SPAWNED
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ class CakeProfileSaveGlTest {
private fun state() = Cake::class.java.getDeclaredField("networkState").apply {
isAccessible = true
}.get(cake).toString()
private fun player() = (SV_MAIN.games.getValue("default").gameExports as GameExportsImpl).getEdict(1) as GameEntity
private fun player() = (host.server!!.games.getValue("default").gameExports as GameExportsImpl).getEdict(1) as GameEntity

override fun render() {
if (failure != null) return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import jake2.qcommon.network.Netchan
import jake2.qcommon.network.messages.server.DownloadMessage
import jake2.qcommon.network.messages.server.ServerMessage
import jake2.qcommon.vfs.EngineWriteRoot
import jake2.server.save.ServerSaveFiles
import org.demoth.cake.download.CakeDownloadCache
import org.demoth.cake.download.CakeDownloadManager
import org.demoth.cake.download.CakeDownloadSession
Expand Down Expand Up @@ -53,7 +54,18 @@ class EmbeddedCakeHostTest {
cvars.clear()
}

private fun game() = SV_MAIN.games.getValue("default").gameExports as GameExportsImpl
@Test
fun `pause checks the hosted Game mode rather than startup defaults`() {
assertEquals(HostStartResult.Started, host.start(request()))
cvars.find<Boolean>("deathmatch").forceSet(true)
host.setPaused(true)
assertTrue(host.server!!.isPaused)
host.server!!.games.getValue("default").findCvar("coop", Boolean::class.java).forceSet(true)
host.setPaused(true)
assertFalse(host.server!!.isPaused)
}

private fun game() = host.server!!.games.getValue("default").gameExports as GameExportsImpl

@Test
fun `missing download receives failure for fresh and resumed requests`() {
Expand All @@ -69,8 +81,8 @@ class EmbeddedCakeHostTest {
val client = ClientNetworkInfo()
val downloads = CakeDownloadSession(
sendReliable = { command ->
SV_USER.userCommands.getValue("download").execute(
Cmd.TokenizeString(command, false), SV_MAIN.games.getValue("default"), client)
client.gameName = "default"
host.server!!.executeClientCommand(client, command)
},
downloadManager = CakeDownloadManager(CakeDownloadCache(cache), cvars),
)
Expand All @@ -95,7 +107,7 @@ class EmbeddedCakeHostTest {
Cmd.ExecuteString("gamemap *testbox")
host.update(100)
assertEquals(HostState.RUNNING, host.state)
assertEquals(1, SV_MAIN.games.getValue("default").serverMain.clients.size)
assertEquals(1, host.server!!.games.getValue("default").serverMain.clients.size)
assertTrue(NET.ip_sockets.all { it == null })
}

Expand All @@ -114,20 +126,21 @@ class EmbeddedCakeHostTest {
assertEquals(HostStartResult.Started, host.start(request()))
val current = temp.resolve("session/save/current/server_mapcmd.ssv.json")
val snapshot = Files.readString(current)
val saveFiles = ServerSaveFiles(temp.resolve("session"))
val sentinel = temp.resolve("sentinel/server_mapcmd.ssv.json")
Files.createDirectories(sentinel.parent)
Files.writeString(sentinel, "outside profile")
for (slot in listOf("../../sentinel", ".", "..", "nested/slot", "nested\\slot", "C:slot", "")) {
// Invalid names must be rejected before the not-yet-spawned player check or any writes.
Cmd.ExecuteString("save \"$slot\"")
Cmd.ExecuteString("load \"$slot\"")
assertThrows(IllegalArgumentException::class.java) { SV_CCMDS.SV_CopySaveGame("current", slot) }
assertThrows(IllegalArgumentException::class.java) { SV_CCMDS.SV_CopySaveGame(slot, "current") }
assertThrows(IllegalArgumentException::class.java) { SV_CCMDS.SV_WipeSavegame(slot) }
assertThrows(IllegalArgumentException::class.java) { saveFiles.copy("current", slot) }
assertThrows(IllegalArgumentException::class.java) { saveFiles.copy(slot, "current") }
assertThrows(IllegalArgumentException::class.java) { saveFiles.wipe(slot) }
assertEquals(snapshot, Files.readString(current))
assertEquals("outside profile", Files.readString(sentinel))
}
SV_CCMDS.SV_CopySaveGame("current", "current")
saveFiles.copy("current", "current")
Cmd.ExecuteString("load current")
Cmd.ExecuteString("save current")
Cmd.ExecuteString("save before-spawn")
Expand All @@ -152,7 +165,7 @@ class EmbeddedCakeHostTest {
host.stop("done")
host.stop("again")
assertEquals(HostState.STOPPED, host.state)
assertTrue(SV_MAIN.games.isEmpty())
assertNull(host.server)
assertNull(Cmd.FindCommand("map"))
assertNull(Cmd.FindCommand("save"))
assertEquals(previousRoot, EngineWriteRoot.path())
Expand Down
Loading
Loading