Compare commits

...
35 changed files with 1630 additions and 327 deletions
@@ -25,7 +25,6 @@ object CobblemonCommands {
ChangeWalkSpeed.register(dispatcher)
ChangeJointScaleCommand.register(dispatcher)
TestCommand.register(dispatcher)
ReloadShowdownCommand.register(dispatcher)
ClickTextCommand.register(dispatcher)
PokemonEditCommand.register(dispatcher)
TeachCommand.register(dispatcher)
@@ -68,4 +67,4 @@ object CobblemonCommands {
CobblemonInfoCommand.register(dispatcher)
CalculateSeatPositionCommand.register(dispatcher)
}
}
}
@@ -19,6 +19,7 @@ import com.cobblemon.mod.common.client.net.callback.party.OpenPartyCallbackHandl
import com.cobblemon.mod.common.client.net.callback.partymove.OpenPartyMoveCallbackHandler
import com.cobblemon.mod.common.client.net.data.DataRegistrySyncPacketHandler
import com.cobblemon.mod.common.client.net.data.UnlockReloadPacketHandler
import com.cobblemon.mod.common.client.net.debug.showdown.ShowdownRefreshInfoHandler
import com.cobblemon.mod.common.client.net.dialogue.DialogueClosedHandler
import com.cobblemon.mod.common.client.net.dialogue.DialogueOpenedHandler
import com.cobblemon.mod.common.client.net.effect.RunPosableMoLangHandler
@@ -48,14 +49,7 @@ import com.cobblemon.mod.common.client.net.storage.party.SetPartyPokemonHandler
import com.cobblemon.mod.common.client.net.storage.party.SetPartyReferenceHandler
import com.cobblemon.mod.common.client.net.storage.pc.*
import com.cobblemon.mod.common.client.net.toast.ToastPacketHandler
import com.cobblemon.mod.common.client.net.trade.TradeAcceptanceChangedHandler
import com.cobblemon.mod.common.client.net.trade.TradeCancelledHandler
import com.cobblemon.mod.common.client.net.trade.TradeCompletedHandler
import com.cobblemon.mod.common.client.net.trade.TradeOfferExpiredHandler
import com.cobblemon.mod.common.client.net.trade.TradeOfferNotificationHandler
import com.cobblemon.mod.common.client.net.trade.TradeProcessStartedHandler
import com.cobblemon.mod.common.client.net.trade.TradeStartedHandler
import com.cobblemon.mod.common.client.net.trade.TradeUpdatedHandler
import com.cobblemon.mod.common.client.net.trade.*
import com.cobblemon.mod.common.net.PacketRegisterInfo
import com.cobblemon.mod.common.net.messages.client.CalculateSeatPositionsPacket
import com.cobblemon.mod.common.net.messages.client.PlayerInteractOptionsPacket
@@ -66,6 +60,7 @@ import com.cobblemon.mod.common.net.messages.client.callback.OpenMoveCallbackPac
import com.cobblemon.mod.common.net.messages.client.callback.OpenPartyCallbackPacket
import com.cobblemon.mod.common.net.messages.client.callback.OpenPartyMoveCallbackPacket
import com.cobblemon.mod.common.net.messages.client.data.*
import com.cobblemon.mod.common.net.messages.client.debug.showdown.ShowdownRefreshInfoPacket
import com.cobblemon.mod.common.net.messages.client.dialogue.DialogueClosedPacket
import com.cobblemon.mod.common.net.messages.client.dialogue.DialogueOpenedPacket
import com.cobblemon.mod.common.net.messages.client.effect.RunPosableMoLangPacket
@@ -96,42 +91,18 @@ import com.cobblemon.mod.common.net.messages.client.storage.party.InitializePart
import com.cobblemon.mod.common.net.messages.client.storage.party.MoveClientPartyPokemonPacket
import com.cobblemon.mod.common.net.messages.client.storage.party.SetPartyPokemonPacket
import com.cobblemon.mod.common.net.messages.client.storage.party.SetPartyReferencePacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.ClosePCPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.InitializePCPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.MoveClientPCPokemonPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.OpenPCPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.RenamePCBoxPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.SetPCBoxPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.SetPCPokemonPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.*
import com.cobblemon.mod.common.net.messages.client.storage.pc.wallpaper.ChangePCBoxWallpaperPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.wallpaper.RequestPCBoxWallpapersPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.wallpaper.SetPCBoxWallpapersPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.wallpaper.UnlockPCBoxWallpaperPacket
import com.cobblemon.mod.common.net.messages.client.toast.ToastPacket
import com.cobblemon.mod.common.net.messages.client.trade.TradeAcceptanceChangedPacket
import com.cobblemon.mod.common.net.messages.client.trade.TradeCancelledPacket
import com.cobblemon.mod.common.net.messages.client.trade.TradeCompletedPacket
import com.cobblemon.mod.common.net.messages.client.trade.TradeOfferExpiredPacket
import com.cobblemon.mod.common.net.messages.client.trade.TradeOfferNotificationPacket
import com.cobblemon.mod.common.net.messages.client.trade.TradeProcessStartedPacket
import com.cobblemon.mod.common.net.messages.client.trade.TradeStartedPacket
import com.cobblemon.mod.common.net.messages.client.trade.TradeUpdatedPacket
import com.cobblemon.mod.common.net.messages.client.trade.*
import com.cobblemon.mod.common.net.messages.client.ui.InteractPokemonUIPacket
import com.cobblemon.mod.common.net.messages.client.ui.PokedexUIPacket
import com.cobblemon.mod.common.net.messages.client.ui.SummaryUIPacket
import com.cobblemon.mod.common.net.messages.server.BattleChallengePacket
import com.cobblemon.mod.common.net.messages.server.BattleChallengeResponsePacket
import com.cobblemon.mod.common.net.messages.server.BenchMovePacket
import com.cobblemon.mod.common.net.messages.server.RequestMoveSwapPacket
import com.cobblemon.mod.common.net.messages.server.RequestPlayerInteractionsPacket
import com.cobblemon.mod.common.net.messages.server.SelectStarterPacket
import com.cobblemon.mod.common.net.messages.server.SendOutPokemonPacket
import com.cobblemon.mod.common.net.messages.server.battle.BattleSelectActionsPacket
import com.cobblemon.mod.common.net.messages.server.battle.BattleTeamLeavePacket
import com.cobblemon.mod.common.net.messages.server.battle.BattleTeamRequestPacket
import com.cobblemon.mod.common.net.messages.server.battle.BattleTeamResponsePacket
import com.cobblemon.mod.common.net.messages.server.battle.RemoveSpectatorPacket
import com.cobblemon.mod.common.net.messages.server.battle.SpectateBattlePacket
import com.cobblemon.mod.common.net.messages.server.*
import com.cobblemon.mod.common.net.messages.server.battle.*
import com.cobblemon.mod.common.net.messages.server.block.AdjustBlockEntityViewerCountPacket
import com.cobblemon.mod.common.net.messages.server.callback.move.MoveSelectCancelledPacket
import com.cobblemon.mod.common.net.messages.server.callback.move.MoveSelectedPacket
@@ -139,6 +110,9 @@ import com.cobblemon.mod.common.net.messages.server.callback.party.PartyPokemonS
import com.cobblemon.mod.common.net.messages.server.callback.party.PartySelectCancelledPacket
import com.cobblemon.mod.common.net.messages.server.callback.partymove.PartyMoveSelectCancelledPacket
import com.cobblemon.mod.common.net.messages.server.callback.partymove.PartyPokemonMoveSelectedPacket
import com.cobblemon.mod.common.net.messages.server.debug.showdown.RequestShowdownInfoPacket
import com.cobblemon.mod.common.net.messages.server.debug.showdown.ShowdownActionPacket
import com.cobblemon.mod.common.net.messages.server.debug.showdown.ShowdownInputPacket
import com.cobblemon.mod.common.net.messages.server.dialogue.EscapeDialoguePacket
import com.cobblemon.mod.common.net.messages.server.dialogue.InputToDialoguePacket
import com.cobblemon.mod.common.net.messages.server.npc.SaveNPCPacket
@@ -158,21 +132,11 @@ import com.cobblemon.mod.common.net.messages.server.storage.party.MovePartyPokem
import com.cobblemon.mod.common.net.messages.server.storage.party.ReleasePartyPokemonPacket
import com.cobblemon.mod.common.net.messages.server.storage.party.SwapPartyPokemonPacket
import com.cobblemon.mod.common.net.messages.server.storage.pc.*
import com.cobblemon.mod.common.net.messages.server.trade.AcceptTradeRequestPacket
import com.cobblemon.mod.common.net.messages.server.trade.CancelTradePacket
import com.cobblemon.mod.common.net.messages.server.trade.ChangeTradeAcceptancePacket
import com.cobblemon.mod.common.net.messages.server.trade.OfferTradePacket
import com.cobblemon.mod.common.net.messages.server.trade.PerformTradePacket
import com.cobblemon.mod.common.net.messages.server.trade.UpdateTradeOfferPacket
import com.cobblemon.mod.common.net.messages.server.trade.*
import com.cobblemon.mod.common.net.serverhandling.ChallengeHandler
import com.cobblemon.mod.common.net.serverhandling.ChallengeResponseHandler
import com.cobblemon.mod.common.net.serverhandling.RequestInteractionsHandler
import com.cobblemon.mod.common.net.serverhandling.battle.BattleSelectActionsHandler
import com.cobblemon.mod.common.net.serverhandling.battle.RemoveSpectatorHandler
import com.cobblemon.mod.common.net.serverhandling.battle.SpectateBattleHandler
import com.cobblemon.mod.common.net.serverhandling.battle.TeamLeaveHandler
import com.cobblemon.mod.common.net.serverhandling.battle.TeamRequestHandler
import com.cobblemon.mod.common.net.serverhandling.battle.TeamRequestResponseHandler
import com.cobblemon.mod.common.net.serverhandling.battle.*
import com.cobblemon.mod.common.net.serverhandling.block.AdjustBlockEntityViewerCountHandler
import com.cobblemon.mod.common.net.serverhandling.callback.move.MoveSelectCancelledHandler
import com.cobblemon.mod.common.net.serverhandling.callback.move.MoveSelectedHandler
@@ -180,6 +144,9 @@ import com.cobblemon.mod.common.net.serverhandling.callback.party.PartyPokemonSe
import com.cobblemon.mod.common.net.serverhandling.callback.party.PartySelectCancelledHandler
import com.cobblemon.mod.common.net.serverhandling.callback.partymove.PartyMoveSelectCancelledHandler
import com.cobblemon.mod.common.net.serverhandling.callback.partymove.PartyPokemonMoveSelectedHandler
import com.cobblemon.mod.common.net.serverhandling.debug.showdown.RequestShowdownInfoHandler
import com.cobblemon.mod.common.net.serverhandling.debug.showdown.ShowdownActionHandler
import com.cobblemon.mod.common.net.serverhandling.debug.showdown.ShowdownInputHandler
import com.cobblemon.mod.common.net.serverhandling.dialogue.EscapeDialogueHandler
import com.cobblemon.mod.common.net.serverhandling.dialogue.InputToDialogueHandler
import com.cobblemon.mod.common.net.serverhandling.evolution.AcceptEvolutionHandler
@@ -203,12 +170,7 @@ import com.cobblemon.mod.common.net.serverhandling.storage.party.MovePartyPokemo
import com.cobblemon.mod.common.net.serverhandling.storage.party.ReleasePCPokemonHandler
import com.cobblemon.mod.common.net.serverhandling.storage.party.SwapPartyPokemonHandler
import com.cobblemon.mod.common.net.serverhandling.storage.pc.*
import com.cobblemon.mod.common.net.serverhandling.trade.AcceptTradeRequestHandler
import com.cobblemon.mod.common.net.serverhandling.trade.CancelTradeHandler
import com.cobblemon.mod.common.net.serverhandling.trade.ChangeTradeAcceptanceHandler
import com.cobblemon.mod.common.net.serverhandling.trade.OfferTradeHandler
import com.cobblemon.mod.common.net.serverhandling.trade.PerformTradeHandler
import com.cobblemon.mod.common.net.serverhandling.trade.UpdateTradeOfferHandler
import com.cobblemon.mod.common.net.serverhandling.trade.*
import com.cobblemon.mod.common.util.server
import net.minecraft.server.level.ServerPlayer
@@ -412,6 +374,7 @@ object CobblemonNetwork {
// Debug / cheats
list.add(PacketRegisterInfo(CalculateSeatPositionsPacket.ID, CalculateSeatPositionsPacket::decode, CalculateSeatPositionsHandler))
list.add(PacketRegisterInfo(ShowdownRefreshInfoPacket.ID, ShowdownRefreshInfoPacket::decode, ShowdownRefreshInfoHandler))
return list
}
@@ -511,6 +474,11 @@ object CobblemonNetwork {
// Orientation packet(s)
list.add(PacketRegisterInfo(C2SUpdateOrientationPacket.ID, C2SUpdateOrientationPacket::decode, OrientationPacketHandler))
// Debug / cheats
list.add(PacketRegisterInfo(RequestShowdownInfoPacket.ID, RequestShowdownInfoPacket::decode, RequestShowdownInfoHandler))
list.add(PacketRegisterInfo(ShowdownInputPacket.ID, ShowdownInputPacket::decode, ShowdownInputHandler))
list.add(PacketRegisterInfo(ShowdownActionPacket.ID, ShowdownActionPacket::decode, ShowdownActionHandler))
return list
}
@@ -37,7 +37,7 @@ object Abilities : DataRegistry {
PotentialAbility.types.add(CommonAbilityType)
PotentialAbility.types.add(HiddenAbilityType)
this.abilityMap.clear()
val abilitiesJson = ShowdownService.service.getAbilityIds()
val abilitiesJson = ShowdownService.get().getAbilityIds()
for (i in 0 until abilitiesJson.size()) {
val id = abilitiesJson[i].asString
val ability = AbilityTemplate(id)
@@ -67,4 +67,4 @@ object Abilities : DataRegistry {
abilities.forEach { ability -> this.register(ability) }
}
}
}
@@ -15,10 +15,7 @@ import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.Cobblemon.LOGGER
import com.cobblemon.mod.common.CobblemonNetwork
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.actor.ActorType
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.battles.model.actor.FleeableBattleActor
import com.cobblemon.mod.common.api.battles.model.actor.*
import com.cobblemon.mod.common.api.events.CobblemonEvents
import com.cobblemon.mod.common.api.events.battles.BattleFledEvent
import com.cobblemon.mod.common.api.molang.MoLangFunctions.asMoLangValue
@@ -74,7 +71,9 @@ import net.minecraft.world.item.ItemStack
open class PokemonBattle(
val format: BattleFormat,
val side1: BattleSide,
val side2: BattleSide
val side2: BattleSide,
val service: ShowdownService = ShowdownService.get(),
val battleId: UUID = UUID.randomUUID()
) {
/** Whether logging will be silenced for this battle. */
var mute = true
@@ -109,8 +108,6 @@ open class PokemonBattle(
val players = playerUUIDs.mapNotNull { it.getPlayer() }
val spectators = mutableSetOf<UUID>()
val battleId = UUID.randomUUID()
val showdownMessages = mutableListOf<String>()
val battleLog = mutableListOf<String>()
val chatLog = mutableListOf<Component>()
@@ -225,7 +222,7 @@ open class PokemonBattle(
fun writeShowdownAction(vararg messages: String) {
log(messages.joinToString("\n"))
ShowdownService.service.send(battleId, messages.toList().toTypedArray())
service.send(battleId, messages.toList().toTypedArray())
}
fun turn(newTurnNumber: Int) {
@@ -378,7 +375,6 @@ open class PokemonBattle(
fun dispatch(dispatcher: () -> DispatchResult) {
dispatches.add(BattleDispatch { dispatcher() })
}
fun dispatchToFront(dispatcher: () -> DispatchResult) {
@@ -502,6 +498,7 @@ open class PokemonBattle(
}
fun checkForInputDispatch() {
actors.filter { it is UnionBattleActor }.forEach { it.mustChoose = false }
if (checkForfeit()) return // ignore actors that are still choosing, their choices don't matter anymore
val readyToInput = (actors.any { !it.mustChoose && it.responses.isNotEmpty() } && actors.none { it.mustChoose })
if (readyToInput && captureActions.isEmpty()) {
@@ -558,4 +555,4 @@ open class PokemonBattle(
queryStruct.addFunction("has_rule") { params -> DoubleValue(params.getString(0) in format.ruleSet) }
return queryStruct
}
}
}
@@ -42,4 +42,4 @@ abstract class AIBattleActor(
setActionResponses(request!!.iterate(this.activePokemon) { _, _, _ -> PassActionResponse })
}
}
}
}
@@ -73,7 +73,7 @@ abstract class BattleActor(
open fun isForPlayer(serverPlayerEntity: ServerPlayer) = serverPlayerEntity.uuid in getPlayerUUIDs()
open fun isForPokemon(pokemonEntity: PokemonEntity) = activePokemon.any { it.battlePokemon?.effectedPokemon?.entity == pokemonEntity }
fun turn() {
open fun turn() {
val request = request ?: return
responses.clear()
if(activePokemon.any { it.isAlive() }) {
@@ -90,7 +90,7 @@ abstract class BattleActor(
}
}
fun upkeep() {
open fun upkeep() {
val request = request ?: return
val forceSwitchPokemon = request.forceSwitch.mapIndexedNotNull { index, b -> if (b) activePokemon[index] else null }
if (forceSwitchPokemon.isEmpty()) {
@@ -125,7 +125,7 @@ abstract class BattleActor(
battle.checkForInputDispatch()
}
fun writeShowdownResponse() {
open fun writeShowdownResponse() {
val showdownMessages = mutableListOf<String>()
var index = 0
request!!.iterate(activePokemon) { activeBattlePokemon, showdownMoveSet, forceSwitch ->
@@ -159,4 +159,4 @@ abstract class BattleActor(
open fun win(otherWinners: List<BattleActor>, losers: List<BattleActor>) {}
open fun lose(winners: List<BattleActor>, otherLosers: List<BattleActor>) {}
}
}
@@ -0,0 +1,25 @@
package com.cobblemon.mod.common.api.battles.model.actor
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.entity.npc.NPCBattleActor
import com.cobblemon.mod.common.entity.npc.NPCEntity
class UnionBattleActor(npc: NPCEntity, pokemonList: List<BattlePokemon>) : NPCBattleActor(npc, pokemonList.toMutableList(), 1) {
override fun writeShowdownResponse() {}
override fun onChoiceRequested() {
this.mustChoose = false
}
override fun turn() {
super.turn()
this.mustChoose = false
}
override fun upkeep() {
super.upkeep()
this.mustChoose = false
}
}
@@ -40,7 +40,7 @@ object Moves : DataRegistry {
override fun reload(manager: ResourceManager) {
this.allMoves.clear()
this.idMapping.clear()
val movesJson = ShowdownService.service.getMoves()
val movesJson = ShowdownService.get().getMoves()
for (i in 0 until movesJson.size()) {
val jsMove = movesJson[i].asJsonObject
val id = jsMove.get("id").asString
@@ -116,4 +116,4 @@ object Moves : DataRegistry {
this.idMapping[move.num] = move
}
}
}
@@ -17,6 +17,7 @@ import com.cobblemon.mod.common.api.storage.party.PlayerPartyStore
import com.cobblemon.mod.common.battles.actor.PlayerBattleActor
import com.cobblemon.mod.common.battles.actor.PokemonBattleActor
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.battles.runner.ShowdownService
import com.cobblemon.mod.common.entity.npc.NPCBattleActor
import com.cobblemon.mod.common.entity.npc.NPCEntity
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
@@ -59,7 +60,9 @@ object BattleBuilder {
battleFormat: BattleFormat = BattleFormat.GEN_9_SINGLES,
cloneParties: Boolean = false,
healFirst: Boolean = false,
partyAccessor: (ServerPlayer) -> PartyStore = { it.party() }
partyAccessor: (ServerPlayer) -> PartyStore = { it.party() },
service: ShowdownService = ShowdownService.get(),
battleId: UUID = UUID.randomUUID()
): BattleStartResult {
val adjustLevel = battleFormat.adjustLevel
@@ -116,7 +119,9 @@ object BattleBuilder {
BattleRegistry.startBattle(
battleFormat = battleFormat,
side1 = BattleSide(player1Actor),
side2 = BattleSide(player2Actor)
side2 = BattleSide(player2Actor),
service = service,
battleId = battleId
).ifSuccessful {
it.battlePartyStores.addAll(battlePartyStores)
}
@@ -132,7 +137,9 @@ object BattleBuilder {
battleFormat: BattleFormat = BattleFormat.GEN_9_MULTI,
cloneParties: Boolean = false,
healFirst: Boolean = false,
partyAccessor: (ServerPlayer) -> PartyStore = { it.party() }
partyAccessor: (ServerPlayer) -> PartyStore = { it.party() },
service: ShowdownService = ShowdownService.get(),
battleId: UUID = UUID.randomUUID()
): BattleStartResult {
val adjustLevel = battleFormat.adjustLevel
val teams = players.mapIndexed { index, it ->
@@ -213,7 +220,9 @@ object BattleBuilder {
BattleRegistry.startBattle(
battleFormat = battleFormat,
side1 = BattleSide(playerActors[0], playerActors[1]),
side2 = BattleSide(playerActors[2], playerActors[3])
side2 = BattleSide(playerActors[2], playerActors[3]),
service = service,
battleId = battleId
).ifSuccessful {
it.battlePartyStores.addAll(battlePartyStores)
}
@@ -245,7 +254,9 @@ object BattleBuilder {
cloneParties: Boolean = false,
healFirst: Boolean = false,
fleeDistance: Float = Cobblemon.config.defaultFleeDistance,
party: PartyStore = player.party()
party: PartyStore = player.party(),
service: ShowdownService = ShowdownService.get(),
battleId: UUID = UUID.randomUUID()
): BattleStartResult {
val playerTeam = party.toBattleTeam(clone = cloneParties, healPokemon = healFirst, leadingPokemon = leadingPokemon).sortedBy { it.health <= 0 }
val playerActor = PlayerBattleActor(player.uuid, playerTeam)
@@ -286,7 +297,9 @@ object BattleBuilder {
BattleRegistry.startBattle(
battleFormat = battleFormat,
side1 = BattleSide(playerActor),
side2 = BattleSide(wildActor)
side2 = BattleSide(wildActor),
service = service,
battleId = battleId
).ifSuccessful {
if (!cloneParties) {
pokemonEntity.battleId = it.battleId
@@ -316,7 +329,9 @@ object BattleBuilder {
battleFormat: BattleFormat = BattleFormat.GEN_9_SINGLES,
cloneParties: Boolean = false,
healFirst: Boolean = false,
party: PartyStore = player.party()
party: PartyStore = player.party(),
service: ShowdownService = ShowdownService.get(),
battleId: UUID = UUID.randomUUID()
): BattleStartResult {
val playerTeam = party.toBattleTeam(clone = cloneParties, healPokemon = healFirst, leadingPokemon = leadingPokemon)
val playerActor = PlayerBattleActor(player.uuid, playerTeam)
@@ -363,7 +378,9 @@ object BattleBuilder {
BattleRegistry.startBattle(
battleFormat = battleFormat,
side1 = BattleSide(playerActor),
side2 = BattleSide(npcActor)
side2 = BattleSide(npcActor),
service = service,
battleId = battleId
).ifSuccessful { battle ->
npcEntity.entityData.update(NPCEntity.BATTLE_IDS) { it + battle.battleId }
}
@@ -537,4 +554,4 @@ open class ErroredBattleStart(
val errors: Iterable<BattleStartError>
get() = generalErrors + participantErrors.flatMap { it.value }
}
}
@@ -125,7 +125,7 @@ object BattleRegistry {
return team.joinToString("]")
}
private fun startShowdown(battle: PokemonBattle) {
private fun startShowdown(battle: PokemonBattle, sendMessages: Boolean = true) {
// Build request message
val messages = mutableListOf<String>()
@@ -176,7 +176,9 @@ object BattleRegistry {
}
// Compiles the request and sends it off
ShowdownService.service.startBattle(battle, messages.toTypedArray())
if (sendMessages) {
battle.service.startBattle(battle, messages.toTypedArray())
}
}
/**
@@ -186,6 +188,7 @@ object BattleRegistry {
* @param side1 The first side for the battle.
* @param side2 The second side for the battle.
* @param canPreempt Whether the battle can be blocked by subscribers to [CobblemonEvents.BATTLE_STARTED_PRE].
* @param service The showdown service to use for the battle.
*
* NOTE: this does NOT validate the [battleFormat] or sides before launching Showdown.
* This creates the battle and hands it off to Showdown - YOU WILL GET ISSUES IF THE BATTLE IS INVALID!
@@ -195,12 +198,15 @@ object BattleRegistry {
battleFormat: BattleFormat,
side1: BattleSide,
side2: BattleSide,
canPreempt: Boolean = true
canPreempt: Boolean = true,
service: ShowdownService = ShowdownService.get(),
battleId: UUID = UUID.randomUUID(),
sendMessages: Boolean = true
): BattleStartResult {
val battle = PokemonBattle(battleFormat, side1, side2)
val battle = PokemonBattle(battleFormat, side1, side2, service, battleId)
val start: () -> Unit = {
battleMap[battle.battleId] = battle
startShowdown(battle)
startShowdown(battle, sendMessages)
}
if (!canPreempt) start().also { return SuccessfulBattleStart(battle) }
@@ -234,4 +240,4 @@ object BattleRegistry {
fun tick() {
battleMap.forEachValue(Long.MAX_VALUE) { it.tick() }
}
}
}
@@ -23,13 +23,13 @@ class ShowdownThread : Thread("Cobblemon Showdown") {
this.start()
this.latch.await()
for (action in whenReady) {
action(ShowdownService.service)
action(ShowdownService.get())
}
}
fun queue(action: (ShowdownService) -> Unit) {
if (this.latch.count == 0L) {
action(ShowdownService.service)
action(ShowdownService.get())
} else {
this.whenReady.add(action)
}
@@ -37,8 +37,8 @@ class ShowdownThread : Thread("Cobblemon Showdown") {
override fun run() {
LOGGER.info("Starting showdown service...")
ShowdownService.service.openConnection()
ShowdownService.get().openConnection()
LOGGER.info("Showdown has been started!")
this.latch.countDown()
}
}
}
@@ -49,4 +49,4 @@ class RequestInstruction(val battleActor: BattleActor, val message: BattleMessag
}
}
}
}
@@ -0,0 +1,64 @@
package com.cobblemon.mod.common.battles.runner
import com.cobblemon.mod.common.Cobblemon
import java.text.SimpleDateFormat
import java.util.Date
/**
* Class responsible for logging debug messages for the showdown service.
*
* @author landonjw
*/
class ShowdownDebugLog {
val log = mutableListOf<LogMessage>()
/**
* Adds a log message to the debug log.
*
* @param source The source of the log message.
* @param message The log message.
*/
fun add(source: String, message: String) {
if (!Cobblemon.config.enableDebugKeys) return
val now = Date()
val timestamp = SimpleDateFormat("HH:mm:ss").format(now)
log.add(LogMessage("[$source][${timestamp}]> $message", now))
}
/**
* Clears all log messages from the debug log.
*/
fun clear() {
log.clear()
}
/**
* Retrieves log messages after a specified date.
*
* @param after The date after which to retrieve log messages. If null, retrieves the last 250 messages.
* @return A list of log messages.
*/
fun get(after: Date?): List<String> {
if (after == null) return log.take(250).map { it.message }
return log.filter { it.time.after(after) }.map { it.message }
}
/**
* Checks if the provided messages are the start of the log.
*
* @param messages The messages to check.
* @return True if the messages are the start of the log, false otherwise.
*/
fun isStartOfLog(messages: List<String>): Boolean {
if (messages.isEmpty() && log.isEmpty()) return true
return messages.size == log.size && messages.first() == log.first().message
}
/**
* Data class representing a log message.
*
* @property message The log message.
* @property time The time the log message was created.
*/
data class LogMessage(val message: String, val time: Date)
}
@@ -10,32 +10,154 @@ package com.cobblemon.mod.common.battles.runner
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.battles.runner.graal.GraalShowdownService
import com.cobblemon.mod.common.battles.runner.remote.UnionShowdownService
import com.cobblemon.mod.common.util.cobblemonResource
import com.google.gson.JsonArray
import net.minecraft.resources.ResourceLocation
import java.util.UUID
/**
* Mediator service for communicating between the Cobblemon Minecraft mod and Cobblemon showdown service.
*
* All outgoing calls to showdown should be done through this layer to decouple the mod from a specific implementation.
* To get an instance of the service, use the singleton provided via `ShowdownService.get()`
* To get an instance of the service, use the singleton provided via `ShowdownService.get()`.
*
* @since February 27th, 2023
* @author landonjw
*/
interface ShowdownService {
/**
* The unique identifier for this showdown service.
*/
val id: ResourceLocation
/**
* The current status of the showdown service.
*/
val status: ShowdownStatus
/**
* The log of messages from the showdown service.
* This is accessed for use in [ShowdownDebugGUI]. It is not intended for general use
* and will be empty in production environments.
*/
val log: ShowdownDebugLog
/**
* Opens a connection to the showdown service.
*/
fun openConnection()
/**
* Closes the connection to the showdown service.
*/
fun closeConnection()
/**
* Restarts the connection to the showdown service.
*/
fun restartConnection()
/**
* Starts a battle in the showdown service.
*
* @param battle The battle to start.
* @param messages The initial messages for the battle.
*/
fun startBattle(battle: PokemonBattle, messages: Array<String>)
/**
* Sends messages to a specific battle in the showdown service.
*
* @param battleId The unique identifier of the battle.
* @param messages The messages to send.
*/
fun send(battleId: UUID, messages: Array<String>)
/**
* Sends a message to the showdown service.
*
* @param message The message to send.
*/
fun send(message: String)
/**
* Retrieves the ability IDs from the showdown service.
*
* @return A `JsonArray` of ability IDs.
*/
fun getAbilityIds(): JsonArray
/**
* Retrieves the moves from the showdown service.
*
* @return A `JsonArray` of moves.
*/
fun getMoves(): JsonArray
/**
* Retrieves the item IDs from the showdown service.
*
* @return A `JsonArray` of item IDs.
*/
fun getItemIds(): JsonArray
/**
* Registers species in the showdown service.
*/
fun registerSpecies()
/**
* Registers bag items in the showdown service.
*/
fun registerBagItems()
/**
* Indicates that species have been initialized in the showdown service.
*/
fun indicateSpeciesInitialized() {}
companion object {
val service: ShowdownService by lazy { GraalShowdownService() }
private val services = mutableMapOf<ResourceLocation, ShowdownService>(
cobblemonResource("embedded") to GraalShowdownService(cobblemonResource("embedded")),
cobblemonResource("union") to UnionShowdownService(cobblemonResource("union"))
)
/**
* Retrieves the showdown service for the specified path.
*
* @param path The resource location of the showdown service.
* @return The showdown service.
* @throws IllegalStateException if no showdown service is found for the specified path.
*/
fun get(path: ResourceLocation = cobblemonResource("embedded")): ShowdownService {
return services[path] ?: error("No showdown service found for path $path")
}
/**
* Checks if a showdown service exists for the specified path.
*
* @param path The resource location of the showdown service.
* @return `true` if the showdown service exists, `false` otherwise.
*/
fun contains(path: ResourceLocation): Boolean {
return services.containsKey(path)
}
/**
* Retrieves all available showdown services.
*
* @return A list of all showdown services.
*/
fun all(): List<ShowdownService> {
return services.values.toList()
}
}
}
}
/**
* Enum representing the status of the showdown service.
*/
enum class ShowdownStatus {
CONNECTED, DISCONNECTED, RESTARTING
}
@@ -13,9 +13,12 @@ import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.battles.BagItems
import com.cobblemon.mod.common.battles.ShowdownInterpreter
import com.cobblemon.mod.common.battles.runner.ShowdownDebugLog
import com.cobblemon.mod.common.battles.runner.ShowdownService
import com.cobblemon.mod.common.battles.runner.ShowdownStatus
import com.google.gson.Gson
import com.google.gson.JsonArray
import net.minecraft.resources.ResourceLocation
import java.io.File
import java.io.IOException
import java.net.URI
@@ -38,12 +41,20 @@ import org.graalvm.polyglot.io.FileSystem
* Mediator service for communicating between the Cobblemon Minecraft mod and Cobblemon showdown service via
* GraalVM. This directly invokes JavaScript functions provided within the showdown service.
*
* @property id The unique identifier for this showdown service.
* @property context The GraalVM context for executing JavaScript.
* @property sendBattleMessageFunction The JavaScript function for sending battle messages.
* @property unbundler The unbundler for extracting necessary showdown files.
* @property gson The Gson instance for JSON operations.
* @property log The debug log for the showdown service.
* @property status The current status of the showdown service.
*
* @see {@code cobbled-exports/cobbled-index.js} within cobbledmon-showdown repository
* @see <a href="https://www.graalvm.org/">
* @since February 27, 2023
* @author Hiroku, landonjw
*/
class GraalShowdownService : ShowdownService {
class GraalShowdownService(override val id: ResourceLocation) : ShowdownService {
@Transient
lateinit var context: Context
@@ -54,12 +65,35 @@ class GraalShowdownService : ShowdownService {
@Transient
val gson = Gson()
override fun openConnection() {
override val log = ShowdownDebugLog()
override var status: ShowdownStatus = ShowdownStatus.DISCONNECTED
private set
/**
* Opens a connection to the showdown service.
*/
override fun openConnection() = openConnection(false)
/**
* Opens a connection to the showdown service.
*
* @param silent If true, suppresses changing the status of the connection process.
*/
private fun openConnection(silent: Boolean) {
log.clear()
log.add(source = "Minecraft", message = "Opening showdown connection")
unbundler.attemptUnbundle()
createContext()
boot()
if (!silent) {
status = ShowdownStatus.CONNECTED
}
}
/**
* Creates the GraalVM context for executing JavaScript.
*/
private fun createContext() {
val wd = Paths.get("./showdown")
val access = HostAccess.newBuilder(HostAccess.EXPLICIT)
@@ -116,42 +150,126 @@ class GraalShowdownService : ShowdownService {
""".trimIndent())
}
override fun closeConnection() {
/**
* Closes the connection to the showdown service.
*/
override fun closeConnection() = closeConnection(false)
/**
* Closes the connection to the showdown service.
*
* @param silent If true, suppresses changing the status of the connection process.
*/
private fun closeConnection(silent: Boolean) {
log.add(source = "Minecraft", message = "Closing showdown connection")
context.close()
if (!silent) {
this.status = ShowdownStatus.DISCONNECTED
}
}
/**
* Restarts the connection to the showdown service.
*/
override fun restartConnection() {
this.status = ShowdownStatus.RESTARTING
try {
closeConnection(true)
openConnection(true)
this.status = ShowdownStatus.CONNECTED
} catch (e: Exception) {
Cobblemon.LOGGER.error("Failed to restart showdown connection", e)
this.status = ShowdownStatus.DISCONNECTED
}
}
/**
* Boots the showdown service by evaluating the JavaScript entry point.
*/
private fun boot() {
log.add(source = "Minecraft", message = "Booting showdown")
context.eval("js", File("showdown/index.js").readText())
sendBattleMessageFunction = context.getBindings("js").getMember("sendBattleMessage")
}
/**
* Starts a battle in the showdown service.
*
* @param battle The battle to start.
* @param messages The initial messages for the battle.
*/
override fun startBattle(battle: PokemonBattle, messages: Array<String>) {
val startBattleFunction = context.getBindings("js").getMember("startBattle")
log.add(source = "Minecraft", message = "Starting battle ${battle.battleId}")
messages.forEach { message ->
log.add(source = "Minecraft", message = "(${battle.battleId}) $message")
}
startBattleFunction.execute(this, battle.battleId.toString(), messages)
}
/**
* Sends messages to a specific battle in the showdown service.
*
* @param battleId The unique identifier of the battle.
* @param messages The messages to send.
*/
override fun send(battleId: UUID, messages: Array<String>) {
sendToShowdown(battleId, messages)
}
/**
* Sends a message to the showdown service.
*
* @param message The message to send.
*/
override fun send(message: String) {
log.add("Minecraft", message)
val regex = Regex("^\\(([^()]*)\\)")
val match = regex.find(message)
val battleId = match?.groups?.get(1)?.value ?: return
val endIndex = match.groups[1]!!.range.last + 1
sendToShowdown(UUID.fromString(battleId), arrayOf(message.substring(endIndex)))
}
/**
* Retrieves the ability IDs from the showdown service.
*
* @return A `JsonArray` of ability IDs.
*/
override fun getAbilityIds(): JsonArray {
val getCobbledAbilityIdsFn = context.getBindings("js").getMember("getCobbledAbilityIds")
val arrayResult = getCobbledAbilityIdsFn.execute().asString()
log.add(source = "Minecraft", message = "Getting ability ids")
return gson.fromJson(arrayResult, JsonArray::class.java)
}
/**
* Retrieves the moves from the showdown service.
*
* @return A `JsonArray` of moves.
*/
override fun getMoves(): JsonArray {
val getCobbledMovesFn = context.getBindings("js").getMember("getCobbledMoves")
log.add(source = "Minecraft", message = "Getting moves")
val arrayResult = getCobbledMovesFn.execute().asString()
return gson.fromJson(arrayResult, JsonArray::class.java)
}
/**
* Retrieves the item IDs from the showdown service.
*
* @return A `JsonArray` of item IDs.
*/
override fun getItemIds(): JsonArray {
val getCobbledItemIdsFn = context.getBindings("js").getMember("getCobbledItemIds")
log.add(source = "Minecraft", message = "Getting item ids")
val arrayResult = getCobbledItemIdsFn.execute().asString()
return gson.fromJson(arrayResult, JsonArray::class.java)
}
/**
* Registers species in the showdown service.
*/
override fun registerSpecies() {
val receiveSpeciesDataFn = this.context.getBindings("js").getMember("receiveSpeciesData")
val jsArray = this.context.eval("js", "new Array();")
@@ -164,32 +282,62 @@ class GraalShowdownService : ShowdownService {
}
}
}
log.add(source = "Minecraft", message = "Registering species")
receiveSpeciesDataFn.execute(jsArray)
}
/**
* Indicates that species have been initialized to the showdown service.
*/
override fun indicateSpeciesInitialized() {
val afterCobbledSpeciesInitFn = this.context.getBindings("js").getMember("afterCobbledSpeciesInit")
log.add(source = "Minecraft", message = "Indicating species initialized")
afterCobbledSpeciesInitFn.execute()
}
/**
* Registers bag items in the showdown service.
*/
override fun registerBagItems() {
val receiveBagItemDataFn = this.context.getBindings("js").getMember("receiveBagItemData")
for ((itemId, js) in BagItems.bagItemsScripts) {
log.add(source = "Minecraft", message = "Registering bag item $itemId")
receiveBagItemDataFn.execute(itemId, js.replace("\n", " "))
}
}
/**
* Sends messages to the showdown service.
*
* @param battleId The unique identifier of the battle.
* @param messages The messages to send.
*/
private fun sendToShowdown(battleId: UUID, messages: Array<String>) {
messages.forEach { message ->
log.add(source = "Minecraft", message = "($battleId) $message")
}
sendBattleMessageFunction.execute(battleId.toString(), messages)
}
/**
* Receives messages from the showdown service.
*
* @param battleId The unique identifier of the battle.
* @param message The message received.
*/
@Export
fun sendFromShowdown(battleId: String, message: String) {
log.add(source = "Showdown", message = "($battleId) $message")
ShowdownInterpreter.interpretMessage(UUID.fromString(battleId), message)
}
/**
* Logs messages from the showdown service.
*
* @param message The message to log.
*/
@Export
fun log(message: String) {
Cobblemon.LOGGER.info(message)
}
}
}
@@ -0,0 +1,381 @@
package com.cobblemon.mod.common.battles.runner.remote
import com.bedrockk.molang.runtime.value.DoubleValue
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.battles.model.actor.UnionBattleActor
import com.cobblemon.mod.common.api.npc.NPCClasses
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.api.storage.party.PartyStore
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.battles.*
import com.cobblemon.mod.common.battles.actor.PlayerBattleActor
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.battles.runner.ShowdownDebugLog
import com.cobblemon.mod.common.battles.runner.ShowdownService
import com.cobblemon.mod.common.battles.runner.ShowdownStatus
import com.cobblemon.mod.common.entity.npc.NPCEntity
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.util.HttpService
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.server
import com.google.gson.JsonArray
import com.google.gson.JsonParser
import net.minecraft.resources.ResourceLocation
import net.minecraft.world.entity.schedule.Activity
import java.net.URI
import java.net.http.HttpClient
import java.net.http.WebSocket
import java.util.*
import java.util.concurrent.CompletionStage
/**
* Implementation of the ShowdownService interface for use with the Union Block.
*
* This wraps around a WebSocket connection to a remote showdown service.
*
* @property id The unique identifier for this showdown service.
* @property host The host address of the showdown service.
* @property httpPort The port for HTTP communication with the showdown service.
* @property socketPort The port for WebSocket communication with the showdown service.
*
* @author landonjw
*/
class UnionShowdownService(
override val id: ResourceLocation,
val host: String = "localhost",
val httpPort: Int = 8003,
val socketPort: Int = 8002
) : ShowdownService {
override var status: ShowdownStatus = ShowdownStatus.DISCONNECTED
private set
override val log = ShowdownDebugLog()
private lateinit var webSocket: WebSocket
/**
* Opens a connection to the showdown service.
*/
override fun openConnection() = openConnection(false)
/**
* Opens a connection to the showdown service.
*
* @param silent If true, suppresses changing the status of the connection process.
*/
private fun openConnection(silent: Boolean) {
log.clear()
log.add(source = "Minecraft", message = "Opening showdown connection")
try {
webSocket = HttpClient.newHttpClient()
.newWebSocketBuilder()
.buildAsync(URI.create("ws://$host:$socketPort"), WebSocketListener(this))
.join()
// TODO: See if this is needed
registerSpecies()
indicateSpeciesInitialized()
registerBagItems()
if (!silent) {
this.status = ShowdownStatus.CONNECTED
}
} catch (e: Exception) {
Cobblemon.LOGGER.warn("Failed to open showdown connection to $host:$socketPort", e)
log.add(source = "Minecraft", message = "Failed to open showdown connection with error: ${e.message}")
if (!silent) {
this.status = ShowdownStatus.DISCONNECTED
}
}
}
/**
* Closes the connection to the showdown service.
*/
override fun closeConnection() = closeConnection(false)
/**
* Closes the connection to the showdown service.
*
* @param silent If true, suppresses changing the status of the disconnection process.
*/
private fun closeConnection(silent: Boolean) {
log.add(source = "Minecraft", message = "Closing showdown connection")
try {
webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "Goodbye!").join()
} catch (e: Exception) {
Cobblemon.LOGGER.warn("Failed to close showdown connection to $host:$socketPort", e)
log.add(source = "Minecraft", message = "Failed to close showdown connection to $host:$socketPort with error: ${e.message}")
}
finally {
if (!silent) {
this.status = ShowdownStatus.DISCONNECTED
}
}
}
/**
* Restarts the connection to the showdown service.
*/
override fun restartConnection() {
this.status = ShowdownStatus.RESTARTING
closeConnection(true)
openConnection(true)
}
/**
* Starts a battle in the showdown service.
*
* @param battle The battle to start.
* @param messages The initial messages for the battle.
*/
override fun startBattle(battle: PokemonBattle, messages: Array<String>) {
send("[${battle.battleId}]>start-battle")
send(battle.battleId, messages)
}
/**
* Sends messages to a specific battle in the showdown service.
*
* @param battleId The unique identifier of the battle.
* @param messages The messages to send.
*/
override fun send(battleId: UUID, messages: Array<String>) {
messages.forEach { send("[$battleId]$it") }
}
/**
* Sends a message to the showdown service.
*
* @param message The message to send.
*/
override fun send(message: String) {
webSocket.sendText(message, true)
}
/**
* Retrieves the ability IDs from the showdown service.
*
* @return A `JsonArray` of ability IDs.
*/
override fun getAbilityIds(): JsonArray {
TODO("Not yet implemented")
}
/**
* Retrieves the moves from the showdown service.
*
* @return A `JsonArray` of moves.
*/
override fun getMoves(): JsonArray {
TODO("Not yet implemented")
}
/**
* Retrieves the item IDs from the showdown service.
*
* @return A `JsonArray` of item IDs.
*/
override fun getItemIds(): JsonArray {
TODO("Not yet implemented")
}
/**
* Registers species in the showdown service.
*/
override fun registerSpecies() {
HttpService.post<Unit>(
url = "http://$host:$httpPort/Species",
body = PokemonSpecies.species.map { PokemonSpecies.ShowdownSpecies(it, null) }
)
}
/**
* Registers bag items in the showdown service.
*/
override fun registerBagItems() {
HttpService.post<Unit>(
url = "http://$host:$httpPort/BagItems",
body = BagItems.bagItemsScripts.map { BagItem(it.key, it.value) }.toList()
)
}
override fun indicateSpeciesInitialized() {
HttpService.post<Unit>(url = "http://$host:$httpPort/InitializeDex")
}
/**
* WebSocket listener for handling events from the showdown service.
*
* @property service The UnionShowdownService instance.
*/
class WebSocketListener(val service: UnionShowdownService) : WebSocket.Listener {
val builder = StringBuilder()
override fun onOpen(webSocket: WebSocket) {
service.log.add(source = "Showdown", message = "Connected to server at ${service.host}:${service.socketPort}")
super.onOpen(webSocket)
}
/**
* Starts a battle based on the received instruction.
*
* @param instruction The instruction to start the battle.
*/
fun startBattle(instruction: String) {
val arguments = instruction.replace("|start-battle|", "").split("|")
if (arguments.size != 7) return
val battleId = UUID.fromString(arguments[0])
val playerIdA = UUID.fromString(arguments[1])
val playerNameA = arguments[2]
val playerIdB = UUID.fromString(arguments[3])
val playerNameB = arguments[4]
val teamA = JsonParser.parseString(arguments[5]).asJsonObject
val teamB = JsonParser.parseString(arguments[6]).asJsonObject
val playerA = server()?.playerList?.getPlayer(playerIdA)
val playerB = server()?.playerList?.getPlayer(playerIdB)
println(arguments[5])
println(arguments[6])
if (playerA == null && playerB == null) {
Cobblemon.LOGGER.warn("Tried to start battle with no players present on server")
return
}
val registryAccess = server()?.registryAccess() ?: return
val playerAParty = PartyStore(UUID.randomUUID()).loadFromJSON(teamA, registryAccess)
val playerBParty = PartyStore(UUID.randomUUID()).loadFromJSON(teamB, registryAccess)
val playerATeam = toShowdownBattleTeam(playerAParty.toList()).onEach {
it.effectedPokemon.level = 100
it.effectedPokemon.heal()
}
val playerBTeam = toShowdownBattleTeam(playerBParty.toList()).onEach {
it.effectedPokemon.level = 100
it.effectedPokemon.heal()
}
if (playerA != null && playerB != null) {
val playerActorA = PlayerBattleActor(playerIdA, playerATeam)
val playerActorB = PlayerBattleActor(playerIdB, playerBTeam)
BattleRegistry.startBattle(
battleId = battleId,
battleFormat = BattleFormat.GEN_9_SINGLES,
side1 = BattleSide(playerActorA),
side2 = BattleSide(playerActorB),
sendMessages = false,
service = service
)
}
else if (playerA != null && playerB == null) {
val playerActorA = PlayerBattleActor(playerIdA, playerATeam)
val npc = NPCEntity(playerA.level())
npc.moveTo(playerA.x + 5, playerA.y, playerA.z, playerA.yRot, (playerA.xRot + 180f) % 360f)
npc.npc = NPCClasses.getByIdentifier(cobblemonResource("standard"))!!
npc.loadTextureFromGameProfileName(playerNameB)
npc.initialize(1)
npc.config.setDirectly("yaw", DoubleValue((playerA.xRot + 90f) % 360f))
npc.uuid = playerIdB
npc.customName = playerNameB.text()
npc.isInvulnerable = true
npc.isMovable = false
playerA.level().addFreshEntity(npc)
val playerActorB = UnionBattleActor(npc, playerBTeam)
BattleRegistry.startBattle(
battleId = battleId,
battleFormat = BattleFormat.GEN_9_SINGLES,
side1 = BattleSide(playerActorA),
side2 = BattleSide(playerActorB),
sendMessages = false,
service = service
)
}
else {
val npc = NPCEntity(playerB!!.level())
npc.moveTo(playerB.x + 5, playerB.y, playerB.z, playerB.yRot, (playerB.xRot + 180f) % 360f)
npc.npc = NPCClasses.getByIdentifier(cobblemonResource("standard"))!!
npc.loadTextureFromGameProfileName(playerNameA)
npc.initialize(1)
npc.uuid = playerIdA
npc.customName = playerNameA.text()
npc.isInvulnerable = true
npc.isMovable = false
npc.config.setDirectly("yaw", DoubleValue((playerB.xRot + 90f) % 360f))
playerB.level().addFreshEntity(npc)
val playerActorA = UnionBattleActor(npc, playerATeam)
val playerActorB = PlayerBattleActor(playerIdB, playerBTeam)
BattleRegistry.startBattle(
battleId = battleId,
battleFormat = BattleFormat.GEN_9_SINGLES,
side1 = BattleSide(playerActorA),
side2 = BattleSide(playerActorB),
sendMessages = false,
service = service
)
}
}
/**
* Converts a list of Pokemon to a list of BattlePokemon for the showdown service.
*
* @param team The list of Pokemon to convert.
* @return A list of BattlePokemon.
*/
fun toShowdownBattleTeam(team: List<Pokemon>): List<BattlePokemon> {
return team.map { pokemon ->
BattlePokemon(
originalPokemon = pokemon,
effectedPokemon = pokemon.clone(newUUID = false),
postBattleEntityOperation = { entity -> entity.discard() }
)
}
}
override fun onText(webSocket: WebSocket, data: CharSequence, last: Boolean): CompletionStage<*>? {
if (last) {
builder.append(data)
val message = builder.toString()
service.log.add(source = "Showdown", message = message)
if (data.startsWith("|start-battle|")) {
startBattle(data.toString())
}
else {
val battleIdRegex = Regex("^\\[([^]]+)]")
val match = battleIdRegex.find(message) ?: return null
val battleId = match.groupValues[1]
val instruction = message.replace("[$battleId]", "")
ShowdownInterpreter.interpretMessage(UUID.fromString(battleId), instruction)
}
builder.clear()
}
else {
builder.append(data)
}
return super.onText(webSocket, data, last)
}
override fun onError(webSocket: WebSocket, error: Throwable) {
service.log.add(source = "Showdown", message = "Error: ${error.message}")
Cobblemon.LOGGER.warn("Error in showdown service ${service.id} websocket", error)
super.onError(webSocket, error)
}
override fun onClose(webSocket: WebSocket, statusCode: Int, reason: String): CompletionStage<*>? {
service.log.add(source = "Showdown", message = "Closed connection with status $statusCode and reason $reason")
return super.onClose(webSocket, statusCode, reason)
}
}
/**
* Data class representing a bag item. We use this for serialization to the showdown service.
*
* @property id The unique identifier of the bag item.
* @property script The script associated with the bag item.
*/
private data class BagItem(val id: String, val script: String)
}
@@ -1,169 +0,0 @@
/*
* 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.runner.socket
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.battles.BagItems
import com.cobblemon.mod.common.battles.ShowdownInterpreter
import com.cobblemon.mod.common.battles.runner.ShowdownService
import com.cobblemon.mod.common.pokemon.FormData
import com.cobblemon.mod.common.pokemon.Species
import com.google.gson.Gson
import com.google.gson.JsonArray
import java.io.BufferedReader
import java.io.InputStreamReader
import java.io.OutputStreamWriter
import java.net.InetAddress
import java.net.Socket
import java.nio.charset.Charset
import java.util.UUID
/**
* Mediator service for communicating between the Cobblemon Minecraft mod and Cobblemon showdown service via
* a socket client.
*
* This is primarily used for debugging purposes, but could be extended in the future to provide
* a means of connecting to a remote Showdown server. This does not provide any fault handling in the
* event that the server goes down.
*
* When messages are sent to showdown, this will await a response from showdown.
* The protocol for messages sent from showdown is <length of characters in payload><payload>,
* and a payload length of 0 indicates that there is no response.
*
* @see {@code sim/cobbled-debug-server.ts} within cobblemon-showdown repository
* @since February 27, 2023
* @author landonjw
*/
class SocketShowdownService(val host: String = "localhost", val port: Int = 18468, val localPort: Int = 0) : ShowdownService {
private lateinit var socket: Socket
private lateinit var writer: OutputStreamWriter
private lateinit var reader: BufferedReader
val gson = Gson()
override fun openConnection() {
socket = Socket(InetAddress.getLocalHost(), port, InetAddress.getLocalHost(), localPort)
writer = socket.getOutputStream().writer(charset = Charset.forName("ascii"))
reader = BufferedReader(InputStreamReader(socket.getInputStream()))
}
override fun closeConnection() {
socket.close()
}
override fun startBattle(battle: PokemonBattle, messages: Array<String>) {
writer.write(">startbattle ${battle.battleId}\n")
acknowledge { Cobblemon.LOGGER.error("Failed to start battle!") }
send(battle.battleId, messages)
}
override fun send(battleId: UUID, messages: Array<String>) {
for (message in messages) {
writer.write("$battleId~$message\n")
writer.flush()
readBattleInput().forEach { interpretMessage(battleId, it) }
}
}
private fun read(reader: BufferedReader, size: Int): String {
val buffer = CharArray(size)
while (true) {
if(reader.read(buffer) == 0) continue
return String(buffer)
}
}
private fun readMessage(): String {
val payloadSize = read(reader, 8).toInt()
val payload = read(reader, payloadSize)
return payload
}
private fun readBattleInput(): List<String> {
val lines = mutableListOf<String>()
val numLines = read(reader, 8).toInt()
if (numLines != 0) {
for (i in 0 until numLines) {
lines.add(readMessage())
}
}
return lines
}
private fun interpretMessage(battleId: UUID, message: String) {
ShowdownInterpreter.interpretMessage(battleId, message)
}
override fun getAbilityIds(): JsonArray {
writer.write(">getCobbledAbilityIds")
writer.flush()
val response = readMessage()
return gson.fromJson(response, JsonArray::class.java)
}
override fun getMoves(): JsonArray {
writer.write(">getCobbledMoves\n")
writer.flush()
val response = readMessage()
return gson.fromJson(response, JsonArray::class.java)
}
override fun getItemIds(): JsonArray {
writer.write(">getCobbledItemIds")
writer.flush()
val response = readMessage()
return gson.fromJson(response, JsonArray::class.java)
}
private fun sendSpeciesData(species: Species, form: FormData?) {
writer.write(">receiveSpeciesData ${gson.toJson(PokemonSpecies.ShowdownSpecies(species, form))}\n")
acknowledge()
}
private fun sendBagItem(itemId: String, js: String) {
writer.write(">receiveBagItemData $itemId $js")
acknowledge { Cobblemon.LOGGER.error("Failed to send bag item to Showdown: $itemId") }
}
override fun registerSpecies() {
writer.write(">resetSpeciesData\n")
acknowledge()
PokemonSpecies.species.forEach { species ->
sendSpeciesData(species, null)
species.forms.forEach { form ->
if (form != species.standardForm) {
sendSpeciesData(species, form)
}
}
}
}
fun acknowledge(ifFails: () -> Unit = {}) {
writer.flush()
val ack = CharArray(3)
reader.read(ack)
if (String(ack) != "ACK") {
ifFails()
}
}
override fun registerBagItems() {
for ((itemId, js) in BagItems.bagItemsScripts) {
sendBagItem(itemId, js.replace("\n", " "))
}
}
override fun indicateSpeciesInitialized() {
writer.write(">afterCobbledSpeciesInit")
acknowledge()
}
}
@@ -0,0 +1,330 @@
package com.cobblemon.mod.common.client.gui.debug
import com.cobblemon.mod.common.CobblemonNetwork
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.battles.runner.ShowdownStatus
import com.cobblemon.mod.common.client.gui.CobblemonRenderable
import com.cobblemon.mod.common.client.gui.debug.ShowdownDebugGUI.Companion.activeShowdownService
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.net.messages.client.debug.showdown.ShowdownRefreshInfoPacket
import com.cobblemon.mod.common.net.messages.server.debug.showdown.RequestShowdownInfoPacket
import com.cobblemon.mod.common.net.messages.server.debug.showdown.ShowdownActionPacket
import com.cobblemon.mod.common.net.messages.server.debug.showdown.ShowdownInputPacket
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.lang
import com.mojang.blaze3d.platform.ClipboardManager
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.components.Button
import net.minecraft.client.gui.components.EditBox
import net.minecraft.client.gui.components.ObjectSelectionList
import net.minecraft.client.gui.components.ObjectSelectionList.Entry
import net.minecraft.client.gui.screens.Screen
import net.minecraft.locale.Language
import net.minecraft.network.chat.Component
import net.minecraft.util.FormattedCharSequence
import java.awt.Color
import java.util.*
/**
* GUI screen for debugging the Showdown service in the Cobblemon mod.
* This screen allows users to view and interact with the Showdown service logs and status.
*
* @property status The current status of the showdown service.
* @property service The resource location of the showdown service.
* @property messages A list of messages from the active showdown service currently being refreshed.
* @property availableServices A list of available showdown services. This allows the client to switch which server they are viewing.
* @property requestProcessedAt The date and time when the request was processed.
* @property includesStartOfLog Indicates if the messages include the start of the log.
*
* @author landonjw
*/
class ShowdownDebugGUI : Screen(lang("ui.debug.showdown")), CobblemonRenderable {
companion object {
var activeShowdownService = cobblemonResource("embedded")
private set
}
private var showdownStatus: ShowdownStatus? = null
val log = mutableListOf<String>()
var availableServices = listOf(cobblemonResource("embedded"))
var lastProcessedLogTime: Date? = null
val outputWindow = addRenderableWidget(ShowdownOutputList())
val serviceButton = addRenderableWidget(
Button.builder(Component.literal("Service: $activeShowdownService")) {
cycleActiveService()
}.bounds(10, 10, 200, 20).build()
)
val startButton = addRenderableWidget(
Button.builder(Component.literal("Start")) { button ->
CobblemonNetwork.sendToServer(ShowdownActionPacket(
service = activeShowdownService,
action = ShowdownActionPacket.Action.START
))
}.bounds(10, 40, 50, 20).build()
)
val stopButton = addRenderableWidget(
Button.builder(Component.literal("Stop")) { button ->
CobblemonNetwork.sendToServer(ShowdownActionPacket(
service = activeShowdownService,
action = ShowdownActionPacket.Action.STOP
))
}.bounds(70, 40, 50, 20).build()
)
val restartButton = addRenderableWidget(
Button.builder(Component.literal("Restart")) { button ->
this.showdownStatus = ShowdownStatus.RESTARTING
CobblemonNetwork.sendToServer(ShowdownActionPacket(
service = activeShowdownService,
action = ShowdownActionPacket.Action.RESTART
))
}.bounds(130, 40, 50, 20).build()
)
val startNPCBattleButton = addRenderableWidget(
Button.builder(Component.literal("Queue")) { button ->
CobblemonNetwork.sendToServer(ShowdownActionPacket(
service = activeShowdownService,
action = ShowdownActionPacket.Action.QUEUE
))
}.bounds(10, 70, 100, 20).build()
)
val clipboardManager = ClipboardManager()
var timer = Timer()
init {
addRenderableWidget(
Button.builder(Component.literal("Copy to Clipboard")) Clipboard@{
if (log.isEmpty()) return@Clipboard
clipboardManager.setClipboard(Minecraft.getInstance().window.window, log.joinToString("\n"))
}.bounds(170, Minecraft.getInstance().window.guiScaledHeight - 40, 100, 30).build()
)
addRenderableWidget(ShowdownInputWidget(this))
startTimer()
}
/**
* Cycles through the available showdown services.
*/
fun cycleActiveService() {
if (availableServices.size == 1) return
val currentIndex = availableServices.indexOf(activeShowdownService)
activeShowdownService = availableServices[(currentIndex + 1) % availableServices.size]
serviceButton.message = Component.literal("Service: $activeShowdownService")
outputWindow.reset()
log.clear()
lastProcessedLogTime = null
startTimer()
}
/**
* Starts the timer for periodically refreshing the showdown info.
*/
fun startTimer() {
timer.cancel()
timer = Timer()
timer.schedule(object : TimerTask() {
override fun run() {
CobblemonNetwork.sendToServer(RequestShowdownInfoPacket(activeShowdownService, lastProcessedLogTime))
}
}, 0, 500)
}
override fun onClose() {
timer.cancel()
super.onClose()
}
override fun render(guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) {
guiGraphics.fill(0, 0, width, height, Color(0, 0, 0, 100).rgb)
super.render(guiGraphics, mouseX, mouseY, partialTick)
if (showdownStatus != null) {
guiGraphics.fill(
190,
45,
200,
55,
when (showdownStatus!!) {
ShowdownStatus.DISCONNECTED -> Color(255, 0, 0, 255).rgb
ShowdownStatus.RESTARTING -> Color(255, 255, 0, 255).rgb
ShowdownStatus.CONNECTED -> Color(0, 255, 0, 255).rgb
}
)
drawScaledText(
context = guiGraphics,
text = Component.literal(showdownStatus!!.name.replaceFirstChar {
if (it.isLowerCase()) it.titlecase(
Locale.getDefault()
) else it.toString()
}),
x = 205,
y = 47
)
}
}
/**
* Refreshes the showdown info with the data from the given packet.
*
* @param packet The packet containing the refreshed showdown info.
*/
fun refreshShowdownInfo(packet: ShowdownRefreshInfoPacket) {
// Prevent false positives from old packets
if (activeShowdownService != packet.service) return
if (packet.includesStartOfLog) {
log.clear()
outputWindow.reset()
}
packet.messages.forEach {
log.add(it)
outputWindow.addNewEntry(it.text())
}
lastProcessedLogTime = packet.requestProcessedAt
showdownStatus = packet.status
when (showdownStatus) {
ShowdownStatus.CONNECTED -> {
startButton.active = false
stopButton.active = true
restartButton.active = true
startNPCBattleButton.active = true
}
ShowdownStatus.DISCONNECTED -> {
startButton.active = true
stopButton.active = false
restartButton.active = false
startNPCBattleButton.active = false
}
ShowdownStatus.RESTARTING -> {
startButton.active = false
stopButton.active = false
restartButton.active = false
startNPCBattleButton.active = false
}
else -> {
startButton.active = false
stopButton.active = false
restartButton.active = false
startNPCBattleButton.active = false
}
}
this.availableServices = packet.availableServices
}
}
/**
* List widget for displaying the output log of the Showdown service.
*/
class ShowdownOutputList : ObjectSelectionList<OutputEntry>(
Minecraft.getInstance(),
Minecraft.getInstance().window.guiScaledWidth - 280 - 10,
Minecraft.getInstance().window.guiScaledHeight - 10 - 40,
10,
14
) {
/**
* Adds a new entry to the output list.
*
* @param text The text to add as a new entry.
*/
fun addNewEntry(text: Component) {
val line = text.copy()
val textRenderer = Minecraft.getInstance().font
val lines = Language.getInstance().getVisualOrder(textRenderer.splitter.splitLines(line, rowWidth, line.style))
val fullyScrolledDown = maxScroll - scrollAmount < 10
lines.forEach { addEntry(OutputEntry(it)) }
if (fullyScrolledDown) {
scrollAmount = maxScroll.toDouble()
}
}
override fun getSelected(): OutputEntry? = null
override fun getRowWidth(): Int {
return Minecraft.getInstance().window.guiScaledWidth - 280 - 10 - 10
}
override fun getScrollbarPosition(): Int {
return this.x + rowWidth + 5
}
override fun renderWidget(guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) {
setRectangle(
Minecraft.getInstance().window.guiScaledWidth - 280 - 10,
Minecraft.getInstance().window.guiScaledHeight - 10 - 10 - 40,
280,
10
)
this.x = 280
super.renderWidget(guiGraphics, mouseX, mouseY, partialTick)
}
/**
* Resets the output list by clearing all entries.
*/
fun reset() {
super.clearEntries()
}
}
/**
* Entry for the output list, representing a single line of text.
*
* @param line The formatted line of text to display.
*/
class OutputEntry(val line: FormattedCharSequence) : Entry<OutputEntry>() {
override fun getNarration() = "".text()
override fun render(
context: GuiGraphics,
index: Int,
rowTop: Int,
rowLeft: Int,
rowWidth: Int,
rowHeight: Int,
mouseX: Int,
mouseY: Int,
isHovered: Boolean,
partialTicks: Float
) {
drawScaledText(
context,
line,
rowLeft,
rowTop - 2
)
}
override fun isFocused() = false
}
/**
* Input widget for entering commands to the Showdown service.
*/
class ShowdownInputWidget(val gui: ShowdownDebugGUI) : EditBox(
Minecraft.getInstance().font,
280,
Minecraft.getInstance().window.guiScaledHeight - 40,
Minecraft.getInstance().window.guiScaledWidth - 280 - 10,
30,
Component.literal("Enter command")
) {
override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
val result = super.keyPressed(keyCode, scanCode, modifiers)
if (keyCode == 257 && this.value.isNotEmpty()) {
CobblemonNetwork.sendToServer(ShowdownInputPacket(
service = activeShowdownService,
input = this.value
))
deleteCharsToPos(0)
CobblemonNetwork.sendToServer(RequestShowdownInfoPacket(activeShowdownService, gui.lastProcessedLogTime))
}
return result
}
}
@@ -10,6 +10,7 @@ package com.cobblemon.mod.common.client.keybind.keybinds
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.client.CobblemonClient
import com.cobblemon.mod.common.client.gui.debug.ShowdownDebugGUI
import com.cobblemon.mod.common.client.keybind.CobblemonKeyBinding
import com.cobblemon.mod.common.client.keybind.KeybindCategories
import com.cobblemon.mod.common.client.render.models.blockbench.FloatingState
@@ -26,8 +27,10 @@ object DebugKeybindings {
TranslateRightKeybinding(),
TranslateUpKeybinding(),
TranslateDownKeybinding(),
PrintModelSettingsKeybinding()
PrintModelSettingsKeybinding(),
ToggleShowdownDebugGUIKeybinding()
)
class ScaleUpKeybinding: CobblemonKeyBinding(
"key.cobblemon.scaleportraitup",
InputConstants.Type.KEYSYM,
@@ -145,4 +148,15 @@ object DebugKeybindings {
}
}
}
}
class ToggleShowdownDebugGUIKeybinding : CobblemonKeyBinding(
"key.cobblemon.toggleshowdowndebuggui",
InputConstants.Type.KEYSYM,
InputConstants.KEY_APOSTROPHE,
KeybindCategories.COBBLEMON_DEBUG_CATEGORY
) {
override fun onPress() {
Minecraft.getInstance().setScreen(ShowdownDebugGUI())
}
}
}
@@ -0,0 +1,26 @@
package com.cobblemon.mod.common.client.net.debug.showdown
import com.cobblemon.mod.common.api.net.ClientNetworkPacketHandler
import com.cobblemon.mod.common.client.gui.debug.ShowdownDebugGUI
import com.cobblemon.mod.common.net.messages.client.debug.showdown.ShowdownRefreshInfoPacket
import net.minecraft.client.Minecraft
/**
* Handles the reception of a [ShowdownRefreshInfoPacket] on the client side.
* This handler updates the Showdown Debug GUI with the latest showdown information.
* @author landonjw
*/
object ShowdownRefreshInfoHandler : ClientNetworkPacketHandler<ShowdownRefreshInfoPacket> {
/**
* Handles the received [ShowdownRefreshInfoPacket] by refreshing the showdown information
* in the [ShowdownDebugGUI] if it is currently open.
*
* @param packet The received packet containing the showdown information.
* @param client The Minecraft client instance.
*/
override fun handle(packet: ShowdownRefreshInfoPacket, client: Minecraft) {
if (client.screen is ShowdownDebugGUI) {
(client.screen as ShowdownDebugGUI).refreshShowdownInfo(packet)
}
}
}
@@ -1,41 +0,0 @@
/*
* 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.command
import com.cobblemon.mod.common.battles.runner.ShowdownService
import com.mojang.brigadier.Command
import com.mojang.brigadier.CommandDispatcher
import com.mojang.brigadier.context.CommandContext
import net.minecraft.commands.CommandSourceStack
import net.minecraft.commands.Commands
import net.minecraft.network.chat.Component
object ReloadShowdownCommand {
fun register(dispatcher: CommandDispatcher<CommandSourceStack>) {
val command = Commands.literal("reloadshowdown")
.requires { it.hasPermission(4) }
.executes(::execute)
dispatcher.register(command)
}
private fun execute(context: CommandContext<CommandSourceStack>): Int {
try {
ShowdownService.service.closeConnection()
ShowdownService.service.openConnection()
ShowdownService.service.registerSpecies()
ShowdownService.service.registerBagItems()
context.source.sendSystemMessage(Component.literal("Reloaded showdown"))
} catch (e: Exception) {
e.printStackTrace()
}
return Command.SINGLE_SUCCESS
}
}
@@ -13,7 +13,6 @@ import com.cobblemon.mod.common.api.text.red
import com.cobblemon.mod.common.command.argument.NPCClassArgumentType
import com.cobblemon.mod.common.entity.npc.NPCEntity
import com.cobblemon.mod.common.util.alias
import com.cobblemon.mod.common.util.commandLang
import com.cobblemon.mod.common.util.permission
import com.cobblemon.mod.common.util.toBlockPos
import com.mojang.brigadier.Command
@@ -84,4 +83,4 @@ object SpawnNPCCommand {
}
throw FAILED_SPAWN_EXCEPTION.create()
}
}
}
@@ -23,7 +23,7 @@ import com.cobblemon.mod.common.util.effectiveName
import com.cobblemon.mod.common.util.update
import java.util.concurrent.CompletableFuture
class NPCBattleActor(
open class NPCBattleActor(
val npc: NPCEntity,
pokemonList: List<BattlePokemon>,
val skill: Int
@@ -84,4 +84,4 @@ class NPCBattleActor(
// }
// }
}
}
}
@@ -0,0 +1,68 @@
package com.cobblemon.mod.common.net.messages.client.debug.showdown
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.battles.runner.ShowdownStatus
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.readString
import com.cobblemon.mod.common.util.writeString
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.resources.ResourceLocation
import java.util.Date
/**
* Packet sent to refresh the showdown information with the current status, messages, and available services.
* This is typically sent when a user is in the Showdown Debug UI after they send a [RequestShowdownInfoPacket].
*
* @property status The current status of the showdown service.
* @property service The resource location of the showdown service.
* @property messages A list of messages from the active showdown service currently being refreshed.
* @property availableServices A list of available showdown services. This allows the client to switch which server they are viewing.
* @property requestProcessedAt The date and time when the request was processed.
* @property includesStartOfLog Indicates if the messages include the start of the log.
*
* @author landonjw
*/
class ShowdownRefreshInfoPacket(
val status: ShowdownStatus,
val service: ResourceLocation,
val messages: List<String>,
val availableServices: List<ResourceLocation>,
val requestProcessedAt: Date,
val includesStartOfLog: Boolean
) : NetworkPacket<ShowdownRefreshInfoPacket> {
companion object {
val ID = cobblemonResource("refresh_showdown_info")
/**
* Decodes a [ShowdownRefreshInfoPacket] from the given [RegistryFriendlyByteBuf].
*
* @param buffer The buffer to read from.
* @return The decoded [ShowdownRefreshInfoPacket].
*/
fun decode(buffer: RegistryFriendlyByteBuf) = ShowdownRefreshInfoPacket(
status = ShowdownStatus.valueOf(buffer.readString()),
service = buffer.readResourceLocation(),
messages = buffer.readList { buffer.readString() },
availableServices = buffer.readList { buffer.readResourceLocation() },
requestProcessedAt = buffer.readDate(),
includesStartOfLog = buffer.readBoolean()
)
}
override val id = ID
/**
* Encodes this [ShowdownRefreshInfoPacket] to the given [RegistryFriendlyByteBuf].
*
* @param buffer The buffer to write to.
*/
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeString(status.name)
buffer.writeResourceLocation(service)
buffer.writeCollection(messages) { pb, value -> pb.writeString(value) }
buffer.writeCollection(availableServices) { pb, value -> pb.writeResourceLocation(value) }
buffer.writeDate(requestProcessedAt)
buffer.writeBoolean(includesStartOfLog)
}
}
@@ -0,0 +1,42 @@
package com.cobblemon.mod.common.net.messages.server.debug.showdown
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.resources.ResourceLocation
import java.util.*
/**
* Packet sent to request showdown information for a specific server.
*
* @property service The id of the showdown service to get showdown information for.
* @author landonjw
*/
class RequestShowdownInfoPacket(val service: ResourceLocation, val lastRequest: Date?) : NetworkPacket<RequestShowdownInfoPacket> {
override val id = ID
/**
* Encodes this [RequestShowdownInfoPacket] to the given [RegistryFriendlyByteBuf].
*
* @param buffer The buffer to write to.
*/
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeResourceLocation(service)
buffer.writeNullable(lastRequest) { pb, value -> pb.writeDate(value) }
}
companion object {
val ID = cobblemonResource("request_showdown_info")
/**
* Decodes a [RequestShowdownInfoPacket] from the given [RegistryFriendlyByteBuf].
*
* @param buffer The buffer to read from.
* @return The decoded [RequestShowdownInfoPacket].
*/
fun decode(buffer: RegistryFriendlyByteBuf) = RequestShowdownInfoPacket(
service = buffer.readResourceLocation(),
lastRequest = buffer.readNullable { pb -> pb.readDate() }
)
}
}
@@ -0,0 +1,49 @@
package com.cobblemon.mod.common.net.messages.server.debug.showdown
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.resources.ResourceLocation
/**
* A network packet for sending action commands to the Showdown service.
*
* @property service The resource location of the Showdown service.
* @property action The action command to be sent to the Showdown service.
* @author landonjw
*/
class ShowdownActionPacket(val service: ResourceLocation, val action: Action) : NetworkPacket<ShowdownActionPacket> {
override val id = ID
/**
* Encodes the packet data into the given buffer.
*
* @param buffer The buffer to encode the data into.
*/
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeResourceLocation(service)
buffer.writeEnum(action)
}
companion object {
val ID = cobblemonResource("showdown_action_request")
/**
* Decodes the packet data from the given buffer.
*
* @param buffer The buffer to decode the data from.
* @return The decoded ShowdownActionPacket.
*/
fun decode(buffer: RegistryFriendlyByteBuf) = ShowdownActionPacket(
service = buffer.readResourceLocation(),
action = buffer.readEnum(Action::class.java)
)
}
/**
* Enum representing possible actions for the Showdown service.
*/
enum class Action {
START, STOP, RESTART, QUEUE
}
}
@@ -0,0 +1,44 @@
package com.cobblemon.mod.common.net.messages.server.debug.showdown
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.readString
import com.cobblemon.mod.common.util.writeString
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.resources.ResourceLocation
/**
* A network packet for sending input commands to the Showdown service.
*
* @property service The resource location of the Showdown service.
* @property input The input command to be sent to the Showdown service.
* @author landonjw
*/
class ShowdownInputPacket(val service: ResourceLocation, val input: String) : NetworkPacket<ShowdownInputPacket> {
override val id = ID
/**
* Encodes the packet data into the given buffer.
*
* @param buffer The buffer to encode the data into.
*/
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeResourceLocation(service)
buffer.writeString(input)
}
companion object {
val ID = cobblemonResource("showdown_info_request")
/**
* Decodes the packet data from the given buffer.
*
* @param buffer The buffer to decode the data from.
* @return The decoded ShowdownInputPacket.
*/
fun decode(buffer: RegistryFriendlyByteBuf) = ShowdownInputPacket(
service = buffer.readResourceLocation(),
input = buffer.readString()
)
}
}
@@ -0,0 +1,40 @@
package com.cobblemon.mod.common.net.serverhandling.debug.showdown
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.CobblemonNetwork.sendPacket
import com.cobblemon.mod.common.api.net.ServerNetworkPacketHandler
import com.cobblemon.mod.common.battles.runner.ShowdownService
import com.cobblemon.mod.common.net.messages.client.debug.showdown.ShowdownRefreshInfoPacket
import com.cobblemon.mod.common.net.messages.server.debug.showdown.RequestShowdownInfoPacket
import net.minecraft.server.MinecraftServer
import net.minecraft.server.level.ServerPlayer
import java.util.Date
/**
* Handles the reception of a [RequestShowdownInfoPacket] on the server side.
* This handler retrieves the showdown information from the specified service
* and sends it back to the client.
*
* @author landonjw
*/
object RequestShowdownInfoHandler : ServerNetworkPacketHandler<RequestShowdownInfoPacket> {
/**
* Handles the received [RequestShowdownInfoPacket] by retrieving the showdown information
* from the specified service and sending it back to the client in a [ShowdownRefreshInfoPacket].
*
* @param packet The received packet requesting showdown information.
* @param server The Minecraft server instance.
* @param player The player who sent the request.
*/
override fun handle(packet: RequestShowdownInfoPacket, server: MinecraftServer, player: ServerPlayer) {
if (!Cobblemon.config.enableDebugKeys) return
if (!ShowdownService.contains(packet.service)) return
val service = ShowdownService.get(packet.service)
val messages = service.log.get(after = packet.lastRequest)
val availableServices = ShowdownService.all().map { it.id }
val includesStartOfLog = if (packet.lastRequest == null) true else service.log.isStartOfLog(messages)
player.sendPacket(ShowdownRefreshInfoPacket(service.status, packet.service, messages, availableServices, Date(), includesStartOfLog))
}
}
@@ -0,0 +1,53 @@
package com.cobblemon.mod.common.net.serverhandling.debug.showdown
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.net.ServerNetworkPacketHandler
import com.cobblemon.mod.common.battles.BattleRegistry.packTeam
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.battles.runner.ShowdownService
import com.cobblemon.mod.common.net.messages.server.debug.showdown.ShowdownActionPacket
import com.cobblemon.mod.common.util.party
import com.google.gson.JsonObject
import net.minecraft.server.MinecraftServer
import net.minecraft.server.level.ServerPlayer
/**
* Handles network packets for Showdown actions on the server side.
*
* This handler processes incoming `ShowdownActionPacket` packets and performs the corresponding
* actions (ie. restart it) on the specified Showdown service.
*
* @author landonjw
*/
object ShowdownActionHandler : ServerNetworkPacketHandler<ShowdownActionPacket> {
/**
* Handles the received `ShowdownActionPacket`.
*
* @param packet The received packet containing the action to perform.
* @param server The Minecraft server instance.
* @param player The player who sent the packet.
*/
override fun handle(packet: ShowdownActionPacket, server: MinecraftServer, player: ServerPlayer) {
if (!Cobblemon.config.enableDebugKeys) return
if (!ShowdownService.contains(packet.service)) return
val service = ShowdownService.get(packet.service)
when (packet.action) {
ShowdownActionPacket.Action.START -> service.openConnection()
ShowdownActionPacket.Action.STOP -> service.closeConnection()
ShowdownActionPacket.Action.RESTART -> service.restartConnection()
ShowdownActionPacket.Action.QUEUE -> {
val cobblemonTeam = player.party().saveToJSON(JsonObject(), player.registryAccess())
val showdownTeam = player.party().map { BattlePokemon(it, it.clone(false)) { entity -> entity.discard() } }
showdownTeam.forEach {
it.effectedPokemon.level = 100
it.effectedPokemon.heal()
}
val showdownTeamStr = showdownTeam.packTeam()
service.send(">queue-battle ${player.uuid} ${player.name.string} $cobblemonTeam@|@$showdownTeamStr")
}
}
}
}
@@ -0,0 +1,33 @@
package com.cobblemon.mod.common.net.serverhandling.debug.showdown
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.net.ServerNetworkPacketHandler
import com.cobblemon.mod.common.battles.runner.ShowdownService
import com.cobblemon.mod.common.net.messages.server.debug.showdown.ShowdownInputPacket
import net.minecraft.server.MinecraftServer
import net.minecraft.server.level.ServerPlayer
/**
* Handles the processing of `ShowdownInputPacket` on the server side.
* This handler sends a command to the given showdown service. This is used for debugging purposes.
*
* @author landonjw
*/
object ShowdownInputHandler : ServerNetworkPacketHandler<ShowdownInputPacket> {
/**
* Handles the received `ShowdownInputPacket`.
*
* @param packet The received packet containing the input command and service information.
* @param server The Minecraft server instance.
* @param player The player who sent the packet.
*/
override fun handle(packet: ShowdownInputPacket, server: MinecraftServer, player: ServerPlayer) {
if (!Cobblemon.config.enableDebugKeys) return
if (!ShowdownService.contains(packet.service)) return
val service = ShowdownService.get(packet.service)
service.send(packet.input)
}
}
@@ -32,7 +32,7 @@ abstract class BaseCobblemonHeldItemManager : HeldItemManager {
internal open fun load() {
this.itemIds.clear()
val itemsJson = ShowdownService.service.getItemIds()
val itemsJson = ShowdownService.get().getItemIds()
val showdownIds = hashSetOf<String>()
for (i in 0 until itemsJson.size()) {
showdownIds += itemsJson[i].asString
@@ -95,4 +95,4 @@ abstract class BaseCobblemonHeldItemManager : HeldItemManager {
}
return null
}
}
}
@@ -0,0 +1,87 @@
package com.cobblemon.mod.common.util
import com.cobblemon.mod.common.Cobblemon
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import java.io.ByteArrayOutputStream
import java.io.InputStream
import java.net.HttpURLConnection
import java.net.URI
object HttpService {
val gson: Gson = GsonBuilder().create()
inline fun <reified T> get(url: String): HttpResponse<T> {
val connection = (URI.create(url).toURL().openConnection() as HttpURLConnection).also {
it.requestMethod = "GET"
it.connectTimeout = 5000
it.readTimeout = 5000
it.useCaches = false
}
return readFromUrlConnection<T>(connection)
}
inline fun <reified T> post(url: String, body: Any? = null): HttpResponse<T> {
val bodyJson = gson.toJson(body)
val bodyBytes = bodyJson.toByteArray(Charsets.UTF_8)
val connection = (URI.create(url).toURL().openConnection() as HttpURLConnection).also {
it.requestMethod = "POST"
it.doOutput = true
it.connectTimeout = 5000
it.readTimeout = 5000
it.useCaches = false
it.setRequestProperty("Content-Type", "application/json; charset=UTF-8")
it.setRequestProperty("Content-Length", bodyBytes.size.toString())
}
connection.connect()
connection.outputStream.use {
it.write(bodyBytes)
}
return readFromUrlConnection<T>(connection)
}
inline fun <reified T> readFromUrlConnection(connection: HttpURLConnection): HttpResponse<T> {
try {
val status = connection.responseCode
if (status >= 400) {
connection.errorStream.use {
val error = readFromStream(it)
return HttpResponse(error = HttpError(status, error), body = null)
}
}
else {
connection.inputStream.use {
val bodyStr = readFromStream(it)
return when {
T::class == Unit::class -> HttpResponse(error = null, body = null)
T::class == String::class -> HttpResponse(error = null, body = bodyStr as T)
else -> HttpResponse(error = null, body = gson.fromJson(bodyStr, T::class.java))
}
}
}
}
catch (e: Exception) {
Cobblemon.LOGGER.error("Failed to read from connection url at ${connection.url}", e)
return HttpResponse(error = HttpError(500, null), body = null)
}
}
fun readFromStream(stream: InputStream): String {
ByteArrayOutputStream().use { result ->
val buffer = ByteArray(1024)
var length: Int
while (stream.read(buffer).also { length = it } != -1) {
result.write(buffer, 0, length)
}
return result.toString("UTF-8")
}
}
}
data class HttpResponse<T>(val error: HttpError?, val body: T?)
data class HttpError(val statusCode: Int, val details: String?)
@@ -1253,6 +1253,7 @@
"key.cobblemon.translateportraitleft": "Translate Portrait Left",
"key.cobblemon.translateportraitright": "Translate Portrait Right",
"key.cobblemon.printmodelsettings": "Print Model Settings",
"key.cobblemon.toggleshowdowndebugui": "Toggle Showdown Debugger UI",
"key.cobblemon.categories.cobblemon.debug": "Cobblemon Debug",
"cobblemon.subtitle.poke_ball.capture_succeeded": "Poké Ball capture succeeds",
@@ -7211,4 +7212,4 @@
"cobblemon.ability.zenmode.desc": "Changes the Pokémon's shape when HP is half or less.",
"cobblemon.ability.zerotohero": "Zero to Hero",
"cobblemon.ability.zerotohero.desc": "The Pokémon transforms into its Hero Form when it switches out."
}
}
@@ -1,7 +1,7 @@
{
"hitbox": "player",
"presets": [
"cobblemon:looks_in_one_direction"
],
"variation": {
"dirt": [
@@ -105,4 +105,4 @@
"garchomp level=100 pokeball=quick_ball held_item=cobblemon:yache_berry ability=roughskin nature=jolly moves=dragonclaw,earthquake,swordsdance,poisonjab hp_iv=31 attack_iv=31 defence_iv=31 special_attack_iv=31 special_defence_iv=31 speed_iv=31 hp_ev=6 attack_ev=252 speed_ev=252"
]
}
}
}
@@ -16,9 +16,9 @@ val bundle: Configuration by configurations.creating {
loom {
val clientConfig = runConfigs.getByName("client")
clientConfig.runDir = "runClient"
clientConfig.programArg("--username=AshKetchum")
// clientConfig.programArg("--username=GaryOak")
//This is AshKetchum's UUID so you get an Ash Ketchum skin
clientConfig.programArg("--uuid=93e4e551-589a-41cb-ab2d-435266c8e035")
// clientConfig.programArg("--uuid=93e4e551-589a-41cb-ab2d-435266c8e035")
val serverConfig = runConfigs.getByName("server")
serverConfig.runDir = "runServer"
}
@@ -59,4 +59,4 @@ tasks {
dependsOn(copyJar)
}
}
}