Compare commits

...
Author SHA1 Message Date
whatsy bacc482bf0 finishing touches 2026-10-14 16:25:16 -04:00
Gertimoshka edb3f906f8 Simpler Mixin 2026-10-14 21:48:14 +02:00
whatsy 4171bf341b Add config option to allow using healing machines as a respawn point 2026-10-14 15:02:54 -04:00
Polymeta 6f3894f27b fix playermolang sometimes failing when they die
Signed-off-by: Polymeta <4726322-polymeta@users.noreply.gitlab.com>
2026-10-14 13:33:40 +01:00
Polymeta bbc9a5e62e Merge branch 'campfire-book-lies' into 'main'
Recipe Book Lies No More

See merge request cable-mc/cobblemon!2487
2026-10-14 11:32:52 +00:00
dhi_holo 8caa82f123 Recipe Book Lies No More
Changed the icon for the misc category
2026-10-14 18:42:58 +10:00
19 changed files with 88 additions and 30 deletions
+7
View File
@@ -2,6 +2,7 @@
## [1.8.3 (MONTH xth, 2026)](#1-8-3)
### Additions
- Added the `healersSetSpawn` gamerule. When enabled, using a healing machine will set your spawnpoint.
### Pokémon Added
#### Generation 4
@@ -21,6 +22,8 @@
### Animation updates for the following Pokémon
### Changes
- Changed the Misc icon in the Campfire Pot Recipe Book to Health Candy
- Moved EXP Candies from the Medicine recipe book category to the Misc category
### Fixes
- Fixed datapacks with item interaction evolutions that don't specify an item crashing the world on load.
@@ -34,9 +37,13 @@
- Fixed conflicts with other mods that would make use of recipe books in certain ways (Farmer's Delight Cooking Pot)
- Fixed Pokémon battles stalling when a sidemod cancels the `THROWN_POKEBALL_HIT` hit event
- Fixed choice feature tab completion in `spawnpokemon/pokegive/pokeedit` commands
- Fixed MoLang that's targeting players intermittedly failing when they have died, needing them to relog
### Developer
- Bumped NeoForge to `21.1.195` to get access to MixinExtras `0.5` and the expressions annotation
- New Platform Event `PLAYER_RESPAWN` which is fired when a player respawns (either through death or returning from the end)
- Platform Events `SERVER_PLAYER_TICK_PRE` and `SERVER_PLAYER_TICK_POST` no longer fire when the player is dead
- it is now tied much closer to the actual entity rather than the player (or their connection)
### Molang & Datapacks
- Fixed `q.pokemon.nature` returning class instance instead of proper string value (`cobblemon:adamant`)
@@ -15,6 +15,7 @@ import com.cobblemon.mod.common.api.riding.RidingStyle;
import com.cobblemon.mod.common.api.spawning.spawner.PlayerSpawner;
import com.cobblemon.mod.common.api.spawning.spawner.PlayerSpawnerFactory;
import com.cobblemon.mod.common.api.stats.CobblemonStats;
import com.cobblemon.mod.common.block.HealingMachineBlock;
import com.cobblemon.mod.common.duck.ServerLevelDuck;
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity;
import com.cobblemon.mod.common.world.gamerules.CobblemonGameRules;
@@ -22,10 +23,14 @@ import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.llamalad7.mixinextras.sugar.Local;
import net.minecraft.core.BlockPos;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
@@ -33,6 +38,9 @@ import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.util.Optional;
@Mixin(ServerPlayer.class)
public abstract class ServerPlayerMixin implements PlayerSpawnerAccessor {
@@ -142,4 +150,17 @@ public abstract class ServerPlayerMixin implements PlayerSpawnerAccessor {
((ServerLevelDuck) this.serverLevel()).cobblemon$getLevelResourceLocation()
);
}
@Inject(
method = "findRespawnAndUseSpawnBlock(Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/core/BlockPos;FZZ)Ljava/util/Optional;",
at = @At(value = "HEAD"),
cancellable = true
)
private static void cobblemon$useHealthStationAsRespawn(ServerLevel level, BlockPos pos, float angle, boolean forced, boolean keepInventory, CallbackInfoReturnable<Optional<ServerPlayer.RespawnPosAngle>> cir) {
BlockState state = level.getBlockState(pos);
Block block = state.getBlock();
if (block instanceof HealingMachineBlock && HealingMachineBlock.Companion.canSetSpawn(level)) {
cir.setReturnValue(HealingMachineBlock.Companion.findStandUpPosition(EntityType.PLAYER, level, pos).map((vec3) -> ServerPlayer.RespawnPosAngle.of(vec3, pos)));
}
}
}
@@ -260,6 +260,9 @@ object Cobblemon {
RequestManager.onLogoff(it.player)
serverPlayerStructs.remove(it.player.uuid)
}
PlatformEvents.PLAYER_RESPAWN.subscribe {
serverPlayerStructs.remove(it.player.uuid)
}
PlatformEvents.PLAYER_DEATH.subscribe {
PCLinkManager.removeLink(it.player.uuid)
battleRegistry.getBattleByParticipatingPlayer(it.player)?.stop()
@@ -24,6 +24,7 @@ import net.minecraft.network.chat.Component
import net.minecraft.server.level.ServerPlayer
import net.minecraft.util.RandomSource
import net.minecraft.world.InteractionResult
import net.minecraft.world.entity.EntityType
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.player.Player
import net.minecraft.world.item.Item
@@ -31,13 +32,9 @@ import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.TooltipFlag
import net.minecraft.world.item.context.BlockPlaceContext
import net.minecraft.world.level.BlockGetter
import net.minecraft.world.level.CollisionGetter
import net.minecraft.world.level.Level
import net.minecraft.world.level.block.BaseEntityBlock
import net.minecraft.world.level.block.Block
import net.minecraft.world.level.block.HorizontalDirectionalBlock
import net.minecraft.world.level.block.Mirror
import net.minecraft.world.level.block.RenderShape
import net.minecraft.world.level.block.Rotation
import net.minecraft.world.level.block.*
import net.minecraft.world.level.block.entity.BlockEntity
import net.minecraft.world.level.block.entity.BlockEntityTicker
import net.minecraft.world.level.block.entity.BlockEntityType
@@ -48,9 +45,11 @@ import net.minecraft.world.level.block.state.properties.IntegerProperty
import net.minecraft.world.level.block.state.properties.Property
import net.minecraft.world.level.pathfinder.PathComputationType
import net.minecraft.world.phys.BlockHitResult
import net.minecraft.world.phys.Vec3
import net.minecraft.world.phys.shapes.CollisionContext
import net.minecraft.world.phys.shapes.Shapes
import net.minecraft.world.phys.shapes.VoxelShape
import java.util.Optional
@Suppress("DEPRECATED", "OVERRIDE_DEPRECATION")
class HealingMachineBlock(settings: Properties) : BaseEntityBlock(settings) {
@@ -79,6 +78,14 @@ class HealingMachineBlock(settings: Properties) : BaseEntityBlock(settings) {
const val MAX_CHARGE_LEVEL = 5
val CHARGE_LEVEL: IntegerProperty = IntegerProperty.create("charge", 0, MAX_CHARGE_LEVEL + 1)
val NATURAL: BooleanProperty = BooleanProperty.create("natural")
fun canSetSpawn(level: Level): Boolean {
return level.gameRules.getBoolean(CobblemonGameRules.HEALER_SET_SPAWN)
}
fun findStandUpPosition(entityType: EntityType<*>, level: CollisionGetter, pos: BlockPos): Optional<Vec3> {
return RespawnAnchorBlock.findStandUpPosition(entityType, level, pos) // Exists if we ever want to make something more custom, but anchor block logic should suffice
}
}
init {
@@ -158,7 +165,12 @@ class HealingMachineBlock(settings: Properties) : BaseEntityBlock(settings) {
}
val party = serverPlayerEntity.party()
val pc = serverPlayerEntity.pc()
val healPC = player.level().gameRules.getBoolean(CobblemonGameRules.HEALERS_HEAL_PC);
val healPC = player.level().gameRules.getBoolean(CobblemonGameRules.HEALERS_HEAL_PC)
// Do spawnpoint stuff
if (canSetSpawn(world)) {
player.setRespawnPosition(world.dimension(), blockPos, player.yRot, false, true)
}
if (party.none() && (!healPC || pc.none())) {
player.sendSystemMessage(lang("healingmachine.nopokemon").red(), true)
@@ -183,6 +195,7 @@ class HealingMachineBlock(settings: Properties) : BaseEntityBlock(settings) {
player.sendSystemMessage(lang("healingmachine.notenoughcharge", "${((neededCharge/party.count())*100f).toInt()}%").red(), true)
}
party.forEach { it.tryRecallWithAnimation() }
return InteractionResult.CONSUME
}
@@ -55,6 +55,8 @@ object PlatformEvents {
@JvmField
val CLIENT_PLAYER_LOGOUT = EventObservable<ClientPlayerEvent.Logout>()
@JvmField
val PLAYER_RESPAWN = EventObservable<ServerPlayerEvent.Respawn>()
@JvmField
val PLAYER_DEATH = CancelableObservable<ServerPlayerEvent.Death>()
@JvmField
val RIGHT_CLICK_BLOCK = CancelableObservable<ServerPlayerEvent.RightClickBlock>()
@@ -50,6 +50,11 @@ interface ServerPlayerEvent {
*/
data class Logout(override val player: ServerPlayer) : ServerPlayerEvent
/**
* Fired when the [player] respawns (either by coming back from the end or dying).
*/
data class Respawn(override val player: ServerPlayer) : ServerPlayerEvent
/**
* Fired when the [player] dies.
* If canceled the death will be prevented but healing is required in order to not be stuck in a loop.
@@ -26,5 +26,7 @@ object CobblemonGameRules {
val SHINY_STARTERS: GameRules.Key<GameRules.BooleanValue> = Cobblemon.implementation.registerGameRule("doShinyStarters", GameRules.Category.MISC, BooleanRuleInvoker.`cobblemon$create`(false))
@JvmField
val HEALERS_HEAL_PC: GameRules.Key<GameRules.BooleanValue> = Cobblemon.implementation.registerGameRule("healersHealPC", GameRules.Category.MISC, BooleanRuleInvoker.`cobblemon$create`(false))
@JvmField
val HEALER_SET_SPAWN: GameRules.Key<GameRules.BooleanValue> = Cobblemon.implementation.registerGameRule("healersSetSpawn", GameRules.Category.MISC, BooleanRuleInvoker.`cobblemon$create`(false))
}
@@ -157,4 +157,6 @@ transitive-accessible method net/minecraft/client/model/HumanoidModel setupAttac
transitive-accessible method net/minecraft/client/model/HumanoidModel poseRightArm (Lnet/minecraft/world/entity/LivingEntity;)V
transitive-accessible method net/minecraft/client/model/HumanoidModel poseLeftArm (Lnet/minecraft/world/entity/LivingEntity;)V
transitive-accessible method net/minecraft/world/level/block/BaseEntityBlock createTickerHelper (Lnet/minecraft/world/level/block/entity/BlockEntityType;Lnet/minecraft/world/level/block/entity/BlockEntityType;Lnet/minecraft/world/level/block/entity/BlockEntityTicker;)Lnet/minecraft/world/level/block/entity/BlockEntityTicker;
transitive-accessible method net/minecraft/world/level/block/BaseEntityBlock createTickerHelper (Lnet/minecraft/world/level/block/entity/BlockEntityType;Lnet/minecraft/world/level/block/entity/BlockEntityType;Lnet/minecraft/world/level/block/entity/BlockEntityTicker;)Lnet/minecraft/world/level/block/entity/BlockEntityTicker;
transitive-accessible class net/minecraft/server/level/ServerPlayer$RespawnPosAngle
@@ -1,6 +1,6 @@
{
"type": "cobblemon:cooking_pot_shapeless",
"category": "medicines",
"category": "misc",
"ingredients": [
{
"item": "cobblemon:exp_candy_m"
@@ -1,6 +1,6 @@
{
"type": "cobblemon:cooking_pot_shapeless",
"category": "medicines",
"category": "misc",
"ingredients": [
{
"item": "cobblemon:exp_candy_s"
@@ -1,6 +1,6 @@
{
"type": "cobblemon:cooking_pot_shapeless",
"category": "medicines",
"category": "misc",
"ingredients": [
{
"item": "cobblemon:exp_candy_xs"
@@ -1,6 +1,6 @@
{
"type": "cobblemon:cooking_pot_shapeless",
"category": "medicines",
"category": "misc",
"ingredients": [
{
"item": "cobblemon:exp_candy_l"
@@ -1,6 +1,6 @@
{
"type": "cobblemon:cooking_pot_shapeless",
"category": "medicines",
"category": "misc",
"ingredients": [
{
"item": "minecraft:sculk"
@@ -56,7 +56,7 @@ public class RecipeBookCategoriesMixin {
types.add(cobblemon$createCategory("COBBLEMON_COOKING_POT_FOODS", $VALUES.length + 1, new ItemStack(CobblemonItems.LEEK_AND_POTATO_STEW)));
types.add(cobblemon$createCategory("COBBLEMON_COOKING_POT_MEDICINES", $VALUES.length + 2, new ItemStack(CobblemonItems.POTION)));
types.add(cobblemon$createCategory("COBBLEMON_COOKING_POT_COMPLEX_DISHES", $VALUES.length + 3, new ItemStack(CobblemonItems.APRIJUICE_RED)));
types.add(cobblemon$createCategory("COBBLEMON_COOKING_POT_MISC", $VALUES.length + 4, new ItemStack(CobblemonItems.PROTEIN)));
types.add(cobblemon$createCategory("COBBLEMON_COOKING_POT_MISC", $VALUES.length + 4, new ItemStack(CobblemonItems.HEALTH_CANDY)));
$VALUES = types.toArray(RecipeBookCategories[]::new);
}
@@ -11,27 +11,22 @@ package com.cobblemon.mod.fabric.mixin;
import com.cobblemon.mod.common.platform.events.PlatformEvents;
import com.cobblemon.mod.common.platform.events.ServerPlayerTickEvent;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.player.Player;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(Player.class)
public class PlayerMixin {
@Mixin(ServerPlayer.class)
public class ServerPlayerMixin {
@Inject(method = "tick", at = @At("HEAD"))
private void onPreTick(CallbackInfo ci) {
Player player = (Player)(Object)this;
if (player instanceof ServerPlayer) {
PlatformEvents.SERVER_PLAYER_TICK_PRE.emit(new ServerPlayerTickEvent.Pre((ServerPlayer)player));
}
ServerPlayer player = (ServerPlayer)(Object)this;
PlatformEvents.SERVER_PLAYER_TICK_PRE.emit(new ServerPlayerTickEvent.Pre(player));
}
@Inject(method = "tick", at = @At("TAIL"))
private void onPostTick(CallbackInfo ci) {
Player player = (Player)(Object)this;
if (player instanceof ServerPlayer) {
PlatformEvents.SERVER_PLAYER_TICK_POST.emit(new ServerPlayerTickEvent.Post((ServerPlayer)player));
}
ServerPlayer player = (ServerPlayer)(Object)this;
PlatformEvents.SERVER_PLAYER_TICK_POST.emit(new ServerPlayerTickEvent.Post(player));
}
}
@@ -37,6 +37,7 @@ import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback
import net.fabricmc.fabric.api.entity.event.v1.EntitySleepEvents
import net.fabricmc.fabric.api.entity.event.v1.ServerEntityWorldChangeEvents
import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents
import net.fabricmc.fabric.api.entity.event.v1.ServerPlayerEvents
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents
import net.fabricmc.fabric.api.event.player.UseBlockCallback
@@ -175,6 +176,7 @@ object CobblemonFabric : CobblemonImplementation {
ServerPlayConnectionEvents.DISCONNECT.register { handler, server ->
if (!server.isStopped) server.executeIfPossible { PlatformEvents.SERVER_PLAYER_LOGOUT.post(ServerPlayerEvent.Logout(handler.player)) }
}
ServerPlayerEvents.AFTER_RESPAWN.register { oldPlayer, newPlayer, alive -> PlatformEvents.PLAYER_RESPAWN.post(ServerPlayerEvent.Respawn(newPlayer)) }
ServerLivingEntityEvents.ALLOW_DEATH.register { entity, _, _ ->
if (entity is ServerPlayer) {
PlatformEvents.PLAYER_DEATH.postThen(
@@ -15,7 +15,7 @@
"EntityMixin",
"FabricBrewingStandMenuMixin",
"FabricSoundEngineMixin",
"PlayerMixin",
"ServerPlayerMixin",
"RecipeBookSettingsMixin",
"RecipeBookTypeMixin",
"ServerEntityMixin"
@@ -34,5 +34,5 @@ object CobblemonEnumExtensions {
Supplier<List<ItemStack>> { listOf(ItemStack(CobblemonItems.APRIJUICE_RED)) as List<ItemStack> })
@JvmField
val COOKING_POT_MISC = EnumProxy(RecipeBookCategories::class.java,
Supplier<List<ItemStack>> { listOf(ItemStack(CobblemonItems.PROTEIN)) as List<ItemStack> })
Supplier<List<ItemStack>> { listOf(ItemStack(CobblemonItems.HEALTH_CANDY)) as List<ItemStack> })
}
@@ -24,7 +24,7 @@ import net.neoforged.neoforge.event.server.ServerAboutToStartEvent
import net.neoforged.neoforge.event.server.ServerStartedEvent
import net.neoforged.neoforge.event.server.ServerStoppedEvent
import net.neoforged.neoforge.event.server.ServerStoppingEvent
import net.neoforged.neoforge.event.tick.PlayerTickEvent
import net.neoforged.neoforge.event.tick.EntityTickEvent
object NeoForgePlatformEventHandler {
@@ -53,14 +53,14 @@ object NeoForgePlatformEventHandler {
}
@SubscribeEvent
fun prePlayerTick(e: PlayerTickEvent.Pre) {
fun prePlayerTick(e: EntityTickEvent.Pre) {
if (e.entity is ServerPlayer) {
PlatformEvents.SERVER_PLAYER_TICK_PRE.post(ServerPlayerTickEvent.Pre(e.entity as ServerPlayer))
}
}
@SubscribeEvent
fun postPlayerTick(e: PlayerTickEvent.Post) {
fun postPlayerTick(e: EntityTickEvent.Post) {
if (e.entity is ServerPlayer) {
PlatformEvents.SERVER_PLAYER_TICK_POST.post(ServerPlayerTickEvent.Post(e.entity as ServerPlayer))
}
@@ -98,6 +98,12 @@ object NeoForgePlatformEventHandler {
)
}
@SubscribeEvent
fun onRespawn(e: PlayerEvent.PlayerRespawnEvent) {
val player = e.entity as? ServerPlayer ?: return
PlatformEvents.PLAYER_RESPAWN.post(ServerPlayerEvent.Respawn(player))
}
@SubscribeEvent
fun onRightClickBlock(e: PlayerInteractEvent.RightClickBlock) {
val player = e.entity as? ServerPlayer ?: return