Merge branch 'battle-lockup-fix' into 'main'

Battle lockup fix

See merge request cable-mc/cobblemon!2199
This commit is contained in:
Polymeta
2026-04-17 12:23:40 +00:00
8 changed files with 38 additions and 51 deletions
+1
View File
@@ -45,6 +45,7 @@
- Fixed a Server warning when recalling Pokemon
- Fixed sliding particles incorrectly handling block collision
- Fixed Full Heal not curing status conditions in battle
- Fixed a rare issue with battles locking up when you are forced to switch, with the switch menu appearing instantly.
### Developer
- Added `PokeSnackSpawnerFactory` which allows influence registration on PokeSnack encounters
@@ -137,7 +137,6 @@ open class PokemonBattle(
var dispatchResult = GO
val dispatches = ConcurrentLinkedDeque<BattleDispatch>()
val afterDispatches = mutableListOf<() -> Unit>()
val captureActions = mutableListOf<BattleCaptureAction>()
@@ -400,12 +399,10 @@ open class PokemonBattle(
fun dispatch(dispatcher: () -> DispatchResult) {
dispatches.add(BattleDispatch { dispatcher() })
}
fun dispatchToFront(dispatcher: () -> DispatchResult) {
dispatches.addFirst(BattleDispatch { dispatcher() })
}
fun dispatchWaitingToFront(delaySeconds: Float = 1F, dispatcher: () -> Unit) {
@@ -456,21 +453,12 @@ open class PokemonBattle(
dispatches.addFirst(dispatcher)
}
fun doWhenClear(action: () -> Unit) {
afterDispatches.add(action)
}
fun tick() {
try {
while (dispatchResult.canProceed()) {
val dispatch = dispatches.poll() ?: break
dispatchResult = dispatch(this)
}
if (dispatches.isEmpty()) {
afterDispatches.toList().forEach { it() }
afterDispatches.clear()
}
} catch (e: Exception) {
LOGGER.error("Exception while ticking a battle. Saving battle log.", e)
val message = battleLang("crash").red()
@@ -8,8 +8,6 @@
package com.cobblemon.mod.common.api.battles.model.actor
import com.bedrockk.molang.runtime.struct.QueryStruct
import com.bedrockk.molang.runtime.value.StringValue
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.molang.MoLangFunctions.asMoLangValue
import com.cobblemon.mod.common.api.net.NetworkPacket
@@ -76,7 +74,7 @@ abstract class BattleActor(
fun turn() {
val request = request ?: return
responses.clear()
if(activePokemon.any { it.isAlive() }) {
if (activePokemon.any { it.isAlive() }) {
mustChoose = true
sendUpdate(BattleMakeChoicePacket())
}
@@ -90,7 +88,7 @@ abstract class BattleActor(
}
}
fun upkeep() {
fun postUpdate() {
val request = request ?: return
val forceSwitchPokemon = request.forceSwitch.mapIndexedNotNull { index, b -> if (b) activePokemon[index] else null }
if (forceSwitchPokemon.isEmpty()) {
@@ -9,10 +9,14 @@
package com.cobblemon.mod.common.battles
import com.cobblemon.mod.common.Cobblemon.LOGGER
import com.cobblemon.mod.common.api.battles.interpreter.*
import com.cobblemon.mod.common.api.battles.interpreter.BasicContext
import com.cobblemon.mod.common.api.battles.interpreter.BattleContext
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.interpreter.Effect
import com.cobblemon.mod.common.api.battles.interpreter.InvalidInstructionException
import com.cobblemon.mod.common.api.battles.interpreter.MissingContext
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.battles.model.actor.BattleActor
import com.cobblemon.mod.common.api.battles.model.actor.EntityBackedBattleActor
import com.cobblemon.mod.common.api.text.red
import com.cobblemon.mod.common.api.text.yellow
import com.cobblemon.mod.common.battles.dispatch.InstructionSet
@@ -22,21 +26,7 @@ import com.cobblemon.mod.common.battles.interpreter.instructions.*
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.util.battleLang
import com.cobblemon.mod.common.util.runOnServer
import net.minecraft.world.level.ClipContext
import net.minecraft.world.phys.HitResult
import net.minecraft.world.phys.Vec3
import java.util.UUID
import kotlin.collections.Iterator
import kotlin.collections.filter
import kotlin.collections.forEach
import kotlin.collections.listOf
import kotlin.collections.map
import kotlin.collections.mutableListOf
import kotlin.collections.mutableMapOf
import kotlin.collections.set
import kotlin.collections.toMutableList
import kotlin.collections.toTypedArray
import kotlin.time.measureTime
@Suppress("KotlinPlaceholderCountMatchesArgumentCount", "UNUSED_PARAMETER")
object ShowdownInterpreter {
@@ -141,7 +131,6 @@ object ShowdownInterpreter {
// Note '-cureteam' is a legacy thing that is only used in generation 2 and 4 mods for heal bell and aromatherapy respectively as such we can just ignore that
}
fun interpretMessage(battleId: UUID, message: String) {
// Check key map and use function if matching
if (message.startsWith("{\"winner\":\"")) {
@@ -170,7 +159,6 @@ object ShowdownInterpreter {
val instructionSet = InstructionSet()
val battleMessages = mutableListOf<BattleMessage>()
try {
val lines = rawMessage.split("\n").toMutableList()
if (lines[0] == "update") {
@@ -184,6 +172,7 @@ object ShowdownInterpreter {
val instruction = updateInstructionParser[id]?.invoke(battle, instructionSet, message, iterator) ?: UnknownInstruction(message)
instructionSet.instructions.add(instruction)
}
instructionSet.instructions.add(PostUpdateInstruction)
}
else if (lines[0] == "sideupdate") {
val showdownId = lines[1]
@@ -330,5 +319,4 @@ object ShowdownInterpreter {
fun registerSideInstructionParser(id: String, callback: (PokemonBattle, BattleActor, InstructionSet, BattleMessage) -> InterpreterInstruction) {
sideInstructionParser[id] = callback
}
}
@@ -17,7 +17,7 @@ import com.cobblemon.mod.common.api.battles.interpreter.BattleContext
* @since April 10th, 2023
*/
@Deprecated(message =
"This was designed before Doubles and Multi battle formats were introduce and has not been maintained since." +
"This was designed before Doubles and Multi battle formats were introduced and has not been maintained since.\n" +
"Accurate context for formats other than Singles is not guaranteed. This will be replaced in the near future.")
class ContextManager {
@@ -0,0 +1,24 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
/**
* Tails every `update` output from Showdown. Used for picking up any forced switches that need to be done.
*
* @author Hiroku
* @since April 13th, 2026
*/
object PostUpdateInstruction : InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
battle.dispatchGo { battle.actors.forEach { it.postUpdate() } }
}
}
@@ -14,13 +14,12 @@ import com.cobblemon.mod.common.api.battles.model.actor.BattleActor
import com.cobblemon.mod.common.battles.BattleRegistry
import com.cobblemon.mod.common.battles.ShowdownActionRequest
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
import com.cobblemon.mod.common.net.messages.client.battle.BattleMakeChoicePacket
import com.cobblemon.mod.common.net.messages.client.battle.BattleQueueRequestPacket
/**
* Format: |request|REQUEST
*
* It's time for the actor to make a decision.
* The actor needs to know its options for when it next has to make a decision.
* @author Deltric
* @since January 22nd, 2022
*/
@@ -35,17 +34,10 @@ class RequestInstruction(val battleActor: BattleActor, val message: BattleMessag
val request = BattleRegistry.gson.fromJson(message.rawMessage.split("|request|")[1], ShowdownActionRequest::class.java)
request.sanitize(battle, battleActor)
battle.dispatchGo {
// This request won't be acted on until the start of next turn
// This request won't be acted on until the start of next turn or post-`update` if there's a force switch
battleActor.sendUpdate(BattleQueueRequestPacket(request))
battleActor.request = request
battleActor.responses.clear()
// We need to send this out because 'upkeep' isn't received until the request is handled since the turn won't swap
if (request.forceSwitch.contains(true)) {
battle.doWhenClear {
battleActor.mustChoose = true
battleActor.sendUpdate(BattleMakeChoicePacket())
}
}
}
}
@@ -9,7 +9,6 @@
package com.cobblemon.mod.common.battles.interpreter.instructions
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.battles.dispatch.GO
import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
/**
@@ -21,9 +20,6 @@ import com.cobblemon.mod.common.battles.dispatch.InterpreterInstruction
*/
class UpkeepInstruction : InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
battle.dispatch {
battle.actors.forEach { it.upkeep() }
GO
}
// We could do some updates here like checking field condition state for client side shenanigans
}
}