mirror of
https://gitlab.com/cable-mc/cobblemon.git
synced 2026-10-25 00:26:26 -04:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8899f1bd08 | ||
|
|
70c7b1d739 | ||
|
|
0dbfc1b119 | ||
|
|
850962b7f1 | ||
|
|
7c54c7815f | ||
|
|
614d2dbe70 |
@@ -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 {
|
||||
|
||||
+153
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+155
@@ -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;
|
||||
}
|
||||
}
|
||||
+14
@@ -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);
|
||||
}
|
||||
+41
@@ -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);
|
||||
}
|
||||
+20
@@ -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();
|
||||
}
|
||||
+7
-1
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+36
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+127
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+178
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+122
-15
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+33
-28
@@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+27
-14
@@ -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]
|
||||
}
|
||||
}
|
||||
+27
@@ -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()
|
||||
}
|
||||
+126
@@ -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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
+28
@@ -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" }
|
||||
}
|
||||
}
|
||||
+13
@@ -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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
+16
@@ -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 }
|
||||
}
|
||||
+23
@@ -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
|
||||
}
|
||||
+13
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
+13
@@ -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
|
||||
}
|
||||
+13
@@ -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
|
||||
}
|
||||
+13
@@ -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
|
||||
}
|
||||
+13
@@ -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
|
||||
}
|
||||
+43
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
+56
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+50
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+60
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+79
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
@@ -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)
|
||||
}
|
||||
}
|
||||
+29
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
@@ -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)
|
||||
}
|
||||
}
|
||||
+31
@@ -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
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
+41
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+53
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+65
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+53
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+57
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+47
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
+149
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+43
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+98
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+44
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
+53
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
+36
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
+43
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+51
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+58
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
@@ -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
|
||||
}
|
||||
+37
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+38
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+79
@@ -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)
|
||||
}
|
||||
}
|
||||
+40
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+69
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+49
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+29
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+62
@@ -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 }
|
||||
+49
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
+79
@@ -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)
|
||||
}
|
||||
}
|
||||
+411
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+87
@@ -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
|
||||
|
||||
}
|
||||
+79
@@ -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()
|
||||
}
|
||||
}
|
||||
+66
@@ -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
|
||||
}
|
||||
}
|
||||
+592
@@ -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)
|
||||
}
|
||||
+180
@@ -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
Reference in New Issue
Block a user