Seat generator

This commit is contained in:
Hiroku
2025-03-12 16:10:03 -07:00
committed by Nick
parent 32ccd15121
commit 5bda81a5c4
9 changed files with 253 additions and 2 deletions
+2 -1
View File
@@ -1,3 +1,4 @@
### Additions
- Added the ability to ride Pokémon by shift-right clicking and selecting the ride option.
- Added the ability to ride Pokémon by shift-right clicking and selecting the ride option.
- Added /calculateseatpositions command for approximating hitbox locations for riding configurations.
@@ -66,5 +66,6 @@ object CobblemonCommands {
// Possibly lock down registration if and only if under dev environment or running in an environment
// with a certain system environment variable set
CobblemonInfoCommand.register(dispatcher)
CalculateSeatPositionCommand.register(dispatcher)
}
}
@@ -9,6 +9,7 @@
package com.cobblemon.mod.common
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.client.net.CalculateSeatPositionsHandler
import com.cobblemon.mod.common.client.net.PlayerInteractOptionsHandler
import com.cobblemon.mod.common.client.net.SetClientPlayerDataHandler
import com.cobblemon.mod.common.client.net.animation.PlayPosableAnimationHandler
@@ -56,6 +57,7 @@ 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.net.PacketRegisterInfo
import com.cobblemon.mod.common.net.messages.client.CalculateSeatPositionsPacket
import com.cobblemon.mod.common.net.messages.client.PlayerInteractOptionsPacket
import com.cobblemon.mod.common.net.messages.client.SetClientPlayerDataPacket
import com.cobblemon.mod.common.net.messages.client.animation.PlayPosableAnimationPacket
@@ -407,6 +409,10 @@ object CobblemonNetwork {
// Pokédex scanning
list.add(PacketRegisterInfo(ServerConfirmedRegisterPacket.ID, ServerConfirmedRegisterPacket::decode, ServerConfirmedRegisterHandler))
// Debug / cheats
list.add(PacketRegisterInfo(CalculateSeatPositionsPacket.ID, CalculateSeatPositionsPacket::decode, CalculateSeatPositionsHandler))
return list
}
@@ -129,6 +129,8 @@ object CobblemonPermissions {
@JvmStatic
val COBBLEMON_CONFIG_RELOAD = this.create("${COMMAND_PREFIX}cobblemonconfig.reload", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
val CALCULATE_SEAT_POSITIONS = this.create("${COMMAND_PREFIX}calculateseatpositions", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
fun all(): Iterable<Permission> = this.permissions
private fun create(node: String, level: PermissionLevel): Permission {
@@ -0,0 +1,102 @@
/*
* 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.client.net
import com.cobblemon.mod.common.api.net.ClientNetworkPacketHandler
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.api.text.green
import com.cobblemon.mod.common.api.text.plus
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.client.render.models.blockbench.FloatingState
import com.cobblemon.mod.common.client.render.models.blockbench.repository.RenderContext
import com.cobblemon.mod.common.client.render.models.blockbench.repository.VaryingModelRepository
import com.cobblemon.mod.common.net.messages.client.CalculateSeatPositionsPacket
import net.minecraft.client.Minecraft
import net.minecraft.network.chat.ClickEvent
import net.minecraft.world.phys.Vec3
object CalculateSeatPositionsHandler : ClientNetworkPacketHandler<CalculateSeatPositionsPacket> {
private const val FORMAT = "%.2f"
private const val SEAT_PREFIX = "seat"
private const val SIMULATION_PERIOD_TICKS = 15 * 20F
class SeatRanges(
var minX: Float = 9999F,
var minY: Float = 9999F,
var minZ: Float = 9999F,
var maxX: Float = -9999F,
var maxY: Float = -9999F,
var maxZ: Float = -9999F
)
override fun handle(packet: CalculateSeatPositionsPacket, client: Minecraft) {
val state = FloatingState()
state.currentAspects = packet.aspects
val model = VaryingModelRepository.getPoser(packet.speciesIdentifier, state)
model.context = RenderContext()
model.context.put(RenderContext.SPECIES, packet.speciesIdentifier)
model.context.put(RenderContext.ASPECTS, packet.aspects)
model.context.put(RenderContext.POSABLE_STATE, state)
model.context.put(RenderContext.RENDER_STATE, RenderContext.RenderState.WORLD)
state.setPoseToFirstSuitable(packet.poseType)
val seatData = mutableMapOf<Int, SeatRanges>()
var ticks = 0F
// This will loop through the animation and find the extremes of the seat positions so we can later find the avg.
// Most animations are going to loop in like 3 seconds but we can't know that, so just run through 15s to be safe.
while (ticks < SIMULATION_PERIOD_TICKS) {
state.updatePartialTicks(ticks)
model.applyAnimations(null, state, 0F, 0F, 0F, 0F, 0F)
model.updateLocators(null, state)
for ((key, locatorState) in state.locatorStates.entries.filter { it.key.startsWith(SEAT_PREFIX) }.sortedBy { it.key }) {
val index = key.replace(SEAT_PREFIX, "").toInt()
val seatPosition = locatorState.transformPosition(Vec3.ZERO)
val seatRanges = seatData.getOrPut(index, ::SeatRanges)
if (seatPosition.x < seatRanges.minX) seatRanges.minX = seatPosition.x.toFloat()
if (seatPosition.y < seatRanges.minY) seatRanges.minY = seatPosition.y.toFloat()
if (seatPosition.z < seatRanges.minZ) seatRanges.minZ = seatPosition.z.toFloat()
if (seatPosition.x > seatRanges.maxX) seatRanges.maxX = seatPosition.x.toFloat()
if (seatPosition.y > seatRanges.maxY) seatRanges.maxY = seatPosition.y.toFloat()
if (seatPosition.z > seatRanges.maxZ) seatRanges.maxZ = seatPosition.z.toFloat()
}
ticks++
}
val species = PokemonSpecies.getByIdentifier(packet.speciesIdentifier)!!
val form = species.getForm(packet.aspects)
val scale = form.baseScale
var text = "Seats:".text()
for ((index, seatRanges) in seatData) {
val x = String.format(FORMAT, ((seatRanges.minX + seatRanges.maxX) / 2 * scale))
val y = String.format(FORMAT, ((seatRanges.minY + seatRanges.maxY) / 2 * -1 * scale)) // Y is inverted between model and world
val z = String.format(FORMAT, ((seatRanges.minZ + seatRanges.maxZ) / 2 * scale))
val str = """
{
"x": $x,
"y": $y,
"z": $z
}
""".trimIndent()
text.plus(" [$index]".green().also {
it.style = it.style.withClickEvent(ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, str))
})
}
Minecraft.getInstance().player!!.sendSystemMessage(text)
}
}
@@ -0,0 +1,50 @@
/*
* 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.CobblemonNetwork.sendPacket
import com.cobblemon.mod.common.api.permission.CobblemonPermissions
import com.cobblemon.mod.common.command.argument.SpeciesArgumentType
import com.cobblemon.mod.common.entity.PoseType
import com.cobblemon.mod.common.net.messages.client.CalculateSeatPositionsPacket
import com.cobblemon.mod.common.util.permission
import com.mojang.brigadier.Command
import com.mojang.brigadier.CommandDispatcher
import com.mojang.brigadier.arguments.StringArgumentType
import com.mojang.brigadier.context.CommandContext
import net.minecraft.commands.CommandSourceStack
import net.minecraft.commands.Commands
object CalculateSeatPositionCommand {
fun register(dispatcher : CommandDispatcher<CommandSourceStack>) {
dispatcher.register(
Commands.literal("calculateseatpositions")
.permission(CobblemonPermissions.CALCULATE_SEAT_POSITIONS)
.then(
Commands.argument("species", SpeciesArgumentType.species())
.then(
Commands.argument("aspects", StringArgumentType.string())
.then(
Commands.argument("pose", StringArgumentType.string()).executes(::run)
)
)
)
)
}
private fun run(context: CommandContext<CommandSourceStack>) : Int {
val player = context.source.playerOrException
val species = SpeciesArgumentType.getPokemon(context, "species")
val aspects = StringArgumentType.getString(context, "aspects").split(",").toSet()
val pose = StringArgumentType.getString(context, "pose")
val poseType = PoseType.valueOf(pose.uppercase())
player.sendPacket(CalculateSeatPositionsPacket(species.resourceIdentifier, aspects, poseType))
return Command.SINGLE_SUCCESS
}
}
@@ -35,7 +35,7 @@ enum class PoseType {
PROFILE,
/** A pose for rendering in the party overlay and in minor spaces like the battle tiles. */
PORTRAIT,
/** For guilded chest */
/** For gilded chest */
OPEN,
/** A simple type for non-living entities or errant cases. */
NONE;
@@ -0,0 +1,45 @@
/*
* 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.net.messages.client
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.entity.PoseType
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
/**
* Packet sent to the client to tell it to simulate animations on the client and generate the most logical
* positions for the hitboxes.
*
* @author Hiroku
* @since March 12th, 2025
*/
class CalculateSeatPositionsPacket(val speciesIdentifier: ResourceLocation, val aspects: Set<String>, val poseType: PoseType) : NetworkPacket<CalculateSeatPositionsPacket> {
override val id: ResourceLocation = ID
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeResourceLocation(speciesIdentifier)
buffer.writeCollection(aspects) { _, aspect -> buffer.writeString(aspect) }
buffer.writeString(poseType.name)
}
companion object {
val ID = cobblemonResource("calculate_seat_positions")
fun decode(buffer: RegistryFriendlyByteBuf): CalculateSeatPositionsPacket {
return CalculateSeatPositionsPacket(
buffer.readResourceLocation(),
buffer.readList { _ -> buffer.readString() }.toSet(),
PoseType.valueOf(buffer.readString().uppercase()),
)
}
}
}
+44
View File
@@ -0,0 +1,44 @@
## Calculating Seat Positions for Riding
The idea of the /calculateseatpositions command is that it ask for a pose type and it will simulate the animations
for the Pokémon in whatever the first pose is for that type. That will help it find the average position that each seat
locator was for the pose, allowing the user to click each seat index to get the JSON of the offset data onto the
clipboard for easy pasting into the species data.
Usage: `/calculateseatpositions <species> <aspects> <pose>`
Example 1: `/calculateseatpositions araquanid . STAND`
Example 2: `/calculateseatpositions charizard megax,shiny FLY`
The aspects are optional and can be a comma separated list of aspects to apply to the Pokémon. The pose is required. If
there are no seats for the pose, it will just list out no seats.
The seat data for a species is a little bit complicated, because you can register a seat that applies regardless of poses
or you can specify for specific sets of pose types what the offset will be.
Here is an example of a seat object that has one offset used in general but a specific one for when they are swimming.
```json
{
"offset": {
"x": 0.0,
"y": 0.4,
"z": 0.0
},
"poseOffsets": [
{
"poseTypes": ["FLOAT", "SWIM"],
"offset": {
"x": 0.0,
"y": 0.7,
"z": 0.2
}
}
]
}
```
The command will copy to clipboard just the `x`, `y`, `z` part, e.g. `{ "x": 0.0, "y": 0.4, "z": 0.0 }`. How you choose
to implement this data in the seat data is up to you based on how close you think different poses are to having the
player in the same position.