Compare commits

...
519 changed files with 42462 additions and 330 deletions
+7
View File
@@ -32,6 +32,7 @@ repositories {
maven(url = "https://api.modrinth.com/maven")
maven(url = "https://maven.neoforged.net/releases")
maven(url = "https://maven.gegy.dev")
mavenCentral()
mavenLocal()
}
@@ -40,6 +41,7 @@ dependencies {
modImplementation(libs.fabric.loader)
modApi(libs.molang)
// Integrations
compileOnlyApi(libs.jei.api)
modCompileOnly(libs.bundles.common.integrations.compileOnly) {
@@ -62,14 +64,19 @@ dependencies {
// Unit Testing
testImplementation(libs.bundles.unitTesting)
testImplementation(libs.graal.sdk)
testRuntimeOnly(libs.bundles.graal)
}
tasks.withType<Test> {
useJUnitPlatform()
// Static GL mocks must not load a graphics driver on headless CI.
systemProperty("org.lwjgl.opengl.explicitInit", "true")
testLogging {
setEvents(listOf("failed"))
setExceptionFormat("full")
}
System.getProperty("camera.benchmark")?.let { systemProperty("camera.benchmark", it) }
}
tasks.processResources {
@@ -0,0 +1,153 @@
/*
* 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.camera;
import com.cobblemon.mod.common.client.render.camera.CameraFeedHandler;
import com.cobblemon.mod.common.client.util.compat.SodiumCompat;
import com.cobblemon.mod.common.platform.events.ClientChunkEvent;
import com.cobblemon.mod.common.platform.events.PlatformEvents;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.core.SectionPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.protocol.game.ClientboundLevelChunkPacketData;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LightLayer;
import net.minecraft.world.level.lighting.LevelLightEngine;
import net.minecraft.world.level.chunk.LevelChunk;
import java.util.ArrayList;
import java.util.function.Consumer;
public final class CameraClientChunkCacheExtension {
private static final Long2ObjectOpenHashMap<LevelChunk> CHUNKS = new Long2ObjectOpenHashMap<>();
private CameraClientChunkCacheExtension() {}
public static boolean shouldKeepChunk(ChunkPos chunkPos) {
int renderDistance = Minecraft.getInstance().options.renderDistance().get();
return CameraChunkVisibilityManager.shouldKeepChunk(chunkPos, renderDistance)
|| CameraFeedHandler.shouldAddChunk(chunkPos, renderDistance);
}
public static void retain(LevelChunk chunk) {
synchronized (CHUNKS) {
CHUNKS.put(chunk.getPos().toLong(), chunk);
}
}
public static LevelChunk take(ClientLevel level, int x, int z) {
synchronized (CHUNKS) {
long position = ChunkPos.asLong(x, z);
LevelChunk chunk = CHUNKS.get(position);
return chunk != null && chunk.getLevel() == level ? CHUNKS.remove(position) : null;
}
}
public static void drop(ClientLevel level, ChunkPos chunkPos) {
LevelChunk chunk = take(level, chunkPos.x, chunkPos.z);
if (chunk != null) {
unload(level, chunk);
}
}
public static LevelChunk getChunk(int x, int z) {
synchronized (CHUNKS) {
return CHUNKS.get(ChunkPos.asLong(x, z));
}
}
public static LevelChunk replaceWithPacketData(ClientLevel level, int x, int z, FriendlyByteBuf packetData, CompoundTag chunkTag, Consumer<ClientboundLevelChunkPacketData.BlockEntityTagOutput> tagOutput) {
if (!Minecraft.getInstance().isSameThread()) {
throw new IllegalStateException("Camera chunk packet applied off the client thread");
}
ChunkPos chunkPos = new ChunkPos(x, z);
LevelChunk chunk = getChunk(x, z);
if (chunk == null || chunk.getLevel() != level) {
chunk = new LevelChunk(level, chunkPos);
}
chunk.replaceWithPacketData(packetData, chunkTag, tagOutput);
retain(chunk);
level.onChunkLoaded(chunkPos);
SodiumCompat.getInstance().notifyChunkAdded(level, x, z);
PlatformEvents.CLIENT_CHUNK_LOAD.postEvents(new ClientChunkEvent.Load(chunk, level));
return chunk;
}
public static void prune(ClientLevel level) {
for (LevelChunk chunk : snapshot()) {
if (chunk.getLevel() == level && !shouldKeepChunk(chunk.getPos())) {
try {
drop(level, chunk.getPos());
} finally {
queueLightRemoval(level, chunk.getPos());
}
}
}
}
public static void clear() {
ArrayList<LevelChunk> removed;
synchronized (CHUNKS) {
removed = new ArrayList<>(CHUNKS.values());
CHUNKS.clear();
}
RuntimeException failure = null;
for (LevelChunk chunk : removed) {
ClientLevel level = (ClientLevel) chunk.getLevel();
try {
try {
unload(level, chunk);
} finally {
queueLightRemoval(level, chunk.getPos());
}
} catch (RuntimeException exception) {
if (failure == null) failure = exception;
else if (failure != exception) failure.addSuppressed(exception);
}
}
if (failure != null) throw failure;
}
public static ArrayList<LevelChunk> snapshot() {
synchronized (CHUNKS) {
return new ArrayList<>(CHUNKS.values());
}
}
private static void queueLightRemoval(ClientLevel level, ChunkPos position) {
level.queueLightUpdate(() -> {
LevelLightEngine lightEngine = level.getLightEngine();
lightEngine.setLightEnabled(position, false);
for (int y = lightEngine.getMinLightSection(); y < lightEngine.getMaxLightSection(); y++) {
SectionPos section = SectionPos.of(position, y);
lightEngine.queueSectionData(LightLayer.BLOCK, section, null);
lightEngine.queueSectionData(LightLayer.SKY, section, null);
}
for (int y = level.getMinSection(); y < level.getMaxSection(); y++) {
lightEngine.updateSectionStatus(SectionPos.of(position, y), true);
}
});
}
public static void unload(ClientLevel level, LevelChunk chunk) {
ChunkPos position = chunk.getPos();
try {
SodiumCompat.getInstance().notifyChunkRemoved(level, position.x, position.z);
} finally {
try {
PlatformEvents.CLIENT_CHUNK_UNLOAD.postEvents(new ClientChunkEvent.Unload(chunk, level));
} finally {
level.unload(chunk);
}
}
}
}
@@ -0,0 +1,155 @@
/*
* 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.render.camera;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import it.unimi.dsi.fastutil.longs.LongSet;
import net.minecraft.client.renderer.chunk.SectionRenderDispatcher;
import net.minecraft.core.BlockPos;
import net.minecraft.core.SectionPos;
import net.minecraft.world.level.Level;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.UUID;
public final class CameraViewAreaExtension {
private static final Long2ObjectOpenHashMap<SectionRenderDispatcher.RenderSection> SECTIONS = new Long2ObjectOpenHashMap<>();
private static final Map<UUID, LongSet> OWNERS = new HashMap<>();
private static SectionRenderDispatcher sectionRenderDispatcher;
private static final AtomicLong REVISION = new AtomicLong();
private static final AtomicLong GRAPH_REVISION = new AtomicLong();
private static final Set<SectionRenderDispatcher.RenderSection> CAMERA_SECTIONS = Collections.synchronizedSet(Collections.newSetFromMap(new WeakHashMap<>()));
private static int minSectionY;
private static int maxSectionY;
private CameraViewAreaExtension() {
}
public static void allChanged(SectionRenderDispatcher dispatcher, Level level) {
clear();
sectionRenderDispatcher = dispatcher;
minSectionY = level.getMinSection();
maxSectionY = level.getMaxSection();
}
public static SectionRenderDispatcher.RenderSection rawFetch(int cx, int cy, int cz, boolean generateNew) {
if (sectionRenderDispatcher == null || cy < minSectionY || cy >= maxSectionY) {
return null;
}
long sectionPos = SectionPos.asLong(cx, cy, cz);
return generateNew ? provideSection(sectionPos) : SECTIONS.get(sectionPos);
}
public static void setDirty(int cx, int cy, int cz, boolean playerChanged) {
SectionRenderDispatcher.RenderSection section = rawFetch(cx, cy, cz, false);
if (section != null) {
section.setDirty(playerChanged);
}
}
public static void onChunkUnload(int sectionX, int sectionZ) {
REVISION.incrementAndGet();
Collection<SectionRenderDispatcher.RenderSection> removed = new ArrayList<>();
for (int sectionY = minSectionY; sectionY < maxSectionY; sectionY++) {
long sectionPos = SectionPos.asLong(sectionX, sectionY, sectionZ);
SectionRenderDispatcher.RenderSection section = SECTIONS.remove(sectionPos);
if (section != null) {
removed.add(section);
}
}
releaseBuffers(removed);
}
public static void clear() {
REVISION.incrementAndGet();
sectionRenderDispatcher = null;
minSectionY = 0;
maxSectionY = 0;
OWNERS.clear();
Collection<SectionRenderDispatcher.RenderSection> removed = new ArrayList<>(SECTIONS.values());
SECTIONS.clear();
releaseBuffers(removed);
}
public static long revision() {
return REVISION.get();
}
public static long graphRevision() {
return GRAPH_REVISION.get();
}
public static boolean onSectionCompiled(SectionRenderDispatcher.RenderSection section) {
// Late camera uploads must not enter the player's graph after their buffers have been released.
if (!CAMERA_SECTIONS.contains(section)) return false;
GRAPH_REVISION.incrementAndGet();
return true;
}
public static void retainSections(UUID cameraId, Collection<SectionRenderDispatcher.RenderSection> sections) {
LongSet positions = new LongOpenHashSet();
for (SectionRenderDispatcher.RenderSection section : sections) {
positions.add(SectionPos.asLong(section.getOrigin()));
}
LongSet previous = OWNERS.put(cameraId, positions);
releaseUnownedSections(previous);
}
public static void releaseSections(UUID cameraId) {
releaseUnownedSections(OWNERS.remove(cameraId));
}
private static void releaseUnownedSections(LongSet positions) {
if (positions == null) return;
Collection<SectionRenderDispatcher.RenderSection> removed = new ArrayList<>();
for (long position : positions) {
if (OWNERS.values().stream().anyMatch(owner -> owner.contains(position))) continue;
SectionRenderDispatcher.RenderSection section = SECTIONS.remove(position);
if (section != null) removed.add(section);
}
releaseBuffers(removed);
}
private static void releaseBuffers(Collection<SectionRenderDispatcher.RenderSection> sections) {
RuntimeException failure = null;
for (SectionRenderDispatcher.RenderSection section : sections) {
try {
section.releaseBuffers();
} catch (RuntimeException exception) {
if (failure == null) failure = exception;
else if (failure != exception) failure.addSuppressed(exception);
}
}
if (failure != null) throw failure;
}
private static SectionRenderDispatcher.RenderSection provideSection(long sectionPos) {
return SECTIONS.computeIfAbsent(sectionPos, CameraViewAreaExtension::createSection);
}
private static SectionRenderDispatcher.RenderSection createSection(long sectionPos) {
if (sectionRenderDispatcher == null) {
throw new IllegalStateException("SectionRenderDispatcher not initialised");
}
BlockPos origin = SectionPos.of(sectionPos).origin();
SectionRenderDispatcher.RenderSection section = sectionRenderDispatcher.new RenderSection(0, origin.getX(), origin.getY(), origin.getZ());
CAMERA_SECTIONS.add(section);
return section;
}
}
@@ -0,0 +1,14 @@
/*
* 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.render.camera.accessor;
public interface CameraFeedRenderState {
boolean cobblemonRenderHand();
boolean cobblemonRenderBlockOutline();
}
@@ -8,6 +8,8 @@
package com.cobblemon.mod.common.mixin;
import com.cobblemon.mod.common.api.cinematic.runtime.CinematicManager;
import net.minecraft.world.damagesource.DamageSource;
import com.cobblemon.mod.common.CobblemonNetwork;
import com.cobblemon.mod.common.OrientationControllable;
import com.cobblemon.mod.common.api.orientation.OrientationController;
@@ -132,6 +134,16 @@ public abstract class LivingEntityMixin extends Entity implements OrientationCon
}
}
@Inject(method = "hurt(Lnet/minecraft/world/damagesource/DamageSource;F)Z", at = @At("HEAD"), cancellable = true)
private void cobblemon$cancelDamageDuringCinematicTakeover(DamageSource source, float amount, CallbackInfoReturnable<Boolean> cir) {
final LivingEntity self = (LivingEntity) (Object) this;
if (self instanceof ServerPlayer serverPlayer) {
if (CinematicManager.INSTANCE.isPlayerUnderImmuneTakeover(serverPlayer.getUUID())) {
cir.setReturnValue(false);
}
}
}
@Inject(method = "decreaseAirSupply", at = @At("HEAD"), cancellable = true)
private void cobblemon$conditionalWaterBreathing(
int currentAir,
@@ -11,9 +11,12 @@ package com.cobblemon.mod.common.mixin.accessor;
import net.minecraft.world.entity.Entity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
import org.spongepowered.asm.mixin.gen.Invoker;
@Mixin(Entity.class)
public interface AccessorEntity {
@Invoker("updateFluidOnEyes")
void cobblemonUpdateFluidOnEyes();
@Accessor("eyeHeight")
void standingEyeHeight(float standingEyeHeight);
@@ -0,0 +1,19 @@
/*
* 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.mixin.accessor;
import net.minecraft.client.Camera;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(Camera.class)
public interface CameraAccessor {
@Accessor("detached")
void setDetached(boolean detached);
}
@@ -0,0 +1,41 @@
/*
* 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.mixin.accessor;
import net.minecraft.client.multiplayer.ClientChunkCache;
import net.minecraft.world.level.chunk.LevelChunk;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
import org.spongepowered.asm.mixin.gen.Invoker;
import java.util.concurrent.atomic.AtomicReferenceArray;
@Mixin(ClientChunkCache.Storage.class)
public interface CameraChunkStorageAccessor {
@Accessor("chunks")
AtomicReferenceArray<LevelChunk> cobblemon$getChunks();
@Accessor("chunkRadius")
int cobblemon$getChunkRadius();
@Accessor("chunkCount")
int cobblemon$getChunkCount();
@Accessor("chunkCount")
void cobblemon$setChunkCount(int count);
@Accessor("viewCenterX")
int cobblemon$getViewCenterX();
@Accessor("viewCenterZ")
int cobblemon$getViewCenterZ();
@Invoker("getIndex")
int cobblemon$getIndex(int x, int z);
}
@@ -0,0 +1,20 @@
/*
* 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.mixin.accessor;
import net.minecraft.server.level.ServerEntity;
import net.minecraft.world.entity.Entity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(ServerEntity.class)
public interface ServerEntityEntityAccessor {
@Accessor("entity")
Entity getEntity();
}
@@ -9,10 +9,13 @@
package com.cobblemon.mod.common.mixin.accessor;
import net.minecraft.server.level.ServerEntity;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.network.ServerPlayerConnection;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
import org.spongepowered.asm.mixin.gen.Invoker;
import java.util.List;
import java.util.Set;
@Mixin(targets = "net.minecraft.server.level.ChunkMap$TrackedEntity")
@@ -22,4 +25,7 @@ public interface TrackedEntityAccessor {
@Accessor("serverEntity")
ServerEntity getServerEntity();
}
@Invoker("updatePlayers")
void cobblemon$updatePlayers(List<ServerPlayer> players);
}
@@ -0,0 +1,47 @@
/*
* 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.mixin.cinematic;
import com.cobblemon.mod.common.api.cinematic.runtime.CinematicViewerScopedEntity;
import com.cobblemon.mod.common.mixin.accessor.ServerEntityEntityAccessor;
import com.cobblemon.mod.common.mixin.accessor.TrackedEntityAccessor;
import net.minecraft.server.level.ServerEntity;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity;
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;
import java.util.Set;
import java.util.UUID;
@Mixin(targets = "net.minecraft.server.level.ChunkMap$TrackedEntity")
public abstract class TrackedEntityMixin {
// Cancellation must also unpair existing viewers, or they keep receiving updates after the scope narrows.
@Inject(method = "updatePlayer", at = @At("HEAD"), cancellable = true)
private void cobblemon$filterViewerScopedEntity(ServerPlayer player, CallbackInfo ci) {
final TrackedEntityAccessor self = (TrackedEntityAccessor) this;
final ServerEntity serverEntity = self.getServerEntity();
if (serverEntity == null) return;
final Entity entity = ((ServerEntityEntityAccessor) serverEntity).getEntity();
if (entity == null) return;
if (entity instanceof CinematicViewerScopedEntity scoped) {
final Set<UUID> allowed = scoped.getCobblemonSceneViewers();
if (allowed != null && !allowed.contains(player.getUUID())) {
if (self.getSeenBy().remove(player.connection)) {
serverEntity.removePairing(player);
}
ci.cancel();
}
}
}
}
@@ -0,0 +1,36 @@
/*
* 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.mixin.client;
import com.cobblemon.mod.common.client.camera.CameraClientChunkCacheExtension;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.multiplayer.ClientPacketListener;
import net.minecraft.network.protocol.game.ClientboundForgetLevelChunkPacket;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.chunk.LevelChunk;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(ClientPacketListener.class)
public abstract class CameraClientPacketListenerMixin {
@Shadow
private ClientLevel level;
@Inject(method = "queueLightRemoval", at = @At("HEAD"), cancellable = true)
private void cobblemon$keepCameraLight(ClientboundForgetLevelChunkPacket packet, CallbackInfo ci) {
ChunkPos position = packet.pos();
LevelChunk retained = CameraClientChunkCacheExtension.getChunk(position.x, position.z);
if (retained != null && retained.getLevel() == level) {
ci.cancel();
}
}
}
@@ -0,0 +1,127 @@
/*
* 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.mixin.client;
import com.cobblemon.mod.common.client.camera.CameraClientChunkCacheExtension;
import com.cobblemon.mod.common.client.render.camera.CameraFeedHandler;
import com.cobblemon.mod.common.client.render.camera.CameraViewAreaExtension;
import com.cobblemon.mod.common.client.util.compat.SodiumCompat;
import com.llamalad7.mixinextras.injector.v2.WrapWithCondition;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import net.minecraft.client.Camera;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.world.entity.Entity;
import net.minecraft.client.renderer.LevelRenderer;
import net.minecraft.client.renderer.ViewArea;
import net.minecraft.client.renderer.chunk.SectionRenderDispatcher;
import net.minecraft.client.renderer.culling.Frustum;
import net.minecraft.core.BlockPos;
import net.minecraft.core.SectionPos;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.ModifyVariable;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(value = LevelRenderer.class, priority = 1100)
public abstract class CameraLevelRendererMixin {
private static final int CINEMATIC_RENDER_DISTANCE_CHUNKS = 32;
private static final float CINEMATIC_FOG_DISTANCE = CINEMATIC_RENDER_DISTANCE_CHUNKS * 16;
@Shadow
private SectionRenderDispatcher sectionRenderDispatcher;
@Shadow
private ClientLevel level;
@WrapWithCondition(method = "setupRender", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/ViewArea;repositionCamera(DD)V"))
private boolean cobblemon$shouldRepositionCamera(ViewArea viewArea, double x, double z) {
return !CameraFeedHandler.isCapturing();
}
// Sodium owns its setup pipeline even while vanilla feed sections are substituted.
@Inject(method = "setupRender", at = @At("HEAD"), cancellable = true)
private void cobblemon$onSetupRender(Camera camera, Frustum frustum, boolean hasCapturedFrustum, boolean isSpectator, CallbackInfo ci) {
if (CameraFeedHandler.isCapturing() && !SodiumCompat.getInstance().isActive()) {
CameraFeedHandler.prepareVisibleSections(frustum);
ci.cancel();
}
}
@Inject(method = "setLevel", at = @At("HEAD"))
private void cobblemon$onSetLevel(ClientLevel level, CallbackInfo ci) {
try {
if (this.level != level) {
CameraClientChunkCacheExtension.clear();
}
} finally {
CameraViewAreaExtension.clear();
}
}
@Inject(method = "allChanged", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/SectionOcclusionGraph;waitAndReset(Lnet/minecraft/client/renderer/ViewArea;)V"))
private void cobblemon$onAllChanged(CallbackInfo ci) {
CameraViewAreaExtension.allChanged(sectionRenderDispatcher, level);
}
@Inject(method = "addRecentlyCompiledSection", at = @At("HEAD"), cancellable = true)
private void cobblemon$onSectionCompiled(SectionRenderDispatcher.RenderSection section, CallbackInfo ci) {
if (CameraViewAreaExtension.onSectionCompiled(section)) {
ci.cancel();
}
}
@Inject(method = "isSectionCompiled", at = @At("HEAD"), cancellable = true)
private void cobblemon$onIsSectionCompiled(BlockPos pos, CallbackInfoReturnable<Boolean> cir) {
if (CameraFeedHandler.isCapturing()) {
SectionPos sectionPos = SectionPos.of(pos);
SectionRenderDispatcher.RenderSection section = CameraViewAreaExtension.rawFetch(sectionPos.x(), sectionPos.y(), sectionPos.z(), false);
if (section != null && section.getCompiled() != SectionRenderDispatcher.CompiledSection.UNCOMPILED) {
cir.setReturnValue(true);
}
}
}
@ModifyVariable(method = "renderLevel", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/FogRenderer;setupColor(Lnet/minecraft/client/Camera;FLnet/minecraft/client/multiplayer/ClientLevel;IF)V"), ordinal = 1)
private float cobblemon$modifyFogRenderDistance(float original) {
if (CameraFeedHandler.isCapturing())
return CINEMATIC_FOG_DISTANCE;
return original;
}
// A Marker camera otherwise excludes the local player from vanilla entity rendering.
@WrapOperation(
method = "renderLevel",
at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Camera;getEntity()Lnet/minecraft/world/entity/Entity;")
)
private Entity cobblemon$getCameraEntityDuringCapture(Camera camera, Operation<Entity> original) {
if (CameraFeedHandler.isCapturing()) {
Entity player = Minecraft.getInstance().player;
if (player != null) return player;
}
return original.call(camera);
}
@WrapOperation(
method = "renderLevel",
at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Camera;isDetached()Z")
)
private boolean cobblemon$isCameraDetachedDuringCapture(Camera camera, Operation<Boolean> original) {
if (CameraFeedHandler.isCapturing()) {
return true;
}
return original.call(camera);
}
}
@@ -8,8 +8,12 @@
package com.cobblemon.mod.common.mixin.client;
import com.cobblemon.mod.common.api.camera.CameraRotation;
import com.cobblemon.mod.common.api.orientation.OrientationController;
import com.cobblemon.mod.common.api.riding.Rideable;
import com.cobblemon.mod.common.client.MountedPokemonAnimationRenderController;
import com.cobblemon.mod.common.client.camera.ClientCameraController;
import com.cobblemon.mod.common.client.render.camera.CameraFeedHandler;
import com.cobblemon.mod.common.client.render.camera.MountedCameraRenderer;
import com.cobblemon.mod.common.duck.CameraDuck;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
@@ -19,12 +23,15 @@ import net.minecraft.client.Camera;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.phys.Vec3;
import java.util.UUID;
import org.joml.Quaternionf;
import org.joml.Vector3f;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(Camera.class)
public abstract class CameraMixin implements CameraDuck {
@@ -32,6 +39,7 @@ public abstract class CameraMixin implements CameraDuck {
@Shadow private float eyeHeightOld;
@Shadow private BlockGetter level;
@Shadow private Entity entity;
@Final @Shadow private Quaternionf rotation;
@Final @Shadow private Vector3f forwards;
@Final @Shadow private Vector3f up;
@Final @Shadow private Vector3f left;
@@ -43,6 +51,7 @@ public abstract class CameraMixin implements CameraDuck {
@Override public BlockGetter cobblemon$getLevel() {
return level;
}
@Shadow protected abstract void setRotation(float yRot, float xRot);
@Override public Entity cobblemon$getEntity() {
return entity;
@@ -86,4 +95,29 @@ public abstract class CameraMixin implements CameraDuck {
MountedCameraRenderer.INSTANCE.setSmoothRotation(null);
original.call(instance, x, y, z);
}
// Apply after vanilla third-person and sleeping offsets so authored transforms remain exact.
@Inject(method = "setup", at = @At("RETURN"))
private void cobblemon$applyCinematicTransform(BlockGetter level, Entity entity, boolean detached, boolean thirdPersonReverse, float partialTick, CallbackInfo ci) {
UUID capturedId = CameraFeedHandler.getCurrentlyCapturedCamera();
CameraFeedHandler.FeedContext context = capturedId != null ? CameraFeedHandler.getFeedContext(capturedId) : null;
float roll;
if (context != null) {
setPosition(context.getPosition());
setRotation(context.getYaw(), context.getPitch());
roll = context.getRoll();
} else {
if (!ClientCameraController.shouldUseCinematicCamera()) return;
ClientCameraController.ActiveTransform transform = ClientCameraController.getCameraTransform();
if (transform == null) return;
CameraRotation rotation = transform.getRotation();
setPosition(transform.getPosition());
setRotation(rotation.getYaw(), rotation.getPitch());
roll = transform.getRoll();
}
this.rotation.rotateZ((float) Math.toRadians(roll));
OrientationController.Companion.getFORWARDS().rotate(this.rotation, this.forwards);
OrientationController.Companion.getUP().rotate(this.rotation, this.up);
OrientationController.Companion.getLEFT().rotate(this.rotation, this.left);
}
}
@@ -0,0 +1,178 @@
/*
* 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.mixin.client;
import com.cobblemon.mod.common.client.camera.CameraClientChunkCacheExtension;
import com.cobblemon.mod.common.mixin.accessor.CameraChunkStorageAccessor;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import net.minecraft.client.multiplayer.ClientChunkCache;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.protocol.game.ClientboundLevelChunkPacketData;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
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.concurrent.atomic.AtomicReferenceArray;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
@Mixin(value = ClientChunkCache.class, priority = 1100)
public abstract class ClientChunkCacheMixin {
@Shadow
volatile ClientChunkCache.Storage storage;
@Shadow
@Final
ClientLevel level;
@Shadow
@Final
private LevelChunk emptyChunk;
@Inject(method = "drop", at = @At("HEAD"), cancellable = true)
private void cobblemon$onDrop(ChunkPos chunkPos, CallbackInfo ci) {
if (CameraClientChunkCacheExtension.shouldKeepChunk(chunkPos)) {
CameraChunkStorageAccessor access = (CameraChunkStorageAccessor) (Object) storage;
int index = access.cobblemon$getIndex(chunkPos.x, chunkPos.z);
LevelChunk chunk = access.cobblemon$getChunks().get(index);
if (chunk != null && chunk.getPos().equals(chunkPos)) {
cobblemon$retain(access, index, chunk);
}
ci.cancel();
} else {
CameraClientChunkCacheExtension.drop(level, chunkPos);
}
}
@Inject(method = "updateViewCenter", at = @At("HEAD"))
private void cobblemon$onViewCenter(int x, int z, CallbackInfo ci) {
CameraChunkStorageAccessor access = (CameraChunkStorageAccessor) (Object) storage;
cobblemon$retainOutsideRange(access, x, z, access.cobblemon$getChunkRadius());
}
@Inject(method = "updateViewRadius", at = @At("HEAD"))
private void cobblemon$onViewRadius(int radius, CallbackInfo ci) {
CameraChunkStorageAccessor access = (CameraChunkStorageAccessor) (Object) storage;
cobblemon$retainOutsideRange(access, access.cobblemon$getViewCenterX(), access.cobblemon$getViewCenterZ(), Math.max(2, radius) + 3);
}
@Inject(method = "updateViewCenter", at = @At("TAIL"))
private void cobblemon$afterViewCenter(int x, int z, CallbackInfo ci) {
cobblemon$promoteCameraChunks((CameraChunkStorageAccessor) (Object) storage);
}
@Inject(method = "updateViewRadius", at = @At("TAIL"))
private void cobblemon$afterViewRadius(int radius, CallbackInfo ci) {
cobblemon$promoteCameraChunks((CameraChunkStorageAccessor) (Object) storage);
}
@Inject(method = "tick", at = @At("HEAD"))
private void cobblemon$onTick(BooleanSupplier hasTimeLeft, boolean tickChunks, CallbackInfo ci) {
CameraClientChunkCacheExtension.prune(level);
}
@Inject(method = "replaceWithPacketData", at = @At("HEAD"), cancellable = true)
private void cobblemon$onReplaceWithPacketData(int x, int z, FriendlyByteBuf buffer, CompoundTag chunkTag, Consumer<ClientboundLevelChunkPacketData.BlockEntityTagOutput> tagOutputConsumer, CallbackInfoReturnable<LevelChunk> cir) {
if (!storage.inRange(x, z)) {
if (CameraClientChunkCacheExtension.shouldKeepChunk(new ChunkPos(x, z))) {
cir.setReturnValue(CameraClientChunkCacheExtension.replaceWithPacketData(level, x, z, buffer, chunkTag, tagOutputConsumer));
}
return;
}
CameraChunkStorageAccessor access = (CameraChunkStorageAccessor) (Object) storage;
int index = access.cobblemon$getIndex(x, z);
LevelChunk previous = access.cobblemon$getChunks().get(index);
if (previous != null && (previous.getPos().x != x || previous.getPos().z != z)
&& CameraClientChunkCacheExtension.shouldKeepChunk(previous.getPos())) {
cobblemon$retain(access, index, previous);
}
}
@WrapOperation(method = "replaceWithPacketData", at = @At(value = "NEW", target = "(Lnet/minecraft/world/level/Level;Lnet/minecraft/world/level/ChunkPos;)Lnet/minecraft/world/level/chunk/LevelChunk;"))
private LevelChunk cobblemon$reuseCameraChunk(Level level, ChunkPos position, Operation<LevelChunk> original) {
LevelChunk retained = CameraClientChunkCacheExtension.take(this.level, position.x, position.z);
return retained != null ? retained : original.call(level, position);
}
@Inject(method = "replaceBiomes", at = @At("HEAD"), cancellable = true)
private void cobblemon$onReplaceBiomes(int x, int z, FriendlyByteBuf buffer, CallbackInfo ci) {
LevelChunk chunk = CameraClientChunkCacheExtension.getChunk(x, z);
if (chunk != null && chunk.getLevel() == level) {
chunk.replaceBiomes(buffer);
ci.cancel();
}
}
@Inject(method = "getChunk(IILnet/minecraft/world/level/chunk/status/ChunkStatus;Z)Lnet/minecraft/world/level/chunk/LevelChunk;", at = @At("RETURN"), cancellable = true)
private void cobblemon$onGetChunk(int x, int z, ChunkStatus chunkStatus, boolean requireChunk, CallbackInfoReturnable<LevelChunk> cir) {
if (cir.getReturnValue() == null || cir.getReturnValue() == emptyChunk) {
LevelChunk chunk = CameraClientChunkCacheExtension.getChunk(x, z);
if (chunk != null && chunk.getLevel() == level) {
cir.setReturnValue(chunk);
}
}
}
@Unique
private void cobblemon$promoteCameraChunks(CameraChunkStorageAccessor access) {
AtomicReferenceArray<LevelChunk> chunks = access.cobblemon$getChunks();
for (LevelChunk chunk : CameraClientChunkCacheExtension.snapshot()) {
ChunkPos position = chunk.getPos();
if (chunk.getLevel() != level
|| Math.abs(position.x - access.cobblemon$getViewCenterX()) > access.cobblemon$getChunkRadius()
|| Math.abs(position.z - access.cobblemon$getViewCenterZ()) > access.cobblemon$getChunkRadius()) continue;
int index = access.cobblemon$getIndex(position.x, position.z);
LevelChunk previous = chunks.get(index);
if (previous != null && CameraClientChunkCacheExtension.shouldKeepChunk(previous.getPos())) {
cobblemon$retain(access, index, previous);
previous = null;
}
CameraClientChunkCacheExtension.take(level, position.x, position.z);
chunks.set(index, chunk);
if (previous == null) {
access.cobblemon$setChunkCount(access.cobblemon$getChunkCount() + 1);
} else {
CameraClientChunkCacheExtension.unload(level, previous);
}
}
}
@Unique
private void cobblemon$retainOutsideRange(CameraChunkStorageAccessor access, int centerX, int centerZ, int radius) {
AtomicReferenceArray<LevelChunk> chunks = access.cobblemon$getChunks();
for (int index = 0; index < chunks.length(); index++) {
LevelChunk chunk = chunks.get(index);
if (chunk == null) continue;
ChunkPos position = chunk.getPos();
if ((Math.abs(position.x - centerX) > radius || Math.abs(position.z - centerZ) > radius)
&& CameraClientChunkCacheExtension.shouldKeepChunk(position)) {
cobblemon$retain(access, index, chunk);
}
}
}
@Unique
private void cobblemon$retain(CameraChunkStorageAccessor access, int index, LevelChunk chunk) {
// Detach before loader unload callbacks: lighting and block entities belong to this same LevelChunk.
if (access.cobblemon$getChunks().compareAndSet(index, chunk, null)) {
access.cobblemon$setChunkCount(access.cobblemon$getChunkCount() - 1);
CameraClientChunkCacheExtension.retain(chunk);
}
}
}
@@ -9,43 +9,142 @@
package com.cobblemon.mod.common.mixin.client;
import com.cobblemon.mod.common.api.scheduling.ClientTaskTracker;
import com.cobblemon.mod.common.client.camera.GameCamera;
import com.cobblemon.mod.common.client.camera.ClientCameraController;
import com.cobblemon.mod.common.client.cinematic.ClientSceneSessionManager;
import com.cobblemon.mod.common.client.render.camera.accessor.CameraFeedRenderState;
import com.cobblemon.mod.common.client.render.camera.CameraFeedHandler;
import com.cobblemon.mod.common.client.render.camera.compositor.CompositorManager;
import net.minecraft.client.Camera;
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.llamalad7.mixinextras.sugar.Local;
import net.minecraft.client.DeltaTracker;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.GameRenderer;
import net.minecraft.world.entity.Entity;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Mutable;
import org.spongepowered.asm.mixin.Shadow;
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.ModifyArg;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.util.function.Predicate;
import java.util.UUID;
@Mixin(GameRenderer.class)
public class GameRendererMixin {
@Unique
long lastTime = -1;
public abstract class GameRendererMixin implements CameraFeedRenderState {
@Inject(
method = "render",
at = @At(value = "TAIL")
)
public void render(DeltaTracker counter, boolean tick, CallbackInfo ci) {
@Shadow private boolean renderHand;
@Shadow private boolean renderBlockOutline;
@Override public boolean cobblemonRenderHand() { return renderHand; }
@Override public boolean cobblemonRenderBlockOutline() { return renderBlockOutline; }
@Shadow @Final @Mutable
private Camera mainCamera;
@Unique
private Camera cobblemon$playerCamera;
@Unique
private final GameCamera cobblemon$gameCamera = new GameCamera();
@Unique
private boolean cobblemon$usingCinematic;
@Unique
private long cobblemon$lastTime = -1;
@Inject(method = "render", at = @At("HEAD"))
private void cobblemon$swapCamera(DeltaTracker counter, boolean tick, CallbackInfo ci) {
Minecraft client = Minecraft.getInstance();
long newTime = System.currentTimeMillis();
// Don't play scheduled animations when the game is paused
if (client.isPaused()) {
lastTime = newTime;
if (client.level == null) {
cobblemon$restoreCamera();
return;
}
if (lastTime != -1) {
ClientTaskTracker.INSTANCE.update((newTime - lastTime) / 1000F);
ClientCameraController.advanceFrame(client.isPaused() ? 0f : counter.getRealtimeDeltaTicks() / 20f);
ClientSceneSessionManager.INSTANCE.getAllSessions().forEach(session -> session.refreshCompositor());
if (ClientCameraController.shouldUseCinematicCamera()) {
if (!cobblemon$usingCinematic) {
cobblemon$playerCamera = mainCamera;
mainCamera = cobblemon$gameCamera;
cobblemon$usingCinematic = true;
}
cobblemon$gameCamera.prepare(client, counter.getGameTimeDeltaPartialTick(tick));
} else if (cobblemon$usingCinematic) {
cobblemon$restoreCamera();
}
lastTime = newTime;
}
// WrapOperation allows Iris/Sodium to share this call site; Redirect claims it exclusively.
@WrapOperation(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/GameRenderer;renderLevel(Lnet/minecraft/client/DeltaTracker;)V"))
private void cobblemon$skipVanillaRenderLevel(GameRenderer instance, DeltaTracker deltaTracker, Operation<Void> original) {
if (!CompositorManager.isFullscreenActive()) {
original.call(instance, deltaTracker);
}
}
@Inject(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/GameRenderer;tryTakeScreenshotIfNeeded()V"))
private void cobblemon$afterLevelRendering(DeltaTracker deltaTracker, boolean renderLevel, CallbackInfo ci) {
boolean compositorNeedsFeeds = CompositorManager.isFullscreenActive()
|| !CompositorManager.INSTANCE.getRequiredFeeds().isEmpty();
if (compositorNeedsFeeds && CameraFeedHandler.hasFeeds()) {
CameraFeedHandler.captureFeeds(deltaTracker);
}
if (CompositorManager.isFullscreenActive()) {
CompositorManager.INSTANCE.compositeAll();
CompositorManager.renderFullscreen();
}
}
@Inject(method = "pick", at = @At("HEAD"), cancellable = true)
private void cobblemon$preventFramePick(float partialTicks, CallbackInfo ci) {
if (CameraFeedHandler.isCapturing())
ci.cancel();
}
@Inject(method = "getFov", at = @At("HEAD"), cancellable = true)
private void cobblemon$overrideCinematicFov(Camera camera, float partialTicks, boolean useFovSetting, CallbackInfoReturnable<Double> cir) {
UUID capturedId = CameraFeedHandler.getCurrentlyCapturedCamera();
Float fov;
if (capturedId != null) {
CameraFeedHandler.FeedContext context = CameraFeedHandler.getFeedContext(capturedId);
fov = context != null ? context.getFov() : null;
} else {
if (camera != cobblemon$gameCamera || !ClientCameraController.shouldUseCinematicCamera()) return;
fov = ClientCameraController.getPrimaryFovOverride();
}
if (fov != null) {
cir.setReturnValue(fov.doubleValue());
}
}
@Inject(method = "render", at = @At(value = "TAIL"))
private void cobblemon$afterRender(DeltaTracker counter, boolean tick, CallbackInfo ci) {
Minecraft client = Minecraft.getInstance();
long newTime = System.currentTimeMillis();
if (client.isPaused()) {
cobblemon$lastTime = newTime;
return;
}
if (cobblemon$lastTime != -1) {
float delta = (newTime - cobblemon$lastTime) / 1000F;
ClientTaskTracker.INSTANCE.update(delta);
}
cobblemon$lastTime = newTime;
}
@ModifyArg(
@@ -60,4 +159,12 @@ public class GameRendererMixin {
return (entityX) -> !(entity.getVehicle() == entityX && entityX instanceof PokemonEntity) && original.test(entityX);
}
@Unique
private void cobblemon$restoreCamera() {
if (cobblemon$usingCinematic) {
mainCamera = cobblemon$playerCamera;
cobblemon$playerCamera = null;
cobblemon$usingCinematic = false;
}
}
}
@@ -9,6 +9,7 @@
package com.cobblemon.mod.common.mixin.client;
import com.cobblemon.mod.common.OrientationControllable;
import com.cobblemon.mod.common.client.camera.ClientCameraController;
import com.cobblemon.mod.common.item.PokedexItem;
import com.mojang.blaze3d.vertex.PoseStack;
import net.minecraft.client.Minecraft;
@@ -28,6 +29,11 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
public class ItemInHandRendererMixin {
@Inject(method = "renderArmWithItem", at = @At(value = "HEAD"), cancellable = true)
private void cobblemon$renderArmWithItem(AbstractClientPlayer abstractClientPlayer, float f, float g, InteractionHand interactionHand, float h, ItemStack itemStack, float i, PoseStack poseStack, MultiBufferSource multiBufferSource, int j, CallbackInfo ci) {
if (ClientCameraController.shouldUseCinematicCamera()) {
ci.cancel();
return;
}
if (abstractClientPlayer.isUsingItem() && abstractClientPlayer.getUseItem().getItem() instanceof PokedexItem) {
ci.cancel();
}
@@ -8,11 +8,14 @@
package com.cobblemon.mod.common.mixin.client;
import com.cobblemon.mod.common.client.camera.ClientCameraController;
import com.cobblemon.mod.common.client.render.ClientPlayerIcon;
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.mojang.blaze3d.vertex.PoseStack;
import net.minecraft.client.CameraType;
import net.minecraft.client.Minecraft;
import net.minecraft.client.model.PlayerModel;
import net.minecraft.client.player.AbstractClientPlayer;
import net.minecraft.client.renderer.MultiBufferSource;
@@ -31,11 +34,34 @@ public abstract class PlayerRendererMixin extends LivingEntityRenderer<AbstractC
super(context, model, shadowRadius);
}
@WrapOperation(
method = "render(Lnet/minecraft/client/player/AbstractClientPlayer;FFLcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/MultiBufferSource;I)V",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/client/CameraType;isFirstPerson()Z"
)
)
private boolean cobblemon$allowBodyRenderInCinematic(CameraType instance, Operation<Boolean> original) {
if (ClientCameraController.shouldUseCinematicCamera() && ClientCameraController.shouldRenderPlayerBody()) {
return false;
}
return original.call(instance);
}
@Inject(method = "render", at = @At(value = "TAIL"))
public void renderPlayer(AbstractClientPlayer player, float f, float g, PoseStack poseStack, MultiBufferSource multiBufferSource, int i, CallbackInfo ci) {
ClientPlayerIcon.Companion.onRenderPlayer(player);
}
@Inject(method = "render(Lnet/minecraft/client/player/AbstractClientPlayer;FFLcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/MultiBufferSource;I)V", at = @At("HEAD"), cancellable = true)
private void cobblemon$hideCinematicPlayer(AbstractClientPlayer player, float yaw, float partialTick, PoseStack poseStack, MultiBufferSource buffers, int light, CallbackInfo ci) {
if (player != Minecraft.getInstance().player) return;
if (ClientCameraController.shouldUseCinematicCamera() && !ClientCameraController.shouldRenderPlayerBody()) {
ci.cancel();
return;
}
}
@WrapOperation(method = "getRenderOffset(Lnet/minecraft/client/player/AbstractClientPlayer;F)Lnet/minecraft/world/phys/Vec3;", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/player/AbstractClientPlayer;isCrouching()Z"))
private boolean cobblemon$disableCrouchOffset(AbstractClientPlayer instance, Operation<Boolean> original) {
if (instance.isPassenger() && instance.getVehicle() instanceof PokemonEntity) {
@@ -0,0 +1,30 @@
/*
* 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.mixin.client;
import com.cobblemon.mod.common.client.render.RenderAlphaScope;
import net.minecraft.client.renderer.RenderStateShard;
import net.minecraft.client.renderer.RenderType;
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(RenderStateShard.class)
public abstract class RenderStateShardMixin {
@Inject(method = "setupRenderState", at = @At("RETURN"))
private void cobblemon$applyScopedAlpha(CallbackInfo ci) {
if ((Object) this instanceof RenderType) RenderAlphaScope.afterMaterialSetup();
}
@Inject(method = "clearRenderState", at = @At("RETURN"))
private void cobblemon$restoreScopedAlpha(CallbackInfo ci) {
if ((Object) this instanceof RenderType) RenderAlphaScope.afterMaterialClear();
}
}
@@ -0,0 +1,24 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.mixin.client;
import com.cobblemon.mod.common.client.render.camera.CameraViewAreaExtension;
import net.minecraft.client.renderer.ViewArea;
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(ViewArea.class)
public class ViewAreaMixin {
@Inject(method = "setDirty", at = @At("HEAD"))
private void cobblemon$markCinematicDirty(int cx, int cy, int cz, boolean reRenderOnMainThread, CallbackInfo ci) {
CameraViewAreaExtension.setDirty(cx, cy, cz, reRenderOnMainThread);
}
}
@@ -9,6 +9,8 @@
package com.cobblemon.mod.common.mixin.entity;
import com.cobblemon.mod.common.OrientationControllable;
import com.cobblemon.mod.common.api.cinematic.runtime.CinematicViewerScopedEntity;
import com.cobblemon.mod.common.api.cinematic.runtime.pathfinding.NativeMotionEntity;
import com.cobblemon.mod.common.duck.RidePassenger;
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity;
import com.cobblemon.mod.common.pokemon.Pokemon;
@@ -25,6 +27,8 @@ import net.minecraft.util.Mth;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.Vec3;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.Mixin;
@@ -35,11 +39,35 @@ 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.Set;
import java.util.UUID;
@Mixin(Entity.class)
public abstract class EntityMixin {
public abstract class EntityMixin implements CinematicViewerScopedEntity {
@Unique
@Nullable
private Set<UUID> cobblemon$sceneViewers = null;
@Nullable
@Override
public Set<UUID> getCobblemonSceneViewers() {
return cobblemon$sceneViewers;
}
@Override
public void setCobblemonSceneViewers(@Nullable Set<UUID> viewers) {
this.cobblemon$sceneViewers = viewers;
}
@Shadow @Nullable public abstract Entity getVehicle();
@WrapOperation(method = "move", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/block/Block;stepOn(Lnet/minecraft/world/level/Level;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/world/entity/Entity;)V"))
private void cobblemon$skipSimulatedFootsteps(Block block, Level level, BlockPos pos, BlockState state,
Entity entity, Operation<Void> original) {
if (!(entity instanceof NativeMotionEntity)) original.call(block, level, pos, state, entity);
}
@Inject(method = "updateInWaterStateAndDoWaterCurrentPushing", at = @At(value = "FIELD", target = "Lnet/minecraft/world/entity/Entity;wasTouchingWater:Z", ordinal = 1), cancellable = true)
public void cobblemon$verifyActuallyTouchingWater(CallbackInfo ci) {
if(this.getVehicle() instanceof PokemonEntity) {
@@ -8,6 +8,11 @@
package com.cobblemon.mod.common
import com.cobblemon.mod.common.api.cinematic.registry.SceneBuiltinSources
import com.cobblemon.mod.common.net.serverhandling.editor.EditorPreviewSessionRegistry
import com.cobblemon.mod.common.api.cinematic.runtime.CinematicManager
import com.cobblemon.mod.common.api.molangui.MolangUiService
import com.cobblemon.mod.common.CobblemonBuildDetails.smallCommitHash
import com.cobblemon.mod.common.api.Priority
import com.cobblemon.mod.common.api.SeasonResolver
@@ -76,6 +81,8 @@ import com.cobblemon.mod.common.battles.BattleSide
import com.cobblemon.mod.common.battles.ShowdownThread
import com.cobblemon.mod.common.battles.actor.PokemonBattleActor
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.battles.stadium.StadiumBattleRegistry
import com.cobblemon.mod.common.battles.stadium.StadiumSpectating
import com.cobblemon.mod.common.block.dispenser.DispenserBehaviorRegistry
import com.cobblemon.mod.common.client.CobblemonPack
import com.cobblemon.mod.common.command.argument.*
@@ -259,6 +266,9 @@ object Cobblemon {
playerDataManager.onPlayerDisconnect(it.player)
RequestManager.onLogoff(it.player)
serverPlayerStructs.remove(it.player.uuid)
MolangUiService.closeAllForPlayer(it.player.uuid)
CinematicManager.onPlayerDisconnect(it.player.uuid)
EditorPreviewSessionRegistry.onPlayerDisconnect(it.player.uuid)
}
PlatformEvents.PLAYER_DEATH.subscribe {
PCLinkManager.removeLink(it.player.uuid)
@@ -301,6 +311,7 @@ object Cobblemon {
HeldItemProvider.register(CobblemonHeldItemManager, Priority.LOWEST)
}
fun sendServerSettingsPacketToPlayer(player: ServerPlayer) {
ServerSettingsPacket(
this.config.preventCompletePartyDeposit,
@@ -353,6 +364,8 @@ object Cobblemon {
CustomPokemonProperty.register(NoAIProperty)
CustomPokemonProperty.register(HeldItemVisibleProperty)
SceneBuiltinSources.register()
CobblemonEvents.POKEMON_PROPERTY_INITIALISED.emit(Unit)
CallbackHandler.setup()
@@ -449,7 +462,14 @@ object Cobblemon {
)
}
PlatformEvents.SERVER_STOPPING.subscribe {
StadiumSpectating.clear()
// Release temporary actor AI/gravity settings before the final world save.
CinematicManager.clear()
}
PlatformEvents.SERVER_STOPPED.subscribe {
StadiumBattleRegistry.clearDynamic()
MolangUiService.closeAll()
storage.unregisterAll(it.server.registryAccess())
playerDataManager.saveAllStores()
playerDataManager.saveExecutor.shutdown()
@@ -109,4 +109,10 @@ object CobblemonBlockEntities : PlatformRegistry<Registry<BlockEntityType<*>>, R
"poke_snack",
BlockEntityType.Builder.of(::PokeSnackBlockEntity, CobblemonBlocks.POKE_SNACK, CobblemonBlocks.POKE_CAKE).build(null)
)
@JvmField
val STADIUM_BLOCK: BlockEntityType<StadiumBlockEntity> = this.create(
"stadium_block",
BlockEntityType.Builder.of(::StadiumBlockEntity, CobblemonBlocks.STADIUM_BLOCK).build(null)
)
}
@@ -682,6 +682,9 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<
@JvmField
val HEALING_MACHINE = create("healing_machine", HealingMachineBlock(BlockBehaviour.Properties.of().mapColor(MapColor.METAL).sound(SoundType.METAL).pushReaction(PushReaction.BLOCK).strength(2F).noOcclusion().lightLevel { if (it.getValue(HealingMachineBlock.CHARGE_LEVEL) >= HealingMachineBlock.MAX_CHARGE_LEVEL) 12 else 5 }))
@JvmField
val STADIUM_BLOCK = create("stadium_block", StadiumBlock(BlockBehaviour.Properties.of().mapColor(MapColor.METAL).sound(SoundType.METAL).strength(3.5F, 6.0F).noOcclusion().pushReaction(PushReaction.BLOCK)))
@JvmField
val PC = create("pc", PCBlock(BlockBehaviour.Properties.of().mapColor(MapColor.METAL).sound(SoundType.METAL).pushReaction(PushReaction.BLOCK).strength(2F).noOcclusion().lightLevel { if ((it.getValue(PCBlock.ON) as Boolean) && (it.getValue(PCBlock.PART) == PCBlock.PCPart.TOP)) 13 else 0 }))
@@ -15,7 +15,11 @@ import net.minecraft.commands.CommandSourceStack
import net.minecraft.commands.Commands
object CobblemonCommands {
fun register(dispatcher: CommandDispatcher<CommandSourceStack>, registry: CommandBuildContext, selection: Commands.CommandSelection) {
fun register(
dispatcher: CommandDispatcher<CommandSourceStack>,
registry: CommandBuildContext,
selection: Commands.CommandSelection
) {
SpawnPokemon.register(dispatcher)
SpawnNPCCommand.register(dispatcher)
GivePokemon.register(dispatcher)
@@ -61,7 +65,6 @@ object CobblemonCommands {
ChangeBoxWallpaperCommand.register(dispatcher)
RunMolangCommand.register(dispatcher)
RunMolangScriptCommand.register(dispatcher)
SpectateBattleCommand.register(dispatcher)
CobblemonConfigCommand.register(dispatcher)
UnlockPCBoxWallpaperCommand.register(dispatcher)
BehaviourEditCommand.register(dispatcher)
@@ -77,5 +80,8 @@ 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)
CinematicCommand.register(dispatcher)
StadiumBattleCommand.register(dispatcher)
MolangUiCommand.register(dispatcher)
}
}
@@ -13,6 +13,7 @@ import com.cobblemon.mod.common.entity.boat.CobblemonBoatEntity
import com.cobblemon.mod.common.entity.boat.CobblemonChestBoatEntity
import com.cobblemon.mod.common.entity.fishing.PokeRodFishingBobberEntity
import com.cobblemon.mod.common.entity.generic.GenericBedrockEntity
import com.cobblemon.mod.common.entity.stadium.StadiumCameraDroneEntity
import com.cobblemon.mod.common.entity.npc.NPCEntity
import com.cobblemon.mod.common.entity.pokeball.EmptyPokeBallEntity
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
@@ -81,6 +82,12 @@ object CobblemonEntities : PlatformRegistry<Registry<EntityType<*>>, ResourceKey
@JvmField
val GENERIC_BEDROCK_ENTITY_KEY = cobblemonResource("generic_bedrock")
@JvmField
val STADIUM_CAMERA_DRONE: EntityType<StadiumCameraDroneEntity> = this.create(
"stadium_camera_drone",
EntityType.Builder.of(::StadiumCameraDroneEntity, MobCategory.MISC)
.build(cobblemonResource("stadium_camera_drone").toString())
)
@JvmField
val GENERIC_BEDROCK_ENTITY: EntityType<GenericBedrockEntity> = this.create(
GENERIC_BEDROCK_ENTITY_KEY.path,
EntityType.Builder.of({ _, world -> GenericBedrockEntity(world) }, MobCategory.MISC)
@@ -99,4 +106,4 @@ object CobblemonEntities : PlatformRegistry<Registry<EntityType<*>>, ResourceKey
consumer(POKEMON, PokemonEntity.createAttributes())
consumer(NPC, NPCEntity.createAttributes())
}
}
}
@@ -38,6 +38,7 @@ import com.cobblemon.mod.common.item.berry.StatusCuringBerryItem
import com.cobblemon.mod.common.item.food.SinisterTeaItem
import com.cobblemon.mod.common.item.interactive.*
import com.cobblemon.mod.common.item.interactive.ability.AbilityChangeItem
import com.cobblemon.mod.common.item.stadium.StadiumPlacementWandItem
import com.cobblemon.mod.common.platform.PlatformRegistry
import com.cobblemon.mod.common.pokeball.PokeBall
import com.cobblemon.mod.common.pokemon.IVs
@@ -74,6 +75,9 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val NPC_EDITOR = create("npc_editor", CobblemonItem(Item.Properties().stacksTo(1)))
@JvmField
val STADIUM_PLACEMENT_WAND = create("stadium_placement_wand", StadiumPlacementWandItem())
@JvmField
val pokeBalls = mutableListOf<PokeBallItem>()
@JvmField
@@ -465,6 +469,8 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
val PASTURE = blockItem("pasture", CobblemonBlocks.PASTURE)
@JvmField
val DISPLAY_CASE = blockItem("display_case", CobblemonBlocks.DISPLAY_CASE)
@JvmField
val STADIUM_BLOCK = blockItem("stadium_block", CobblemonBlocks.STADIUM_BLOCK)
// @JvmField
// val INCENSE_SWEET = blockItem("incense_sweet", CobblemonBlocks.INCENSE_SWEET) // TODO after 1.7
@@ -9,6 +9,7 @@
package com.cobblemon.mod.common
import com.cobblemon.mod.common.api.molangui.container.MolangContainerMenu
import com.cobblemon.mod.common.block.campfirepot.CookingPotMenu
import com.cobblemon.mod.common.block.tmmachine.TMMachineMenu
import com.cobblemon.mod.common.platform.PlatformRegistry
@@ -16,15 +17,17 @@ import net.minecraft.core.Registry
import net.minecraft.core.registries.BuiltInRegistries
import net.minecraft.core.registries.Registries
import net.minecraft.resources.ResourceKey
import net.minecraft.world.flag.FeatureFlags
import net.minecraft.world.inventory.MenuType
object CobblemonMenuType : PlatformRegistry<Registry<MenuType<*>>, ResourceKey<Registry<MenuType<*>>>, MenuType<*>>() {
val COOKING_POT = MenuType.register("cooking_pot_menu", ::CookingPotMenu)
val TM_MACHINE = MenuType.register("tm_machine_menu", ::TMMachineMenu)
val MOLANG_UI = create("molang_ui", MenuType(::MolangContainerMenu, FeatureFlags.DEFAULT_FLAGS))
override val registry: Registry<MenuType<*>>
get() = BuiltInRegistries.MENU
override val resourceKey: ResourceKey<Registry<MenuType<*>>>
get() = Registries.MENU
}
}
@@ -17,6 +17,20 @@ import com.cobblemon.mod.common.client.net.battle.*
import com.cobblemon.mod.common.client.net.callback.move.OpenMoveCallbackHandler
import com.cobblemon.mod.common.client.net.callback.party.OpenPartyCallbackHandler
import com.cobblemon.mod.common.client.net.callback.partymove.OpenPartyMoveCallbackHandler
import com.cobblemon.mod.common.client.net.cinematic.BeatChangeHandler
import com.cobblemon.mod.common.client.net.cinematic.FeedAssignmentHandler
import com.cobblemon.mod.common.client.net.cinematic.FeedTransformHandler
import com.cobblemon.mod.common.client.net.cinematic.SceneEndHandler
import com.cobblemon.mod.common.client.net.cinematic.SceneStartHandler
import com.cobblemon.mod.common.client.net.editor.EditorPreviewAckHandler
import com.cobblemon.mod.common.client.net.editor.EditorPreviewStateHandler
import com.cobblemon.mod.common.client.net.editor.EditorRouteResultHandler
import com.cobblemon.mod.common.net.messages.client.editor.EditorRouteResultS2CPacket
import com.cobblemon.mod.common.net.messages.server.editor.EditorRouteRequestC2SPacket
import com.cobblemon.mod.common.net.serverhandling.editor.EditorRouteRequestHandler
import com.cobblemon.mod.common.net.messages.client.editor.EditorPreviewStateS2CPacket
import com.cobblemon.mod.common.net.messages.server.editor.EditorPreviewControlC2SPacket
import com.cobblemon.mod.common.net.serverhandling.editor.EditorPreviewControlHandler
import com.cobblemon.mod.common.client.net.cooking.ToggleCookingPotLidHandler
import com.cobblemon.mod.common.client.net.data.CobblemonMechanicsSyncHandler
import com.cobblemon.mod.common.client.net.data.DataRegistrySyncPacketHandler
@@ -32,6 +46,10 @@ import com.cobblemon.mod.common.client.net.effect.SpawnSnowstormParticleHandler
import com.cobblemon.mod.common.client.net.gui.ExpGainedDataPacketHandler
import com.cobblemon.mod.common.client.net.gui.InteractPokemonUIPacketHandler
import com.cobblemon.mod.common.client.net.gui.PokedexUIPacketHandler
import com.cobblemon.mod.common.client.net.molangui.MolangUiCloseHandler
import com.cobblemon.mod.common.client.net.molangui.MolangUiCompatibilityRequestHandler
import com.cobblemon.mod.common.client.net.molangui.MolangUiOpenHandler
import com.cobblemon.mod.common.client.net.molangui.MolangUiUpdateHandler
import com.cobblemon.mod.common.client.net.gui.SetActiveTMPacketHandler
import com.cobblemon.mod.common.client.net.gui.SetTMMachineContainerDataPacketHandler
import com.cobblemon.mod.common.client.net.gui.SummaryUIPacketHandler
@@ -53,6 +71,16 @@ import com.cobblemon.mod.common.client.net.riding.ClientboundUpdateDriverInputHa
import com.cobblemon.mod.common.client.net.settings.OpenCobblemonConfigEditorHandler
import com.cobblemon.mod.common.client.net.settings.ServerSettingsPacketHandler
import com.cobblemon.mod.common.client.net.sound.UnvalidatedPlaySoundS2CPacketHandler
import com.cobblemon.mod.common.client.net.stadium.OpenStadiumConfigHandler
import com.cobblemon.mod.common.client.net.stadium.StadiumConfigResultHandler
import com.cobblemon.mod.common.client.net.stadium.StadiumSpectateStateHandler
import com.cobblemon.mod.common.net.messages.client.stadium.StadiumSpectateStatePacket
import com.cobblemon.mod.common.net.messages.server.stadium.StadiumSpectatePacket
import com.cobblemon.mod.common.net.serverhandling.stadium.StadiumSpectateHandler
import com.cobblemon.mod.common.client.net.stadium.StadiumCinematicHandler
import com.cobblemon.mod.common.net.messages.client.stadium.StadiumCinematicPacket
import com.cobblemon.mod.common.net.messages.server.stadium.EditStadiumCinematicPacket
import com.cobblemon.mod.common.net.serverhandling.stadium.EditStadiumCinematicHandler
import com.cobblemon.mod.common.client.net.spawn.SpawnExtraDataEntityHandler
import com.cobblemon.mod.common.client.net.starter.StarterUIPacketHandler
import com.cobblemon.mod.common.client.net.storage.RemoveClientPokemonHandler
@@ -72,17 +100,30 @@ 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.serverhandling.stadium.RequestStadiumPlacementWandHandler
import com.cobblemon.mod.common.net.serverhandling.stadium.UpdateStadiumBlockHandler
import com.cobblemon.mod.common.net.messages.client.CalculateSeatPositionsPacket
import com.cobblemon.mod.common.net.messages.client.OpenBehaviourEditorPacket
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
import com.cobblemon.mod.common.net.messages.client.battle.*
import com.cobblemon.mod.common.net.messages.client.cinematic.BeatChangePacket
import com.cobblemon.mod.common.net.messages.client.cinematic.FeedAssignmentPacket
import com.cobblemon.mod.common.net.messages.client.cinematic.FeedTransformPacket
import com.cobblemon.mod.common.net.messages.client.cinematic.SceneEndPacket
import com.cobblemon.mod.common.net.messages.client.cinematic.SceneStartPacket
import com.cobblemon.mod.common.net.messages.client.editor.EditorPreviewAckS2CPacket
import com.cobblemon.mod.common.net.messages.client.callback.OpenMoveCallbackPacket
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.cooking.SeasoningRegistrySyncPacket
import com.cobblemon.mod.common.net.messages.client.cooking.ToggleCookingPotLidPacket
import com.cobblemon.mod.common.net.messages.client.data.*
import com.cobblemon.mod.common.net.messages.client.molangui.MolangUiClosePacket
import com.cobblemon.mod.common.net.messages.client.molangui.MolangUiCompatibilityRequestPacket
import com.cobblemon.mod.common.net.messages.client.molangui.MolangUiOpenPacket
import com.cobblemon.mod.common.net.messages.client.molangui.MolangUiUpdatePacket
import com.cobblemon.mod.common.net.messages.client.debug.RequestOpenRidingStatsDebugGUIPacket
import com.cobblemon.mod.common.net.messages.client.dialogue.DialogueClosedPacket
import com.cobblemon.mod.common.net.messages.client.dialogue.DialogueOpenedPacket
@@ -108,6 +149,10 @@ import com.cobblemon.mod.common.net.messages.client.pokemon.update.evolution.Cle
import com.cobblemon.mod.common.net.messages.client.pokemon.update.evolution.ClientboundSeatAssignmentPacket
import com.cobblemon.mod.common.net.messages.client.pokemon.update.evolution.RemoveEvolutionPacket
import com.cobblemon.mod.common.net.messages.client.settings.OpenCobblemonConfigScreenPacket
import com.cobblemon.mod.common.net.messages.client.stadium.OpenStadiumConfigPacket
import com.cobblemon.mod.common.net.messages.client.stadium.StadiumConfigResultPacket
import com.cobblemon.mod.common.net.messages.server.stadium.RequestStadiumPlacementWandPacket
import com.cobblemon.mod.common.net.messages.server.stadium.UpdateStadiumBlockPacket
import com.cobblemon.mod.common.net.messages.client.settings.ServerSettingsPacket
import com.cobblemon.mod.common.net.messages.client.sound.UnvalidatedPlaySoundS2CPacket
import com.cobblemon.mod.common.net.messages.client.spawn.SpawnGenericBedrockPacket
@@ -157,6 +202,8 @@ import com.cobblemon.mod.common.net.messages.server.RequestPlayerInteractionsPac
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.StadiumPreviewChoicePacket
import com.cobblemon.mod.common.net.serverhandling.battle.StadiumPreviewChoiceHandler
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
@@ -176,10 +223,15 @@ import com.cobblemon.mod.common.net.messages.server.debug.OpenRidingStatsDebugGU
import com.cobblemon.mod.common.net.messages.server.debug.ServerboundUpdateRidingSettingsPacket
import com.cobblemon.mod.common.net.messages.server.debug.ServerboundUpdateRidingStatRangePacket
import com.cobblemon.mod.common.net.messages.server.debug.ServerboundUpdateRidingStatsPacket
import com.cobblemon.mod.common.net.messages.server.editor.EditorPreviewStartC2SPacket
import com.cobblemon.mod.common.net.messages.server.editor.EditorPreviewStopC2SPacket
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.molangui.MolangUiActionPacket
import com.cobblemon.mod.common.net.messages.server.molangui.MolangUiClosedPacket
import com.cobblemon.mod.common.net.messages.server.habitat.SaveHabitatBlockSettingsPacket
import com.cobblemon.mod.common.net.messages.server.npc.SaveNPCPacket
import com.cobblemon.mod.common.net.messages.server.molangui.MolangUiCompatibilityResponsePacket
import com.cobblemon.mod.common.net.messages.server.orientation.ServerboundUpdateOrientationPacket
import com.cobblemon.mod.common.net.messages.server.pasture.PasturePokemonPacket
import com.cobblemon.mod.common.net.messages.server.pasture.SetPastureConflictPacket
@@ -234,11 +286,16 @@ import com.cobblemon.mod.common.net.serverhandling.debug.RequestOpenRidingStatsD
import com.cobblemon.mod.common.net.serverhandling.debug.ServerboundUpdateRidingSettingsHandler
import com.cobblemon.mod.common.net.serverhandling.debug.ServerboundUpdateRidingStatRangeHandler
import com.cobblemon.mod.common.net.serverhandling.debug.ServerboundUpdateRidingStatsHandler
import com.cobblemon.mod.common.net.serverhandling.editor.EditorPreviewStartHandler
import com.cobblemon.mod.common.net.serverhandling.editor.EditorPreviewStopHandler
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.molangui.MolangUiActionHandler
import com.cobblemon.mod.common.net.serverhandling.molangui.MolangUiClosedHandler
import com.cobblemon.mod.common.net.serverhandling.evolution.AcceptEvolutionHandler
import com.cobblemon.mod.common.net.serverhandling.habitat.SaveHabitatBlockSettingsHandler
import com.cobblemon.mod.common.net.serverhandling.npc.SaveNPCHandler
import com.cobblemon.mod.common.net.serverhandling.molangui.MolangUiCompatibilityResponseHandler
import com.cobblemon.mod.common.net.serverhandling.orientation.ServerboundUpdateOrientationHandler
import com.cobblemon.mod.common.net.serverhandling.pasture.PasturePokemonHandler
import com.cobblemon.mod.common.net.serverhandling.pasture.SetPastureConflictHandler
@@ -387,6 +444,7 @@ object CobblemonNetwork {
list.add(PacketRegisterInfo(BattleEndPacket.ID, BattleEndPacket::decode, BattleEndHandler))
list.add(PacketRegisterInfo(BattleInitializePacket.ID, BattleInitializePacket::decode, BattleInitializeHandler))
list.add(PacketRegisterInfo(BattleQueueRequestPacket.ID, BattleQueueRequestPacket::decode, BattleQueueRequestHandler))
list.add(PacketRegisterInfo(StadiumBattleStatePacket.ID, StadiumBattleStatePacket::decode, StadiumBattleStateHandler))
list.add(PacketRegisterInfo(BattleFaintPacket.ID, BattleFaintPacket::decode, BattleFaintHandler))
list.add(PacketRegisterInfo(BattleMakeChoicePacket.ID, BattleMakeChoicePacket::decode, BattleMakeChoiceHandler))
list.add(PacketRegisterInfo(BattleHealthChangePacket.ID, BattleHealthChangePacket::decode, BattleHealthChangeHandler))
@@ -403,9 +461,37 @@ object CobblemonNetwork {
list.add(PacketRegisterInfo(BattlePersistentStatusPacket.ID, BattlePersistentStatusPacket::decode, BattlePersistentStatusHandler))
list.add(PacketRegisterInfo(BattleMadeInvalidChoicePacket.ID, BattleMadeInvalidChoicePacket::decode, BattleMadeInvalidChoiceHandler))
list.add(PacketRegisterInfo(BattleMusicPacket.ID, BattleMusicPacket::decode, BattleMusicHandler))
list.add(PacketRegisterInfo(BattleChallengeExpiredPacket.ID, BattleChallengeExpiredPacket::decode, BattleChallengeExpiredHandler))
list.add(PacketRegisterInfo(BattleReplacePokemonPacket.ID, BattleReplacePokemonPacket::decode, BattleReplacePokemonHandler))
list.add(PacketRegisterInfo(BattleTransformPokemonPacket.ID, BattleTransformPokemonPacket::decode, BattleTransformPokemonHandler))
list.add(
PacketRegisterInfo(
BattleChallengeExpiredPacket.ID,
BattleChallengeExpiredPacket::decode,
BattleChallengeExpiredHandler
)
)
list.add(
PacketRegisterInfo(
BattleReplacePokemonPacket.ID,
BattleReplacePokemonPacket::decode,
BattleReplacePokemonHandler
)
)
list.add(
PacketRegisterInfo(
BattleTransformPokemonPacket.ID,
BattleTransformPokemonPacket::decode,
BattleTransformPokemonHandler
)
)
list.add(PacketRegisterInfo(SceneStartPacket.ID, SceneStartPacket::decode, SceneStartHandler))
list.add(PacketRegisterInfo(SceneEndPacket.ID, SceneEndPacket::decode, SceneEndHandler))
list.add(PacketRegisterInfo(FeedTransformPacket.ID, FeedTransformPacket::decode, FeedTransformHandler))
list.add(PacketRegisterInfo(FeedAssignmentPacket.ID, FeedAssignmentPacket::decode, FeedAssignmentHandler))
list.add(PacketRegisterInfo(BeatChangePacket.ID, BeatChangePacket::decode, BeatChangeHandler))
list.add(PacketRegisterInfo(EditorPreviewAckS2CPacket.ID, EditorPreviewAckS2CPacket::decode, EditorPreviewAckHandler))
list.add(PacketRegisterInfo(EditorPreviewStateS2CPacket.ID, EditorPreviewStateS2CPacket::decode, EditorPreviewStateHandler))
list.add(PacketRegisterInfo(EditorRouteResultS2CPacket.ID, EditorRouteResultS2CPacket::decode, EditorRouteResultHandler))
// MultiBattleTeam Packets
list.add(PacketRegisterInfo(TeamRequestNotificationPacket.ID, TeamRequestNotificationPacket::decode, TeamRequestNotificationHandler))
@@ -445,6 +531,11 @@ object CobblemonNetwork {
list.add(PacketRegisterInfo(RideSettingsSyncPacket.ID, RideSettingsSyncPacket::decode, RideSettingsSyncHandler))
list.add(PacketRegisterInfo(TechnicalMachineRegistrySyncPacket.ID, TechnicalMachineRegistrySyncPacket::decode, DataRegistrySyncPacketHandler()))
list.add(PacketRegisterInfo(MolangUiOpenPacket.ID, MolangUiOpenPacket::decode, MolangUiOpenHandler))
list.add(PacketRegisterInfo(MolangUiClosePacket.ID, MolangUiClosePacket::decode, MolangUiCloseHandler))
list.add(PacketRegisterInfo(MolangUiUpdatePacket.ID, MolangUiUpdatePacket::decode, MolangUiUpdateHandler))
list.add(PacketRegisterInfo(MolangUiCompatibilityRequestPacket.ID, MolangUiCompatibilityRequestPacket::decode, MolangUiCompatibilityRequestHandler))
// Effects
list.add(PacketRegisterInfo(SpawnSnowstormParticlePacket.ID, SpawnSnowstormParticlePacket::decode, SpawnSnowstormParticleHandler))
list.add(PacketRegisterInfo(SpawnSnowstormEntityParticlePacket.ID, SpawnSnowstormEntityParticlePacket::decode, SpawnSnowstormEntityParticleHandler))
@@ -474,8 +565,25 @@ object CobblemonNetwork {
list.add(PacketRegisterInfo(OpenPasturePacket.ID, OpenPasturePacket::decode, OpenPastureHandler))
list.add(PacketRegisterInfo(ClosePasturePacket.ID, ClosePasturePacket::decode, ClosePastureHandler))
list.add(PacketRegisterInfo(PokemonPasturedPacket.ID, PokemonPasturedPacket::decode, PokemonPasturedHandler))
list.add(PacketRegisterInfo(PokemonUnpasturedPacket.ID, PokemonUnpasturedPacket::decode, PokemonUnpasturedHandler))
list.add(PacketRegisterInfo(UpdatePastureConflictFlagPacket.ID, UpdatePastureConflictFlagPacket::decode, UpdatePastureConflictFlagHandler))
list.add(
PacketRegisterInfo(
PokemonUnpasturedPacket.ID,
PokemonUnpasturedPacket::decode,
PokemonUnpasturedHandler
)
)
list.add(
PacketRegisterInfo(
UpdatePastureConflictFlagPacket.ID,
UpdatePastureConflictFlagPacket::decode,
UpdatePastureConflictFlagHandler
)
)
list.add(PacketRegisterInfo(OpenStadiumConfigPacket.ID, OpenStadiumConfigPacket::decode, OpenStadiumConfigHandler))
list.add(PacketRegisterInfo(StadiumConfigResultPacket.ID, StadiumConfigResultPacket::decode, StadiumConfigResultHandler))
list.add(PacketRegisterInfo(StadiumSpectateStatePacket.ID, StadiumSpectateStatePacket::decode, StadiumSpectateStateHandler))
list.add(PacketRegisterInfo(StadiumCinematicPacket.ID, StadiumCinematicPacket::decode, StadiumCinematicHandler))
// Orientation
list.add(PacketRegisterInfo(ClientboundUpdateOrientationPacket.ID, ClientboundUpdateOrientationPacket::decode, ClientboundUpdateOrientationHandler))
@@ -573,6 +681,7 @@ object CobblemonNetwork {
// Battle packets
list.add(PacketRegisterInfo(BattleSelectActionsPacket.ID, BattleSelectActionsPacket::decode, BattleSelectActionsHandler))
list.add(PacketRegisterInfo(StadiumPreviewChoicePacket.ID, StadiumPreviewChoicePacket::decode, StadiumPreviewChoiceHandler))
list.add(PacketRegisterInfo(SpectateBattlePacket.ID, SpectateBattlePacket::decode, SpectateBattleHandler))
list.add(PacketRegisterInfo(RemoveSpectatorPacket.ID, RemoveSpectatorPacket::decode, RemoveSpectatorHandler))
@@ -640,8 +749,29 @@ object CobblemonNetwork {
list.add(PacketRegisterInfo(SetTMMachineContainerDataPacket.ID, SetTMMachineContainerDataPacket::decode, SetTMMachineContainerDataPacketHandler))
// Behaviour Packets
list.add(PacketRegisterInfo(SetEntityBehaviourPacket.ID, SetEntityBehaviourPacket::decode, SetEntityBehaviourHandler))
list.add(PacketRegisterInfo(DamageOnCollisionPacket.ID, DamageOnCollisionPacket::decode, DamageOnCollisionPacketHandler))
list.add(
PacketRegisterInfo(
SetEntityBehaviourPacket.ID,
SetEntityBehaviourPacket::decode,
SetEntityBehaviourHandler
)
)
list.add(
PacketRegisterInfo(
DamageOnCollisionPacket.ID,
DamageOnCollisionPacket::decode,
DamageOnCollisionPacketHandler
)
)
list.add(PacketRegisterInfo(UpdateStadiumBlockPacket.ID, UpdateStadiumBlockPacket::decode, UpdateStadiumBlockHandler))
list.add(PacketRegisterInfo(EditStadiumCinematicPacket.ID, EditStadiumCinematicPacket::decode, EditStadiumCinematicHandler))
list.add(PacketRegisterInfo(RequestStadiumPlacementWandPacket.ID, RequestStadiumPlacementWandPacket::decode, RequestStadiumPlacementWandHandler))
list.add(PacketRegisterInfo(StadiumSpectatePacket.ID, StadiumSpectatePacket::decode, StadiumSpectateHandler))
list.add(PacketRegisterInfo(MolangUiActionPacket.ID, MolangUiActionPacket::decode, MolangUiActionHandler))
list.add(PacketRegisterInfo(MolangUiClosedPacket.ID, MolangUiClosedPacket::decode, MolangUiClosedHandler))
list.add(PacketRegisterInfo(MolangUiCompatibilityResponsePacket.ID, MolangUiCompatibilityResponsePacket::decode, MolangUiCompatibilityResponseHandler))
// Habitat blocks
list.add(PacketRegisterInfo(SaveHabitatBlockSettingsPacket.ID, SaveHabitatBlockSettingsPacket::decode, SaveHabitatBlockSettingsHandler))
@@ -649,6 +779,10 @@ object CobblemonNetwork {
// Debug / cheats
list.add(PacketRegisterInfo(RequestOpenRidingStatsDebugGUIPacket.ID, RequestOpenRidingStatsDebugGUIPacket::decode, RequestOpenRidingStatsDebugGUIHandler))
list.add(PacketRegisterInfo(EditorPreviewStartC2SPacket.ID, EditorPreviewStartC2SPacket::decode, EditorPreviewStartHandler))
list.add(PacketRegisterInfo(EditorRouteRequestC2SPacket.ID, EditorRouteRequestC2SPacket::decode, EditorRouteRequestHandler))
list.add(PacketRegisterInfo(EditorPreviewStopC2SPacket.ID, EditorPreviewStopC2SPacket::decode, EditorPreviewStopHandler))
list.add(PacketRegisterInfo(EditorPreviewControlC2SPacket.ID, EditorPreviewControlC2SPacket::decode, EditorPreviewControlHandler))
return list
}
@@ -656,4 +790,4 @@ object CobblemonNetwork {
fun sendPacketToPlayer(player: ServerPlayer, packet: NetworkPacket<*>) {
Cobblemon.implementation.networkManager.sendPacketToPlayer(player, packet)
}
}
}
@@ -35,6 +35,7 @@ import com.cobblemon.mod.common.battles.ActiveBattlePokemon
import com.cobblemon.mod.common.battles.BattleCaptureAction
import com.cobblemon.mod.common.battles.BattleFormat
import com.cobblemon.mod.common.battles.BattleRegistry
import com.cobblemon.mod.common.battles.stadium.StadiumBattleSession
import com.cobblemon.mod.common.battles.BattleSide
import com.cobblemon.mod.common.battles.FleeAttemptActionResponse
import com.cobblemon.mod.common.battles.ForfeitActionResponse
@@ -80,14 +81,18 @@ import net.minecraft.world.item.ItemStack
open class PokemonBattle(
val format: BattleFormat,
val side1: BattleSide,
val side2: BattleSide
val side2: BattleSide,
initializeProgress: Boolean = true
) {
/** Whether logging will be silenced for this battle. */
var mute = !CobblemonBuildDetails.SNAPSHOT
val struct = this.asMoLangValue()
val runtime = MoLangRuntime().also { it.environment.query = struct }
val runtime = MoLangRuntime().also {
it.environment.query = struct
}
val onEndHandlers: MutableList<(PokemonBattle) -> Unit> = mutableListOf()
var stadiumSession: StadiumBattleSession? = null
val battlePartyStores = mutableListOf<PlayerPartyStore>()
@@ -96,6 +101,12 @@ open class PokemonBattle(
side2.battle = this
this.actors.forEach { actor ->
actor.battle = this
}
if (initializeProgress) resetEvolutionProgress()
}
internal fun resetEvolutionProgress() {
actors.forEach { actor ->
actor.pokemonList.forEach { battlePokemon ->
battlePokemon.effectedPokemon.evolutionProxy.current().progress()
.filterIsInstance<LastBattleCriticalHitsEvolutionProgress>()
@@ -237,7 +248,9 @@ open class PokemonBattle(
}
fun turn(newTurnNumber: Int) {
actors.forEach { it.turn() }
// Prepare all actors before notifying synchronous AI, or its first response can clear other actors' requests.
actors.forEach { it.prepareTurn() }
actors.forEach { it.fireChoiceRequest() }
// TODO: If a pokemon switches in the same turn another pokemon is KO'd it will not receive exp for the KO
for (side in sides) {
val opposite = side.getOppositeSide()
@@ -250,6 +263,7 @@ open class PokemonBattle(
}
fun end() {
if (ended) return
ended = true
val awardToFainted = Cobblemon.config.awardExperienceToFaintedPokemon
val awardOnLoss = Cobblemon.config.awardExperienceOnBattleLoss
@@ -464,7 +478,7 @@ open class PokemonBattle(
val message = battleLang("crash").red()
this.actors.filterIsInstance<PlayerBattleActor>().forEach { it.entity?.sendSystemMessage(message) }
this.saveBattleLog()
this.stop()
if (stadiumSession != null) BattleRegistry.abortStadiumBattle(battleId) else this.stop()
return
}
@@ -472,6 +486,7 @@ open class PokemonBattle(
ticks++
if (isPvW && !ended && dispatches.isEmpty()) checkFlee()
}
stadiumSession?.tick()
}
open fun checkFlee() {
@@ -507,6 +522,7 @@ open class PokemonBattle(
}
fun stop() {
if (ended) return
writeShowdownAction(">forcetie") // This will terminate the Showdown connection
end()
}
@@ -583,4 +599,4 @@ open class PokemonBattle(
queryStruct.addFunction("has_rule") { params -> DoubleValue(params.getString(0) in format.ruleSet) }
return queryStruct
}
}
}
@@ -11,8 +11,9 @@ package com.cobblemon.mod.common.api.battles.model.actor
import com.cobblemon.mod.common.Cobblemon.LOGGER
import com.cobblemon.mod.common.api.battles.model.ai.BattleAI
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.battles.DefaultActionResponse
import com.cobblemon.mod.common.battles.ForcePassActionResponse
import com.cobblemon.mod.common.battles.PassActionResponse
import com.cobblemon.mod.common.battles.ShowdownActionResponse
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.exception.IllegalActionChoiceException
import com.cobblemon.mod.common.net.messages.client.battle.BattleFaintPacket
@@ -49,36 +50,40 @@ abstract class AIBattleActor(
* Called when the AI is requested to make a choice.
*/
open fun onChoiceRequested() {
val req = request
if (req == null) {
responses.clear()
expectingPassActions.clear()
mustChoose = false
battle.checkForInputDispatch()
return
}
val passActions = expectingPassActions.toList()
try {
request?.let {
setActionResponses(it.iterate(this.activePokemon) { battleMon, moveset, forceSwitch ->
battleAI.choose(battleMon, battle, getSide(), moveset, forceSwitch)
})
pokemonList.forEach { pokemon ->
pokemon.willBeSwitchedIn = false
}
} ?: {
val response = mutableListOf<ShowdownActionResponse>()
repeat(activePokemon.size) {
response.add(PassActionResponse)
}
setActionResponses(response)
LOGGER.warn("AI requested choice, but no request was set. Returning PassActionResponses.")
}
setActionResponses(req.iterate(this.activePokemon) { battleMon, moveset, forceSwitch ->
battleAI.choose(battleMon, battle, getSide(), moveset, forceSwitch)
})
} catch (exception: IllegalActionChoiceException) {
// Validation may have consumed forced passes and accepted a prefix before rejecting a later slot.
responses.clear()
expectingPassActions.clear()
expectingPassActions.addAll(passActions)
if (req.forceSwitch.any { it }) {
LOGGER.error("AI could not choose replacements; using Showdown's default choices", exception)
setActionResponses(req.iterate(activePokemon) { _, _, _ -> DefaultActionResponse() })
return
}
LOGGER.error("AI was unable to choose an action, we're going to need to pass!")
exception.printStackTrace()
request?.let {
setActionResponses(it.iterate(this.activePokemon) { _, _ , _->
PassActionResponse
})
} ?: {
val response = mutableListOf<ShowdownActionResponse>()
repeat(activePokemon.size) {
response.add(PassActionResponse)
}
setActionResponses(response)
}
var forcedPasses = passActions.size
setActionResponses(req.iterate(this.activePokemon) { _, moveset, forceSwitch ->
if (!forceSwitch && moveset != null && forcedPasses > 0) {
forcedPasses--
ForcePassActionResponse()
} else PassActionResponse
})
} finally {
pokemonList.forEach { it.willBeSwitchedIn = false }
}
}
}
}
@@ -24,8 +24,9 @@ import com.cobblemon.mod.common.net.messages.client.battle.BattleMakeChoicePacke
import com.cobblemon.mod.common.net.messages.client.battle.BattleMessagePacket
import net.minecraft.network.chat.Component
import net.minecraft.network.chat.MutableComponent
import java.util.UUID
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.phys.Vec3
import java.util.UUID
abstract class BattleActor(
val uuid: UUID,
@@ -52,6 +53,8 @@ abstract class BattleActor(
var itemsUsed = mutableListOf<BagItem>()
/** For when battles start, it's the number of Pokémon that are still in the process of being sent out (animation wise) */
var stillSendingOutCount = 0
/** Positions follow active slot order: slot a is index 0. */
var stadiumSendOutPositions: List<Vec3>? = null
abstract val type: ActorType
@@ -71,21 +74,31 @@ 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() {
val request = request ?: return
responses.clear()
if (activePokemon.any { it.isAlive() }) {
mustChoose = true
sendUpdate(BattleMakeChoicePacket())
}
val requestActive = request.active
if (requestActive == null || requestActive.isEmpty() || request.wait) {
this.request = null
/** Prepare every actor before notifying any AI, since synchronous choices can dispatch immediately. */
fun prepareTurn() {
mustChoose = false
val req = request ?: run {
responses.clear()
expectingPassActions.clear()
return
}
responses.clear()
if (activePokemon.any { it.isAlive() }) {
mustChoose = true
}
val requestActive = req.active
if (req.wait || (requestActive.isNullOrEmpty() && req.forceSwitch.none { it })) {
this.request = null
expectingPassActions.clear()
mustChoose = false
} else if (req.forceSwitch.any { it }) {
mustChoose = true
}
}
/** Call only after every actor has been prepared for this turn. */
fun fireChoiceRequest() {
if (mustChoose) sendUpdate(BattleMakeChoicePacket())
}
fun postUpdate() {
@@ -162,4 +175,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,15 @@
/*
* 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.api.camera
/** Minecraft yaw and pitch, in degrees. */
data class CameraRotation(
val yaw: Float,
val pitch: Float
)
@@ -0,0 +1,34 @@
/*
* 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.api.camera.model
// Air bubbles have no separate vanilla call that Fabric can cancel.
enum class HudElement(val id: String) {
CROSSHAIR("crosshair"),
HOTBAR("hotbar"),
EXPERIENCE_BAR("experience_bar"),
PLAYER_HEALTH("player_health"),
ARMOR("armor"),
FOOD("food"),
EFFECTS("effects"),
BOSS_BAR("boss_bar"),
SCOREBOARD("scoreboard"),
CHAT("chat"),
PLAYER_LIST("player_list"),
SELECTED_ITEM_NAME("selected_item_name"),
COBBLEMON_OVERLAY("cobblemon_overlay");
companion object {
val ALL: Set<HudElement> = entries.toSet()
private val BY_ID = entries.associateBy { it.id }
fun fromId(id: String): HudElement? = BY_ID[id]
}
}
@@ -0,0 +1,27 @@
/*
* 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.api.camera.model
data class ParameterDefinition(
val type: ParameterType,
val description: String? = null,
val default: ParameterDefault? = null
)
enum class ParameterType {
NUMBER,
VEC3,
STRING
}
sealed class ParameterDefault {
data class NumberDefault(val value: Double) : ParameterDefault()
data class Vec3Default(val x: Double, val y: Double, val z: Double) : ParameterDefault()
data class StringDefault(val value: String) : ParameterDefault()
}
@@ -0,0 +1,126 @@
/*
* 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.api.camera.model
import com.bedrockk.molang.runtime.MoLangRuntime
import com.bedrockk.molang.runtime.value.MoValue
import com.cobblemon.mod.common.api.molang.ExpressionLike
import com.cobblemon.mod.common.util.asExpressionLike
import com.cobblemon.mod.common.util.resolve
import com.google.gson.JsonElement
import net.minecraft.world.phys.Vec3
data class PositionExpression(
val x: CoordinateValue,
val y: CoordinateValue,
val z: CoordinateValue
) {
fun toVec3(
context: Map<String, MoValue> = emptyMap(),
runtime: MoLangRuntime,
origin: Vec3 = Vec3.ZERO,
isRelative: Boolean = false
): Vec3 {
val base = Vec3(
x.evaluate(context, runtime),
y.evaluate(context, runtime),
z.evaluate(context, runtime)
)
return if (isRelative) base.add(origin) else base
}
fun isFinite(context: Map<String, MoValue>, runtime: MoLangRuntime): Boolean {
val vec = toVec3(context, runtime)
return vec.x.isFinite() && vec.y.isFinite() && vec.z.isFinite()
}
companion object {
fun fromJson(element: JsonElement): PositionExpression {
return when {
element.isJsonObject -> {
val obj = element.asJsonObject
PositionExpression(
x = deserializeCoordinate(obj.get("x")),
y = deserializeCoordinate(obj.get("y")),
z = deserializeCoordinate(obj.get("z"))
)
}
element.isJsonArray -> {
val arr = element.asJsonArray
if (arr.size() != 3) {
throw IllegalArgumentException("Position array must have exactly 3 elements [x, y, z], got ${arr.size()}")
}
PositionExpression(
x = deserializeCoordinate(arr[0]),
y = deserializeCoordinate(arr[1]),
z = deserializeCoordinate(arr[2])
)
}
else -> {
throw IllegalArgumentException("Position must be either an object {x, y, z} or array [x, y, z]")
}
}
}
private fun deserializeCoordinate(element: JsonElement?): CoordinateValue {
if (element == null) {
return CoordinateValue.Static(0.0)
}
return when {
element.isJsonPrimitive && element.asJsonPrimitive.isNumber -> {
CoordinateValue.Static(element.asDouble)
}
element.isJsonPrimitive && element.asJsonPrimitive.isString -> {
CoordinateValue.Dynamic(element.asString.asExpressionLike())
}
element.isJsonObject -> {
val obj = element.asJsonObject
val type = obj.get("type")?.asString
val expression = obj.get("expression")?.asString
when (type) {
"expression" -> {
if (expression == null) {
throw IllegalArgumentException("Expression coordinate missing 'expression' field")
}
CoordinateValue.Dynamic(expression.asExpressionLike())
}
else -> {
throw IllegalArgumentException("Unknown coordinate type: $type")
}
}
}
else -> {
throw IllegalArgumentException("Invalid coordinate value: $element")
}
}
}
}
}
sealed class CoordinateValue {
data class Static(val value: Double) : CoordinateValue() {
init { require(value.isFinite()) { "Static coordinate must be finite" } }
}
data class Dynamic(val expression: ExpressionLike) : CoordinateValue()
fun evaluate(
context: Map<String, MoValue>,
runtime: MoLangRuntime
): Double {
return when (this) {
is Static -> value
is Dynamic -> runtime.resolve(expression, context).asDouble().also {
require(it.isFinite()) { "Dynamic coordinate must evaluate to a finite value" }
}
}
}
}
@@ -0,0 +1,21 @@
/*
* 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.api.camera.result
import net.minecraft.network.chat.Component
data class ValidationError(
val path: String,
val message: Component,
val severity: Severity = Severity.ERROR
) {
enum class Severity { WARNING, ERROR }
override fun toString(): String = "[$severity] $path: ${message.string}"
}
@@ -0,0 +1,49 @@
/*
* 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.api.cinematic.model
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.entity.Entity
sealed class ActorBinding {
abstract val kind: ActorKind
/** Visible player actors override server position and suppress movement packets. */
data class Player(
val player: ServerPlayer,
val fallbackPokemon: PokemonProperties? = null
) : ActorBinding() {
override val kind: ActorKind = ActorKind.PlayerTakeover
}
data class Real(val entity: Entity) : ActorBinding() {
override val kind: ActorKind = ActorKind.RealEntity
}
data class SynthPokemon(
val properties: PokemonProperties,
val aspects: Set<String> = emptySet()
) : ActorBinding() {
override val kind: ActorKind = ActorKind.SynthesizedPokemon
}
data class SynthBedrock(
val category: ResourceLocation,
val aspects: Set<String> = emptySet(),
val scale: Float = 1.0f
) : ActorBinding() {
override val kind: ActorKind = ActorKind.SynthesizedBedrock
init {
require(scale.isFinite() && scale > 0f) { "SynthBedrock scale must be positive, got $scale" }
}
}
}
@@ -0,0 +1,28 @@
/*
* 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.api.cinematic.model
data class ActorDeclaration(
val id: String,
val kind: ActorKind
) {
init {
require(id.isNotBlank()) { "ActorDeclaration id must be non-blank" }
}
}
sealed class ActorKind {
object PlayerTakeover : ActorKind()
object RealEntity : ActorKind()
object SynthesizedPokemon : ActorKind()
object SynthesizedBedrock : ActorKind()
}
@@ -0,0 +1,27 @@
/*
* 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.api.cinematic.model
import com.cobblemon.mod.common.api.cinematic.model.tracks.ActorTrack
/** Null visibility carries over between beats; tracks never carry over. */
data class ActorDirection(
val visible: Boolean? = null,
val startPose: StartPose? = null,
val startPoseMode: StartPoseMode? = null,
val tracks: List<ActorTrack> = emptyList()
) {
init {
if (startPose != null) {
require(visible != false) {
"ActorDirection.startPose declared but visible = false; hidden actors cannot have a start pose"
}
}
}
}
@@ -0,0 +1,33 @@
/*
* 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.api.cinematic.model
import com.cobblemon.mod.common.api.camera.model.HudElement
/** Null duration leaves the beat unbounded; feeds without cuts are inactive. */
data class Beat(
val id: String,
val duration: Float? = null,
val isLooping: Boolean = false,
val speed: Float = 1.0f,
val actorDirections: Map<String, ActorDirection> = emptyMap(),
val cuts: Map<String, CutTrack> = emptyMap(),
val guiTrack: SceneGuiTrack? = null,
val renderPlayerBody: Boolean = true,
val hiddenHudElements: Set<HudElement> = emptySet(),
val cues: List<SceneCue> = emptyList()
) {
init {
require(id.isNotBlank()) { "Beat id must be non-blank" }
require(speed.isFinite() && speed > 0f) { "Beat speed must be positive, got $speed" }
duration?.let {
require(it.isFinite() && it >= 0f) { "Beat duration must be non-negative, got $it" }
}
}
}
@@ -0,0 +1,25 @@
/*
* 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.api.cinematic.model
import com.cobblemon.mod.common.api.molang.ExpressionLike
/** Clamping at a beat's duration does not end the scene; completion requires an explicit transition. */
data class BeatTransition(
val sourceBeatId: String,
val targetBeatId: String,
val condition: ExpressionLike,
val transition: CutTransition = CutTransition.Blend(0.4f),
val endsScene: Boolean = false
) {
init {
require(sourceBeatId.isNotBlank()) { "BeatTransition sourceBeatId must be non-blank" }
require(endsScene || targetBeatId.isNotBlank()) { "BeatTransition targetBeatId must be non-blank" }
}
}
@@ -0,0 +1,13 @@
/*
* 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.api.cinematic.model
import net.minecraft.network.chat.Component
class CinematicValidationException(val reason: Component) : IllegalArgumentException(reason.string)
@@ -0,0 +1,41 @@
/*
* 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.api.cinematic.model
data class CutTrack(
val entries: List<CutEntry>
) {
init {
require(entries.isNotEmpty()) { "CutTrack must have at least one entry" }
require(entries.zipWithNext().all { (a, b) -> a.time <= b.time }) {
"CutTrack entries must be sorted ascending by time"
}
}
}
data class CutEntry(
val time: Float,
val shotId: String,
val transition: CutTransition = CutTransition.Instant
) {
init {
require(time.isFinite() && time >= 0f) { "CutEntry time must be non-negative, got $time" }
require(shotId.isNotBlank()) { "CutEntry shotId must be non-blank" }
}
}
sealed class CutTransition {
object Instant : CutTransition()
data class Blend(val duration: Float) : CutTransition() {
init {
require(duration.isFinite() && duration > 0f) { "Blend duration must be positive, got $duration" }
}
}
}
@@ -0,0 +1,16 @@
/*
* 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.api.cinematic.model
enum class DisconnectPolicy {
END_SCENE,
SUBSTITUTE,
HIDE_ACTOR,
FAIL
}
@@ -0,0 +1,48 @@
/*
* 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.api.cinematic.model
data class FeedDeclaration(
val id: String,
val kind: FeedKind
) {
init {
require(id.isNotBlank()) { "FeedDeclaration id must be non-blank" }
}
companion object {
/** Bound for feed counts read off the wire, so a crafted packet can't drive a huge allocation. */
const val MAX_FEEDS = 64
}
}
sealed class FeedKind {
/** Occupies the full viewport. */
object Fullscreen : FeedKind()
/** Picture-in-picture overlay in a viewport corner. [size] is a fraction of viewport. */
data class Pip(val corner: Corner, val size: Float) : FeedKind() {
init {
require(size > 0f && size <= 1f) { "Pip size must be in (0, 1], got $size" }
}
}
/** Relative share of a paired split. */
data class Split(val side: Side, val size: Float) : FeedKind() {
init {
require(size > 0f && size <= 1f) { "Split size must be in (0, 1], got $size" }
}
}
data class Diagonal(val half: DiagonalHalf) : FeedKind()
enum class Corner { TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT }
enum class Side { LEFT, RIGHT, TOP, BOTTOM }
enum class DiagonalHalf { UPPER, LOWER }
}
@@ -0,0 +1,23 @@
/*
* 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.api.cinematic.model
fun <T> orderedKeyframes(keyframes: List<T>, time: (T) -> Float): List<T> {
require(keyframes.isNotEmpty()) { "At least one keyframe is required" }
require(keyframes.all { time(it).isFinite() && time(it) >= 0f }) { "Keyframe times must be finite and nonnegative" }
val ordered = ArrayList<T>(keyframes.size)
for (keyframe in keyframes.sortedBy(time)) {
if (ordered.isNotEmpty() && time(ordered.last()) == time(keyframe)) {
ordered[ordered.lastIndex] = keyframe
} else {
ordered.add(keyframe)
}
}
return ordered
}
@@ -0,0 +1,231 @@
/*
* 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.api.cinematic.model
import net.minecraft.network.chat.Component
import com.cobblemon.mod.common.api.cinematic.runtime.ActorFraming
import com.cobblemon.mod.common.api.camera.model.ParameterDefinition
import com.cobblemon.mod.common.api.camera.result.ValidationError
import com.cobblemon.mod.common.api.cinematic.runtime.ActorDependencyOrder
import com.cobblemon.mod.common.api.cinematic.model.sources.builtin.OrbitPosition
import com.cobblemon.mod.common.api.cinematic.model.sources.builtin.LookAt
import com.cobblemon.mod.common.api.cinematic.model.sources.builtin.LookAtTarget
data class Scene(
val id: String,
val parameters: Map<String, ParameterDefinition> = emptyMap(),
val actors: Map<String, ActorDeclaration> = emptyMap(),
val shots: Map<String, Shot> = emptyMap(),
val feeds: List<FeedDeclaration> = emptyList(),
val startBeatId: String,
val beats: Map<String, Beat>,
val transitions: List<BeatTransition> = emptyList(),
val version: Int = 2,
val damageImmunityDuringTakeover: Boolean = false,
val disconnectPolicy: DisconnectPolicy = DisconnectPolicy.END_SCENE
) {
init {
require(id.isNotBlank()) { "Scene id must be non-blank" }
require(startBeatId.isNotBlank()) { "Scene startBeatId must be non-blank" }
require(version == 2) { "Scene.version must be 2, got $version" }
}
fun getBeat(beatId: String): Beat? = beats[beatId]
fun getShot(shotId: String): Shot? = shots[shotId]
fun getActor(actorId: String): ActorDeclaration? = actors[actorId]
fun getFeed(feedId: String): FeedDeclaration? = feeds.firstOrNull { it.id == feedId }
fun getTransitionsFrom(beatId: String): List<BeatTransition> =
transitions.filter { it.sourceBeatId == beatId }
fun validate(): List<ValidationError> {
val errors = mutableListOf<ValidationError>()
if (startBeatId !in beats) {
errors.add(ValidationError(
path = "startBeatId",
message = Component.translatable("cobblemon.cinematic_editor.validation.start_missing", startBeatId)
))
}
actors.forEach { (key, actor) ->
if (actor.id != key) {
errors.add(ValidationError(
path = "actors[$key].id",
message = Component.translatable("cobblemon.cinematic_editor.validation.actor_id", key, actor.id)
))
}
}
shots.forEach { (key, shot) ->
val framingActors = (shot.position as? OrbitPosition)?.actorIds.orEmpty() +
listOfNotNull((shot.position as? OrbitPosition)?.bearingActorId) +
((shot.rotation as? LookAt)?.target as? LookAtTarget.Actors)?.actorIds.orEmpty()
framingActors.distinct().filter { it !in actors && it !in ActorFraming.groups }.forEach { actorId ->
errors.add(ValidationError(path = "shots[$key]", message = Component.translatable("cobblemon.cinematic_editor.validation.framing_actor", actorId)))
}
if (shot.id != key) {
errors.add(ValidationError(
path = "shots[$key].id",
message = Component.translatable("cobblemon.cinematic_editor.validation.shot_id", key, shot.id)
))
}
}
val feedIds = feeds.map { it.id }
val duplicateFeedIds = feedIds.groupingBy { it }.eachCount().filter { it.value > 1 }.keys
duplicateFeedIds.forEach { duplicateId ->
errors.add(ValidationError(
path = "feeds",
message = Component.translatable("cobblemon.cinematic_editor.validation.duplicate_feed", duplicateId)
))
}
val cueIds = mutableSetOf<String>()
beats.forEach { (beatKey, beat) ->
beat.actorDirections.forEach { (actorId, direction) ->
if (direction.tracks.any { it.trackType == "pathfinding" } &&
direction.tracks.count { it.trackType in setOf("pathfinding", "spline_movement", "keyframe_movement", "follow_actor", "attach", "pose_lock") } > 1) {
errors.add(ValidationError(
path = "beats[$beatKey].actorDirections[$actorId].tracks",
message = Component.translatable("cobblemon.cinematic_editor.validation.movement_conflict")
))
}
}
beat.cues.forEachIndexed { index, cue ->
if (!cueIds.add(cue.id)) errors.add(ValidationError(
path = "beats[$beatKey].cues[$index].id", message = Component.translatable("cobblemon.cinematic_editor.validation.duplicate_cue", cue.id)))
if (beat.duration != null && cue.time > beat.duration) errors.add(ValidationError(
path = "beats[$beatKey].cues[$index].time", message = Component.translatable("cobblemon.cinematic_editor.validation.late_cue", cue.id)))
}
try {
ActorDependencyOrder.resolve(actors.keys, beat)
} catch (error: CinematicValidationException) {
errors.add(ValidationError(path = "beats[$beatKey].actorDirections", message = error.reason))
}
if (beat.id != beatKey) {
errors.add(ValidationError(
path = "beats[$beatKey].id",
message = Component.translatable("cobblemon.cinematic_editor.validation.beat_id", beatKey, beat.id)
))
}
beat.actorDirections.keys.forEach { actorRef ->
if (actorRef !in actors) {
errors.add(ValidationError(
path = "beats[$beatKey].actorDirections[$actorRef]",
message = Component.translatable("cobblemon.cinematic_editor.validation.actor_missing", actorRef)
))
}
}
beat.cuts.forEach { (feedRef, cutTrack) ->
if (feedRef !in feedIds) {
errors.add(ValidationError(
path = "beats[$beatKey].cuts[$feedRef]",
message = Component.translatable("cobblemon.cinematic_editor.validation.feed_missing", feedRef)
))
}
cutTrack.entries.forEach { entry ->
if (entry.shotId !in shots) {
errors.add(ValidationError(
path = "beats[$beatKey].cuts[$feedRef]",
message = Component.translatable("cobblemon.cinematic_editor.validation.shot_missing", entry.shotId, entry.time)
))
}
}
}
}
transitions.forEachIndexed { index, transition ->
if (transition.sourceBeatId !in beats) {
errors.add(ValidationError(
path = "transitions[$index].sourceBeatId",
message = Component.translatable("cobblemon.cinematic_editor.validation.source_missing", transition.sourceBeatId)
))
}
if (!transition.endsScene && transition.targetBeatId !in beats) {
errors.add(ValidationError(
path = "transitions[$index].targetBeatId",
message = Component.translatable("cobblemon.cinematic_editor.validation.target_missing", transition.targetBeatId)
))
}
}
val graph = transitions.filter { !it.endsScene && it.sourceBeatId in beats && it.targetBeatId in beats }
.groupBy({ it.sourceBeatId }, { it.targetBeatId })
val reachable = findReachableBeats(graph)
beats.keys.forEach { beatId ->
if (beatId !in reachable) {
errors.add(ValidationError(
path = "beats[$beatId]",
message = Component.translatable("cobblemon.cinematic_editor.validation.unreachable", startBeatId),
severity = ValidationError.Severity.WARNING
))
}
}
val cycle = findCycle(graph)
if (cycle != null) {
errors.add(ValidationError(
path = "transitions",
message = Component.translatable("cobblemon.cinematic_editor.validation.cycle", cycle.joinToString(" -> ")),
severity = ValidationError.Severity.WARNING
))
}
return errors
}
private fun findReachableBeats(graph: Map<String, List<String>>): Set<String> {
val reachable = mutableSetOf<String>()
val queue = ArrayDeque<String>()
if (startBeatId in beats) {
queue.add(startBeatId)
reachable.add(startBeatId)
}
while (queue.isNotEmpty()) {
for (target in graph[queue.removeFirst()].orEmpty()) {
if (reachable.add(target)) queue.addLast(target)
}
}
return reachable
}
private fun findCycle(graph: Map<String, List<String>>): List<String>? {
val visited = mutableSetOf<String>()
val active = mutableMapOf<String, Int>()
val path = mutableListOf<String>()
val stack = ArrayDeque<Iterator<String>>()
fun enter(id: String) {
visited.add(id)
active[id] = path.size
path.add(id)
stack.addLast(graph[id].orEmpty().iterator())
}
for (id in beats.keys) {
if (id in visited) continue
enter(id)
while (stack.isNotEmpty()) {
val remaining = stack.last()
if (remaining.hasNext()) {
val target = remaining.next()
val cycleStart = active[target]
if (cycleStart != null) return path.subList(cycleStart, path.size) + target
if (target !in visited) enter(target)
} else {
stack.removeLast()
active.remove(path.removeAt(path.lastIndex))
}
}
}
return null
}
}
@@ -0,0 +1,17 @@
/*
* 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.api.cinematic.model
/** A scene-unique, one-shot signal; loops and beat re-entry never fire it again. */
data class SceneCue(val id: String, val time: Float) {
init {
require(id.isNotBlank() && id.length <= 128) { "Cue id must contain 1 to 128 characters" }
require(time.isFinite() && time >= 0f) { "Cue time must be finite and non-negative" }
}
}
@@ -0,0 +1,42 @@
/*
* 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.api.cinematic.model
data class SceneGuiTrack(
val actions: List<SceneGuiAction>
) {
init {
require(actions.zipWithNext().all { (a, b) -> a.time <= b.time }) {
"SceneGuiTrack actions must be sorted by time"
}
}
}
data class SceneGuiAction(
val time: Float,
val type: SceneGuiActionType,
val guiId: String,
val args: Map<String, SceneGuiArgValue> = emptyMap()
) {
init {
require(time.isFinite() && time >= 0f) { "SceneGuiAction time must be non-negative, got $time" }
}
}
enum class SceneGuiActionType {
OPEN,
CLOSE,
UPDATE_ARGS
}
sealed class SceneGuiArgValue {
data class Expression(val value: String) : SceneGuiArgValue()
data class StringLiteral(val value: String) : SceneGuiArgValue()
}
@@ -0,0 +1,30 @@
/*
* 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.api.cinematic.model
import com.cobblemon.mod.common.api.cinematic.model.sources.FovSource
import com.cobblemon.mod.common.api.cinematic.model.sources.Lens
import com.cobblemon.mod.common.api.cinematic.model.sources.PositionSource
import com.cobblemon.mod.common.api.cinematic.model.sources.RollSource
import com.cobblemon.mod.common.api.cinematic.model.sources.RotationSource
import com.cobblemon.mod.common.api.cinematic.model.sources.ShotModifier
data class Shot(
val id: String,
val position: PositionSource,
val rotation: RotationSource,
val fov: FovSource? = null,
val roll: RollSource? = null,
val lens: Lens? = null,
val modifiers: List<ShotModifier> = emptyList()
) {
init {
require(id.isNotBlank()) { "Shot id must be non-blank" }
}
}
@@ -0,0 +1,34 @@
/*
* 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.api.cinematic.model
import com.cobblemon.mod.common.api.camera.CameraRotation
import com.cobblemon.mod.common.api.camera.model.PositionExpression
data class StartPose(
val position: PositionExpression,
val rotation: CameraRotation,
val blendDurationSeconds: Float = 0.5f
) {
init {
require(rotation.yaw.isFinite() && rotation.pitch.isFinite()) { "Start pose rotation must be finite" }
require(blendDurationSeconds.isFinite() && blendDurationSeconds >= 0f) { "Start pose blend duration must be finite and nonnegative" }
}
}
/** Unset mode teleports newly visible actors and ignores the start pose for already visible actors. */
enum class StartPoseMode {
TELEPORT,
BLEND,
EXPECT,
IGNORE
}
@@ -0,0 +1,13 @@
/*
* 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.api.cinematic.model.sources
abstract class FovSource {
abstract val type: String
}
@@ -0,0 +1,23 @@
/*
* 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.api.cinematic.model.sources
data class Lens(
val aspectCrop: Float? = null,
val letterbox: Float = 0f
) {
init {
aspectCrop?.let {
require(it.isFinite() && it > 0f) { "Lens aspectCrop must be positive, got $it" }
}
require(letterbox >= 0f && letterbox <= 0.5f) {
"Lens letterbox must be in [0, 0.5], got $letterbox"
}
}
}
@@ -0,0 +1,13 @@
/*
* 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.api.cinematic.model.sources
abstract class PositionSource {
abstract val type: String
}
@@ -0,0 +1,13 @@
/*
* 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.api.cinematic.model.sources
abstract class RollSource {
abstract val type: String
}
@@ -0,0 +1,13 @@
/*
* 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.api.cinematic.model.sources
abstract class RotationSource {
abstract val type: String
}
@@ -0,0 +1,13 @@
/*
* 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.api.cinematic.model.sources
abstract class ShotModifier {
abstract val type: String
}
@@ -0,0 +1,43 @@
/*
* 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.api.cinematic.model.sources.builtin
import com.cobblemon.mod.common.api.cinematic.model.sources.ShotModifier
import com.google.gson.JsonObject
import net.minecraft.world.phys.Vec3
data class ConstraintModifier(
val minX: Double? = null, val maxX: Double? = null,
val minY: Double? = null, val maxY: Double? = null,
val minZ: Double? = null, val maxZ: Double? = null
) : ShotModifier() {
override val type: String = TYPE
init {
require(listOfNotNull(minX, maxX, minY, maxY, minZ, maxZ).all { it.isFinite() }) { "Constraint bounds must be finite" }
require(minX == null || maxX == null || minX <= maxX) { "Minimum X must not exceed maximum X" }
require(minY == null || maxY == null || minY <= maxY) { "Minimum Y must not exceed maximum Y" }
require(minZ == null || maxZ == null || minZ <= maxZ) { "Minimum Z must not exceed maximum Z" }
}
fun apply(pos: Vec3): Vec3 = Vec3(
pos.x.let { if (minX != null && it < minX) minX else if (maxX != null && it > maxX) maxX else it },
pos.y.let { if (minY != null && it < minY) minY else if (maxY != null && it > maxY) maxY else it },
pos.z.let { if (minZ != null && it < minZ) minZ else if (maxZ != null && it > maxZ) maxZ else it }
)
companion object {
const val TYPE = "constraint"
fun fromJson(obj: JsonObject): ConstraintModifier = ConstraintModifier(
minX = obj.get("minX")?.asDouble, maxX = obj.get("maxX")?.asDouble,
minY = obj.get("minY")?.asDouble, maxY = obj.get("maxY")?.asDouble,
minZ = obj.get("minZ")?.asDouble, maxZ = obj.get("maxZ")?.asDouble
)
}
}
@@ -0,0 +1,56 @@
/*
* 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.api.cinematic.model.sources.builtin
import kotlin.math.cos
import kotlin.math.sin
import com.cobblemon.mod.common.api.cinematic.model.sources.PositionSource
import com.google.gson.JsonObject
import net.minecraft.world.phys.Vec3
data class CranePosition(
val pivot: Vec3,
val armLength: Float,
val startAngleDeg: Float,
val angularVelocityDegPerSec: Float,
val height: Float = 0f
) : PositionSource() {
override val type: String = TYPE
init {
require(armLength.isFinite() && armLength > 0f) { "CranePosition armLength must be finite and positive" }
require(startAngleDeg.isFinite() && angularVelocityDegPerSec.isFinite() && height.isFinite()) {
"CranePosition angles, speed and height must be finite"
}
require(pivot.x.isFinite() && pivot.y.isFinite() && pivot.z.isFinite()) { "CranePosition pivot must be finite" }
}
fun evaluate(time: Float): Vec3 {
val angleRad = Math.toRadians(startAngleDeg + angularVelocityDegPerSec.toDouble() * time)
return Vec3(
pivot.x + armLength * cos(angleRad),
pivot.y + height,
pivot.z + armLength * sin(angleRad)
)
}
companion object {
const val TYPE = "crane"
fun fromJson(obj: JsonObject): CranePosition {
val p = obj.getAsJsonObject("pivot")
return CranePosition(
pivot = Vec3(p.get("x").asDouble, p.get("y").asDouble, p.get("z").asDouble),
armLength = obj.get("armLength").asFloat,
startAngleDeg = obj.get("startAngle")?.asFloat ?: 0f,
angularVelocityDegPerSec = obj.get("angularVelocity").asFloat,
height = obj.get("height")?.asFloat ?: 0f
)
}
}
}
@@ -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.api.cinematic.model.sources.builtin
import com.cobblemon.mod.common.api.cinematic.model.sources.PositionSource
import com.google.gson.JsonObject
import net.minecraft.world.phys.Vec3
data class DollyPosition(
val from: Vec3,
val to: Vec3,
val duration: Float
) : PositionSource() {
override val type: String = TYPE
init {
require(duration.isFinite() && duration > 0f) { "DollyPosition duration must be finite and positive" }
require(listOf(from.x, from.y, from.z, to.x, to.y, to.z).all { it.isFinite() }) {
"DollyPosition endpoints must be finite"
}
}
fun evaluate(time: Float): Vec3 {
val t = (time / duration).coerceIn(0f, 1f)
return Vec3(
from.x + (to.x - from.x) * t,
from.y + (to.y - from.y) * t,
from.z + (to.z - from.z) * t
)
}
companion object {
const val TYPE = "dolly"
fun fromJson(obj: JsonObject): DollyPosition {
val f = obj.getAsJsonObject("from")
val t = obj.getAsJsonObject("to")
return DollyPosition(
from = Vec3(f.get("x").asDouble, f.get("y").asDouble, f.get("z").asDouble),
to = Vec3(t.get("x").asDouble, t.get("y").asDouble, t.get("z").asDouble),
duration = obj.get("duration").asFloat
)
}
}
}
@@ -0,0 +1,60 @@
/*
* 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.api.cinematic.model.sources.builtin
import com.bedrockk.molang.runtime.MoLangRuntime
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.cinematic.model.sources.FovSource
import com.cobblemon.mod.common.api.molang.ExpressionLike
import com.cobblemon.mod.common.util.asExpressionLike
import com.cobblemon.mod.common.util.resolveDouble
import com.google.gson.JsonObject
import java.util.concurrent.atomic.AtomicBoolean
data class ExpressionFov(
val expression: ExpressionLike,
val fallbackFov: Float = 70f
) : FovSource() {
override val type: String = "expression"
@Transient private val warningReported = AtomicBoolean()
init {
require(fallbackFov in 1f..179f) { "ExpressionFov fallbackFov must be in [1, 179]" }
}
fun evaluate(runtime: MoLangRuntime): Float {
return try {
// coerceIn does not filter NaN — its comparisons are false either way — so check first.
val raw = runtime.resolveDouble(expression).toFloat()
if (!raw.isFinite()) {
if (warningReported.compareAndSet(false, true)) {
Cobblemon.LOGGER.warn("[ExpressionFov] Non-finite result $raw; using fallback $fallbackFov")
}
fallbackFov
} else {
raw.coerceIn(1f, 179f)
}
} catch (e: Exception) {
if (warningReported.compareAndSet(false, true)) {
Cobblemon.LOGGER.warn("[ExpressionFov] Evaluation failed: ${e.message}; using fallback $fallbackFov", e)
}
fallbackFov
}
}
companion object {
const val TYPE = "expression"
fun fromJson(obj: JsonObject): ExpressionFov {
val exprStr = obj.get("expression")?.asString
?: throw IllegalArgumentException("ExpressionFov missing 'expression' field")
val fallback = obj.get("fallback")?.asFloat ?: 70f
return ExpressionFov(expression = exprStr.asExpressionLike(), fallbackFov = fallback)
}
}
}
@@ -0,0 +1,79 @@
/*
* 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.api.cinematic.model.sources.builtin
import com.bedrockk.molang.runtime.MoLangRuntime
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.cinematic.model.sources.PositionSource
import com.cobblemon.mod.common.api.molang.ExpressionLike
import com.cobblemon.mod.common.util.asExpressionLike
import com.cobblemon.mod.common.util.resolveDouble
import com.google.gson.JsonObject
import net.minecraft.world.phys.Vec3
import java.util.concurrent.atomic.AtomicBoolean
data class ExpressionVec3Position(
val xExpression: ExpressionLike,
val yExpression: ExpressionLike,
val zExpression: ExpressionLike,
val fallback: Vec3 = Vec3.ZERO
) : PositionSource() {
override val type: String = "expression_vec3"
@Transient private val warningReported = AtomicBoolean()
init {
require(fallback.x.isFinite() && fallback.y.isFinite() && fallback.z.isFinite()) { "Expression position fallback must be finite" }
}
fun evaluate(runtime: MoLangRuntime?): Vec3 {
if (runtime == null) return fallback
return try {
val x = runtime.resolveDouble(xExpression)
val y = runtime.resolveDouble(yExpression)
val z = runtime.resolveDouble(zExpression)
// MoLang can return NaN or infinity without throwing, so validate before sending transforms.
if (!x.isFinite() || !y.isFinite() || !z.isFinite()) {
if (warningReported.compareAndSet(false, true)) {
Cobblemon.LOGGER.warn("[ExpressionVec3Position] Non-finite result ($x, $y, $z); using fallback $fallback")
}
fallback
} else {
Vec3(x, y, z)
}
} catch (e: Exception) {
if (warningReported.compareAndSet(false, true)) {
Cobblemon.LOGGER.warn("[ExpressionVec3Position] Evaluation failed: ${e.message}; using fallback $fallback", e)
}
fallback
}
}
companion object {
const val TYPE = "expression_vec3"
fun fromJson(obj: JsonObject): ExpressionVec3Position {
val x = obj.get("x")?.asString
?: throw IllegalArgumentException("ExpressionVec3Position missing 'x'")
val y = obj.get("y")?.asString
?: throw IllegalArgumentException("ExpressionVec3Position missing 'y'")
val z = obj.get("z")?.asString
?: throw IllegalArgumentException("ExpressionVec3Position missing 'z'")
val fb = obj.get("fallback")?.asJsonObject
val fallback = if (fb != null) {
Vec3(fb.get("x").asDouble, fb.get("y").asDouble, fb.get("z").asDouble)
} else Vec3.ZERO
return ExpressionVec3Position(
xExpression = x.asExpressionLike(),
yExpression = y.asExpressionLike(),
zExpression = z.asExpressionLike(),
fallback = fallback
)
}
}
}
@@ -0,0 +1,25 @@
/*
* 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.api.cinematic.model.sources.builtin
import com.cobblemon.mod.common.api.cinematic.model.sources.FovSource
import com.google.gson.JsonObject
data class FixedFov(val fov: Float) : FovSource() {
override val type: String = "fixed"
init {
require(fov in 1f..179f) { "FixedFov must be in [1, 179], got $fov" }
}
companion object {
const val TYPE = "fixed"
fun fromJson(obj: JsonObject): FixedFov = FixedFov(obj.get("value").asFloat)
}
}
@@ -0,0 +1,29 @@
/*
* 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.api.cinematic.model.sources.builtin
import com.cobblemon.mod.common.api.cinematic.model.sources.PositionSource
import com.google.gson.JsonObject
import net.minecraft.world.phys.Vec3
data class FixedPosition(val position: Vec3) : PositionSource() {
override val type: String = "fixed"
init {
require(position.x.isFinite() && position.y.isFinite() && position.z.isFinite()) { "Fixed position must be finite" }
}
companion object {
const val TYPE = "fixed"
fun fromJson(obj: JsonObject): FixedPosition {
val pos = obj.getAsJsonObject("position") ?: throw IllegalArgumentException("FixedPosition missing 'position'")
return FixedPosition(Vec3(pos.get("x").asDouble, pos.get("y").asDouble, pos.get("z").asDouble))
}
}
}
@@ -0,0 +1,25 @@
/*
* 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.api.cinematic.model.sources.builtin
import com.cobblemon.mod.common.api.cinematic.model.sources.RollSource
import com.google.gson.JsonObject
data class FixedRoll(val roll: Float) : RollSource() {
override val type: String = "fixed"
init {
require(roll.isFinite()) { "Fixed roll must be finite" }
}
companion object {
const val TYPE = "fixed"
fun fromJson(obj: JsonObject): FixedRoll = FixedRoll(obj.get("value").asFloat)
}
}
@@ -0,0 +1,31 @@
/*
* 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.api.cinematic.model.sources.builtin
import com.cobblemon.mod.common.api.camera.CameraRotation
import com.cobblemon.mod.common.api.cinematic.model.sources.RotationSource
import com.google.gson.JsonObject
data class FixedRotation(val rotation: CameraRotation) : RotationSource() {
override val type: String = "fixed"
init {
require(rotation.yaw.isFinite() && rotation.pitch.isFinite()) { "Fixed rotation must be finite" }
}
companion object {
const val TYPE = "fixed"
fun fromJson(obj: JsonObject): FixedRotation {
return FixedRotation(CameraRotation(
yaw = obj.get("yaw").asFloat,
pitch = obj.get("pitch").asFloat
))
}
}
}
@@ -0,0 +1,41 @@
/*
* 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.api.cinematic.model.sources.builtin
import com.cobblemon.mod.common.api.cinematic.model.sources.PositionSource
import com.google.gson.JsonObject
import net.minecraft.world.phys.Vec3
/** Offset is world-space and ignores actor orientation. */
data class FollowActorPosition(
val actorId: String,
val offset: Vec3,
val fallback: Vec3 = Vec3.ZERO
) : PositionSource() {
override val type: String = TYPE
init {
require(listOf(offset.x, offset.y, offset.z, fallback.x, fallback.y, fallback.z).all { it.isFinite() }) {
"FollowActorPosition offset and fallback must be finite"
}
}
companion object {
const val TYPE = "follow_actor"
fun fromJson(obj: JsonObject): FollowActorPosition {
val o = obj.getAsJsonObject("offset")
val fObj = obj.getAsJsonObject("fallback")
return FollowActorPosition(
actorId = obj.get("actorId").asString,
offset = Vec3(o.get("x").asDouble, o.get("y").asDouble, o.get("z").asDouble),
fallback = if (fObj != null) Vec3(fObj.get("x").asDouble, fObj.get("y").asDouble, fObj.get("z").asDouble) else Vec3.ZERO
)
}
}
}
@@ -0,0 +1,53 @@
/*
* 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.api.cinematic.model.sources.builtin
import com.cobblemon.mod.common.api.cinematic.model.sources.FovSource
import com.cobblemon.mod.common.api.cinematic.model.orderedKeyframes
import com.google.gson.JsonObject
data class KeyframeFov(val keyframes: List<Kf>) : FovSource() {
override val type: String = TYPE
data class Kf(val time: Float, val fov: Float)
init {
keyframes.forEach { require(it.fov in 1f..179f) { "Fov must be in [1, 179], got ${it.fov}" } }
}
@Transient private val playbackKeyframes = orderedKeyframes(keyframes) { it.time }
fun evaluate(time: Float): Float {
val keyframes = playbackKeyframes
if (keyframes.size == 1) return keyframes[0].fov
if (time <= keyframes.first().time) return keyframes.first().fov
if (time >= keyframes.last().time) return keyframes.last().fov
for (i in 0 until keyframes.size - 1) {
val a = keyframes[i]
val b = keyframes[i + 1]
if (time >= a.time && time <= b.time) {
val d = b.time - a.time
val t = if (d > 0f) (time - a.time) / d else 0f
return a.fov + (b.fov - a.fov) * t
}
}
return keyframes.last().fov
}
companion object {
const val TYPE = "keyframe"
fun fromJson(obj: JsonObject): KeyframeFov {
val kfs = obj.getAsJsonArray("keyframes").map { el ->
val k = el.asJsonObject
Kf(time = k.get("time").asFloat, fov = k.get("fov").asFloat)
}
return KeyframeFov(kfs)
}
}
}
@@ -0,0 +1,65 @@
/*
* 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.api.cinematic.model.sources.builtin
import com.cobblemon.mod.common.api.cinematic.model.sources.PositionSource
import com.cobblemon.mod.common.api.cinematic.model.orderedKeyframes
import com.cobblemon.mod.common.api.tween.EasingRegistry
import com.google.gson.JsonObject
import net.minecraft.world.phys.Vec3
data class KeyframePosition(
val keyframes: List<Kf>,
val defaultEasing: String = "linear"
) : PositionSource() {
override val type: String = "keyframe"
init {
require(keyframes.all { it.position.x.isFinite() && it.position.y.isFinite() && it.position.z.isFinite() }) {
"Keyframe positions must be finite"
}
}
// Keep authoring indices intact; coincident keyframes use the last authored value during playback.
@Transient private val playbackKeyframes = orderedKeyframes(keyframes) { it.time }
data class Kf(val time: Float, val position: Vec3, val easing: String? = null)
fun evaluate(time: Float): Vec3 {
if (time <= playbackKeyframes.first().time) return playbackKeyframes.first().position
for (index in 1 until playbackKeyframes.size) {
val start = playbackKeyframes[index - 1]
val end = playbackKeyframes[index]
if (time <= end.time) {
val progress = ((time - start.time) / (end.time - start.time)).coerceIn(0f, 1f)
val eased = EasingRegistry.getEasingFunction(end.easing ?: defaultEasing)(progress)
return start.position.lerp(end.position, eased.toDouble())
}
}
return playbackKeyframes.last().position
}
companion object {
const val TYPE = "keyframe"
fun fromJson(obj: JsonObject): KeyframePosition {
val kfArray = obj.getAsJsonArray("keyframes") ?: throw IllegalArgumentException("KeyframePosition missing 'keyframes'")
val kfs = kfArray.map { el ->
val kfObj = el.asJsonObject
val pos = kfObj.getAsJsonObject("position")
Kf(
time = kfObj.get("time").asFloat,
position = Vec3(pos.get("x").asDouble, pos.get("y").asDouble, pos.get("z").asDouble),
easing = kfObj.get("easing")?.asString
)
}
val easing = obj.get("easing")?.asString ?: "linear"
return KeyframePosition(kfs, easing)
}
}
}
@@ -0,0 +1,53 @@
/*
* 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.api.cinematic.model.sources.builtin
import com.cobblemon.mod.common.api.cinematic.model.sources.RollSource
import com.cobblemon.mod.common.api.cinematic.model.orderedKeyframes
import com.google.gson.JsonObject
data class KeyframeRoll(val keyframes: List<Kf>) : RollSource() {
override val type: String = TYPE
data class Kf(val time: Float, val roll: Float)
init {
require(keyframes.all { it.roll.isFinite() }) { "Keyframe roll must be finite" }
}
@Transient private val playbackKeyframes = orderedKeyframes(keyframes) { it.time }
fun evaluate(time: Float): Float {
val keyframes = playbackKeyframes
if (keyframes.size == 1) return keyframes[0].roll
if (time <= keyframes.first().time) return keyframes.first().roll
if (time >= keyframes.last().time) return keyframes.last().roll
for (i in 0 until keyframes.size - 1) {
val a = keyframes[i]
val b = keyframes[i + 1]
if (time >= a.time && time <= b.time) {
val d = b.time - a.time
val t = if (d > 0f) (time - a.time) / d else 0f
return (a.roll + (b.roll.toDouble() - a.roll) * t).toFloat()
}
}
return keyframes.last().roll
}
companion object {
const val TYPE = "keyframe"
fun fromJson(obj: JsonObject): KeyframeRoll {
val kfs = obj.getAsJsonArray("keyframes").map { el ->
val k = el.asJsonObject
Kf(time = k.get("time").asFloat, roll = k.get("roll").asFloat)
}
return KeyframeRoll(kfs)
}
}
}
@@ -0,0 +1,57 @@
/*
* 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.api.cinematic.model.sources.builtin
import com.cobblemon.mod.common.api.camera.CameraRotation
import com.cobblemon.mod.common.api.cinematic.model.orderedKeyframes
import com.cobblemon.mod.common.api.cinematic.model.sources.RotationSource
import com.google.gson.JsonObject
data class KeyframeRotation(val keyframes: List<Kf>) : RotationSource() {
override val type: String = TYPE
data class Kf(val time: Float, val yaw: Float, val pitch: Float)
init {
require(keyframes.all { it.yaw.isFinite() && it.pitch.isFinite() }) { "Keyframe rotations must be finite" }
}
@Transient private val playbackKeyframes = orderedKeyframes(keyframes) { it.time }
fun evaluate(time: Float): CameraRotation {
val keyframes = playbackKeyframes
if (keyframes.size == 1) return CameraRotation(keyframes[0].yaw, keyframes[0].pitch)
if (time <= keyframes.first().time) return CameraRotation(keyframes.first().yaw, keyframes.first().pitch)
if (time >= keyframes.last().time) return CameraRotation(keyframes.last().yaw, keyframes.last().pitch)
for (i in 0 until keyframes.size - 1) {
val a = keyframes[i]
val b = keyframes[i + 1]
if (time >= a.time && time <= b.time) {
val d = b.time - a.time
val t = if (d > 0f) (time - a.time) / d else 0f
return CameraRotation(
(a.yaw + (b.yaw.toDouble() - a.yaw) * t).toFloat(),
(a.pitch + (b.pitch.toDouble() - a.pitch) * t).toFloat()
)
}
}
return CameraRotation(keyframes.last().yaw, keyframes.last().pitch)
}
companion object {
const val TYPE = "keyframe"
fun fromJson(obj: JsonObject): KeyframeRotation {
val kfs = obj.getAsJsonArray("keyframes").map { el ->
val k = el.asJsonObject
Kf(time = k.get("time").asFloat, yaw = k.get("yaw").asFloat, pitch = k.get("pitch").asFloat)
}
return KeyframeRotation(kfs)
}
}
}
@@ -0,0 +1,47 @@
/*
* 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.api.cinematic.model.sources.builtin
import com.cobblemon.mod.common.api.camera.CameraRotation
import com.cobblemon.mod.common.api.cinematic.model.sources.RotationSource
import com.google.gson.JsonObject
import net.minecraft.world.phys.Vec3
import kotlin.math.atan2
import kotlin.math.sqrt
data class LookAlongPath(
val epsilon: Float = 1f / 60f
) : RotationSource() {
override val type: String = "look_along_path"
init {
require(epsilon.isFinite() && epsilon > 0f) { "LookAlongPath epsilon must be finite and positive" }
}
fun computeRotation(current: Vec3, ahead: Vec3): CameraRotation {
val dx = ahead.x - current.x
val dy = ahead.y - current.y
val dz = ahead.z - current.z
if (dx * dx + dy * dy + dz * dz < 1e-8) {
// Motion near zero — return forward-facing default to avoid NaN.
return CameraRotation(0f, 0f)
}
val horizontal = sqrt(dx * dx + dz * dz)
val yaw = Math.toDegrees(atan2(-dx, dz)).toFloat()
val pitch = Math.toDegrees(-atan2(dy, horizontal)).toFloat()
return CameraRotation(yaw = yaw, pitch = pitch)
}
companion object {
const val TYPE = "look_along_path"
fun fromJson(obj: JsonObject): LookAlongPath = LookAlongPath(
epsilon = obj.get("epsilon")?.asFloat ?: (1f / 60f)
)
}
}
@@ -0,0 +1,149 @@
/*
* 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.api.cinematic.model.sources.builtin
import com.bedrockk.molang.runtime.MoLangRuntime
import com.cobblemon.mod.common.api.camera.CameraRotation
import com.cobblemon.mod.common.api.cinematic.model.sources.RotationSource
import com.cobblemon.mod.common.api.molang.ExpressionLike
import com.cobblemon.mod.common.util.asExpressionLike
import com.google.gson.JsonObject
import net.minecraft.world.phys.Vec3
import kotlin.math.atan2
import kotlin.math.sqrt
sealed class LookAtTarget {
data class Position(val pos: Vec3) : LookAtTarget() {
init {
require(pos.x.isFinite() && pos.y.isFinite() && pos.z.isFinite()) { "Look-at position must be finite" }
}
}
data class Actor(val actorId: String) : LookAtTarget() {
init { require(actorId.isNotBlank()) { "Look-at actor id must be non-blank" } }
}
data class Actors(val actorIds: List<String>) : LookAtTarget() {
init {
require(actorIds.size in 1..16)
require(actorIds.all(String::isNotBlank)) { "Look-at actor ids must be non-blank" }
}
}
data class Variable(val prefix: String, val fallback: Vec3 = Vec3.ZERO) : LookAtTarget() {
@Transient private val position = VariableVec3Position(prefix, fallback = fallback)
fun evaluate(runtime: MoLangRuntime?): Vec3 = position.evaluate(runtime)
}
data class Expression(
val xExpression: ExpressionLike,
val yExpression: ExpressionLike,
val zExpression: ExpressionLike,
val fallback: Vec3 = Vec3.ZERO
) : LookAtTarget() {
@Transient private val position = ExpressionVec3Position(xExpression, yExpression, zExpression, fallback)
fun evaluate(runtime: MoLangRuntime?): Vec3 = position.evaluate(runtime)
}
/** Bones resolve client-side; offsets are in world coordinates. */
data class ActorBone(
val actorId: String,
val boneName: String,
val offset: Vec3 = Vec3.ZERO,
val smooth: Boolean = true
) : LookAtTarget() {
init {
require(actorId.isNotBlank()) { "Look-at bone actor id must be non-blank" }
require(boneName.isNotBlank()) { "Look-at bone name must be non-blank" }
require(offset.x.isFinite() && offset.y.isFinite() && offset.z.isFinite()) { "Look-at bone offset must be finite" }
}
}
}
data class LookAt(
val target: LookAtTarget
) : RotationSource() {
override val type: String = "look_at"
fun computeRotation(cameraPos: Vec3, targetPos: Vec3): CameraRotation {
val dx = targetPos.x - cameraPos.x
val dy = targetPos.y - cameraPos.y
val dz = targetPos.z - cameraPos.z
val horizontal = sqrt(dx * dx + dz * dz)
val yaw = Math.toDegrees(atan2(-dx, dz)).toFloat()
val pitch = Math.toDegrees(-atan2(dy, horizontal)).toFloat()
return CameraRotation(yaw = yaw, pitch = pitch)
}
companion object {
const val TYPE = "look_at"
fun fromJson(obj: JsonObject): LookAt {
val targetObj = obj.getAsJsonObject("target")
?: throw IllegalArgumentException("LookAt missing 'target'")
val targetType = targetObj.get("type")?.asString ?: "position"
val target = when (targetType) {
"actors" -> LookAtTarget.Actors(targetObj.getAsJsonArray("actorIds").map { it.asString })
"position" -> LookAtTarget.Position(
Vec3(
targetObj.get("x").asDouble,
targetObj.get("y").asDouble,
targetObj.get("z").asDouble
)
)
"actor" -> LookAtTarget.Actor(
targetObj.get("actorId")?.asString
?: throw IllegalArgumentException("LookAt actor target missing 'actorId'")
)
"actor_bone" -> {
val actorId = targetObj.get("actorId")?.asString
?: throw IllegalArgumentException("LookAt actor_bone target missing 'actorId'")
val boneName = targetObj.get("bone")?.asString
?: throw IllegalArgumentException("LookAt actor_bone target missing 'bone'")
val off = targetObj.getAsJsonObject("offset")
val offset = if (off != null) {
Vec3(off.get("x").asDouble, off.get("y").asDouble, off.get("z").asDouble)
} else Vec3.ZERO
val smooth = targetObj.get("smooth")?.asBoolean ?: true
LookAtTarget.ActorBone(actorId, boneName, offset, smooth)
}
"variable" -> {
val prefix = targetObj.get("prefix")?.asString
?: throw IllegalArgumentException("LookAt variable target missing 'prefix'")
val fb = targetObj.getAsJsonObject("fallback")
val fallback = if (fb != null) {
Vec3(fb.get("x").asDouble, fb.get("y").asDouble, fb.get("z").asDouble)
} else Vec3.ZERO
LookAtTarget.Variable(prefix, fallback)
}
"expression" -> {
val x = targetObj.get("x")?.asString
?: throw IllegalArgumentException("LookAt expression target missing 'x'")
val y = targetObj.get("y")?.asString
?: throw IllegalArgumentException("LookAt expression target missing 'y'")
val z = targetObj.get("z")?.asString
?: throw IllegalArgumentException("LookAt expression target missing 'z'")
val fb = targetObj.getAsJsonObject("fallback")
val fallback = if (fb != null) {
Vec3(fb.get("x").asDouble, fb.get("y").asDouble, fb.get("z").asDouble)
} else Vec3.ZERO
LookAtTarget.Expression(
xExpression = x.asExpressionLike(),
yExpression = y.asExpressionLike(),
zExpression = z.asExpressionLike(),
fallback = fallback
)
}
else -> throw IllegalArgumentException("LookAt target unknown type '$targetType'")
}
return LookAt(target)
}
}
}
@@ -0,0 +1,43 @@
/*
* 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.api.cinematic.model.sources.builtin
import com.cobblemon.mod.common.api.cinematic.model.sources.ShotModifier
import com.google.gson.JsonObject
import net.minecraft.world.phys.Vec3
data class OffsetModifier(
val positionOffset: Vec3 = Vec3.ZERO,
val yawOffset: Float = 0f,
val pitchOffset: Float = 0f,
val fovOffset: Float = 0f,
val rollOffset: Float = 0f
) : ShotModifier() {
override val type: String = TYPE
init {
require(positionOffset.x.isFinite() && positionOffset.y.isFinite() && positionOffset.z.isFinite()) { "Position offset must be finite" }
require(listOf(yawOffset, pitchOffset, fovOffset, rollOffset).all { it.isFinite() }) { "Camera offsets must be finite" }
}
companion object {
const val TYPE = "offset"
fun fromJson(obj: JsonObject): OffsetModifier {
val p = obj.getAsJsonObject("position")
val posOff = if (p != null) Vec3(p.get("x").asDouble, p.get("y").asDouble, p.get("z").asDouble) else Vec3.ZERO
return OffsetModifier(
positionOffset = posOff,
yawOffset = obj.get("yaw")?.asFloat ?: 0f,
pitchOffset = obj.get("pitch")?.asFloat ?: 0f,
fovOffset = obj.get("fov")?.asFloat ?: 0f,
rollOffset = obj.get("roll")?.asFloat ?: 0f
)
}
}
}
@@ -0,0 +1,98 @@
/*
* 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.api.cinematic.model.sources.builtin
import com.cobblemon.mod.common.api.cinematic.model.sources.PositionSource
import com.google.gson.JsonObject
import net.minecraft.world.phys.Vec3
import net.minecraft.world.phys.AABB
import kotlin.math.max
import kotlin.math.sin
import kotlin.math.sqrt
import kotlin.math.cos
import kotlin.math.atan2
data class OrbitPosition(
val center: Vec3,
val radius: Float,
val angularVelocityDegPerSec: Float,
val startAngleDeg: Float = 0f,
val height: Float = 0f,
val actorIds: List<String> = emptyList(),
val padding: Float = 2f,
val framingFov: Float = 60f,
val bearingActorId: String? = null,
val avoidBlocks: Boolean = true,
val avoidanceSmoothing: Float = 0f,
val fitArena: Boolean = false,
val arenaWidth: Float? = null,
val arenaDepth: Float? = null,
val rememberProgress: Boolean = false
) : PositionSource() {
override val type: String = TYPE
init {
require(center.x.isFinite() && center.y.isFinite() && center.z.isFinite()) { "Orbit center must be finite" }
require(radius > 0f) { "OrbitPosition radius must be positive" }
require(listOf(radius, angularVelocityDegPerSec, startAngleDeg, height, padding, framingFov).all { it.isFinite() })
require(padding >= 0f && framingFov in 10f..120f && actorIds.size <= 16)
require(avoidanceSmoothing.isFinite() && avoidanceSmoothing in 0f..8f)
require(listOfNotNull(arenaWidth, arenaDepth).all { it.isFinite() && it in 2f..128f })
}
val hasArena get() = fitArena && arenaWidth != null && arenaDepth != null
fun focus(bounds: AABB?) = if (hasArena) center else bounds?.center ?: center
fun evaluate(time: Float, bounds: AABB? = null, lensFov: Float = framingFov, bearingPosition: Vec3? = null): Vec3 {
require(time.isFinite()) { "Orbit time must be finite" }
val focus = focus(bounds)
val bearing = bearingPosition?.subtract(focus)?.takeIf { it.horizontalDistanceSqr() > 0.000001 }
?.let { atan2(it.z, it.x) } ?: 0.0
val angleDeg = (startAngleDeg.toDouble() + angularVelocityDegPerSec.toDouble() * time.toDouble()) % 360.0
val angleRad = bearing + Math.toRadians(angleDeg)
if (hasArena) return Vec3(
focus.x + max(radius, arenaWidth!! / 2 + padding) * cos(angleRad),
focus.y + height,
focus.z + max(radius, arenaDepth!! / 2 + padding) * sin(angleRad))
val fitFov = lensFov.takeIf { it.isFinite() }?.coerceIn(1f, 179f) ?: framingFov
val fitRadius = bounds?.let {
sqrt(it.xsize * it.xsize + it.ysize * it.ysize + it.zsize * it.zsize) / 2 / sin(Math.toRadians(fitFov / 2.0)) + padding
} ?: radius.toDouble()
val distance = max(radius.toDouble(), fitRadius)
return Vec3(
focus.x + distance * cos(angleRad),
focus.y + height,
focus.z + distance * sin(angleRad)
)
}
companion object {
const val TYPE = "orbit"
fun fromJson(obj: JsonObject): OrbitPosition {
val c = obj.getAsJsonObject("center")
return OrbitPosition(
center = Vec3(c.get("x").asDouble, c.get("y").asDouble, c.get("z").asDouble),
radius = obj.get("radius").asFloat,
angularVelocityDegPerSec = obj.get("angularVelocity").asFloat,
startAngleDeg = obj.get("startAngle")?.asFloat ?: 0f,
height = obj.get("height")?.asFloat ?: 0f,
actorIds = obj.getAsJsonArray("actorIds")?.map { it.asString } ?: emptyList(),
padding = obj.get("padding")?.asFloat ?: 2f,
framingFov = obj.get("framingFov")?.asFloat ?: 60f,
bearingActorId = obj.get("bearingActorId")?.asString,
avoidBlocks = obj.get("avoidBlocks")?.asBoolean ?: true,
avoidanceSmoothing = obj.get("avoidanceSmoothing")?.asFloat ?: 0f,
fitArena = obj.get("fitArena")?.asBoolean ?: false,
arenaWidth = obj.get("arenaWidth")?.asFloat,
arenaDepth = obj.get("arenaDepth")?.asFloat,
rememberProgress = obj.get("rememberProgress")?.asBoolean ?: false
)
}
}
}
@@ -0,0 +1,44 @@
/*
* 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.api.cinematic.model.sources.builtin
import kotlin.math.sin
import com.cobblemon.mod.common.api.cinematic.model.sources.RollSource
import com.google.gson.JsonObject
data class ShakeDerivedRoll(
val amplitudeDeg: Float,
val frequencyHz: Float = 8f,
val decaySecs: Float = 0f
) : RollSource() {
override val type: String = "shake"
init {
require(amplitudeDeg.isFinite() && amplitudeDeg >= 0f)
require(frequencyHz in 0f..60f)
require(decaySecs.isFinite() && decaySecs >= 0f)
}
fun evaluate(time: Float): Float {
val decay = if (decaySecs <= 0f) 1f else (1f - (time / decaySecs)).coerceAtLeast(0f)
if (decay <= 0f) return 0f
val t = time.toDouble() * frequencyHz
val n = (sin(1.0 * t) + sin(2.3 * t + 1.3) + sin(3.7 * t + 2.7)) / 3.0
return (n.toFloat() * amplitudeDeg * decay)
}
companion object {
const val TYPE = "shake"
fun fromJson(obj: JsonObject): ShakeDerivedRoll = ShakeDerivedRoll(
amplitudeDeg = obj.get("amplitude").asFloat,
frequencyHz = obj.get("frequency")?.asFloat ?: 8f,
decaySecs = obj.get("decay")?.asFloat ?: 0f
)
}
}
@@ -0,0 +1,53 @@
/*
* 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.api.cinematic.model.sources.builtin
import kotlin.math.sin
import com.cobblemon.mod.common.api.cinematic.model.sources.ShotModifier
import com.google.gson.JsonObject
data class ShakeModifier(
val positionAmplitude: Float = 0f,
val rotationAmplitudeDeg: Float = 0f,
val frequencyHz: Float = 8f,
val decaySecs: Float = 0f
) : ShotModifier() {
override val type: String = TYPE
init {
require(positionAmplitude.isFinite() && positionAmplitude >= 0f) { "positionAmplitude must be >= 0" }
require(rotationAmplitudeDeg.isFinite() && rotationAmplitudeDeg >= 0f) { "rotationAmplitudeDeg must be >= 0" }
require(frequencyHz in 0f..60f) { "frequency must be in [0, 60] Hz" }
require(decaySecs.isFinite() && decaySecs >= 0f) { "decaySecs must be >= 0" }
}
fun noise3(time: Float): Triple<Float, Float, Float> {
val t = time.toDouble() * frequencyHz
fun s(a: Double, b: Double, c: Double) =
(sin(a * t) + sin(b * t + 1.3) + sin(c * t + 2.7)) / 3.0
return Triple(
s(1.0, 2.3, 3.7).toFloat(),
s(1.1, 2.7, 3.3).toFloat(),
s(0.9, 2.1, 4.1).toFloat()
)
}
fun decayFactor(time: Float): Float =
if (decaySecs <= 0f) 1f else (1f - (time / decaySecs)).coerceAtLeast(0f)
companion object {
const val TYPE = "shake"
fun fromJson(obj: JsonObject): ShakeModifier = ShakeModifier(
positionAmplitude = obj.get("positionAmplitude")?.asFloat ?: 0f,
rotationAmplitudeDeg = obj.get("rotationAmplitude")?.asFloat ?: 0f,
frequencyHz = obj.get("frequency")?.asFloat ?: 8f,
decaySecs = obj.get("decay")?.asFloat ?: 0f
)
}
}
@@ -0,0 +1,36 @@
/*
* 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.api.cinematic.model.sources.builtin
import com.cobblemon.mod.common.api.cinematic.model.sources.ShotModifier
import com.google.gson.JsonObject
/** Factor 0 disables smoothing; factor 1 freezes the previous value. */
data class SmoothingModifier(
val positionFactor: Float = 0f,
val rotationFactor: Float = 0f,
val fovFactor: Float = 0f
) : ShotModifier() {
override val type: String = "smoothing"
init {
require(positionFactor in 0f..1f) { "positionFactor must be in [0, 1]" }
require(rotationFactor in 0f..1f) { "rotationFactor must be in [0, 1]" }
require(fovFactor in 0f..1f) { "fovFactor must be in [0, 1]" }
}
companion object {
const val TYPE = "smoothing"
fun fromJson(obj: JsonObject): SmoothingModifier = SmoothingModifier(
positionFactor = obj.get("position")?.asFloat ?: 0f,
rotationFactor = obj.get("rotation")?.asFloat ?: 0f,
fovFactor = obj.get("fov")?.asFloat ?: 0f
)
}
}
@@ -0,0 +1,43 @@
/*
* 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.api.cinematic.model.sources.builtin
import com.cobblemon.mod.common.api.cinematic.model.tracks.builtin.CinematicSpline
import com.cobblemon.mod.common.api.cinematic.model.sources.PositionSource
import com.google.gson.JsonObject
import net.minecraft.world.phys.Vec3
data class SplinePosition(
val controlPoints: List<Vec3>,
val duration: Float
) : PositionSource() {
override val type: String = TYPE
init {
require(controlPoints.size >= 2) { "SplinePosition needs at least 2 control points" }
require(duration.isFinite() && duration > 0f) { "SplinePosition duration must be positive" }
}
@Transient private val path = CinematicSpline(controlPoints)
val pathLength: Double get() = path.length
fun evaluate(time: Float): Vec3 = path.position((time / duration).toDouble().coerceIn(0.0, 1.0))
companion object {
const val TYPE = "spline"
fun fromJson(obj: JsonObject): SplinePosition {
val points = obj.getAsJsonArray("controlPoints").map { el ->
val p = el.asJsonObject
Vec3(p.get("x").asDouble, p.get("y").asDouble, p.get("z").asDouble)
}
val duration = obj.get("duration").asFloat
return SplinePosition(points, duration)
}
}
}
@@ -0,0 +1,51 @@
/*
* 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.api.cinematic.model.sources.builtin
import com.cobblemon.mod.common.api.cinematic.model.sources.PositionSource
import com.google.gson.JsonObject
import net.minecraft.world.phys.Vec3
/** The axis is not normalized; callers must supply a unit vector for predictable speed. */
data class TruckPosition(
val origin: Vec3,
val axis: Vec3,
val velocity: Float
) : PositionSource() {
override val type: String = TYPE
init {
require(velocity.isFinite()) { "TruckPosition velocity must be finite" }
require(listOf(origin.x, origin.y, origin.z, axis.x, axis.y, axis.z).all { it.isFinite() }) {
"TruckPosition origin and axis must be finite"
}
}
fun evaluate(time: Float): Vec3 {
val dist = velocity.toDouble() * time
return Vec3(
origin.x + axis.x * dist,
origin.y + axis.y * dist,
origin.z + axis.z * dist
)
}
companion object {
const val TYPE = "truck"
fun fromJson(obj: JsonObject): TruckPosition {
val o = obj.getAsJsonObject("origin")
val a = obj.getAsJsonObject("axis")
return TruckPosition(
origin = Vec3(o.get("x").asDouble, o.get("y").asDouble, o.get("z").asDouble),
axis = Vec3(a.get("x").asDouble, a.get("y").asDouble, a.get("z").asDouble),
velocity = obj.get("velocity").asFloat
)
}
}
}
@@ -0,0 +1,58 @@
/*
* 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.api.cinematic.model.sources.builtin
import com.bedrockk.molang.runtime.MoLangRuntime
import com.cobblemon.mod.common.api.cinematic.model.sources.PositionSource
import com.google.gson.JsonObject
import net.minecraft.world.phys.Vec3
data class VariableVec3Position(
val prefix: String,
val offset: Vec3 = Vec3.ZERO,
val fallback: Vec3 = Vec3.ZERO
) : PositionSource() {
override val type: String = "variable_vec3"
init {
require(listOf(offset, fallback, fallback.add(offset)).all { it.x.isFinite() && it.y.isFinite() && it.z.isFinite() }) {
"Variable position offset and fallback must be finite"
}
}
fun evaluate(runtime: MoLangRuntime?): Vec3 {
if (runtime == null) return fallback.add(offset)
val vars = runtime.environment.variable.map
return runCatching {
val x = vars["${prefix}_x"]?.asDouble() ?: return fallback.add(offset)
val y = vars["${prefix}_y"]?.asDouble() ?: return fallback.add(offset)
val z = vars["${prefix}_z"]?.asDouble() ?: return fallback.add(offset)
Vec3(x + offset.x, y + offset.y, z + offset.z).takeIf {
it.x.isFinite() && it.y.isFinite() && it.z.isFinite()
} ?: fallback.add(offset)
}.getOrElse { fallback.add(offset) }
}
companion object {
const val TYPE = "variable_vec3"
fun fromJson(obj: JsonObject): VariableVec3Position {
val prefix = obj.get("prefix")?.asString
?: throw IllegalArgumentException("VariableVec3Position missing 'prefix'")
val off = obj.get("offset")?.asJsonObject
val offset = if (off != null) {
Vec3(off.get("x").asDouble, off.get("y").asDouble, off.get("z").asDouble)
} else Vec3.ZERO
val fb = obj.get("fallback")?.asJsonObject
val fallback = if (fb != null) {
Vec3(fb.get("x").asDouble, fb.get("y").asDouble, fb.get("z").asDouble)
} else Vec3.ZERO
return VariableVec3Position(prefix, offset, fallback)
}
}
}
@@ -0,0 +1,15 @@
/*
* 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.api.cinematic.model.tracks
abstract class ActorTrack {
abstract val trackType: String
abstract val duration: Float
}
@@ -0,0 +1,37 @@
/*
* 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.api.cinematic.model.tracks.builtin
import com.cobblemon.mod.common.api.cinematic.model.tracks.ActorTrack
import com.google.gson.JsonObject
data class AnimationTrack(
val cues: List<Cue>
) : ActorTrack() {
override val trackType: String = "animation"
override val duration: Float = cues.maxOfOrNull { it.time } ?: 0f
data class Cue(val time: Float, val animationName: String) {
init {
require(time.isFinite() && time >= 0f) { "Animation cue time must be finite and nonnegative" }
require(animationName.isNotBlank()) { "Animation name must be nonblank" }
}
}
companion object {
const val TYPE = "animation"
fun fromJson(obj: JsonObject): AnimationTrack {
val cues = obj.getAsJsonArray("cues").map { el ->
val cueObj = el.asJsonObject
Cue(time = cueObj.get("time").asFloat, animationName = cueObj.get("name").asString)
}
return AnimationTrack(cues)
}
}
}
@@ -0,0 +1,38 @@
/*
* 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.api.cinematic.model.tracks.builtin
import com.cobblemon.mod.common.api.cinematic.model.tracks.ActorTrack
import com.google.gson.JsonObject
import net.minecraft.world.phys.Vec3
/** Position-only attachment; zero duration keeps evaluation active throughout the beat. */
data class AttachTrack(
val parentActorId: String,
val offset: Vec3 = Vec3.ZERO
) : ActorTrack() {
override val trackType: String = TYPE
override val duration: Float = 0f
init {
require(offset.x.isFinite() && offset.y.isFinite() && offset.z.isFinite()) { "Attachment offset must be finite" }
}
companion object {
const val TYPE = "attach"
fun fromJson(obj: JsonObject): AttachTrack {
val o = obj.get("offset")?.asJsonObject
val off = if (o != null) Vec3(o.get("x").asDouble, o.get("y").asDouble, o.get("z").asDouble) else Vec3.ZERO
return AttachTrack(
parentActorId = obj.get("parentActorId").asString,
offset = off
)
}
}
}
@@ -0,0 +1,79 @@
/*
* 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.api.cinematic.model.tracks.builtin
import net.minecraft.world.phys.Vec3
import kotlin.math.sqrt
class CinematicSpline(controlPoints: List<Vec3>) {
private val points = controlPoints.fold(mutableListOf<Vec3>()) { result, point ->
require(point.x.isFinite() && point.y.isFinite() && point.z.isFinite()) { "Path points must be finite" }
if (result.isEmpty() || result.last().distanceToSqr(point) > 1e-12) result.add(point)
result
}.toList()
private val sampleCount = ((points.size - 1) * 64).coerceIn(1, 16384)
private val distances = DoubleArray(sampleCount + 1)
val length: Double get() = distances.last()
init {
require(points.isNotEmpty()) { "A path needs at least one point" }
var previous = parameterPosition(0.0)
for (i in 1..sampleCount) {
val position = parameterPosition(i.toDouble() / sampleCount)
distances[i] = distances[i - 1] + previous.distanceTo(position)
previous = position
}
}
fun position(progress: Double): Vec3 {
require(progress.isFinite()) { "Path progress must be finite" }
if (progress <= 0.0 || length <= 1e-9) return points.first()
if (progress >= 1.0) return points.last()
val distance = progress * length
var low = 0
var high = sampleCount
while (high - low > 1) {
val middle = (low + high) ushr 1
if (distances[middle] < distance) low = middle else high = middle
}
val span = distances[high] - distances[low]
val fraction = if (span > 1e-12) (distance - distances[low]) / span else 0.0
return parameterPosition((low + fraction) / sampleCount)
}
fun tangent(progress: Double): Vec3 {
val t = progress.coerceIn(0.0, 1.0)
val step = if (length > 0.0) (0.01 / length).coerceIn(1e-5, 0.01) else 0.01
val direction = position((t + step).coerceAtMost(1.0)).subtract(position((t - step).coerceAtLeast(0.0)))
return if (direction.lengthSqr() > 1e-12) direction.normalize() else Vec3.ZERO
}
private fun parameterPosition(progress: Double): Vec3 {
if (points.size == 1) return points.first()
if (points.size == 2) return points[0].lerp(points[1], progress)
val segment = progress.coerceIn(0.0, 1.0) * (points.size - 1)
val index = segment.toInt().coerceAtMost(points.size - 2)
val p1 = points[index]
val p2 = points[index + 1]
val p0 = points.getOrNull(index - 1) ?: p1.scale(2.0).subtract(p2)
val p3 = points.getOrNull(index + 2) ?: p2.scale(2.0).subtract(p1)
val t0 = 0.0
val t1 = sqrt(p0.distanceTo(p1))
val t2 = t1 + sqrt(p1.distanceTo(p2))
val t3 = t2 + sqrt(p2.distanceTo(p3))
val t = t1 + (segment - index) * (t2 - t1)
fun blend(a: Vec3, b: Vec3, start: Double, end: Double) = a.lerp(b, (t - start) / (end - start))
val a1 = blend(p0, p1, t0, t1)
val a2 = blend(p1, p2, t1, t2)
val a3 = blend(p2, p3, t2, t3)
val b1 = blend(a1, a2, t0, t2)
val b2 = blend(a2, a3, t1, t3)
return blend(b1, b2, t1, t2)
}
}
@@ -0,0 +1,40 @@
/*
* 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.api.cinematic.model.tracks.builtin
import com.cobblemon.mod.common.api.cinematic.model.tracks.ActorTrack
import com.google.gson.JsonObject
import net.minecraft.world.phys.Vec3
/** Zero duration keeps follow evaluation active throughout the beat. */
data class FollowActor(
val targetActorId: String,
val offset: Vec3,
val lagSecs: Float = 0f
) : ActorTrack() {
override val trackType: String = TYPE
override val duration: Float = 0f
init {
require(offset.x.isFinite() && offset.y.isFinite() && offset.z.isFinite()) { "Follow offset must be finite" }
require(lagSecs.isFinite() && lagSecs >= 0f) { "FollowActor lagSecs must be non-negative" }
}
companion object {
const val TYPE = "follow_actor"
fun fromJson(obj: JsonObject): FollowActor {
val o = obj.getAsJsonObject("offset")
return FollowActor(
targetActorId = obj.get("targetActorId").asString,
offset = Vec3(o.get("x").asDouble, o.get("y").asDouble, o.get("z").asDouble),
lagSecs = obj.get("lag")?.asFloat ?: 0f
)
}
}
}
@@ -0,0 +1,69 @@
/*
* 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.api.cinematic.model.tracks.builtin
import com.cobblemon.mod.common.api.cinematic.model.tracks.ActorTrack
import com.cobblemon.mod.common.api.cinematic.model.orderedKeyframes
import com.google.gson.JsonObject
import net.minecraft.world.phys.Vec3
/** Movement does not drive rotation; pair with a LookAtTrack to orient the actor. */
data class KeyframeMovement(
val keyframes: List<Kf>
) : ActorTrack() {
override val trackType: String = "keyframe_movement"
override val duration: Float = keyframes.maxOfOrNull { it.time } ?: 0f
init {
require(keyframes.all { it.position.x.isFinite() && it.position.y.isFinite() && it.position.z.isFinite() }) {
"Keyframe positions must be finite"
}
}
@Transient private val playbackKeyframes = orderedKeyframes(keyframes) { it.time }
data class Kf(val time: Float, val position: Vec3)
fun evaluate(time: Float): Vec3 {
val keyframes = playbackKeyframes
if (keyframes.size == 1) return keyframes[0].position
if (time <= keyframes.first().time) return keyframes.first().position
if (time >= keyframes.last().time) return keyframes.last().position
for (i in 0 until keyframes.size - 1) {
val a = keyframes[i]
val b = keyframes[i + 1]
if (time >= a.time && time <= b.time) {
val duration = b.time - a.time
val t = if (duration > 0f) (time - a.time) / duration else 0f
return Vec3(
a.position.x + (b.position.x - a.position.x) * t,
a.position.y + (b.position.y - a.position.y) * t,
a.position.z + (b.position.z - a.position.z) * t
)
}
}
return keyframes.last().position
}
companion object {
const val TYPE = "keyframe_movement"
fun fromJson(obj: JsonObject): KeyframeMovement {
val kfs = obj.getAsJsonArray("keyframes").map { el ->
val kfObj = el.asJsonObject
val pos = kfObj.getAsJsonObject("position")
Kf(
time = kfObj.get("time").asFloat,
position = Vec3(pos.get("x").asDouble, pos.get("y").asDouble, pos.get("z").asDouble)
)
}
return KeyframeMovement(kfs)
}
}
}
@@ -0,0 +1,49 @@
/*
* 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.api.cinematic.model.tracks.builtin
import com.cobblemon.mod.common.api.cinematic.model.tracks.ActorTrack
import com.google.gson.JsonObject
import net.minecraft.world.phys.Vec3
import kotlin.math.atan2
import kotlin.math.sqrt
data class LookAtTrack(
val target: Vec3
) : ActorTrack() {
override val trackType: String = "look_at"
override val duration: Float = 0f
init {
require(target.x.isFinite() && target.y.isFinite() && target.z.isFinite()) { "Look-at target must be finite" }
}
fun computeYRot(fromPos: Vec3): Float {
val dx = target.x - fromPos.x
val dz = target.z - fromPos.z
return Math.toDegrees(atan2(-dx, dz)).toFloat()
}
fun computeXRot(fromPos: Vec3): Float {
val dx = target.x - fromPos.x
val dy = target.y - fromPos.y
val dz = target.z - fromPos.z
val horizontal = sqrt(dx * dx + dz * dz)
return Math.toDegrees(-atan2(dy, horizontal)).toFloat()
}
companion object {
const val TYPE = "look_at"
fun fromJson(obj: JsonObject): LookAtTrack {
val targetObj = obj.getAsJsonObject("target")
val target = Vec3(targetObj.get("x").asDouble, targetObj.get("y").asDouble, targetObj.get("z").asDouble)
return LookAtTrack(target)
}
}
}
@@ -0,0 +1,29 @@
/*
* 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.api.cinematic.model.tracks.builtin
import com.cobblemon.mod.common.api.cinematic.model.tracks.ActorTrack
import com.cobblemon.mod.common.entity.PoseType
import com.google.gson.JsonObject
data class PoseLockTrack(
val pose: PoseType
) : ActorTrack() {
override val trackType: String = TYPE
override val duration: Float = 0f
companion object {
const val TYPE = "pose_lock"
fun fromJson(obj: JsonObject): PoseLockTrack {
val name = obj.get("pose").asString
val pose = PoseType.valueOf(name.uppercase())
return PoseLockTrack(pose)
}
}
}
@@ -0,0 +1,62 @@
/*
* 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.api.cinematic.model.tracks.builtin
import kotlin.math.atan2
import kotlin.math.sqrt
import java.util.Locale
import com.cobblemon.mod.common.api.cinematic.model.tracks.ActorTrack
import com.google.gson.JsonObject
import net.minecraft.world.phys.Vec3
data class SplineMovement(
val controlPoints: List<Vec3>,
val durationSecs: Float,
val facing: PathFacing = PathFacing.GROUND,
val headingOffset: Float = 0f
) : ActorTrack() {
override val trackType: String = TYPE
override val duration: Float = durationSecs
init {
require(controlPoints.size >= 2) { "SplineMovement needs at least 2 control points" }
require(durationSecs.isFinite() && durationSecs > 0f) { "SplineMovement duration must be positive" }
require(headingOffset.isFinite()) { "SplineMovement heading offset must be finite" }
}
@Transient private val path = CinematicSpline(controlPoints)
val pathLength: Double get() = path.length
fun evaluate(time: Float): Vec3 = path.position((time / durationSecs).toDouble().coerceIn(0.0, 1.0))
fun facingAt(time: Float): Pair<Float, Float>? {
if (facing == PathFacing.FIXED) return null
val direction = path.tangent((time / durationSecs).toDouble())
if (direction.lengthSqr() < 1e-12) return null
val yaw = Math.toDegrees(atan2(-direction.x, direction.z)).toFloat() + headingOffset
val pitch = if (facing == PathFacing.FLIGHT) -Math.toDegrees(atan2(direction.y,
sqrt(direction.x * direction.x + direction.z * direction.z))).toFloat() else 0f
return yaw to pitch
}
companion object {
const val TYPE = "spline_movement"
fun fromJson(obj: JsonObject): SplineMovement {
val points = obj.getAsJsonArray("controlPoints").map { el ->
val p = el.asJsonObject
Vec3(p.get("x").asDouble, p.get("y").asDouble, p.get("z").asDouble)
}
val d = obj.get("duration").asFloat
val facing = obj.get("facing")?.asString?.let { PathFacing.valueOf(it.uppercase(Locale.ROOT)) } ?: PathFacing.GROUND
return SplineMovement(points, d, facing, obj.get("headingOffset")?.asFloat ?: 0f)
}
}
}
enum class PathFacing { FIXED, GROUND, FLIGHT }
@@ -0,0 +1,49 @@
/*
* 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.api.cinematic.model.tracks.builtin
import com.cobblemon.mod.common.api.cinematic.model.tracks.ActorTrack
import com.google.gson.JsonObject
import java.util.Locale
import kotlin.math.abs
import net.minecraft.world.phys.Vec3
data class WaypointPathfinding(
val controlPoints: List<Vec3>,
val speedMultiplier: Float = 1f,
val durationSecs: Float = 30f,
val travelMode: TravelMode = TravelMode.AUTO
) : ActorTrack() {
override val trackType = TYPE
override val duration = durationSecs
init {
require(controlPoints.size in 2..64) { "Pathfinding needs 2 to 64 waypoints" }
require(controlPoints.all { it.x.isFinite() && it.y.isFinite() && it.z.isFinite() &&
abs(it.x) <= 30_000_000 && abs(it.z) <= 30_000_000 }) { "Waypoints must be finite world positions" }
require(controlPoints.zipWithNext().all { (a, b) -> a.distanceTo(b) <= 64 }) { "Consecutive waypoints must be within 64 blocks" }
require(speedMultiplier.isFinite() && speedMultiplier in 0.1f..4f) { "Native movement speed multiplier must be between 0.1 and 4" }
require(durationSecs.isFinite() && durationSecs in 0.05f..600f) { "Pathfinding time budget must be between 0.05 and 600 seconds" }
}
enum class TravelMode { AUTO, GROUND, FLIGHT }
companion object {
const val TYPE = "pathfinding"
fun fromJson(data: JsonObject) = WaypointPathfinding(
controlPoints = data.getAsJsonArray("controlPoints").map {
val point = it.asJsonObject
Vec3(point.get("x").asDouble, point.get("y").asDouble, point.get("z").asDouble)
},
speedMultiplier = data.get("speedMultiplier")?.asFloat ?: 1f,
durationSecs = data.get("duration")?.asFloat ?: 30f,
travelMode = data.get("travelMode")?.asString?.let { TravelMode.valueOf(it.uppercase(Locale.ROOT)) } ?: TravelMode.AUTO
)
}
}
@@ -0,0 +1,79 @@
/*
* 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.api.cinematic.registry
import com.cobblemon.mod.common.api.cinematic.model.sources.builtin.ConstraintModifier
import com.cobblemon.mod.common.api.cinematic.model.sources.builtin.CranePosition
import com.cobblemon.mod.common.api.cinematic.model.sources.builtin.DollyPosition
import com.cobblemon.mod.common.api.cinematic.model.sources.builtin.ExpressionFov
import com.cobblemon.mod.common.api.cinematic.model.sources.builtin.ExpressionVec3Position
import com.cobblemon.mod.common.api.cinematic.model.sources.builtin.FixedFov
import com.cobblemon.mod.common.api.cinematic.model.sources.builtin.FixedPosition
import com.cobblemon.mod.common.api.cinematic.model.sources.builtin.FixedRoll
import com.cobblemon.mod.common.api.cinematic.model.sources.builtin.FixedRotation
import com.cobblemon.mod.common.api.cinematic.model.sources.builtin.FollowActorPosition
import com.cobblemon.mod.common.api.cinematic.model.sources.builtin.KeyframeFov
import com.cobblemon.mod.common.api.cinematic.model.sources.builtin.KeyframePosition
import com.cobblemon.mod.common.api.cinematic.model.sources.builtin.KeyframeRoll
import com.cobblemon.mod.common.api.cinematic.model.sources.builtin.KeyframeRotation
import com.cobblemon.mod.common.api.cinematic.model.sources.builtin.LookAlongPath
import com.cobblemon.mod.common.api.cinematic.model.sources.builtin.LookAt
import com.cobblemon.mod.common.api.cinematic.model.sources.builtin.OffsetModifier
import com.cobblemon.mod.common.api.cinematic.model.sources.builtin.ShakeDerivedRoll
import com.cobblemon.mod.common.api.cinematic.model.sources.builtin.OrbitPosition
import com.cobblemon.mod.common.api.cinematic.model.sources.builtin.ShakeModifier
import com.cobblemon.mod.common.api.cinematic.model.sources.builtin.SmoothingModifier
import com.cobblemon.mod.common.api.cinematic.model.sources.builtin.SplinePosition
import com.cobblemon.mod.common.api.cinematic.model.sources.builtin.TruckPosition
import com.cobblemon.mod.common.api.cinematic.model.sources.builtin.VariableVec3Position
import com.cobblemon.mod.common.api.cinematic.model.tracks.builtin.AnimationTrack
import com.cobblemon.mod.common.api.cinematic.model.tracks.builtin.AttachTrack
import com.cobblemon.mod.common.api.cinematic.model.tracks.builtin.FollowActor
import com.cobblemon.mod.common.api.cinematic.model.tracks.builtin.KeyframeMovement
import com.cobblemon.mod.common.api.cinematic.model.tracks.builtin.LookAtTrack
import com.cobblemon.mod.common.api.cinematic.model.tracks.builtin.PoseLockTrack
import com.cobblemon.mod.common.api.cinematic.model.tracks.builtin.SplineMovement
import com.cobblemon.mod.common.api.cinematic.model.tracks.builtin.WaypointPathfinding
object SceneBuiltinSources {
fun register() {
SceneSourceRegistry.registerPositionSource(FixedPosition.TYPE, FixedPosition::fromJson)
SceneSourceRegistry.registerPositionSource(KeyframePosition.TYPE, KeyframePosition::fromJson)
SceneSourceRegistry.registerPositionSource(SplinePosition.TYPE, SplinePosition::fromJson)
SceneSourceRegistry.registerPositionSource(OrbitPosition.TYPE, OrbitPosition::fromJson)
SceneSourceRegistry.registerPositionSource(FollowActorPosition.TYPE, FollowActorPosition::fromJson)
SceneSourceRegistry.registerPositionSource(DollyPosition.TYPE, DollyPosition::fromJson)
SceneSourceRegistry.registerPositionSource(TruckPosition.TYPE, TruckPosition::fromJson)
SceneSourceRegistry.registerPositionSource(CranePosition.TYPE, CranePosition::fromJson)
SceneSourceRegistry.registerPositionSource(VariableVec3Position.TYPE, VariableVec3Position::fromJson)
SceneSourceRegistry.registerPositionSource(ExpressionVec3Position.TYPE, ExpressionVec3Position::fromJson)
SceneSourceRegistry.registerRotationSource(FixedRotation.TYPE, FixedRotation::fromJson)
SceneSourceRegistry.registerRotationSource(LookAt.TYPE, LookAt::fromJson)
SceneSourceRegistry.registerRotationSource(KeyframeRotation.TYPE, KeyframeRotation::fromJson)
SceneSourceRegistry.registerRotationSource(LookAlongPath.TYPE, LookAlongPath::fromJson)
SceneSourceRegistry.registerFovSource(FixedFov.TYPE, FixedFov::fromJson)
SceneSourceRegistry.registerFovSource(KeyframeFov.TYPE, KeyframeFov::fromJson)
SceneSourceRegistry.registerFovSource(ExpressionFov.TYPE, ExpressionFov::fromJson)
SceneSourceRegistry.registerRollSource(FixedRoll.TYPE, FixedRoll::fromJson)
SceneSourceRegistry.registerRollSource(KeyframeRoll.TYPE, KeyframeRoll::fromJson)
SceneSourceRegistry.registerRollSource(ShakeDerivedRoll.TYPE, ShakeDerivedRoll::fromJson)
SceneSourceRegistry.registerModifier(SmoothingModifier.TYPE, SmoothingModifier::fromJson)
SceneSourceRegistry.registerModifier(ShakeModifier.TYPE, ShakeModifier::fromJson)
SceneSourceRegistry.registerModifier(OffsetModifier.TYPE, OffsetModifier::fromJson)
SceneSourceRegistry.registerModifier(ConstraintModifier.TYPE, ConstraintModifier::fromJson)
SceneSourceRegistry.registerActorTrack(KeyframeMovement.TYPE, KeyframeMovement::fromJson)
SceneSourceRegistry.registerActorTrack(AnimationTrack.TYPE, AnimationTrack::fromJson)
SceneSourceRegistry.registerActorTrack(LookAtTrack.TYPE, LookAtTrack::fromJson)
SceneSourceRegistry.registerActorTrack(SplineMovement.TYPE, SplineMovement::fromJson)
SceneSourceRegistry.registerActorTrack(WaypointPathfinding.TYPE, WaypointPathfinding::fromJson)
SceneSourceRegistry.registerActorTrack(FollowActor.TYPE, FollowActor::fromJson)
SceneSourceRegistry.registerActorTrack(PoseLockTrack.TYPE, PoseLockTrack::fromJson)
SceneSourceRegistry.registerActorTrack(AttachTrack.TYPE, AttachTrack::fromJson)
}
}
@@ -0,0 +1,411 @@
/*
* 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.api.cinematic.registry
import com.cobblemon.mod.common.api.camera.CameraRotation
import com.cobblemon.mod.common.api.camera.model.HudElement
import com.cobblemon.mod.common.api.camera.model.ParameterDefault
import com.cobblemon.mod.common.api.camera.model.ParameterDefinition
import com.cobblemon.mod.common.api.camera.model.ParameterType
import com.cobblemon.mod.common.api.camera.model.PositionExpression
import com.cobblemon.mod.common.api.cinematic.model.ActorDeclaration
import com.cobblemon.mod.common.api.cinematic.model.ActorDirection
import com.cobblemon.mod.common.api.cinematic.model.ActorKind
import com.cobblemon.mod.common.api.cinematic.model.Beat
import com.cobblemon.mod.common.api.cinematic.model.BeatTransition
import com.cobblemon.mod.common.api.cinematic.model.CutEntry
import com.cobblemon.mod.common.api.cinematic.model.CutTrack
import com.cobblemon.mod.common.api.cinematic.model.CutTransition
import com.cobblemon.mod.common.api.cinematic.model.DisconnectPolicy
import com.cobblemon.mod.common.api.cinematic.model.FeedDeclaration
import com.cobblemon.mod.common.api.cinematic.model.FeedKind
import com.cobblemon.mod.common.api.cinematic.model.Scene
import com.cobblemon.mod.common.api.cinematic.model.SceneCue
import com.cobblemon.mod.common.api.cinematic.model.SceneGuiAction
import com.cobblemon.mod.common.api.cinematic.model.SceneGuiActionType
import com.cobblemon.mod.common.api.cinematic.model.SceneGuiArgValue
import com.cobblemon.mod.common.api.cinematic.model.SceneGuiTrack
import com.cobblemon.mod.common.api.cinematic.model.Shot
import com.cobblemon.mod.common.api.cinematic.model.StartPose
import com.cobblemon.mod.common.api.cinematic.model.StartPoseMode
import com.cobblemon.mod.common.api.cinematic.model.sources.Lens
import com.cobblemon.mod.common.util.asExpressionLike
import com.google.gson.JsonArray
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import com.google.gson.JsonParseException
import java.lang.reflect.Type
object SceneDeserializer : JsonDeserializer<Scene> {
override fun deserialize(json: JsonElement, typeOfT: Type, ctx: JsonDeserializationContext): Scene {
val obj = json.asJsonObject
val version = obj.get("version")
if (version?.isJsonPrimitive != true || !version.asJsonPrimitive.isNumber ||
version.asString.toBigDecimalOrNull()?.compareTo(2.toBigDecimal()) != 0) {
throw JsonParseException("Unsupported cinematic scene version $version; expected 2")
}
val id = obj.get("sceneId")?.asString
?: throw JsonParseException("Scene missing 'sceneId' field")
val startBeatId = obj.get("startBeatId")?.asString
?: throw JsonParseException("Scene '$id' missing 'startBeatId' field")
val parameters = parseParameters(obj.get("parameters")?.asJsonObject, id)
val actors = parseActors(obj.get("actors")?.asJsonObject, id)
val shots = parseShots(obj.get("shots")?.asJsonObject, id)
val feeds = parseFeeds(obj.get("feeds")?.asJsonArray)
val beats = parseBeats(obj.get("beats")?.asJsonObject, id)
val transitions = parseTransitions(obj.get("transitions")?.asJsonArray, id)
val damageImmunity = obj.get("damageImmunityDuringTakeover")?.asBoolean ?: false
val disconnectPolicy = obj.get("disconnectPolicy")?.asString
?.let { parseEnum<DisconnectPolicy>(it, "Scene '$id' disconnectPolicy") }
?: DisconnectPolicy.END_SCENE
return Scene(
id = id,
parameters = parameters,
actors = actors,
shots = shots,
feeds = feeds,
startBeatId = startBeatId,
beats = beats,
transitions = transitions,
version = 2,
damageImmunityDuringTakeover = damageImmunity,
disconnectPolicy = disconnectPolicy
)
}
private fun parseParameters(obj: JsonObject?, sceneId: String): Map<String, ParameterDefinition> {
if (obj == null) return emptyMap()
return obj.entrySet().associate { (name, value) ->
name to parseParameter(value.asJsonObject, name, sceneId)
}
}
private fun parseParameter(obj: JsonObject, paramName: String, sceneId: String): ParameterDefinition {
val typeStr = obj.get("type")?.asString
?: throw JsonParseException("Parameter '$paramName' in scene '$sceneId' missing 'type'")
val type = when (typeStr.lowercase()) {
"number" -> ParameterType.NUMBER
"vec3" -> ParameterType.VEC3
"string" -> ParameterType.STRING
else -> throw JsonParseException("Parameter '$paramName' in scene '$sceneId' has unknown type '$typeStr'")
}
val description = obj.get("description")?.asString
val default: ParameterDefault? = when {
!obj.has("default") -> null
type == ParameterType.NUMBER -> {
val number = obj.get("default").asDouble
if (!number.isFinite()) throw JsonParseException("Parameter '$paramName' in scene '$sceneId' requires a finite default")
ParameterDefault.NumberDefault(number)
}
type == ParameterType.VEC3 -> {
val vec = obj.get("default").asJsonObject
val x = vec.get("x").asDouble
val y = vec.get("y").asDouble
val z = vec.get("z").asDouble
if (!x.isFinite() || !y.isFinite() || !z.isFinite()) {
throw JsonParseException("Parameter '$paramName' in scene '$sceneId' requires a finite default vector")
}
ParameterDefault.Vec3Default(x, y, z)
}
type == ParameterType.STRING -> ParameterDefault.StringDefault(obj.get("default").asString)
else -> null
}
return ParameterDefinition(type = type, description = description, default = default)
}
private fun parseActors(obj: JsonObject?, sceneId: String): Map<String, ActorDeclaration> {
if (obj == null) return emptyMap()
return obj.entrySet().associate { (key, value) ->
val actorObj = value.asJsonObject
val id = actorObj.get("id")?.asString ?: key
val kindStr = actorObj.get("kind")?.asString
?: throw JsonParseException("Actor '$key' in scene '$sceneId' missing 'kind'")
val kind = when (kindStr.lowercase()) {
"player_takeover" -> ActorKind.PlayerTakeover
"real_entity" -> ActorKind.RealEntity
"synthesized_pokemon" -> ActorKind.SynthesizedPokemon
"synthesized_bedrock" -> ActorKind.SynthesizedBedrock
else -> throw JsonParseException("Actor '$key' in scene '$sceneId' has unknown kind '$kindStr'")
}
key to ActorDeclaration(id = id, kind = kind)
}
}
private fun parseShots(obj: JsonObject?, sceneId: String): Map<String, Shot> {
if (obj == null) return emptyMap()
return obj.entrySet().associate { (key, value) ->
val shotObj = value.asJsonObject
val id = shotObj.get("id")?.asString ?: key
val positionObj = shotObj.get("position")?.asJsonObject
?: throw JsonParseException("Shot '$key' in scene '$sceneId' missing 'position'")
val rotationObj = shotObj.get("rotation")?.asJsonObject
?: throw JsonParseException("Shot '$key' in scene '$sceneId' missing 'rotation'")
val positionType = positionObj.get("type")?.asString
?: throw JsonParseException("Shot '$key' position missing 'type' discriminator")
val position = SceneSourceRegistry.deserializePositionSource(positionType, positionObj)
?: throw JsonParseException("Shot '$key' position type '$positionType' not registered")
val rotationType = rotationObj.get("type")?.asString
?: throw JsonParseException("Shot '$key' rotation missing 'type' discriminator")
val rotation = SceneSourceRegistry.deserializeRotationSource(rotationType, rotationObj)
?: throw JsonParseException("Shot '$key' rotation type '$rotationType' not registered")
val fov = shotObj.get("fov")?.asJsonObject?.let { fovObj ->
val tag = fovObj.get("type")?.asString
?: throw JsonParseException("Shot '$key' fov missing 'type' discriminator")
SceneSourceRegistry.deserializeFovSource(tag, fovObj)
?: throw JsonParseException("Shot '$key' fov type '$tag' not registered")
}
val roll = shotObj.get("roll")?.asJsonObject?.let { rollObj ->
val tag = rollObj.get("type")?.asString
?: throw JsonParseException("Shot '$key' roll missing 'type' discriminator")
SceneSourceRegistry.deserializeRollSource(tag, rollObj)
?: throw JsonParseException("Shot '$key' roll type '$tag' not registered")
}
val lens = shotObj.get("lens")?.asJsonObject?.let { parseLens(it) }
val modifiers = shotObj.get("modifiers")?.asJsonArray?.map { element ->
val modObj = element.asJsonObject
val tag = modObj.get("type")?.asString
?: throw JsonParseException("Shot '$key' modifier missing 'type' discriminator")
SceneSourceRegistry.deserializeModifier(tag, modObj)
?: throw JsonParseException("Shot '$key' modifier type '$tag' not registered")
} ?: emptyList()
key to Shot(
id = id,
position = position,
rotation = rotation,
fov = fov,
roll = roll,
lens = lens,
modifiers = modifiers
)
}
}
private fun parseLens(obj: JsonObject): Lens {
val aspectCrop = if (obj.has("aspectCrop")) obj.get("aspectCrop").asFloat else null
val letterbox = obj.get("letterbox")?.asFloat ?: 0f
return Lens(aspectCrop = aspectCrop, letterbox = letterbox)
}
private fun parseFeeds(array: JsonArray?): List<FeedDeclaration> {
if (array == null) return emptyList()
return array.map { element ->
val obj = element.asJsonObject
val id = obj.get("id")?.asString
?: throw JsonParseException("Feed missing 'id'")
val kindObj = obj.get("kind")?.asJsonObject
?: throw JsonParseException("Feed '$id' missing 'kind'")
val kindType = kindObj.get("type")?.asString
?: throw JsonParseException("Feed '$id' kind missing 'type' discriminator")
val kind = when (kindType.lowercase()) {
"fullscreen" -> FeedKind.Fullscreen
"pip" -> FeedKind.Pip(
corner = FeedKind.Corner.valueOf(kindObj.get("corner").asString.uppercase()),
size = kindObj.get("size").asFloat
)
"split" -> FeedKind.Split(
side = FeedKind.Side.valueOf(kindObj.get("side").asString.uppercase()),
size = kindObj.get("size").asFloat
)
"diagonal" -> FeedKind.Diagonal(
half = FeedKind.DiagonalHalf.valueOf(kindObj.get("half").asString.uppercase())
)
else -> throw JsonParseException("Feed '$id' unknown kind type '$kindType'")
}
FeedDeclaration(id = id, kind = kind)
}
}
private fun parseBeats(obj: JsonObject?, sceneId: String): Map<String, Beat> {
if (obj == null) throw JsonParseException("Scene '$sceneId' missing 'beats'")
return obj.entrySet().associate { (key, value) ->
val beatObj = value.asJsonObject
val id = beatObj.get("id")?.asString ?: key
val duration = if (beatObj.has("duration")) beatObj.get("duration").asFloat else null
val isLooping = beatObj.get("isLooping")?.asBoolean ?: false
val speed = beatObj.get("speed")?.asFloat ?: 1.0f
val actorDirections = parseActorDirections(beatObj.get("actorDirections")?.asJsonObject, id, sceneId)
val cuts = parseCuts(beatObj.get("cuts")?.asJsonObject, id)
val renderPlayerBody = beatObj.get("renderPlayerBody")?.asBoolean ?: true
val hiddenHudElements = parseHudSet(beatObj.get("hiddenHudElements"))
val guiTrack: SceneGuiTrack? = parseGuiTrack(beatObj.get("guiTrack")?.asJsonObject)
key to Beat(
id = id,
duration = duration,
isLooping = isLooping,
speed = speed,
actorDirections = actorDirections,
cuts = cuts,
guiTrack = guiTrack,
renderPlayerBody = renderPlayerBody,
hiddenHudElements = hiddenHudElements,
cues = beatObj.getAsJsonArray("cues")?.map { element ->
val cue = element.asJsonObject
SceneCue(cue.get("id").asString, cue.get("time").asFloat)
}.orEmpty()
)
}
}
private fun parseGuiTrack(obj: JsonObject?): SceneGuiTrack? {
if (obj == null) return null
val actionsArray = obj.getAsJsonArray("actions") ?: throw JsonParseException("guiTrack missing 'actions'")
val actions = actionsArray.map { element ->
val a = element.asJsonObject
val time = a.get("time")?.asFloat
?: throw JsonParseException("guiTrack action missing 'time'")
val actionType = a.get("action")?.asString
?: throw JsonParseException("guiTrack action missing 'action'")
val guiId = a.get("guiId")?.asString
?: throw JsonParseException("guiTrack action missing 'guiId'")
val args = mutableMapOf<String, SceneGuiArgValue>()
a.getAsJsonObject("args")?.entrySet()?.forEach { (key, value) ->
val entry = value.asJsonObject
val argType = entry.get("type")?.asString
?: throw JsonParseException("guiTrack action '$guiId' arg '$key' missing 'type' (expected 'expression' or 'string')")
val argVal = entry.get("value")?.asString
?: throw JsonParseException("guiTrack action '$guiId' arg '$key' missing 'value'")
args[key] = when (argType.lowercase()) {
"expression" -> SceneGuiArgValue.Expression(argVal)
"string" -> SceneGuiArgValue.StringLiteral(argVal)
else -> throw JsonParseException("guiTrack action '$guiId' arg '$key' unknown type '$argType'")
}
}
SceneGuiAction(
time = time,
type = parseEnum<SceneGuiActionType>(actionType, "guiTrack '$guiId' action"),
guiId = guiId,
args = args
)
}
return SceneGuiTrack(actions)
}
private fun parseActorDirections(obj: JsonObject?, beatId: String, sceneId: String): Map<String, ActorDirection> {
if (obj == null) return emptyMap()
return obj.entrySet().associate { (key, value) ->
val dirObj = value.asJsonObject
val visible = if (dirObj.has("visible")) dirObj.get("visible").asBoolean else null
val startPose = dirObj.get("startPose")?.asJsonObject?.let { parseStartPose(it) }
val startPoseMode = dirObj.get("startPoseMode")?.asString?.let { s ->
parseEnum<StartPoseMode>(s, "Actor '$key' in scene '$sceneId' beat '$beatId' startPoseMode")
}
val tracks = dirObj.get("tracks")?.asJsonArray?.map { element ->
val trackObj = element.asJsonObject
val tag = trackObj.get("trackType")?.asString
?: throw JsonParseException("Actor '$key' in scene '$sceneId' beat '$beatId' track missing 'trackType' discriminator")
SceneSourceRegistry.deserializeActorTrack(tag, trackObj)
?: throw JsonParseException("Actor '$key' track type '$tag' not registered in scene '$sceneId' beat '$beatId'")
} ?: emptyList()
key to ActorDirection(
visible = visible,
startPose = startPose,
startPoseMode = startPoseMode,
tracks = tracks
)
}
}
private fun parseStartPose(obj: JsonObject): StartPose {
val positionObj = obj.get("position")
?: throw JsonParseException("StartPose missing 'position'")
val position = PositionExpression.fromJson(positionObj)
val rotObj = obj.get("rotation")?.asJsonObject
?: throw JsonParseException("StartPose missing 'rotation'")
val rotation = CameraRotation(
yaw = rotObj.get("yaw").asFloat,
pitch = rotObj.get("pitch").asFloat
)
val blendDurationSeconds = obj.get("blendDurationSeconds")?.asFloat ?: 0.5f
return StartPose(position = position, rotation = rotation, blendDurationSeconds = blendDurationSeconds)
}
private fun parseCuts(obj: JsonObject?, beatId: String): Map<String, CutTrack> {
if (obj == null) return emptyMap()
return obj.entrySet().associate { (feedId, cutValue) ->
val entriesArray = cutValue.asJsonObject.get("entries")?.asJsonArray
?: throw JsonParseException("Beat '$beatId' cut for feed '$feedId' missing 'entries'")
val entries = entriesArray.map { element ->
val entryObj = element.asJsonObject
CutEntry(
time = entryObj.get("time").asFloat,
shotId = entryObj.get("shotId").asString,
transition = parseCutTransition(entryObj.get("transition"))
)
}
feedId to CutTrack(entries = entries)
}
}
private fun parseCutTransition(element: JsonElement?): CutTransition {
if (element == null) return CutTransition.Instant
val obj = element.asJsonObject
val type = obj.get("type")?.asString ?: "instant"
return when (type.lowercase()) {
"instant" -> CutTransition.Instant
"blend" -> CutTransition.Blend(duration = obj.get("duration").asFloat)
else -> throw JsonParseException("Unknown CutTransition type '$type'")
}
}
private fun parseHudSet(element: JsonElement?): Set<HudElement> {
if (element == null) return emptySet()
if (element.isJsonPrimitive && element.asJsonPrimitive.isBoolean) {
return if (element.asBoolean) HudElement.ALL else emptySet()
}
if (element.isJsonArray) {
return element.asJsonArray.map {
val id = it.asString
HudElement.fromId(id) ?: throw JsonParseException("Unknown hidden HUD element '$id'")
}.toSet()
}
throw JsonParseException("hiddenHudElements must be a boolean or an array of HUD element IDs")
}
private inline fun <reified T : Enum<T>> parseEnum(value: String, field: String): T =
enumValues<T>().firstOrNull { it.name.equals(value, ignoreCase = true) }
?: throw JsonParseException("$field has unknown value '$value'")
private fun parseTransitions(array: JsonArray?, sceneId: String): List<BeatTransition> {
if (array == null) return emptyList()
return array.mapIndexed { index, element ->
val obj = element.asJsonObject
val sourceBeatId = obj.get("sourceBeatId")?.asString
?: throw JsonParseException("Scene '$sceneId' transition $index missing 'sourceBeatId'")
val endsScene = obj.get("endsScene")?.asBoolean ?: false
val targetBeatId = obj.get("targetBeatId")?.asString
?: if (endsScene) "" else throw JsonParseException("Scene '$sceneId' transition $index missing 'targetBeatId'")
val condition = (obj.get("condition")?.asString ?: "1.0").asExpressionLike()
val transition = parseCutTransition(obj.get("transition"))
BeatTransition(
sourceBeatId = sourceBeatId,
targetBeatId = targetBeatId,
condition = condition,
transition = transition,
endsScene = endsScene
)
}
}
}
@@ -0,0 +1,87 @@
/*
* 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.api.cinematic.registry
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.camera.result.ValidationError
import com.cobblemon.mod.common.api.cinematic.model.Scene
import com.cobblemon.mod.common.api.data.DataRegistry
import com.cobblemon.mod.common.api.reactive.SimpleObservable
import com.cobblemon.mod.common.util.cobblemonResource
import com.google.gson.GsonBuilder
import com.google.gson.JsonParser
import net.minecraft.server.level.ServerPlayer
import net.minecraft.server.packs.PackType
import net.minecraft.server.packs.resources.ResourceManager
import java.io.InputStreamReader
/** Scenes load only on the server, from data/<namespace>/cinematic_scenes/<id>.json. */
object SceneRegistry : DataRegistry {
override val id = cobblemonResource("cinematic_scenes")
override val type = PackType.SERVER_DATA
override val observable = SimpleObservable<SceneRegistry>()
private val gson = GsonBuilder()
.registerTypeAdapter(Scene::class.java, SceneDeserializer)
.create()
private val scenes = mutableMapOf<String, Scene>()
private val sources = mutableMapOf<String, String>()
override fun reload(manager: ResourceManager) {
scenes.clear()
sources.clear()
val resources = manager.listResources("cinematic_scenes") { path ->
path.path.endsWith(".json")
}
for ((identifier, resource) in resources.entries.sortedBy { it.key.toString() }) {
try {
resource.open().use { stream ->
val source = JsonParser.parseReader(InputStreamReader(stream, Charsets.UTF_8)).asJsonObject
val scene = gson.fromJson(source, Scene::class.java)
val errors = scene.validate()
val hasErrors = errors.any { it.severity == ValidationError.Severity.ERROR }
if (hasErrors) {
Cobblemon.LOGGER.error("[SceneRegistry] Validation failed for '$identifier':")
errors.forEach { Cobblemon.LOGGER.error(" [$it]") }
return@use
}
if (errors.isNotEmpty()) {
Cobblemon.LOGGER.warn("[SceneRegistry] Warnings for scene '${scene.id}':")
errors.forEach { Cobblemon.LOGGER.warn(" $it") }
}
if (scenes.containsKey(scene.id)) {
Cobblemon.LOGGER.warn("[SceneRegistry] Duplicate scene id '${scene.id}' in $identifier, overwriting")
}
scenes[scene.id] = scene
sources[scene.id] = source.toString()
Cobblemon.LOGGER.debug("[SceneRegistry] Loaded scene '${scene.id}' from $identifier")
}
} catch (e: Exception) {
Cobblemon.LOGGER.error("[SceneRegistry] Error loading scene from $identifier", e)
}
}
Cobblemon.LOGGER.info("[SceneRegistry] Loaded ${scenes.size} scenes")
observable.emit(this)
}
override fun sync(player: ServerPlayer) {
}
fun getScene(id: String): Scene? = scenes[id]
fun getSource(id: String): String? = sources[id]
fun getAllScenes(): Collection<Scene> = scenes.values
}
@@ -0,0 +1,79 @@
/*
* 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.api.cinematic.registry
import com.cobblemon.mod.common.api.cinematic.model.sources.FovSource
import com.cobblemon.mod.common.api.cinematic.model.sources.PositionSource
import com.cobblemon.mod.common.api.cinematic.model.sources.RollSource
import com.cobblemon.mod.common.api.cinematic.model.sources.RotationSource
import com.cobblemon.mod.common.api.cinematic.model.sources.ShotModifier
import com.cobblemon.mod.common.api.cinematic.model.tracks.ActorTrack
import com.google.gson.JsonObject
/** Registration is single-threaded at initialization; subsequent lookups are read-only. */
object SceneSourceRegistry {
private val positionSources = mutableMapOf<String, (JsonObject) -> PositionSource>()
private val rotationSources = mutableMapOf<String, (JsonObject) -> RotationSource>()
private val fovSources = mutableMapOf<String, (JsonObject) -> FovSource>()
private val rollSources = mutableMapOf<String, (JsonObject) -> RollSource>()
private val modifiers = mutableMapOf<String, (JsonObject) -> ShotModifier>()
private val actorTracks = mutableMapOf<String, (JsonObject) -> ActorTrack>()
fun registerPositionSource(type: String, factory: (JsonObject) -> PositionSource) {
positionSources[type] = factory
}
fun registerRotationSource(type: String, factory: (JsonObject) -> RotationSource) {
rotationSources[type] = factory
}
fun registerFovSource(type: String, factory: (JsonObject) -> FovSource) {
fovSources[type] = factory
}
fun registerRollSource(type: String, factory: (JsonObject) -> RollSource) {
rollSources[type] = factory
}
fun registerModifier(type: String, factory: (JsonObject) -> ShotModifier) {
modifiers[type] = factory
}
fun registerActorTrack(type: String, factory: (JsonObject) -> ActorTrack) {
actorTracks[type] = factory
}
fun deserializePositionSource(type: String, obj: JsonObject): PositionSource? =
positionSources[type]?.invoke(obj)
fun deserializeRotationSource(type: String, obj: JsonObject): RotationSource? =
rotationSources[type]?.invoke(obj)
fun deserializeFovSource(type: String, obj: JsonObject): FovSource? =
fovSources[type]?.invoke(obj)
fun deserializeRollSource(type: String, obj: JsonObject): RollSource? =
rollSources[type]?.invoke(obj)
fun deserializeModifier(type: String, obj: JsonObject): ShotModifier? =
modifiers[type]?.invoke(obj)
fun deserializeActorTrack(type: String, obj: JsonObject): ActorTrack? =
actorTracks[type]?.invoke(obj)
fun clear() {
positionSources.clear()
rotationSources.clear()
fovSources.clear()
rollSources.clear()
modifiers.clear()
actorTracks.clear()
}
}
@@ -0,0 +1,66 @@
/*
* 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.api.cinematic.runtime
import com.cobblemon.mod.common.api.cinematic.model.Beat
import com.cobblemon.mod.common.api.cinematic.model.CinematicValidationException
import com.cobblemon.mod.common.api.cinematic.model.tracks.builtin.AttachTrack
import com.cobblemon.mod.common.api.cinematic.model.tracks.builtin.FollowActor
import net.minecraft.network.chat.Component
object ActorDependencyOrder {
fun resolve(actorIds: Set<String>, beat: Beat): List<String> {
val dependencies = actorIds.associateWith { id ->
beat.actorDirections[id]?.tracks.orEmpty().mapNotNull { track ->
val target = when (track) {
is FollowActor -> track.targetActorId
is AttachTrack -> track.parentActorId
else -> null
}
if (target != null) {
if (target !in actorIds) throw CinematicValidationException(Component.translatable(
"cobblemon.cinematic_editor.validation.actor_dependency_missing", id, target, beat.id))
}
target
}
}
val ordered = mutableListOf<String>()
val visited = mutableSetOf<String>()
val active = mutableMapOf<String, Int>()
val path = mutableListOf<String>()
val stack = ArrayDeque<Iterator<String>>()
fun enter(id: String) {
active[id] = path.size
path.add(id)
stack.addLast(dependencies.getValue(id).iterator())
}
for (id in actorIds) {
if (id in visited) continue
enter(id)
while (stack.isNotEmpty()) {
val remaining = stack.last()
if (remaining.hasNext()) {
val target = remaining.next()
val cycleStart = active[target]
if (cycleStart != null) throw CinematicValidationException(Component.translatable(
"cobblemon.cinematic_editor.validation.actor_dependency_cycle", beat.id,
(path.subList(cycleStart, path.size) + target).joinToString(" -> ")))
if (target !in visited) enter(target)
} else {
stack.removeLast()
val completed = path.removeAt(path.lastIndex)
active.remove(completed)
visited.add(completed)
ordered.add(completed)
}
}
}
return ordered
}
}
@@ -0,0 +1,592 @@
/*
* 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.api.cinematic.runtime
import net.minecraft.network.chat.Component
import com.bedrockk.molang.runtime.MoLangRuntime
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.cinematic.model.ActorBinding
import com.cobblemon.mod.common.api.cinematic.model.ActorDirection
import com.cobblemon.mod.common.api.cinematic.model.Beat
import com.cobblemon.mod.common.api.cinematic.model.Scene
import com.cobblemon.mod.common.api.cinematic.model.StartPose
import com.cobblemon.mod.common.api.cinematic.model.StartPoseMode
import com.cobblemon.mod.common.api.cinematic.model.tracks.ActorTrack
import com.cobblemon.mod.common.api.cinematic.model.tracks.builtin.AnimationTrack
import com.cobblemon.mod.common.api.cinematic.model.tracks.builtin.AttachTrack
import com.cobblemon.mod.common.api.cinematic.model.tracks.builtin.FollowActor
import com.cobblemon.mod.common.api.cinematic.model.tracks.builtin.KeyframeMovement
import com.cobblemon.mod.common.api.cinematic.model.tracks.builtin.LookAtTrack
import com.cobblemon.mod.common.api.cinematic.model.tracks.builtin.PoseLockTrack
import com.cobblemon.mod.common.api.cinematic.model.tracks.builtin.SplineMovement
import com.cobblemon.mod.common.api.cinematic.model.tracks.builtin.WaypointPathfinding
import com.cobblemon.mod.common.api.cinematic.runtime.pathfinding.WaypointActorMotion
import com.cobblemon.mod.common.entity.PosableEntity
import com.cobblemon.mod.common.entity.generic.GenericBedrockEntity
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.mixin.accessor.ChunkMapAccessor
import com.cobblemon.mod.common.mixin.accessor.TrackedEntityAccessor
import com.cobblemon.mod.common.net.messages.client.animation.PlayPosableAnimationPacket
import com.cobblemon.mod.common.util.server
import java.util.UUID
import net.minecraft.server.level.ServerLevel
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.entity.Entity
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.phys.Vec3
import net.minecraft.world.phys.AABB
class ActorDirector(
val scene: Scene,
bindings: Map<String, ActorBinding>,
val networkSync: SceneNetworkSync? = null,
serverLevel: ServerLevel? = null,
val sceneOrigin: Vec3 = Vec3.ZERO,
val sceneId: UUID = UUID(0, 0),
val previewMode: Boolean = false,
private val runtimeProvider: () -> MoLangRuntime = { MoLangRuntime() }
) {
private val _bindings: MutableMap<String, ActorBinding> = bindings.toMutableMap()
val bindings: Map<String, ActorBinding> get() = _bindings
private data class CameraAnchor(val position: Vec3, val bounds: AABB)
private val cameraAnchors = mutableMapOf<UUID, CameraAnchor>()
var serverLevel: ServerLevel? = serverLevel
private set
fun setServerLevel(level: ServerLevel) {
if (serverLevel == null) serverLevel = level
}
fun updateBinding(actorId: String, newBinding: ActorBinding) {
activeBlends.remove(actorId)
startPoseErrors.remove(actorId)
waypointMotions.remove(actorId)?.restore()
previewPoseResets.remove(actorId)
val oldBinding = _bindings[actorId]
_bindings[actorId] = newBinding
if (oldBinding is ActorBinding.SynthPokemon || oldBinding is ActorBinding.SynthBedrock) {
if (previewMode) {
previewActors.remove(actorId)?.despawn()
}
spawnedEntities.remove(actorId)?.let { entity ->
if (!previewMode && !entity.isRemoved) {
entity.remove(Entity.RemovalReason.DISCARDED)
}
}
}
}
private val visibleActors: MutableMap<String, Boolean> = mutableMapOf()
private val firedAnimationCues: MutableMap<String, MutableSet<Pair<Float, String>>> = mutableMapOf()
private val spawnedEntities: MutableMap<String, Entity> = mutableMapOf()
private val previewActors: MutableMap<String, PreviewActor> = mutableMapOf()
private fun currentViewerPlayers(): Collection<ServerPlayer> {
val sync = networkSync ?: return emptyList()
val srv = try {
server()
} catch (_: UninitializedPropertyAccessException) {
return emptyList()
} ?: return emptyList()
return sync.viewerIdsSnapshot().mapNotNull { srv.playerList.getPlayer(it) }
}
internal fun hasTrackedPreviewEntity(actorId: String): Boolean =
previewMode && previewActors.containsKey(actorId)
private val activeBlends: MutableMap<String, BlendState> = mutableMapOf()
private val startPoseErrors = mutableMapOf<String, Component>()
private val previewPoseResets = mutableMapOf<String, () -> Unit>()
private val waypointMotions = mutableMapOf<String, WaypointActorMotion>()
val previewError: Component? get() = startPoseErrors.values.firstOrNull() ?: waypointMotions.entries.firstNotNullOfOrNull { (id, motion) ->
motion.playback.error?.let { Component.translatable("cobblemon.cinematic_editor.route.actor_error", id, it) }
}
private fun releaseWaypointMotions() {
val motions = waypointMotions.toMap()
waypointMotions.clear()
motions.forEach { (actorId, motion) ->
cleanupActor(actorId) { motion.restore() }
}
}
private inline fun cleanupActor(actorId: String, action: () -> Unit) {
try {
action()
} catch (error: Exception) {
Cobblemon.LOGGER.warn("Could not release cinematic actor '{}'", actorId, error)
}
}
private var evaluationOrder = scene.actors.keys.toList()
private data class BlendState(
val startPosition: Vec3,
val targetPosition: Vec3,
val startYaw: Float,
val targetYaw: Float,
val startPitch: Float,
val targetPitch: Float,
val durationSeconds: Float
)
init {
bindings.forEach { (actorId, binding) ->
val declared = scene.getActor(actorId)
if (declared != null && declared.kind != binding.kind) {
Cobblemon.LOGGER.warn(
"[ActorDirector] Binding for actor '{}' has kind {} but scene declared {}; proceeding but behavior undefined",
actorId, binding.kind::class.simpleName, declared.kind::class.simpleName
)
}
}
}
fun isVisible(actorId: String): Boolean = visibleActors[actorId] ?: false
/** Called once per beat transition before tick evaluation. */
fun onBeatEnter(beat: Beat) {
releaseWaypointMotions()
activeBlends.clear()
startPoseErrors.clear()
evaluationOrder = ActorDependencyOrder.resolve(scene.actors.keys, beat)
firedAnimationCues.clear()
scene.actors.keys.forEach { actorId ->
val direction = beat.actorDirections[actorId]
applyVisibilityAndPose(actorId, direction)
}
}
fun evaluate(beat: Beat, beatTime: Float) {
val blendIter = activeBlends.entries.iterator()
while (blendIter.hasNext()) {
val (actorId, blend) = blendIter.next()
val t = (beatTime / blend.durationSeconds).coerceIn(0f, 1f)
val entity = resolveEntity(actorId)
if (entity == null) {
blendIter.remove()
continue
}
entity.setPos(
blend.startPosition.x + (blend.targetPosition.x - blend.startPosition.x) * t,
blend.startPosition.y + (blend.targetPosition.y - blend.startPosition.y) * t,
blend.startPosition.z + (blend.targetPosition.z - blend.startPosition.z) * t
)
entity.yRot = blend.startYaw + (blend.targetYaw - blend.startYaw) * t
entity.xRot = blend.startPitch + (blend.targetPitch - blend.startPitch) * t
if (t >= 1f) blendIter.remove()
}
evaluationOrder.forEach { actorId ->
if (!isVisible(actorId)) return@forEach
val direction = beat.actorDirections[actorId] ?: return@forEach
direction.tracks.forEach { track ->
if (track !is LookAtTrack) applyTrack(actorId, track, beatTime)
}
direction.tracks.filterIsInstance<LookAtTrack>().forEach { applyTrack(actorId, it, beatTime) }
}
bindings.filterValues { it is ActorBinding.Real || it is ActorBinding.Player }.keys.forEach { cameraAnchor(it) }
}
fun dispose() {
cameraAnchors.clear()
releaseWaypointMotions()
previewPoseResets.clear()
visibleActors.clear()
firedAnimationCues.clear()
startPoseErrors.clear()
activeBlends.clear()
// Virtual actors never enter the world; removing them through the level lifecycle is inappropriate.
if (previewMode) {
val actors = previewActors.toMap()
previewActors.clear()
actors.forEach { (actorId, actor) -> cleanupActor(actorId) { actor.despawn() } }
} else {
val entities = spawnedEntities.toMap()
spawnedEntities.clear()
entities.forEach { (actorId, entity) ->
cleanupActor(actorId) {
if (!entity.isRemoved) entity.remove(Entity.RemovalReason.DISCARDED)
}
}
}
spawnedEntities.clear()
}
private fun applyVisibilityAndPose(actorId: String, direction: ActorDirection?) {
val wasVisible = visibleActors[actorId] ?: false
val targetVisibility = direction?.visible ?: wasVisible
visibleActors[actorId] = targetVisibility
if (!previewMode) spawnedEntities[actorId]?.let { updateWorldActorScope(actorId, it) }
if (targetVisibility && direction?.startPose != null) {
val mode = direction.startPoseMode ?: defaultModeFor(actorId, wasVisible, targetVisibility)
applyStartPose(actorId, direction.startPose, mode)
}
}
private fun defaultModeFor(actorId: String, wasVisible: Boolean, nowVisible: Boolean): StartPoseMode {
val binding = bindings[actorId] ?: return StartPoseMode.IGNORE
return when {
!wasVisible && nowVisible -> StartPoseMode.TELEPORT
binding is ActorBinding.Player -> StartPoseMode.EXPECT
else -> StartPoseMode.IGNORE
}
}
private fun applyStartPose(actorId: String, pose: StartPose, mode: StartPoseMode) {
if (mode == StartPoseMode.IGNORE) return
val entity = resolveEntity(actorId) ?: return
val target = try {
pose.position.toVec3(context = emptyMap(), runtime = runtimeProvider(), origin = sceneOrigin).also {
require(it.x.isFinite() && it.y.isFinite() && it.z.isFinite()) { "Start pose position must be finite" }
}
} catch (error: Exception) {
startPoseErrors[actorId] = Component.translatable("cobblemon.cinematic_editor.preview.invalid_start_pose", actorId)
Cobblemon.LOGGER.warn("Could not evaluate start pose for actor '$actorId'", error)
return
}
if (mode == StartPoseMode.BLEND && pose.blendDurationSeconds == 0f) {
entity.setPos(target.x, target.y, target.z)
entity.yRot = pose.rotation.yaw
entity.xRot = pose.rotation.pitch
return
}
when (mode) {
StartPoseMode.TELEPORT -> {
entity.setPos(target.x, target.y, target.z)
entity.yRot = pose.rotation.yaw
entity.xRot = pose.rotation.pitch
}
StartPoseMode.BLEND -> {
activeBlends[actorId] = BlendState(
startPosition = Vec3(entity.x, entity.y, entity.z),
targetPosition = target,
startYaw = entity.yRot,
targetYaw = pose.rotation.yaw,
startPitch = entity.xRot,
targetPitch = pose.rotation.pitch,
durationSeconds = pose.blendDurationSeconds
)
}
StartPoseMode.EXPECT -> {
val currentPos = Vec3(entity.x, entity.y, entity.z)
if (currentPos.distanceTo(target) > 1.5) {
Cobblemon.LOGGER.warn(
"[ActorDirector] Actor '{}' with EXPECT pose is not near declared start ({} vs {})",
actorId, currentPos, target
)
}
}
StartPoseMode.IGNORE -> { }
}
}
private fun applyTrack(actorId: String, track: ActorTrack, beatTime: Float) {
val entity = resolveEntity(actorId) ?: return
when (track) {
is WaypointPathfinding -> {
val previousError = waypointMotions[actorId]?.playback?.error
val motion = waypointMotions.getOrPut(actorId) { WaypointActorMotion(track, entity) }
motion.sample(beatTime)
if (motion.playback.error != null && previousError == null) {
Cobblemon.LOGGER.warn("Pathfinding actor '{}': {}", actorId, motion.playback.error)
}
}
is KeyframeMovement -> {
val newPos = track.evaluate(beatTime)
entity.setPos(newPos.x, newPos.y, newPos.z)
}
is LookAtTrack -> {
val currentPos = Vec3(entity.x, entity.y, entity.z)
entity.yRot = track.computeYRot(currentPos)
entity.xRot = track.computeXRot(currentPos)
if (entity is LivingEntity) {
entity.yBodyRot = entity.yRot
entity.yHeadRot = entity.yRot
}
}
is AnimationTrack -> {
if (previewMode) return
val firedSet = firedAnimationCues.getOrPut(actorId) { mutableSetOf() }
track.cues.forEach { cue ->
val key = cue.time to cue.animationName
if (cue.time <= beatTime && key !in firedSet) {
firedSet.add(key)
fireAnimation(actorId, entity, cue.animationName)
}
}
}
is SplineMovement -> {
val newPos = track.evaluate(beatTime)
entity.setPos(newPos.x, newPos.y, newPos.z)
track.facingAt(beatTime)?.let { (yaw, pitch) ->
entity.yRot = yaw
entity.xRot = pitch
if (entity is LivingEntity) {
entity.yBodyRot = yaw
entity.yHeadRot = yaw
}
}
}
is FollowActor -> {
val target = resolveEntity(track.targetActorId)
if (target != null) {
val desired = Vec3(
target.x + track.offset.x,
target.y + track.offset.y,
target.z + track.offset.z
)
if (track.lagSecs > 0f) {
val currentPos = Vec3(entity.x, entity.y, entity.z)
val alpha = (1f / (1f + track.lagSecs * 20f)).coerceIn(0f, 1f)
entity.setPos(
currentPos.x + (desired.x - currentPos.x) * alpha,
currentPos.y + (desired.y - currentPos.y) * alpha,
currentPos.z + (desired.z - currentPos.z) * alpha
)
} else {
entity.setPos(desired.x, desired.y, desired.z)
}
}
}
is PoseLockTrack -> {
if (entity is PokemonEntity) {
entity.entityData.set(PokemonEntity.POSE_TYPE, track.pose)
} else if (entity is GenericBedrockEntity) {
entity.entityData.set(GenericBedrockEntity.POSE_TYPE, track.pose)
} else {
Cobblemon.LOGGER.warn("[ActorDirector] Actor '$actorId' (${entity::class.simpleName}) is not posable; PoseLockTrack no-op")
}
}
is AttachTrack -> {
val parent = resolveEntity(track.parentActorId)
if (parent != null) {
entity.setPos(
parent.x + track.offset.x,
parent.y + track.offset.y,
parent.z + track.offset.z
)
}
}
else -> {
Cobblemon.LOGGER.warn("[ActorDirector] Unknown ActorTrack type '${track.trackType}' on actor '$actorId'")
}
}
}
private fun fireAnimation(actorId: String, entity: Entity, name: String) {
if (entity !is PosableEntity) {
Cobblemon.LOGGER.warn("[ActorDirector] Actor '$actorId' entity is not PosableEntity; skipping animation '$name'")
return
}
val packet = PlayPosableAnimationPacket(
entityId = entity.id,
animation = setOf(name),
expressions = emptyList()
)
val level = entity.level() as? ServerLevel ?: run {
Cobblemon.LOGGER.warn("[ActorDirector] Actor '$actorId' is not in a ServerLevel; cannot dispatch animation '$name'")
return
}
packet.sendToPlayersAround(
x = entity.x,
y = entity.y,
z = entity.z,
distance = 64.0,
worldKey = level.dimension()
)
Cobblemon.LOGGER.debug("[ActorDirector] Actor '$actorId' animation '$name' dispatched")
}
private fun resolveEntity(actorId: String): Entity? {
val binding = bindings[actorId] ?: return null
return when (binding) {
is ActorBinding.Player -> binding.player as Entity
is ActorBinding.Real -> binding.entity
is ActorBinding.SynthPokemon -> spawnedEntities.getOrPut(actorId) {
spawnSynthPokemon(actorId, binding) ?: return null
}
is ActorBinding.SynthBedrock -> spawnedEntities.getOrPut(actorId) {
spawnSynthBedrock(actorId, binding) ?: return null
}
}
}
fun resolveEntityPublic(actorId: String): Entity? = resolveEntity(actorId)
private fun cameraAnchor(actorId: String): CameraAnchor? {
val entity = resolveEntity(actorId) ?: return null
val previous = cameraAnchors[entity.uuid]
if (previous != null && (!entity.isAlive || entity.isRemoved)) return previous
return CameraAnchor(entity.position(), entity.boundingBox).also { cameraAnchors[entity.uuid] = it }
}
fun cameraPosition(actorId: String): Vec3? = cameraAnchor(actorId)?.position
fun cameraBounds(actorId: String): AABB? = cameraAnchor(actorId)?.bounds
fun previewActorBounds() = previewActors.mapValues { it.value.entity.boundingBox }
internal fun resetPreview(beat: Beat) {
cameraAnchors.clear()
check(previewMode && bindings.values.none { it is ActorBinding.Real || it is ActorBinding.Player })
releaseWaypointMotions()
visibleActors.clear()
activeBlends.clear()
firedAnimationCues.clear()
startPoseErrors.clear()
scene.actors.keys.forEach { actorId ->
val entity = resolveEntity(actorId) ?: return@forEach
previewPoseResets.getOrPut(actorId) {
when (entity) {
is PokemonEntity -> {
val pose = entity.entityData.get(PokemonEntity.POSE_TYPE)
val reset: () -> Unit = { entity.entityData.set(PokemonEntity.POSE_TYPE, pose) }
reset
}
is GenericBedrockEntity -> {
val pose = entity.entityData.get(GenericBedrockEntity.POSE_TYPE)
val reset: () -> Unit = { entity.entityData.set(GenericBedrockEntity.POSE_TYPE, pose) }
reset
}
else -> ({ })
}
}.invoke()
entity.setPos(sceneOrigin)
entity.yRot = 0f
entity.xRot = 0f
if (entity is LivingEntity) {
entity.yBodyRot = 0f
entity.yHeadRot = 0f
}
}
onBeatEnter(beat)
}
private fun spawnSynthPokemon(actorId: String, binding: ActorBinding.SynthPokemon): Entity? {
val level = serverLevel ?: run {
Cobblemon.LOGGER.debug("[ActorDirector] serverLevel is null; cannot spawn SynthPokemon '$actorId'")
return null
}
val pokemon = binding.properties.create()
if (binding.aspects.isNotEmpty()) {
pokemon.forcedAspects = pokemon.forcedAspects + binding.aspects
}
val entity = PokemonEntity(level, pokemon)
if (previewMode) {
// Defer spawning until sync so the packet contains the transform assigned later in this tick.
previewActors[actorId] = PreviewPokemonActor(entity, actorId, ::currentViewerPlayers)
} else {
return spawnWorldActor(actorId, entity, level)
}
return entity
}
private fun spawnSynthBedrock(actorId: String, binding: ActorBinding.SynthBedrock): Entity? {
val level = serverLevel ?: run {
Cobblemon.LOGGER.debug("[ActorDirector] serverLevel is null; cannot spawn SynthBedrock '$actorId'")
return null
}
val entity = GenericBedrockEntity(level).apply {
category = binding.category
aspects = binding.aspects
scale = binding.scale
savesToWorld = false
}
if (previewMode) {
// Defer spawning until sync so the packet contains the transform assigned later in this tick.
previewActors[actorId] = PreviewBedrockActor(entity, actorId, ::currentViewerPlayers)
} else {
return spawnWorldActor(actorId, entity, level)
}
return entity
}
internal fun spawnWorldActor(actorId: String, entity: Entity, level: ServerLevel): Entity? {
try {
updateWorldActorScope(actorId, entity)
if (level.addFreshEntity(entity)) return entity
} catch (error: Exception) {
cleanupActor(actorId) { entity.remove(Entity.RemovalReason.DISCARDED) }
throw error
}
cleanupActor(actorId) { entity.remove(Entity.RemovalReason.DISCARDED) }
return null
}
private fun updateWorldActorScope(actorId: String, entity: Entity, viewers: Set<UUID>? = networkSync?.viewerIdsSnapshot()) {
if (viewers == null) return
val scoped = entity as CinematicViewerScopedEntity
val allowed = if (isVisible(actorId)) viewers else emptySet()
if (scoped.cobblemonSceneViewers == allowed) return
scoped.cobblemonSceneViewers = allowed
val level = entity.level() as? ServerLevel ?: return
val tracker = (level.chunkSource.chunkMap as ChunkMapAccessor).entityMap[entity.id] as? TrackedEntityAccessor ?: return
// Stationary entities otherwise retain their old pairings until a section change.
tracker.`cobblemon$updatePlayers`(level.players())
}
fun updateSynthActorViewerScope(viewers: Set<UUID>) {
// Virtual actors bypass the tracker; only world entities need the mixin-provided viewer scope.
if (previewMode) {
previewActors.values.forEach { it.retainViewers(viewers) }
return
}
spawnedEntities.forEach { (actorId, entity) ->
updateWorldActorScope(actorId, entity, viewers)
}
}
fun isPlayerTakeoverActive(actorId: String): Boolean {
val binding = bindings[actorId] ?: return false
return binding is ActorBinding.Player && isVisible(actorId)
}
fun getTakeoverPlayer(actorId: String): ServerPlayer? {
if (!isPlayerTakeoverActive(actorId)) return null
return (bindings[actorId] as? ActorBinding.Player)?.player
}
fun forceHide(actorId: String) {
activeBlends.remove(actorId)
startPoseErrors.remove(actorId)
waypointMotions.remove(actorId)?.restore()
visibleActors[actorId] = false
if (!previewMode) spawnedEntities[actorId]?.let { updateWorldActorScope(actorId, it) }
}
private fun flushPreviewSync() {
if (!previewMode) return
previewActors.forEach { (actorId, actor) ->
if (isVisible(actorId)) actor.syncTransform() else actor.despawn()
}
}
/** Call after all actor tracks have been evaluated for the tick. */
fun tickPreviewSync() {
flushPreviewSync()
}
fun sendPreviewSpawnsToPlayer(player: ServerPlayer) {
if (!previewMode) return
previewActors.values.forEach { it.sendCatchUpTo(player) }
}
}
@@ -0,0 +1,31 @@
/*
* 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.api.cinematic.runtime
import net.minecraft.world.phys.AABB
object ActorFraming {
val groups = mapOf(
"@trainers" to "cobblemon.cinematic_editor.actor_group.trainers",
"@pokemon" to "cobblemon.cinematic_editor.actor_group.pokemon"
)
fun expand(ids: List<String>, available: Set<String>): List<String> = ids.flatMap { id ->
when (id) {
"@trainers" -> available.filter { it.startsWith("trainer_") }
"@pokemon" -> available.filter { it.startsWith("pokemon_") }
else -> listOf(id)
}
}.distinct()
fun bounds(ids: List<String>, director: ActorDirector?): AABB? = expand(ids,
director?.let { it.scene.actors.keys + it.bindings.keys }.orEmpty()).mapNotNull {
director?.cameraBounds(it)
}.reduceOrNull(AABB::minmax)
}
@@ -0,0 +1,180 @@
/*
* 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.api.cinematic.runtime
import com.bedrockk.molang.runtime.MoLangRuntime
import com.bedrockk.molang.runtime.value.DoubleValue
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.camera.model.ParameterDefault
import com.cobblemon.mod.common.api.cinematic.model.Beat
import com.cobblemon.mod.common.api.cinematic.model.BeatTransition
import com.cobblemon.mod.common.api.cinematic.model.Scene
import com.cobblemon.mod.common.util.resolveBoolean
/** Non-looping beats remain active at their duration until a transition fires or the scene stops. */
class BeatStateMachine(
val scene: Scene
) {
var runtime: MoLangRuntime = MoLangRuntime()
private set
private val timeOnCurrentPathValue = DoubleValue(0.0)
private val totalTimeValue = DoubleValue(0.0)
private var _currentBeat: Beat? = null
private var _beatTime = 0.0
private var _totalTime = 0.0
private var _active: Boolean = false
val currentBeat: Beat? get() = _currentBeat
val beatTime: Float get() = _beatTime.coerceAtMost(Float.MAX_VALUE.toDouble()).toFloat()
val totalTime: Float get() = _totalTime.coerceAtMost(Float.MAX_VALUE.toDouble()).toFloat()
fun isActive(): Boolean = _active
fun start(): Boolean {
val start = scene.getBeat(scene.startBeatId) ?: return false
_currentBeat = start
_beatTime = 0.0
_totalTime = 0.0
_active = true
injectParameterDefaults()
injectTimeVariables()
return true
}
private fun injectParameterDefaults() {
val vars = runtime.environment.variable.map
for ((name, definition) in scene.parameters) {
// Parameter overrides may already be present; defaults must not replace them.
when (val default = definition.default) {
is ParameterDefault.NumberDefault -> {
if (name !in vars) vars[name] = DoubleValue(default.value)
}
is ParameterDefault.Vec3Default -> {
if ("${name}_x" !in vars) vars["${name}_x"] = DoubleValue(default.x)
if ("${name}_y" !in vars) vars["${name}_y"] = DoubleValue(default.y)
if ("${name}_z" !in vars) vars["${name}_z"] = DoubleValue(default.z)
}
is ParameterDefault.StringDefault -> {
}
null -> { }
}
}
}
fun stop() {
_active = false
_currentBeat = null
}
internal fun samplePreview(beat: Beat, seconds: Float) {
require(seconds.isFinite() && seconds >= 0f)
_currentBeat = beat
_beatTime = seconds.toDouble()
_totalTime = seconds.toDouble()
injectTimeVariables()
}
internal fun resetPreview(beat: Beat) {
runtime = MoLangRuntime()
injectParameterDefaults()
samplePreview(beat, 0f)
}
/** Delta is clamped to 0–0.5 seconds; the first matching outgoing transition wins. */
fun advance(deltaSeconds: Float, beforeTransitions: (Beat, Float) -> Unit = { _, _ -> }): AdvanceResult {
if (!_active) return AdvanceResult.Inactive
val beat = _currentBeat ?: return AdvanceResult.Inactive
val clampedDelta = if (deltaSeconds.isFinite()) deltaSeconds.toDouble().coerceIn(0.0, 0.5) else 0.0
val elapsedBeatSeconds = clampedDelta * beat.speed.toDouble()
val previousBeatTime = _beatTime
_beatTime += elapsedBeatSeconds
_totalTime += clampedDelta
injectTimeVariables()
beforeTransitions(beat, beatTime)
if (!_active) return AdvanceResult.Inactive
val firedTransition = findFiringTransition(beat)
if (firedTransition != null) {
if (firedTransition.endsScene) return AdvanceResult.Finished
val targetBeat = scene.getBeat(firedTransition.targetBeatId)
if (targetBeat != null) {
_currentBeat = targetBeat
_beatTime = 0.0
injectTimeVariables()
return AdvanceResult.Running(targetBeat, entered = true, transition = firedTransition)
} else {
Cobblemon.LOGGER.warn(
"[BeatStateMachine] Transition from '{}' to '{}' dropped — target beat not found",
beat.id, firedTransition.targetBeatId
)
}
}
val duration = beat.duration
val effectiveElapsed = if (duration != null && duration > 0f && !beat.isLooping) {
elapsedBeatSeconds.coerceAtMost((duration.toDouble() - previousBeatTime).coerceAtLeast(0.0))
} else elapsedBeatSeconds
if (duration != null && duration > 0f) {
if (beat.isLooping) {
if (_beatTime >= duration.toDouble()) {
_beatTime %= duration.toDouble()
injectTimeVariables()
return AdvanceResult.Running(beat, entered = true, elapsedBeatSeconds = effectiveElapsed)
}
} else {
if (_beatTime > duration.toDouble()) {
_beatTime = duration.toDouble()
injectTimeVariables()
}
}
}
return AdvanceResult.Running(beat, elapsedBeatSeconds = effectiveElapsed)
}
private fun findFiringTransition(beat: Beat): BeatTransition? {
for (transition in scene.getTransitionsFrom(beat.id)) {
try {
if (runtime.resolveBoolean(transition.condition)) return transition
} catch (e: Exception) {
Cobblemon.LOGGER.error(
"[BeatStateMachine] Condition '{}' on transition {} -> {} threw: {}",
transition.condition, transition.sourceBeatId, transition.targetBeatId, e.message
)
}
}
return null
}
private fun injectTimeVariables() {
timeOnCurrentPathValue.value = _beatTime
totalTimeValue.value = _totalTime
runtime.environment.variable.apply {
map["time_on_current_path"] = timeOnCurrentPathValue
map["total_time"] = totalTimeValue
}
}
sealed class AdvanceResult {
object Inactive : AdvanceResult()
data class Running(
val beat: Beat,
val entered: Boolean = false,
val transition: BeatTransition? = null,
val elapsedBeatSeconds: Double = 0.0
) : AdvanceResult()
object Finished : AdvanceResult()
}
}

Some files were not shown because too many files have changed in this diff Show More