Compare commits

..
2341 changed files with 61294 additions and 95204 deletions
-3
View File
@@ -157,9 +157,6 @@ build:
paths:
- neoforge/build/libs
- fabric/build/libs
exclude:
- neoforge/build/libs/*-dev-shadow.jar
- fabric/build/libs/*-dev-shadow.jar
publish:
stage: deploy
+34 -117
View File
@@ -1,143 +1,60 @@
# Changelog
## [1.8.2 (MONTH xth, 2026)](#1-8-2)
## [1.8.1 (MONTH Xth, 2026)](#1-8-1)
### Additions
- The evolution tree can now be viewed on the Pokédex, showing a Pokémon's full family line and what's needed to evolve into each stage.
### New rideable Pokémon
- The following items can now be placed on the ground as blocks:
- King's Rock, Galarica Cuff, Unremarkable Teacup, Masterpiece Teacup, Chipped Pot, Cracked Pot, Sachet, Metal Powder, Quick Powder, Bright Powder, Icy Rock, Heat Rock, Smooth Rock, Damp Rock, Destiny Knot, Luminous Moss, Never-Melt Ice, Wide Lens, Scope Lens, and Zoom Lens.
- Iron Ball, Metal Coat, Throat Spray, HP Up, Protein, Iron, Carbos, Calcium, Zinc, PP Up, PP Max, Upgrade, Dubious Disc, and Loaded Dice.
- The following items can now be placed on various surfaces:
- Galarica Wreath, Magnet, Flame Orb, Life Orb, Toxic Orb, Light Ball, and Smoke Ball.
- The following items can now be placed as blocks and have additional functionality:
- Ring Target, Metronome, and Eject Button.
- The following items can now be worn by Players and Pokémon:
- Galarica Wreath, Cleanse Tag, Spell Tag, Sharp Beak, Covert Cloak, Shell Helmet, and Never-Melt Ice.
- Wearable items can now be equipped by a player by holding it and right-clicking (or rebound use/interact)
- Wearable items that can be placed down are exempt from this behaviour.
### Pokémon Added
#### Generation 4
- Snover
- Abomasnow
- Burmy
- Wormadam
- Mothim
#### Generation 5
- Tympole
- Palpitoad
- Seismitoad
#### Generation 6
- Helioptile
- Heliolisk
#### Generation 7
- Grubbin
- Charjabug
- Vikavolt
- Minior
#### Generation 8
- Eternatus
#### Generation 9
- Greavard
- Houndstone
### Model updates for the following Pokémon
- Sudowoodo
- Spheal
- Cacnea
- Cacturne
- Binacle
- Barbaracle
- Glimmet
- Glimmora
- Deerling
- Sawsbuck (All forms)
- Absol
- Poliwag
- Politoed
- Ekans
- Oddish
- Gloom
- Vileplume
- Bellossom
- Shroomish
### Animation updates for the following Pokémon
- Treecko (New Shoulder Mount!)
- Bonsly (New Shoulder Mount!)
- Sudowoodo
- Cacnea
- Cacturne
- Ekans
- Arbok
- Oddish
- Gloom
- Vileplume
- Bellossom
### Changes
- Updated various recipes to display in Recipe Groups within the Recipe Book
- Updated most recipes to display more logically/consistently within the Recipe Book's categories
- Make ride rotations less jittery in multiplayer.
- Changes tm shelf discs to use textures instead of tinting and overlays to line up with item display
- Blank TM can now be inserted into the Disc Shelf
- Recall animation now renders ball in hand with the player arm raised.
- Made regional and Vivillon split-evo locations now mutually exclusive
### Fixes
- Correctly grants "They're Minerals!" advancement when planting a type gem
- Shoulder mounted Pokémon now follow player body animations, improving their positioning with custom player animations and default actions.
- Shoulder mounted Pokémon now sit slightly higher with chest armor equipped for better alignment, with a configurable blacklist.
- Type Gem Blocks now have advancements to give you the recipe in the recipe book (the data for the advancements were inside the recipes somehow??)
- Some other recipes without advancements have also been corrected
- Corrected the TM Recipes for Charge Beam and Payday
- Type Gem blocks now properly generate clusters off of the block.
- Starter Screen Categories will no longer stop rendering if they are bigger than the viewport, and you are not viewing top or bottom edges
- Random Starter option now properly handled in height calculation, so now it won't run off into other categories or be un-accessible due to scroll limits if it causes a new row
- Mochi and Feathers no longer drop glass bottles on use
- Fixed a rare set of concurrency bugs related to reloading packs on NeoForge; fixes a ModernFix issue
- Removed unnecessary conditions for Mimikyu's standing pose. Hopefully fixes the rare instance of it using its shoulder animations during battle in multiplayer scenarios.
- Fixed Rockruff line not blinking
- Fixed Flygon sometimes using its running animation instead of walking
- Fixed Bramblin always sleeping outside of battle
- Fixed Dragonair using its battle idle as a normal idle
- Fixed the `stat_compare` and `stat_equal` evolution requirements crashing the game when given `special_attack` or `special_defence`, and made them treat any non-permanent stat as a failed requirement instead of crashing.
- Fixed the Beast Ball not applying its 0.1× penalty to Pokémon that are not Ultra Beasts.
- Fixed the Moon Ball reading the world's total elapsed time instead of the time of day, so its bonus applied during the day instead.
- Fixed the Love Ball never awarding its 2.5× opposite gender bonus, and matching species by display name instead of identifier.
- Fixed the Timer Ball being weaker than a regular Poké Ball on early turns and only reaching 4× on turn 14 instead of turn 10.
- Fixed the Luxury Ball only doubling Friendship gains of 1, leaving level up and berry gains unaffected.
- Fixed NPCs getting their own TM progress file.
- Fixed the TM Machine refusing move selections that the `unlockAllMoveDexMovesByDefault` config had made visible, leaving the move slot blank and the craft button unresponsive.
- Fixed Rufflet, Braviary, and Hisuian Braviary flooding logs with errors about missing sound events.
- Fixed Pokémon property typos in Pangoro and Dedenne herd spawn files.
- Fixed form typo in Rockruff dusk spawn entry.
- Fixed a potential world gen stall with the Type Gem clusters.
- Fixed Rowap and Jacoba berries not correctly altering Pokémon sizes when used as bait
- Fixed Ampharos' eyes not emitting particles
- Fixed Alolan Raticate's cry not playing
- Fixed unintentional watermelon cosmetic for Hisuian Electrode.
- Fixed Blastoise having an unintentional passenger seat
- Fixed odd NeoForge crash due to a dependency being outdated. It will now flag the dependency for being outdated correctly.
- Fixed form typo in Rockruff-Dusk spawn, preventing it from being able to spawn.
- Fixed a potential world gen stall caused by Type Gem clusters.
- Fixed Rowap and Jacoba berries not correctly altering Pokémon sizes when used as bait.
- Fixed Ampharos' eyes not emitting particles.
- Fixed Alolan Raticate's cry not playing.
- Fixed odd NeoForge crash due to a dependency being outdated. It will now correctly flag the dependency for being outdated.
- Fixed several incorrect light-based spawning conditions for ghosts and bugs.
- Removed unused, placeholder habitat pools.
- Fixed Mantyke, Mantine, and Quaxly getting stuck in a recoil 'expression' during battles due to a having an expression mistakenly assigned as a recoil animation.
- Fixed Pokémon on shoulders not animating if they are visibly holding a held item.
- Fixed Mantyke, Mantine, and Quaxly getting stuck in a recoil 'expression' during battles due to a having an expression mistakenly assigned as a recoil animation.
- Fixed Lycanroc's head movement
- Added head movement to Archaludon
- Fixed Duraludon line not having any sleep animations
- Fixed Duraludon line not having their sleep animations
- Fixed Diglett line not having their faint animations
- Fixed Kingambit moving its legs while walking
- Fixed Alolan Diglett's cry not playing
- Fixed shiny alpha Snorlax using the non-shiny texture
- Various Structure fixes
- Fixed shiny pokemon under certain circumstances forgetting taught TMs
- Fixed certain Pokémon sounds sometimes being doubled when played at specific times in their animations
- Fixed ball capture beam origin coming from behind the ball.
- Fixed Safari Ball catch rate outside of battle
### Developer
- Evolution requirements can now set `pokedexIcon` and `pokedexTooltip` directly in their JSON to control how they're shown on the Pokédex's evolution tree, for example:
```json
{
"variant": "attack_defence_ratio",
"pokedexIcon": "minecraft:diamond_sword",
"pokedexTooltip": "cobblemon.ui.pokedex.requirement.attack_defence_ratio.tooltip"
}
```
- Fixed wrong order of operations in the code flow prior to the `FOSSIL_REVIVED` event causing pokemon to appear without entity link
- Fixed TestPartySlot command not correctly comparing the provided spec with the target Pokémon
### Molang & Datapacks
- A new `negate` variant for evolution requirements has been added, which will negate (invert) the provided inner condition.
- Added Molang functions for individual Pokédex ids, wherein a subdex is passed in as a string to `dex_caught_count` etc.
### Particles
#### New Move Animations
### Localization
- Updated translations for:
- Updated translations for:
+9
View File
@@ -0,0 +1,9 @@
- Some kind of particle effect for them? they're meant to have glowing eyes, so maybe some red particles on the eye locator
--- global posers? stacked posers?
--- red texture seems to be what some people wanted.
/- shoulder mounts part of it
- omni boost
- roar when player comes within reach, adjust levels
- Riding settings to control what the min scale is for a pokemon to be ridable.
+2 -5
View File
@@ -1,6 +1,3 @@
import utilities.gitBranch
import utilities.gitShortCommit
plugins {
id("cobblemon.root-conventions")
}
@@ -9,6 +6,6 @@ version = "${project.property("mod_version")}+${project.property("mc_version")}"
val isSnapshot = project.property("snapshot")?.equals("true") == true
if (isSnapshot) {
val fixedBranchName = gitBranch().substringAfter("/")
version = "$version-${fixedBranchName}-${gitShortCommit()}"
val fixedBranchName = versioning.info.branch.substringAfter("/")
version = "$version-${fixedBranchName}-${versioning.info.build}"
}
-41
View File
@@ -1,41 +0,0 @@
# Changelog
## [1.8.1 (September 13th, 2026)](#1-8-1)
### Animation updates for the following Pokémon
- Flapple, Appletun, Dipplin, Hydrapple (placeholder animations only).
### Fixes
- Fixed a potential world generation stall caused by Type Gem clusters.
- Fixed Pokémon on shoulders not animating if they are visibly holding a held item.
- Fixed NPCs getting their own TM progress file.
- Fixed odd NeoForge crash due to a dependency being outdated. It will now correctly flag the dependency for being outdated.
- Fixed the TM Machine refusing move selections that the `unlockAllMoveDexMovesByDefault` config had made visible, leaving the move slot blank and the craft button unresponsive.
- Fixed the Beast Ball not applying its 0.1× penalty to Pokémon that are not Ultra Beasts.
- Fixed the Moon Ball reading the world's total elapsed time instead of the time of day, so its bonus applied during the day instead.
- Fixed the Love Ball never awarding its 2.5× opposite gender bonus, and matching species by display name instead of identifier.
- Fixed the Timer Ball being weaker than a regular Poké Ball on early turns and only reaching 4× on turn 14 instead of turn 10.
- Fixed the Luxury Ball only doubling Friendship gains of 1, leaving level up and berry gains unaffected.
- Fixed Rufflet, Braviary, and Hisuian Braviary flooding logs with errors about missing sound events.
- Fixed Pokémon property typos in Pangoro and Dedenne herd spawn files.
- Fixed unintentional watermelon cosmetic for Hisuian Electrode.
- Fixed Blastoise having an unintentional passenger seat.
- Fixed form typo in Rockruff-Dusk spawn, preventing it from being able to spawn.
- Fixed the `stat_compare` and `stat_equal` evolution requirements crashing the game when given `special_attack` or `special_defence`, and made them treat any non-permanent stat as a failed requirement instead of crashing.
- Fixed Rowap and Jacoba berries not correctly altering Pokémon sizes when used as bait.
- Fixed Ampharos' eyes not emitting particles.
- Fixed Alolan Raticate's cry not playing.
- Fixed several incorrect light-based spawning conditions for ghosts and bugs.
- Removed unused, placeholder habitat pools.
- Fixed Mantyke, Mantine, and Quaxly getting stuck in a recoil 'expression' during battles due to having an expression assigned as a recoil animation.
- Fixed Lucario's aura particles not emitting.
- Fixed Lycanroc's head movement.
- Added head movement to Archaludon.
- Fixed Duraludon line not having their sleep animations.
- Fixed Diglett line not having their faint animations.
- Fixed Kingambit moving its legs while walking.
### Localization
- Updated translations for:
- Brazilian Portuguese
- Simplified Chinese
- Japanese
+13 -15
View File
@@ -1,8 +1,9 @@
import utilities.isSnapshot
import utilities.version
import utilities.gitBranch
import utilities.gitCommit
import java.time.OffsetDateTime
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
/*
*
@@ -31,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()
}
@@ -39,6 +41,7 @@ dependencies {
modImplementation(libs.fabric.loader)
modApi(libs.molang)
// Integrations
compileOnlyApi(libs.jei.api)
modCompileOnly(libs.bundles.common.integrations.compileOnly) {
@@ -61,24 +64,21 @@ 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) }
}
// Git info lives in its own output dir (not processResources) so a new commit only rewrites this one file.
val writeGitInfo by tasks.registering(WriteProperties::class) {
destinationFile = layout.buildDirectory.file("generated/gitInfo/cobblemon-git.properties")
property("commit", gitCommit())
property("branch", gitBranch())
}
sourceSets.main { output.dir(mapOf("builtBy" to writeGitInfo), layout.buildDirectory.dir("generated/gitInfo")) }
tasks.processResources {
exclude("data/**/README.md")
}
@@ -105,13 +105,11 @@ sourceSets {
property("modid", "cobblemon")
property("version", project.version())
property("isSnapshot", if(rootProject.isSnapshot()) "true" else "false")
property("gitCommit", versioning.info.commit)
property("branch", versioning.info.branch)
System.getProperty("buildNumber")?.let { property("buildNumber", it) }
property("timestamp", OffsetDateTime.now(ZoneOffset.UTC).format(DateTimeFormatter.ofPattern("MM/dd/yyyy hh:mm:ss")) + " UTC")
}
}
}
}
// The platform jars are built from :common:jar, so the remapped common jars are only needed by `publish`
tasks.assemble {
setDependsOn(dependsOn.filterNot { (it as? TaskProvider<*>)?.name in setOf("remapJar", "remapSourcesJar") })
}
@@ -0,0 +1,153 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.camera;
import com.cobblemon.mod.common.client.render.camera.CameraFeedHandler;
import com.cobblemon.mod.common.client.util.compat.SodiumCompat;
import com.cobblemon.mod.common.platform.events.ClientChunkEvent;
import com.cobblemon.mod.common.platform.events.PlatformEvents;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.core.SectionPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.protocol.game.ClientboundLevelChunkPacketData;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LightLayer;
import net.minecraft.world.level.lighting.LevelLightEngine;
import net.minecraft.world.level.chunk.LevelChunk;
import java.util.ArrayList;
import java.util.function.Consumer;
public final class CameraClientChunkCacheExtension {
private static final Long2ObjectOpenHashMap<LevelChunk> CHUNKS = new Long2ObjectOpenHashMap<>();
private CameraClientChunkCacheExtension() {}
public static boolean shouldKeepChunk(ChunkPos chunkPos) {
int renderDistance = Minecraft.getInstance().options.renderDistance().get();
return CameraChunkVisibilityManager.shouldKeepChunk(chunkPos, renderDistance)
|| CameraFeedHandler.shouldAddChunk(chunkPos, renderDistance);
}
public static void retain(LevelChunk chunk) {
synchronized (CHUNKS) {
CHUNKS.put(chunk.getPos().toLong(), chunk);
}
}
public static LevelChunk take(ClientLevel level, int x, int z) {
synchronized (CHUNKS) {
long position = ChunkPos.asLong(x, z);
LevelChunk chunk = CHUNKS.get(position);
return chunk != null && chunk.getLevel() == level ? CHUNKS.remove(position) : null;
}
}
public static void drop(ClientLevel level, ChunkPos chunkPos) {
LevelChunk chunk = take(level, chunkPos.x, chunkPos.z);
if (chunk != null) {
unload(level, chunk);
}
}
public static LevelChunk getChunk(int x, int z) {
synchronized (CHUNKS) {
return CHUNKS.get(ChunkPos.asLong(x, z));
}
}
public static LevelChunk replaceWithPacketData(ClientLevel level, int x, int z, FriendlyByteBuf packetData, CompoundTag chunkTag, Consumer<ClientboundLevelChunkPacketData.BlockEntityTagOutput> tagOutput) {
if (!Minecraft.getInstance().isSameThread()) {
throw new IllegalStateException("Camera chunk packet applied off the client thread");
}
ChunkPos chunkPos = new ChunkPos(x, z);
LevelChunk chunk = getChunk(x, z);
if (chunk == null || chunk.getLevel() != level) {
chunk = new LevelChunk(level, chunkPos);
}
chunk.replaceWithPacketData(packetData, chunkTag, tagOutput);
retain(chunk);
level.onChunkLoaded(chunkPos);
SodiumCompat.getInstance().notifyChunkAdded(level, x, z);
PlatformEvents.CLIENT_CHUNK_LOAD.postEvents(new ClientChunkEvent.Load(chunk, level));
return chunk;
}
public static void prune(ClientLevel level) {
for (LevelChunk chunk : snapshot()) {
if (chunk.getLevel() == level && !shouldKeepChunk(chunk.getPos())) {
try {
drop(level, chunk.getPos());
} finally {
queueLightRemoval(level, chunk.getPos());
}
}
}
}
public static void clear() {
ArrayList<LevelChunk> removed;
synchronized (CHUNKS) {
removed = new ArrayList<>(CHUNKS.values());
CHUNKS.clear();
}
RuntimeException failure = null;
for (LevelChunk chunk : removed) {
ClientLevel level = (ClientLevel) chunk.getLevel();
try {
try {
unload(level, chunk);
} finally {
queueLightRemoval(level, chunk.getPos());
}
} catch (RuntimeException exception) {
if (failure == null) failure = exception;
else if (failure != exception) failure.addSuppressed(exception);
}
}
if (failure != null) throw failure;
}
public static ArrayList<LevelChunk> snapshot() {
synchronized (CHUNKS) {
return new ArrayList<>(CHUNKS.values());
}
}
private static void queueLightRemoval(ClientLevel level, ChunkPos position) {
level.queueLightUpdate(() -> {
LevelLightEngine lightEngine = level.getLightEngine();
lightEngine.setLightEnabled(position, false);
for (int y = lightEngine.getMinLightSection(); y < lightEngine.getMaxLightSection(); y++) {
SectionPos section = SectionPos.of(position, y);
lightEngine.queueSectionData(LightLayer.BLOCK, section, null);
lightEngine.queueSectionData(LightLayer.SKY, section, null);
}
for (int y = level.getMinSection(); y < level.getMaxSection(); y++) {
lightEngine.updateSectionStatus(SectionPos.of(position, y), true);
}
});
}
public static void unload(ClientLevel level, LevelChunk chunk) {
ChunkPos position = chunk.getPos();
try {
SodiumCompat.getInstance().notifyChunkRemoved(level, position.x, position.z);
} finally {
try {
PlatformEvents.CLIENT_CHUNK_UNLOAD.postEvents(new ClientChunkEvent.Unload(chunk, level));
} finally {
level.unload(chunk);
}
}
}
}
@@ -0,0 +1,155 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.render.camera;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import it.unimi.dsi.fastutil.longs.LongSet;
import net.minecraft.client.renderer.chunk.SectionRenderDispatcher;
import net.minecraft.core.BlockPos;
import net.minecraft.core.SectionPos;
import net.minecraft.world.level.Level;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.UUID;
public final class CameraViewAreaExtension {
private static final Long2ObjectOpenHashMap<SectionRenderDispatcher.RenderSection> SECTIONS = new Long2ObjectOpenHashMap<>();
private static final Map<UUID, LongSet> OWNERS = new HashMap<>();
private static SectionRenderDispatcher sectionRenderDispatcher;
private static final AtomicLong REVISION = new AtomicLong();
private static final AtomicLong GRAPH_REVISION = new AtomicLong();
private static final Set<SectionRenderDispatcher.RenderSection> CAMERA_SECTIONS = Collections.synchronizedSet(Collections.newSetFromMap(new WeakHashMap<>()));
private static int minSectionY;
private static int maxSectionY;
private CameraViewAreaExtension() {
}
public static void allChanged(SectionRenderDispatcher dispatcher, Level level) {
clear();
sectionRenderDispatcher = dispatcher;
minSectionY = level.getMinSection();
maxSectionY = level.getMaxSection();
}
public static SectionRenderDispatcher.RenderSection rawFetch(int cx, int cy, int cz, boolean generateNew) {
if (sectionRenderDispatcher == null || cy < minSectionY || cy >= maxSectionY) {
return null;
}
long sectionPos = SectionPos.asLong(cx, cy, cz);
return generateNew ? provideSection(sectionPos) : SECTIONS.get(sectionPos);
}
public static void setDirty(int cx, int cy, int cz, boolean playerChanged) {
SectionRenderDispatcher.RenderSection section = rawFetch(cx, cy, cz, false);
if (section != null) {
section.setDirty(playerChanged);
}
}
public static void onChunkUnload(int sectionX, int sectionZ) {
REVISION.incrementAndGet();
Collection<SectionRenderDispatcher.RenderSection> removed = new ArrayList<>();
for (int sectionY = minSectionY; sectionY < maxSectionY; sectionY++) {
long sectionPos = SectionPos.asLong(sectionX, sectionY, sectionZ);
SectionRenderDispatcher.RenderSection section = SECTIONS.remove(sectionPos);
if (section != null) {
removed.add(section);
}
}
releaseBuffers(removed);
}
public static void clear() {
REVISION.incrementAndGet();
sectionRenderDispatcher = null;
minSectionY = 0;
maxSectionY = 0;
OWNERS.clear();
Collection<SectionRenderDispatcher.RenderSection> removed = new ArrayList<>(SECTIONS.values());
SECTIONS.clear();
releaseBuffers(removed);
}
public static long revision() {
return REVISION.get();
}
public static long graphRevision() {
return GRAPH_REVISION.get();
}
public static boolean onSectionCompiled(SectionRenderDispatcher.RenderSection section) {
// Late camera uploads must not enter the player's graph after their buffers have been released.
if (!CAMERA_SECTIONS.contains(section)) return false;
GRAPH_REVISION.incrementAndGet();
return true;
}
public static void retainSections(UUID cameraId, Collection<SectionRenderDispatcher.RenderSection> sections) {
LongSet positions = new LongOpenHashSet();
for (SectionRenderDispatcher.RenderSection section : sections) {
positions.add(SectionPos.asLong(section.getOrigin()));
}
LongSet previous = OWNERS.put(cameraId, positions);
releaseUnownedSections(previous);
}
public static void releaseSections(UUID cameraId) {
releaseUnownedSections(OWNERS.remove(cameraId));
}
private static void releaseUnownedSections(LongSet positions) {
if (positions == null) return;
Collection<SectionRenderDispatcher.RenderSection> removed = new ArrayList<>();
for (long position : positions) {
if (OWNERS.values().stream().anyMatch(owner -> owner.contains(position))) continue;
SectionRenderDispatcher.RenderSection section = SECTIONS.remove(position);
if (section != null) removed.add(section);
}
releaseBuffers(removed);
}
private static void releaseBuffers(Collection<SectionRenderDispatcher.RenderSection> sections) {
RuntimeException failure = null;
for (SectionRenderDispatcher.RenderSection section : sections) {
try {
section.releaseBuffers();
} catch (RuntimeException exception) {
if (failure == null) failure = exception;
else if (failure != exception) failure.addSuppressed(exception);
}
}
if (failure != null) throw failure;
}
private static SectionRenderDispatcher.RenderSection provideSection(long sectionPos) {
return SECTIONS.computeIfAbsent(sectionPos, CameraViewAreaExtension::createSection);
}
private static SectionRenderDispatcher.RenderSection createSection(long sectionPos) {
if (sectionRenderDispatcher == null) {
throw new IllegalStateException("SectionRenderDispatcher not initialised");
}
BlockPos origin = SectionPos.of(sectionPos).origin();
SectionRenderDispatcher.RenderSection section = sectionRenderDispatcher.new RenderSection(0, origin.getX(), origin.getY(), origin.getZ());
CAMERA_SECTIONS.add(section);
return section;
}
}
@@ -0,0 +1,14 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.render.camera.accessor;
public interface CameraFeedRenderState {
boolean cobblemonRenderHand();
boolean cobblemonRenderBlockOutline();
}
@@ -8,6 +8,8 @@
package com.cobblemon.mod.common.mixin;
import com.cobblemon.mod.common.api.cinematic.runtime.CinematicManager;
import net.minecraft.world.damagesource.DamageSource;
import com.cobblemon.mod.common.CobblemonNetwork;
import com.cobblemon.mod.common.OrientationControllable;
import com.cobblemon.mod.common.api.orientation.OrientationController;
@@ -132,6 +134,16 @@ public abstract class LivingEntityMixin extends Entity implements OrientationCon
}
}
@Inject(method = "hurt(Lnet/minecraft/world/damagesource/DamageSource;F)Z", at = @At("HEAD"), cancellable = true)
private void cobblemon$cancelDamageDuringCinematicTakeover(DamageSource source, float amount, CallbackInfoReturnable<Boolean> cir) {
final LivingEntity self = (LivingEntity) (Object) this;
if (self instanceof ServerPlayer serverPlayer) {
if (CinematicManager.INSTANCE.isPlayerUnderImmuneTakeover(serverPlayer.getUUID())) {
cir.setReturnValue(false);
}
}
}
@Inject(method = "decreaseAirSupply", at = @At("HEAD"), cancellable = true)
private void cobblemon$conditionalWaterBreathing(
int currentAir,
@@ -11,9 +11,12 @@ package com.cobblemon.mod.common.mixin.accessor;
import net.minecraft.world.entity.Entity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
import org.spongepowered.asm.mixin.gen.Invoker;
@Mixin(Entity.class)
public interface AccessorEntity {
@Invoker("updateFluidOnEyes")
void cobblemonUpdateFluidOnEyes();
@Accessor("eyeHeight")
void standingEyeHeight(float standingEyeHeight);
@@ -0,0 +1,19 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.mixin.accessor;
import net.minecraft.client.Camera;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(Camera.class)
public interface CameraAccessor {
@Accessor("detached")
void setDetached(boolean detached);
}
@@ -0,0 +1,41 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.mixin.accessor;
import net.minecraft.client.multiplayer.ClientChunkCache;
import net.minecraft.world.level.chunk.LevelChunk;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
import org.spongepowered.asm.mixin.gen.Invoker;
import java.util.concurrent.atomic.AtomicReferenceArray;
@Mixin(ClientChunkCache.Storage.class)
public interface CameraChunkStorageAccessor {
@Accessor("chunks")
AtomicReferenceArray<LevelChunk> cobblemon$getChunks();
@Accessor("chunkRadius")
int cobblemon$getChunkRadius();
@Accessor("chunkCount")
int cobblemon$getChunkCount();
@Accessor("chunkCount")
void cobblemon$setChunkCount(int count);
@Accessor("viewCenterX")
int cobblemon$getViewCenterX();
@Accessor("viewCenterZ")
int cobblemon$getViewCenterZ();
@Invoker("getIndex")
int cobblemon$getIndex(int x, int z);
}
@@ -0,0 +1,20 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.mixin.accessor;
import net.minecraft.server.level.ServerEntity;
import net.minecraft.world.entity.Entity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(ServerEntity.class)
public interface ServerEntityEntityAccessor {
@Accessor("entity")
Entity getEntity();
}
@@ -9,10 +9,13 @@
package com.cobblemon.mod.common.mixin.accessor;
import net.minecraft.server.level.ServerEntity;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.network.ServerPlayerConnection;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
import org.spongepowered.asm.mixin.gen.Invoker;
import java.util.List;
import java.util.Set;
@Mixin(targets = "net.minecraft.server.level.ChunkMap$TrackedEntity")
@@ -22,4 +25,7 @@ public interface TrackedEntityAccessor {
@Accessor("serverEntity")
ServerEntity getServerEntity();
}
@Invoker("updatePlayers")
void cobblemon$updatePlayers(List<ServerPlayer> players);
}
@@ -0,0 +1,47 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.mixin.cinematic;
import com.cobblemon.mod.common.api.cinematic.runtime.CinematicViewerScopedEntity;
import com.cobblemon.mod.common.mixin.accessor.ServerEntityEntityAccessor;
import com.cobblemon.mod.common.mixin.accessor.TrackedEntityAccessor;
import net.minecraft.server.level.ServerEntity;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.Set;
import java.util.UUID;
@Mixin(targets = "net.minecraft.server.level.ChunkMap$TrackedEntity")
public abstract class TrackedEntityMixin {
// Cancellation must also unpair existing viewers, or they keep receiving updates after the scope narrows.
@Inject(method = "updatePlayer", at = @At("HEAD"), cancellable = true)
private void cobblemon$filterViewerScopedEntity(ServerPlayer player, CallbackInfo ci) {
final TrackedEntityAccessor self = (TrackedEntityAccessor) this;
final ServerEntity serverEntity = self.getServerEntity();
if (serverEntity == null) return;
final Entity entity = ((ServerEntityEntityAccessor) serverEntity).getEntity();
if (entity == null) return;
if (entity instanceof CinematicViewerScopedEntity scoped) {
final Set<UUID> allowed = scoped.getCobblemonSceneViewers();
if (allowed != null && !allowed.contains(player.getUUID())) {
if (self.getSeenBy().remove(player.connection)) {
serverEntity.removePairing(player);
}
ci.cancel();
}
}
}
}
@@ -0,0 +1,36 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.mixin.client;
import com.cobblemon.mod.common.client.camera.CameraClientChunkCacheExtension;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.multiplayer.ClientPacketListener;
import net.minecraft.network.protocol.game.ClientboundForgetLevelChunkPacket;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.chunk.LevelChunk;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(ClientPacketListener.class)
public abstract class CameraClientPacketListenerMixin {
@Shadow
private ClientLevel level;
@Inject(method = "queueLightRemoval", at = @At("HEAD"), cancellable = true)
private void cobblemon$keepCameraLight(ClientboundForgetLevelChunkPacket packet, CallbackInfo ci) {
ChunkPos position = packet.pos();
LevelChunk retained = CameraClientChunkCacheExtension.getChunk(position.x, position.z);
if (retained != null && retained.getLevel() == level) {
ci.cancel();
}
}
}
@@ -0,0 +1,127 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.mixin.client;
import com.cobblemon.mod.common.client.camera.CameraClientChunkCacheExtension;
import com.cobblemon.mod.common.client.render.camera.CameraFeedHandler;
import com.cobblemon.mod.common.client.render.camera.CameraViewAreaExtension;
import com.cobblemon.mod.common.client.util.compat.SodiumCompat;
import com.llamalad7.mixinextras.injector.v2.WrapWithCondition;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import net.minecraft.client.Camera;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.world.entity.Entity;
import net.minecraft.client.renderer.LevelRenderer;
import net.minecraft.client.renderer.ViewArea;
import net.minecraft.client.renderer.chunk.SectionRenderDispatcher;
import net.minecraft.client.renderer.culling.Frustum;
import net.minecraft.core.BlockPos;
import net.minecraft.core.SectionPos;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.ModifyVariable;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(value = LevelRenderer.class, priority = 1100)
public abstract class CameraLevelRendererMixin {
private static final int CINEMATIC_RENDER_DISTANCE_CHUNKS = 32;
private static final float CINEMATIC_FOG_DISTANCE = CINEMATIC_RENDER_DISTANCE_CHUNKS * 16;
@Shadow
private SectionRenderDispatcher sectionRenderDispatcher;
@Shadow
private ClientLevel level;
@WrapWithCondition(method = "setupRender", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/ViewArea;repositionCamera(DD)V"))
private boolean cobblemon$shouldRepositionCamera(ViewArea viewArea, double x, double z) {
return !CameraFeedHandler.isCapturing();
}
// Sodium owns its setup pipeline even while vanilla feed sections are substituted.
@Inject(method = "setupRender", at = @At("HEAD"), cancellable = true)
private void cobblemon$onSetupRender(Camera camera, Frustum frustum, boolean hasCapturedFrustum, boolean isSpectator, CallbackInfo ci) {
if (CameraFeedHandler.isCapturing() && !SodiumCompat.getInstance().isActive()) {
CameraFeedHandler.prepareVisibleSections(frustum);
ci.cancel();
}
}
@Inject(method = "setLevel", at = @At("HEAD"))
private void cobblemon$onSetLevel(ClientLevel level, CallbackInfo ci) {
try {
if (this.level != level) {
CameraClientChunkCacheExtension.clear();
}
} finally {
CameraViewAreaExtension.clear();
}
}
@Inject(method = "allChanged", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/SectionOcclusionGraph;waitAndReset(Lnet/minecraft/client/renderer/ViewArea;)V"))
private void cobblemon$onAllChanged(CallbackInfo ci) {
CameraViewAreaExtension.allChanged(sectionRenderDispatcher, level);
}
@Inject(method = "addRecentlyCompiledSection", at = @At("HEAD"), cancellable = true)
private void cobblemon$onSectionCompiled(SectionRenderDispatcher.RenderSection section, CallbackInfo ci) {
if (CameraViewAreaExtension.onSectionCompiled(section)) {
ci.cancel();
}
}
@Inject(method = "isSectionCompiled", at = @At("HEAD"), cancellable = true)
private void cobblemon$onIsSectionCompiled(BlockPos pos, CallbackInfoReturnable<Boolean> cir) {
if (CameraFeedHandler.isCapturing()) {
SectionPos sectionPos = SectionPos.of(pos);
SectionRenderDispatcher.RenderSection section = CameraViewAreaExtension.rawFetch(sectionPos.x(), sectionPos.y(), sectionPos.z(), false);
if (section != null && section.getCompiled() != SectionRenderDispatcher.CompiledSection.UNCOMPILED) {
cir.setReturnValue(true);
}
}
}
@ModifyVariable(method = "renderLevel", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/FogRenderer;setupColor(Lnet/minecraft/client/Camera;FLnet/minecraft/client/multiplayer/ClientLevel;IF)V"), ordinal = 1)
private float cobblemon$modifyFogRenderDistance(float original) {
if (CameraFeedHandler.isCapturing())
return CINEMATIC_FOG_DISTANCE;
return original;
}
// A Marker camera otherwise excludes the local player from vanilla entity rendering.
@WrapOperation(
method = "renderLevel",
at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Camera;getEntity()Lnet/minecraft/world/entity/Entity;")
)
private Entity cobblemon$getCameraEntityDuringCapture(Camera camera, Operation<Entity> original) {
if (CameraFeedHandler.isCapturing()) {
Entity player = Minecraft.getInstance().player;
if (player != null) return player;
}
return original.call(camera);
}
@WrapOperation(
method = "renderLevel",
at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Camera;isDetached()Z")
)
private boolean cobblemon$isCameraDetachedDuringCapture(Camera camera, Operation<Boolean> original) {
if (CameraFeedHandler.isCapturing()) {
return true;
}
return original.call(camera);
}
}
@@ -8,8 +8,12 @@
package com.cobblemon.mod.common.mixin.client;
import com.cobblemon.mod.common.api.camera.CameraRotation;
import com.cobblemon.mod.common.api.orientation.OrientationController;
import com.cobblemon.mod.common.api.riding.Rideable;
import com.cobblemon.mod.common.client.MountedPokemonAnimationRenderController;
import com.cobblemon.mod.common.client.camera.ClientCameraController;
import com.cobblemon.mod.common.client.render.camera.CameraFeedHandler;
import com.cobblemon.mod.common.client.render.camera.MountedCameraRenderer;
import com.cobblemon.mod.common.duck.CameraDuck;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
@@ -19,12 +23,15 @@ import net.minecraft.client.Camera;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.phys.Vec3;
import java.util.UUID;
import org.joml.Quaternionf;
import org.joml.Vector3f;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(Camera.class)
public abstract class CameraMixin implements CameraDuck {
@@ -32,6 +39,7 @@ public abstract class CameraMixin implements CameraDuck {
@Shadow private float eyeHeightOld;
@Shadow private BlockGetter level;
@Shadow private Entity entity;
@Final @Shadow private Quaternionf rotation;
@Final @Shadow private Vector3f forwards;
@Final @Shadow private Vector3f up;
@Final @Shadow private Vector3f left;
@@ -43,6 +51,7 @@ public abstract class CameraMixin implements CameraDuck {
@Override public BlockGetter cobblemon$getLevel() {
return level;
}
@Shadow protected abstract void setRotation(float yRot, float xRot);
@Override public Entity cobblemon$getEntity() {
return entity;
@@ -86,4 +95,29 @@ public abstract class CameraMixin implements CameraDuck {
MountedCameraRenderer.INSTANCE.setSmoothRotation(null);
original.call(instance, x, y, z);
}
// Apply after vanilla third-person and sleeping offsets so authored transforms remain exact.
@Inject(method = "setup", at = @At("RETURN"))
private void cobblemon$applyCinematicTransform(BlockGetter level, Entity entity, boolean detached, boolean thirdPersonReverse, float partialTick, CallbackInfo ci) {
UUID capturedId = CameraFeedHandler.getCurrentlyCapturedCamera();
CameraFeedHandler.FeedContext context = capturedId != null ? CameraFeedHandler.getFeedContext(capturedId) : null;
float roll;
if (context != null) {
setPosition(context.getPosition());
setRotation(context.getYaw(), context.getPitch());
roll = context.getRoll();
} else {
if (!ClientCameraController.shouldUseCinematicCamera()) return;
ClientCameraController.ActiveTransform transform = ClientCameraController.getCameraTransform();
if (transform == null) return;
CameraRotation rotation = transform.getRotation();
setPosition(transform.getPosition());
setRotation(rotation.getYaw(), rotation.getPitch());
roll = transform.getRoll();
}
this.rotation.rotateZ((float) Math.toRadians(roll));
OrientationController.Companion.getFORWARDS().rotate(this.rotation, this.forwards);
OrientationController.Companion.getUP().rotate(this.rotation, this.up);
OrientationController.Companion.getLEFT().rotate(this.rotation, this.left);
}
}
@@ -0,0 +1,178 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.mixin.client;
import com.cobblemon.mod.common.client.camera.CameraClientChunkCacheExtension;
import com.cobblemon.mod.common.mixin.accessor.CameraChunkStorageAccessor;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import net.minecraft.client.multiplayer.ClientChunkCache;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.protocol.game.ClientboundLevelChunkPacketData;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.util.concurrent.atomic.AtomicReferenceArray;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
@Mixin(value = ClientChunkCache.class, priority = 1100)
public abstract class ClientChunkCacheMixin {
@Shadow
volatile ClientChunkCache.Storage storage;
@Shadow
@Final
ClientLevel level;
@Shadow
@Final
private LevelChunk emptyChunk;
@Inject(method = "drop", at = @At("HEAD"), cancellable = true)
private void cobblemon$onDrop(ChunkPos chunkPos, CallbackInfo ci) {
if (CameraClientChunkCacheExtension.shouldKeepChunk(chunkPos)) {
CameraChunkStorageAccessor access = (CameraChunkStorageAccessor) (Object) storage;
int index = access.cobblemon$getIndex(chunkPos.x, chunkPos.z);
LevelChunk chunk = access.cobblemon$getChunks().get(index);
if (chunk != null && chunk.getPos().equals(chunkPos)) {
cobblemon$retain(access, index, chunk);
}
ci.cancel();
} else {
CameraClientChunkCacheExtension.drop(level, chunkPos);
}
}
@Inject(method = "updateViewCenter", at = @At("HEAD"))
private void cobblemon$onViewCenter(int x, int z, CallbackInfo ci) {
CameraChunkStorageAccessor access = (CameraChunkStorageAccessor) (Object) storage;
cobblemon$retainOutsideRange(access, x, z, access.cobblemon$getChunkRadius());
}
@Inject(method = "updateViewRadius", at = @At("HEAD"))
private void cobblemon$onViewRadius(int radius, CallbackInfo ci) {
CameraChunkStorageAccessor access = (CameraChunkStorageAccessor) (Object) storage;
cobblemon$retainOutsideRange(access, access.cobblemon$getViewCenterX(), access.cobblemon$getViewCenterZ(), Math.max(2, radius) + 3);
}
@Inject(method = "updateViewCenter", at = @At("TAIL"))
private void cobblemon$afterViewCenter(int x, int z, CallbackInfo ci) {
cobblemon$promoteCameraChunks((CameraChunkStorageAccessor) (Object) storage);
}
@Inject(method = "updateViewRadius", at = @At("TAIL"))
private void cobblemon$afterViewRadius(int radius, CallbackInfo ci) {
cobblemon$promoteCameraChunks((CameraChunkStorageAccessor) (Object) storage);
}
@Inject(method = "tick", at = @At("HEAD"))
private void cobblemon$onTick(BooleanSupplier hasTimeLeft, boolean tickChunks, CallbackInfo ci) {
CameraClientChunkCacheExtension.prune(level);
}
@Inject(method = "replaceWithPacketData", at = @At("HEAD"), cancellable = true)
private void cobblemon$onReplaceWithPacketData(int x, int z, FriendlyByteBuf buffer, CompoundTag chunkTag, Consumer<ClientboundLevelChunkPacketData.BlockEntityTagOutput> tagOutputConsumer, CallbackInfoReturnable<LevelChunk> cir) {
if (!storage.inRange(x, z)) {
if (CameraClientChunkCacheExtension.shouldKeepChunk(new ChunkPos(x, z))) {
cir.setReturnValue(CameraClientChunkCacheExtension.replaceWithPacketData(level, x, z, buffer, chunkTag, tagOutputConsumer));
}
return;
}
CameraChunkStorageAccessor access = (CameraChunkStorageAccessor) (Object) storage;
int index = access.cobblemon$getIndex(x, z);
LevelChunk previous = access.cobblemon$getChunks().get(index);
if (previous != null && (previous.getPos().x != x || previous.getPos().z != z)
&& CameraClientChunkCacheExtension.shouldKeepChunk(previous.getPos())) {
cobblemon$retain(access, index, previous);
}
}
@WrapOperation(method = "replaceWithPacketData", at = @At(value = "NEW", target = "(Lnet/minecraft/world/level/Level;Lnet/minecraft/world/level/ChunkPos;)Lnet/minecraft/world/level/chunk/LevelChunk;"))
private LevelChunk cobblemon$reuseCameraChunk(Level level, ChunkPos position, Operation<LevelChunk> original) {
LevelChunk retained = CameraClientChunkCacheExtension.take(this.level, position.x, position.z);
return retained != null ? retained : original.call(level, position);
}
@Inject(method = "replaceBiomes", at = @At("HEAD"), cancellable = true)
private void cobblemon$onReplaceBiomes(int x, int z, FriendlyByteBuf buffer, CallbackInfo ci) {
LevelChunk chunk = CameraClientChunkCacheExtension.getChunk(x, z);
if (chunk != null && chunk.getLevel() == level) {
chunk.replaceBiomes(buffer);
ci.cancel();
}
}
@Inject(method = "getChunk(IILnet/minecraft/world/level/chunk/status/ChunkStatus;Z)Lnet/minecraft/world/level/chunk/LevelChunk;", at = @At("RETURN"), cancellable = true)
private void cobblemon$onGetChunk(int x, int z, ChunkStatus chunkStatus, boolean requireChunk, CallbackInfoReturnable<LevelChunk> cir) {
if (cir.getReturnValue() == null || cir.getReturnValue() == emptyChunk) {
LevelChunk chunk = CameraClientChunkCacheExtension.getChunk(x, z);
if (chunk != null && chunk.getLevel() == level) {
cir.setReturnValue(chunk);
}
}
}
@Unique
private void cobblemon$promoteCameraChunks(CameraChunkStorageAccessor access) {
AtomicReferenceArray<LevelChunk> chunks = access.cobblemon$getChunks();
for (LevelChunk chunk : CameraClientChunkCacheExtension.snapshot()) {
ChunkPos position = chunk.getPos();
if (chunk.getLevel() != level
|| Math.abs(position.x - access.cobblemon$getViewCenterX()) > access.cobblemon$getChunkRadius()
|| Math.abs(position.z - access.cobblemon$getViewCenterZ()) > access.cobblemon$getChunkRadius()) continue;
int index = access.cobblemon$getIndex(position.x, position.z);
LevelChunk previous = chunks.get(index);
if (previous != null && CameraClientChunkCacheExtension.shouldKeepChunk(previous.getPos())) {
cobblemon$retain(access, index, previous);
previous = null;
}
CameraClientChunkCacheExtension.take(level, position.x, position.z);
chunks.set(index, chunk);
if (previous == null) {
access.cobblemon$setChunkCount(access.cobblemon$getChunkCount() + 1);
} else {
CameraClientChunkCacheExtension.unload(level, previous);
}
}
}
@Unique
private void cobblemon$retainOutsideRange(CameraChunkStorageAccessor access, int centerX, int centerZ, int radius) {
AtomicReferenceArray<LevelChunk> chunks = access.cobblemon$getChunks();
for (int index = 0; index < chunks.length(); index++) {
LevelChunk chunk = chunks.get(index);
if (chunk == null) continue;
ChunkPos position = chunk.getPos();
if ((Math.abs(position.x - centerX) > radius || Math.abs(position.z - centerZ) > radius)
&& CameraClientChunkCacheExtension.shouldKeepChunk(position)) {
cobblemon$retain(access, index, chunk);
}
}
}
@Unique
private void cobblemon$retain(CameraChunkStorageAccessor access, int index, LevelChunk chunk) {
// Detach before loader unload callbacks: lighting and block entities belong to this same LevelChunk.
if (access.cobblemon$getChunks().compareAndSet(index, chunk, null)) {
access.cobblemon$setChunkCount(access.cobblemon$getChunkCount() - 1);
CameraClientChunkCacheExtension.retain(chunk);
}
}
}
@@ -9,43 +9,142 @@
package com.cobblemon.mod.common.mixin.client;
import com.cobblemon.mod.common.api.scheduling.ClientTaskTracker;
import com.cobblemon.mod.common.client.camera.GameCamera;
import com.cobblemon.mod.common.client.camera.ClientCameraController;
import com.cobblemon.mod.common.client.cinematic.ClientSceneSessionManager;
import com.cobblemon.mod.common.client.render.camera.accessor.CameraFeedRenderState;
import com.cobblemon.mod.common.client.render.camera.CameraFeedHandler;
import com.cobblemon.mod.common.client.render.camera.compositor.CompositorManager;
import net.minecraft.client.Camera;
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.llamalad7.mixinextras.sugar.Local;
import net.minecraft.client.DeltaTracker;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.GameRenderer;
import net.minecraft.world.entity.Entity;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Mutable;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.ModifyArg;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.util.function.Predicate;
import java.util.UUID;
@Mixin(GameRenderer.class)
public class GameRendererMixin {
@Unique
long lastTime = -1;
public abstract class GameRendererMixin implements CameraFeedRenderState {
@Inject(
method = "render",
at = @At(value = "TAIL")
)
public void render(DeltaTracker counter, boolean tick, CallbackInfo ci) {
@Shadow private boolean renderHand;
@Shadow private boolean renderBlockOutline;
@Override public boolean cobblemonRenderHand() { return renderHand; }
@Override public boolean cobblemonRenderBlockOutline() { return renderBlockOutline; }
@Shadow @Final @Mutable
private Camera mainCamera;
@Unique
private Camera cobblemon$playerCamera;
@Unique
private final GameCamera cobblemon$gameCamera = new GameCamera();
@Unique
private boolean cobblemon$usingCinematic;
@Unique
private long cobblemon$lastTime = -1;
@Inject(method = "render", at = @At("HEAD"))
private void cobblemon$swapCamera(DeltaTracker counter, boolean tick, CallbackInfo ci) {
Minecraft client = Minecraft.getInstance();
long newTime = System.currentTimeMillis();
// Don't play scheduled animations when the game is paused
if (client.isPaused()) {
lastTime = newTime;
if (client.level == null) {
cobblemon$restoreCamera();
return;
}
if (lastTime != -1) {
ClientTaskTracker.INSTANCE.update((newTime - lastTime) / 1000F);
ClientCameraController.advanceFrame(client.isPaused() ? 0f : counter.getRealtimeDeltaTicks() / 20f);
ClientSceneSessionManager.INSTANCE.getAllSessions().forEach(session -> session.refreshCompositor());
if (ClientCameraController.shouldUseCinematicCamera()) {
if (!cobblemon$usingCinematic) {
cobblemon$playerCamera = mainCamera;
mainCamera = cobblemon$gameCamera;
cobblemon$usingCinematic = true;
}
cobblemon$gameCamera.prepare(client, counter.getGameTimeDeltaPartialTick(tick));
} else if (cobblemon$usingCinematic) {
cobblemon$restoreCamera();
}
lastTime = newTime;
}
// WrapOperation allows Iris/Sodium to share this call site; Redirect claims it exclusively.
@WrapOperation(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/GameRenderer;renderLevel(Lnet/minecraft/client/DeltaTracker;)V"))
private void cobblemon$skipVanillaRenderLevel(GameRenderer instance, DeltaTracker deltaTracker, Operation<Void> original) {
if (!CompositorManager.isFullscreenActive()) {
original.call(instance, deltaTracker);
}
}
@Inject(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/GameRenderer;tryTakeScreenshotIfNeeded()V"))
private void cobblemon$afterLevelRendering(DeltaTracker deltaTracker, boolean renderLevel, CallbackInfo ci) {
boolean compositorNeedsFeeds = CompositorManager.isFullscreenActive()
|| !CompositorManager.INSTANCE.getRequiredFeeds().isEmpty();
if (compositorNeedsFeeds && CameraFeedHandler.hasFeeds()) {
CameraFeedHandler.captureFeeds(deltaTracker);
}
if (CompositorManager.isFullscreenActive()) {
CompositorManager.INSTANCE.compositeAll();
CompositorManager.renderFullscreen();
}
}
@Inject(method = "pick", at = @At("HEAD"), cancellable = true)
private void cobblemon$preventFramePick(float partialTicks, CallbackInfo ci) {
if (CameraFeedHandler.isCapturing())
ci.cancel();
}
@Inject(method = "getFov", at = @At("HEAD"), cancellable = true)
private void cobblemon$overrideCinematicFov(Camera camera, float partialTicks, boolean useFovSetting, CallbackInfoReturnable<Double> cir) {
UUID capturedId = CameraFeedHandler.getCurrentlyCapturedCamera();
Float fov;
if (capturedId != null) {
CameraFeedHandler.FeedContext context = CameraFeedHandler.getFeedContext(capturedId);
fov = context != null ? context.getFov() : null;
} else {
if (camera != cobblemon$gameCamera || !ClientCameraController.shouldUseCinematicCamera()) return;
fov = ClientCameraController.getPrimaryFovOverride();
}
if (fov != null) {
cir.setReturnValue(fov.doubleValue());
}
}
@Inject(method = "render", at = @At(value = "TAIL"))
private void cobblemon$afterRender(DeltaTracker counter, boolean tick, CallbackInfo ci) {
Minecraft client = Minecraft.getInstance();
long newTime = System.currentTimeMillis();
if (client.isPaused()) {
cobblemon$lastTime = newTime;
return;
}
if (cobblemon$lastTime != -1) {
float delta = (newTime - cobblemon$lastTime) / 1000F;
ClientTaskTracker.INSTANCE.update(delta);
}
cobblemon$lastTime = newTime;
}
@ModifyArg(
@@ -60,4 +159,12 @@ public class GameRendererMixin {
return (entityX) -> !(entity.getVehicle() == entityX && entityX instanceof PokemonEntity) && original.test(entityX);
}
@Unique
private void cobblemon$restoreCamera() {
if (cobblemon$usingCinematic) {
mainCamera = cobblemon$playerCamera;
cobblemon$playerCamera = null;
cobblemon$usingCinematic = false;
}
}
}
@@ -10,13 +10,11 @@ package com.cobblemon.mod.common.mixin.client;
import com.cobblemon.mod.common.OrientationControllable;
import com.cobblemon.mod.common.client.render.player.MountedPlayerRenderer;
import com.cobblemon.mod.common.client.render.player.RecallArmAnimationTracker;
import com.cobblemon.mod.common.duck.RidePassenger;
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity;
import net.minecraft.client.model.HumanoidModel;
import net.minecraft.client.model.geom.ModelPart;
import net.minecraft.client.player.AbstractClientPlayer;
import net.minecraft.util.Mth;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import org.spongepowered.asm.mixin.Final;
@@ -98,14 +96,4 @@ public class HumanoidModelMixin {
MountedPlayerRenderer.INSTANCE.animate((HumanoidModel<AbstractClientPlayer>)(Object)this, pokemonEntity, player, netHeadYaw, headPitch, ageInTicks, limbSwing, limbSwingAmount);
}
}
@Inject(method = "setupAnim*", at = @At("TAIL"))
public void cobblemon$poseRecallArm(LivingEntity entity, float limbSwing, float limbSwingAmount, float ageInTicks, float netHeadYaw, float headPitch, CallbackInfo ci) {
if (!(entity instanceof AbstractClientPlayer) || entity.getVehicle() instanceof PokemonEntity) return;
float raise = RecallArmAnimationTracker.INSTANCE.raiseFactor(entity.getUUID());
if (raise <= 0f) return;
this.rightArm.xRot = Mth.lerp(raise, this.rightArm.xRot, (float) (-Math.PI / 2.0));
this.rightArm.yRot = Mth.lerp(raise, this.rightArm.yRot, 0f);
this.rightArm.zRot = Mth.lerp(raise, this.rightArm.zRot, 0f);
}
}
@@ -1,32 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.mixin.client;
import com.cobblemon.mod.common.client.render.player.RecallArmAnimationTracker;
import com.mojang.blaze3d.vertex.PoseStack;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.entity.layers.ItemInHandLayer;
import net.minecraft.world.entity.HumanoidArm;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.item.ItemDisplayContext;
import net.minecraft.world.item.ItemStack;
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(ItemInHandLayer.class)
public class ItemInHandLayerMixin {
@Inject(method = "renderArmWithItem", at = @At("HEAD"), cancellable = true)
private void cobblemon$hideRecallHandItem(LivingEntity entity, ItemStack itemStack, ItemDisplayContext itemDisplayContext, HumanoidArm humanoidArm, PoseStack poseStack, MultiBufferSource multiBufferSource, int light, CallbackInfo ci) {
if (humanoidArm == HumanoidArm.RIGHT && RecallArmAnimationTracker.INSTANCE.isRecalling(entity.getUUID())) {
ci.cancel();
}
}
}
@@ -9,7 +9,7 @@
package com.cobblemon.mod.common.mixin.client;
import com.cobblemon.mod.common.OrientationControllable;
import com.cobblemon.mod.common.client.render.player.FirstPersonRecallBallRenderer;
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;
@@ -29,10 +29,12 @@ 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 (abstractClientPlayer.isUsingItem() && abstractClientPlayer.getUseItem().getItem() instanceof PokedexItem) {
if (ClientCameraController.shouldUseCinematicCamera()) {
ci.cancel();
return;
}
else if (interactionHand == InteractionHand.MAIN_HAND && FirstPersonRecallBallRenderer.INSTANCE.render(poseStack, multiBufferSource, j, f)) {
if (abstractClientPlayer.isUsingItem() && abstractClientPlayer.getUseItem().getItem() instanceof PokedexItem) {
ci.cancel();
}
else if (abstractClientPlayer instanceof OrientationControllable controllable && controllable.getOrientationController().isActive()) {
@@ -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,12 +8,8 @@ object CobblemonBuildDetails {
const val VERSION = "{{ version }}"
const val SNAPSHOT = {{ isSnapshot }}
// Read at runtime rather than templated in, so a new commit doesn't invalidate compiled code.
private val git = java.util.Properties().apply {
CobblemonBuildDetails::class.java.getResourceAsStream("/cobblemon-git.properties")?.use(::load)
}
@JvmField val GIT_COMMIT: String = git.getProperty("commit", "Not Specified")
@JvmField val BRANCH: String = git.getProperty("branch", "Not Specified")
const val GIT_COMMIT = "{{ gitCommit | default("Not Specified") }}"
const val BRANCH = "{{ branch | default("Not Specified") }}"
fun smallCommitHash(): String {
return this.GIT_COMMIT.substring(0, 7)
@@ -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)
)
}
@@ -16,8 +16,6 @@ import com.cobblemon.mod.common.block.MintBlock.MintType
import com.cobblemon.mod.common.block.campfirepot.CampfireBlock
import com.cobblemon.mod.common.block.campfirepot.CampfirePotBlock
import com.cobblemon.mod.common.block.chest.GildedChestBlock
import com.cobblemon.mod.common.block.decor.ActivatableDecorationBlock
import com.cobblemon.mod.common.block.decor.RingTargetBlock
import com.cobblemon.mod.common.block.general.BaleBlock
import com.cobblemon.mod.common.block.general.HorizontalRotationCarpetBlock
import com.cobblemon.mod.common.block.general.HorizontalRotationalBlock
@@ -622,88 +620,6 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<
@JvmField
val WEAKNESS_POLICY = create("weakness_policy", WallAttachedDirectionalShapeBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_PAPER_LARGE_SOUNDS).mapColor(MapColor.NONE).noOcclusion().noCollission(), 14, 12, 1))
@JvmField
val RING_TARGET = create("ring_target", RingTargetBlock(BlockBehaviour.Properties.of().sound(SoundType.WOOD).mapColor(MapColor.NONE).noOcclusion().strength(0.125f)))
@JvmField
val MAGNET = create("magnet", MagnetBlock(BlockBehaviour.Properties.of().sound(SoundType.METAL).mapColor(MapColor.NONE).noOcclusion().strength(0.075f)))
@JvmField
val GALARICA_WREATH = create("galarica_wreath", GalaricaWreathBlock(BlockBehaviour.Properties.of().sound(SoundType.AZALEA_LEAVES).mapColor(MapColor.NONE).noOcclusion().noCollission()))
@JvmField
val FLAME_ORB = create("flame_orb", OrbBlock(BlockBehaviour.Properties.of().sound(SoundType.GLASS).mapColor(MapColor.NONE).noOcclusion().lightLevel{15}))
@JvmField
val TOXIC_ORB = create("toxic_orb", OrbBlock(BlockBehaviour.Properties.of().sound(SoundType.GLASS).mapColor(MapColor.NONE).noOcclusion()))
@JvmField
val LIFE_ORB = create("life_orb", OrbBlock(BlockBehaviour.Properties.of().sound(SoundType.GLASS).mapColor(MapColor.NONE).noOcclusion().lightLevel{7}))
@JvmField
val LIGHT_BALL = create("light_ball", OrbBlock(BlockBehaviour.Properties.of().sound(SoundType.GLASS).mapColor(MapColor.NONE).noOcclusion().lightLevel{13}))
@JvmField
val SMOKE_BALL = create("smoke_ball", OrbBlock(BlockBehaviour.Properties.of().sound(SoundType.GLASS).mapColor(MapColor.NONE).noOcclusion()))
@JvmField
val KINGS_ROCK = create("kings_rock", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.TUMBLESTONE_BLOCK_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.CENTERED_6x))
@JvmField
val GALARICA_CUFF = create("galarica_cuff", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.AZALEA_LEAVES).mapColor(MapColor.NONE).noCollission().noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.FLATTENED_10x4))
@JvmField
val UNREMARKABLE_TEACUP = create("unremarkable_teacup", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.DECORATED_POT).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.TEACUP))
@JvmField
val MASTERPIECE_TEACUP = create("masterpiece_teacup", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.DECORATED_POT).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.TEACUP))
@JvmField
val CHIPPED_POT = create("chipped_pot", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.DECORATED_POT).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.TEAPOT))
@JvmField
val CRACKED_POT = create("cracked_pot", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.DECORATED_POT).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.TEAPOT))
@JvmField
val SACHET = create("sachet", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.WOOL).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.POUCH))
@JvmField
val METAL_POWDER = create("metal_powder", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.WOOL).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.POUCH))
@JvmField
val QUICK_POWDER = create("quick_powder", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.WOOL).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.POUCH))
@JvmField
val BRIGHT_POWDER = create("bright_powder", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.WOOL).mapColor(MapColor.NONE).noOcclusion().lightLevel{3}, DecorativeItemBlock.DecorativeItemBlockType.POUCH))
@JvmField
val NEVER_MELT_ICE = create("never_melt_ice", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.GLASS).mapColor(MapColor.NONE).noOcclusion().friction(0.989F), DecorativeItemBlock.DecorativeItemBlockType.NEVERMELT))
@JvmField
val SCOPE_LENS = create("scope_lens", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.COPPER).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.SCOPE))
@JvmField
val WIDE_LENS = create("wide_lens", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.COPPER).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.WIDE))
@JvmField
val ZOOM_LENS = create("zoom_lens", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.COPPER).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.ZOOM))
@JvmField
val ICY_ROCK = create("icy_rock", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.DEEPSLATE).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.FLATTENED_8x4))
@JvmField
val HEAT_ROCK = create("heat_rock", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.DEEPSLATE).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.FLATTENED_8x4))
@JvmField
val SMOOTH_ROCK = create("smooth_rock", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.DEEPSLATE).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.FLATTENED_8x4))
@JvmField
val DAMP_ROCK = create("damp_rock", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.DEEPSLATE).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.FLATTENED_8x6))
@JvmField
val DESTINY_KNOT = create("destiny_knot", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.WOOL).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.KNOT))
@JvmField
val LUMINOUS_MOSS = create("luminous_moss", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.MOSS).mapColor(MapColor.NONE).noOcclusion().lightLevel{5}, DecorativeItemBlock.DecorativeItemBlockType.CENTERED_8x))
@JvmField
val METRONOME = create("metronome", ActivatableDecorationBlock(BlockBehaviour.Properties.of().sound(SoundType.METAL).mapColor(MapColor.NONE).pushReaction(PushReaction.DESTROY).noOcclusion(), ActivatableDecorationBlock.ActivatableDecorationBlockType.METRONOME))
@JvmField
val EJECT_BUTTON = create("eject_button", EjectButtonBlock(BlockBehaviour.Properties.of().sound(SoundType.METAL).mapColor(MapColor.NONE).pushReaction(PushReaction.DESTROY).noOcclusion().strength(0.125f), 40))
@JvmField
val LOADED_DICE = create("loaded_dice", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.DICE))
@JvmField
val IRON_BALL = create("iron_ball", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.METAL).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.IRON_BALL))
@JvmField
val METAL_COAT = create("metal_coat", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.METAL).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.METAL_COAT))
@JvmField
val UPGRADE = create("upgrade", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.UPGRADE))
@JvmField
val DUBIOUS_DISC = create("dubious_disc", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.UPGRADE))
@JvmField
val THROAT_SPRAY = create("throat_spray", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.THROAT_SPRAY))
@JvmField
val FULL_HEAL = create("full_heal", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.STATUS_RESTORE))
@JvmField
@@ -754,23 +670,6 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<
@JvmField
val X_SPEED = create("x_${Stats.SPEED.identifier.path}", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.BATTLE_ITEM))
@JvmField
val HP_UP = create("hp_up", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.VITAMIN))
@JvmField
val PROTEIN = create("protein", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.VITAMIN))
@JvmField
val IRON = create("iron", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.VITAMIN))
@JvmField
val CARBOS = create("carbos", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.VITAMIN))
@JvmField
val CALCIUM = create("calcium", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.VITAMIN))
@JvmField
val ZINC = create("zinc", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.VITAMIN))
@JvmField
val PP_UP = create("pp_up", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.VITAMIN))
@JvmField
val PP_MAX = create("pp_max", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.VITAMIN))
@JvmField
val MONITOR = create("monitor", MonitorBlock(BlockBehaviour.Properties.of().mapColor(MapColor.METAL).sound(SoundType.METAL).pushReaction(PushReaction.BLOCK).requiresCorrectToolForDrops().strength(5.0F, 6.0F).lightLevel { if (it.getValue(MonitorBlock.SCREEN) != MonitorBlock.MonitorScreen.OFF) 13 else 0 }))
@JvmField
@@ -784,9 +683,11 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<
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 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 }))
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 }))
val TM_MACHINE = create(
"tm_machine",
TMMachineBlock(
@@ -815,7 +716,7 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<
@JvmField
val PASTURE = create("pasture", PastureBlock(BlockBehaviour.Properties.of().mapColor(MapColor.COLOR_BROWN).sound(SoundType.WOOD).strength(2F).noOcclusion().pushReaction(PushReaction.BLOCK).lightLevel { if ((it.getValue(PastureBlock.ON) as Boolean) && (it.getValue(PastureBlock.PART) == PastureBlock.PasturePart.TOP)) 13 else 0 }))
@JvmField
val RED_MINT = create("red_mint", MintBlock(MintType.RED, BlockBehaviour.Properties.of().mapColor(MapColor.COLOR_RED).noCollission().randomTicks().instabreak().sound(CobblemonSounds.MINT_SOUNDS).pushReaction(PushReaction.DESTROY)))
@JvmField
val BLUE_MINT = create("blue_mint", MintBlock(MintType.BLUE, BlockBehaviour.Properties.of().mapColor(MapColor.COLOR_BLUE).noCollission().randomTicks().instabreak().sound(CobblemonSounds.MINT_SOUNDS).pushReaction(PushReaction.DESTROY)))
@@ -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())
}
}
}
@@ -12,7 +12,6 @@ import com.cobblemon.mod.common.api.net.NetworkPacket
import com.mojang.brigadier.arguments.ArgumentType
import kotlin.reflect.KClass
import net.minecraft.commands.synchronization.ArgumentTypeInfo
import net.minecraft.core.HolderLookup
import net.minecraft.resources.ResourceKey
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.MinecraftServer
@@ -169,16 +168,12 @@ interface CobblemonImplementation {
* TODO
*
* @param identifier
* @param reloaderFactory
* @param reloader
* @param type
* @param dependencies
*/
fun registerResourceReloader(
identifier: ResourceLocation,
type: PackType,
dependencies: Collection<ResourceLocation>,
reloaderFactory: (HolderLookup.Provider) -> PreparableReloadListener
)
fun registerResourceReloader(identifier: ResourceLocation, reloader: PreparableReloadListener, type: PackType, dependencies: Collection<ResourceLocation>)
/**
* TODO
*
@@ -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
@@ -472,9 +478,9 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
// Evolution items
@JvmField val LINK_CABLE = create("link_cable", LinkCableItem())
@JvmField val DRAGON_SCALE = noSettingsItem("dragon_scale")
@JvmField val METAL_COAT = itemNameBlockItem("metal_coat", CobblemonBlocks.METAL_COAT)
@JvmField val UPGRADE = itemNameBlockItem("upgrade", CobblemonBlocks.UPGRADE)
@JvmField val DUBIOUS_DISC = itemNameBlockItem("dubious_disc", CobblemonBlocks.DUBIOUS_DISC)
@JvmField val METAL_COAT = noSettingsItem("metal_coat")
@JvmField val UPGRADE = noSettingsItem("upgrade")
@JvmField val DUBIOUS_DISC = noSettingsItem("dubious_disc")
@JvmField val DEEP_SEA_SCALE = noSettingsItem("deep_sea_scale")
@JvmField val DEEP_SEA_TOOTH = noSettingsItem("deep_sea_tooth")
@JvmField val ELECTIRIZER = noSettingsItem("electirizer")
@@ -483,7 +489,7 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField val PROTECTOR = noSettingsItem("protector")
@JvmField val REAPER_CLOTH = noSettingsItem("reaper_cloth")
@JvmField val PRISM_SCALE = noSettingsItem("prism_scale")
@JvmField val SACHET = itemNameBlockItem("sachet", CobblemonBlocks.SACHET)
@JvmField val SACHET = noSettingsItem("sachet")
@JvmField val WHIPPED_DREAM = create("whipped_dream", foodItem(8, 0.3f))
@JvmField val STRAWBERRY_SWEET = create("strawberry_sweet", foodItem(6, 0.125f))
@JvmField val LOVE_SWEET = create("love_sweet", foodItem(6, 0.125f))
@@ -492,20 +498,22 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField val FLOWER_SWEET = create("flower_sweet", foodItem(6, 0.125f))
@JvmField val STAR_SWEET = create("star_sweet", foodItem(6, 0.125f))
@JvmField val RIBBON_SWEET = create("ribbon_sweet", foodItem(6, 0.125f))
@JvmField val CHIPPED_POT = itemNameBlockItem("chipped_pot", CobblemonBlocks.CHIPPED_POT)
@JvmField val CRACKED_POT = itemNameBlockItem("cracked_pot", CobblemonBlocks.CRACKED_POT)
@JvmField val MASTERPIECE_TEACUP = itemNameBlockItem("masterpiece_teacup", CobblemonBlocks.MASTERPIECE_TEACUP)
@JvmField val UNREMARKABLE_TEACUP = itemNameBlockItem("unremarkable_teacup", CobblemonBlocks.UNREMARKABLE_TEACUP)
@JvmField val CHIPPED_POT = noSettingsItem("chipped_pot")
@JvmField val CRACKED_POT = noSettingsItem("cracked_pot")
@JvmField val MASTERPIECE_TEACUP = noSettingsItem("masterpiece_teacup")
@JvmField val UNREMARKABLE_TEACUP = noSettingsItem("unremarkable_teacup")
@JvmField val SWEET_APPLE = compostableItem("sweet_apple", foodItem(4, 0.3f), 0.65f)
@JvmField val TART_APPLE = compostableItem("tart_apple", foodItem(4, 0.3f), 0.65f)
@JvmField val SYRUPY_APPLE = compostableItem("syrupy_apple", foodItem(4, 0.3f), 0.65f)
@JvmField val GALARICA_CUFF = itemNameBlockItem("galarica_cuff", CobblemonBlocks.GALARICA_CUFF)
@JvmField val GALARICA_CUFF = noSettingsItem("galarica_cuff")
@JvmField val GALARICA_WREATH = noSettingsItem("galarica_wreath")
@JvmField val BLACK_AUGURITE = noSettingsItem("black_augurite")
@JvmField val PEAT_BLOCK = noSettingsItem("peat_block")
@JvmField val RAZOR_CLAW = noSettingsItem("razor_claw")
@JvmField val RAZOR_FANG = noSettingsItem("razor_fang")
@JvmField val AUSPICIOUS_ARMOR = heldItem("auspicious_armor")
@JvmField val MALICIOUS_ARMOR = heldItem("malicious_armor")
@JvmField val SHELL_HELMET = heldItem("shell_helmet")
@JvmField val METAL_ALLOY = noSettingsItem("metal_alloy")
@JvmField val SCROLL_OF_DARKNESS = noSettingsItem("scroll_of_darkness")
@JvmField val SCROLL_OF_WATERS = noSettingsItem("scroll_of_waters")
@@ -649,17 +657,17 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val EXPERIENCE_CANDY_XL = candyItem("exp_candy_xl") { _, _ -> CandyItem.DEFAULT_XL_CANDY_YIELD }
@JvmField
val CALCIUM = create("calcium", VitaminItem(Stats.SPECIAL_ATTACK, CobblemonBlocks.CALCIUM))
val CALCIUM = create("calcium", VitaminItem(Stats.SPECIAL_ATTACK))
@JvmField
val CARBOS = create("carbos", VitaminItem(Stats.SPEED, CobblemonBlocks.CARBOS))
val CARBOS = create("carbos", VitaminItem(Stats.SPEED))
@JvmField
val HP_UP = create("hp_up", VitaminItem(Stats.HP, CobblemonBlocks.HP_UP))
val HP_UP = create("hp_up", VitaminItem(Stats.HP))
@JvmField
val IRON = create("iron", VitaminItem(Stats.DEFENCE, CobblemonBlocks.IRON))
val IRON = create("iron", VitaminItem(Stats.DEFENCE))
@JvmField
val PROTEIN = create("protein", VitaminItem(Stats.ATTACK, CobblemonBlocks.PROTEIN))
val PROTEIN = create("protein", VitaminItem(Stats.ATTACK))
@JvmField
val ZINC = create("zinc", VitaminItem(Stats.SPECIAL_DEFENCE, CobblemonBlocks.ZINC))
val ZINC = create("zinc", VitaminItem(Stats.SPECIAL_DEFENCE))
@JvmField
val HEALTH_MOCHI = create("health_mochi", MochiItem(Stats.HP))
@JvmField
@@ -741,9 +749,9 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val MAX_REVIVE = create("max_revive", ReviveItem(max = true))
@JvmField
val PP_UP = create("pp_up", PPUpItem(1, CobblemonBlocks.PP_UP))
val PP_UP = create("pp_up", PPUpItem(1))
@JvmField
val PP_MAX = create("pp_max", PPUpItem(3, CobblemonBlocks.PP_MAX))
val PP_MAX = create("pp_max", PPUpItem(3))
@JvmField
val RED_MINT_SEEDS = mintSeed("red", MintType.RED.getCropBlock())
@@ -920,18 +928,11 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField val CHOICE_SPECS = wearableItem("choice_specs")
@JvmField val EXP_SHARE = wearableItem("exp_share")
@JvmField val FOCUS_BAND = wearableItem("focus_band")
@JvmField val KINGS_ROCK = wearableBlockItem("kings_rock", CobblemonBlocks.KINGS_ROCK)
@JvmField val NEVER_MELT_ICE = wearableBlockItem("never_melt_ice", CobblemonBlocks.NEVER_MELT_ICE)
@JvmField val KINGS_ROCK = wearableItem("kings_rock")
@JvmField val MUSCLE_BAND = wearableItem("muscle_band")
@JvmField val ROCKY_HELMET = wearableItem("rocky_helmet")
@JvmField val SAFETY_GOGGLES = wearableItem("safety_goggles")
@JvmField val WISE_GLASSES = wearableItem("wise_glasses")
@JvmField val SHARP_BEAK = wearableItem("sharp_beak")
@JvmField val COVERT_CLOAK = wearableItem("covert_cloak")
@JvmField val SHELL_HELMET = wearableItem("shell_helmet")
@JvmField val GALARICA_WREATH = wearableBlockItem("galarica_wreath", CobblemonBlocks.GALARICA_WREATH)
@JvmField val CLEANSE_TAG = wearableBlockItem("cleanse_tag", CobblemonBlocks.CLEANSE_TAG)
@JvmField val SPELL_TAG = wearableBlockItem("spell_tag", CobblemonBlocks.SPELL_TAG)
// Held Items
@JvmField
@@ -959,13 +960,17 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val CHOICE_SCARF = heldItem("choice_scarf")
@JvmField
val CLEANSE_TAG = itemNameBlockItem("cleanse_tag", CobblemonBlocks.CLEANSE_TAG)
@JvmField
val CLEAR_AMULET = heldItem("clear_amulet")
@JvmField
val DESTINY_KNOT = itemNameBlockItem("destiny_knot", CobblemonBlocks.DESTINY_KNOT)
val COVERT_CLOAK = heldItem("covert_cloak")
@JvmField
val DESTINY_KNOT = heldItem("destiny_knot")
@JvmField
val DRAGON_FANG = heldItem("dragon_fang")
@JvmField
val EJECT_BUTTON = itemNameBlockItem("eject_button", CobblemonBlocks.EJECT_BUTTON)
val EJECT_BUTTON = heldItem("eject_button")
@JvmField
val EJECT_PACK = heldItem("eject_pack")
@JvmField
@@ -979,7 +984,7 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val FAIRY_FEATHER = heldItem("fairy_feather")
@JvmField
val FLAME_ORB = itemNameBlockItem("flame_orb", CobblemonBlocks.FLAME_ORB)
val FLAME_ORB = heldItem("flame_orb")
@JvmField
val FLOAT_STONE = heldItem("float_stone")
@JvmField
@@ -993,32 +998,34 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val HEAVY_DUTY_BOOTS = heldItem("heavy_duty_boots")
@JvmField
val IRON_BALL = itemNameBlockItem("iron_ball", CobblemonBlocks.IRON_BALL)
val IRON_BALL = heldItem("iron_ball")
@JvmField
val LAGGING_TAIL = heldItem("lagging_tail")
@JvmField
val LEFTOVERS = compostableHeldItem("leftovers", null, 0.50f)
@JvmField
val LIFE_ORB = itemNameBlockItem("life_orb", CobblemonBlocks.LIFE_ORB)
val LIFE_ORB = heldItem("life_orb")
@JvmField
val LIGHT_BALL = itemNameBlockItem("light_ball", CobblemonBlocks.LIGHT_BALL)
val LIGHT_BALL = heldItem("light_ball")
@JvmField
val LIGHT_CLAY = heldItem("light_clay")
@JvmField
val LOADED_DICE = itemNameBlockItem("loaded_dice", CobblemonBlocks.LOADED_DICE)
val LOADED_DICE = heldItem("loaded_dice")
@JvmField
val LUCKY_EGG = heldItem("lucky_egg")
@JvmField
val LUMINOUS_MOSS = itemNameBlockItem("luminous_moss", CobblemonBlocks.LUMINOUS_MOSS)
val LUMINOUS_MOSS = heldItem("luminous_moss")
@JvmField
val MAGNET = itemNameBlockItem("magnet", CobblemonBlocks.MAGNET)
val MAGNET = heldItem("magnet")
@JvmField
val METRONOME = itemNameBlockItem("metronome", CobblemonBlocks.METRONOME)
val METRONOME = heldItem("metronome")
@JvmField
val MIRACLE_SEED = heldItem("miracle_seed")
@JvmField
val MYSTIC_WATER = heldItem("mystic_water")
@JvmField
val NEVER_MELT_ICE = heldItem("never_melt_ice")
@JvmField
val POISON_BARB = heldItem("poison_barb")
@JvmField
val POWER_ANKLET = heldItem("power_anklet")
@@ -1043,11 +1050,13 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val RED_CARD = heldItem("red_card")
@JvmField
val RING_TARGET = itemNameBlockItem("ring_target", CobblemonBlocks.RING_TARGET)
val RING_TARGET = heldItem("ring_target")
@JvmField
val ROOM_SERVICE = heldItem("room_service")
@JvmField
val SCOPE_LENS = itemNameBlockItem("scope_lens", CobblemonBlocks.SCOPE_LENS)
val SCOPE_LENS = heldItem("scope_lens")
@JvmField
val SHARP_BEAK = heldItem("sharp_beak")
@JvmField
val SHED_SHELL = heldItem("shed_shell")
@JvmField
@@ -1059,7 +1068,9 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val SOFT_SAND = heldItem("soft_sand")
@JvmField
val SMOKE_BALL = itemNameBlockItem("smoke_ball", CobblemonBlocks.SMOKE_BALL)
val SPELL_TAG = itemNameBlockItem("spell_tag", CobblemonBlocks.SPELL_TAG)
@JvmField
val SMOKE_BALL = heldItem("smoke_ball")
@JvmField
val SOOTHE_BELL = heldItem("soothe_bell")
@JvmField
@@ -1067,9 +1078,9 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val TERRAIN_EXTENDER = heldItem("terrain_extender")
@JvmField
val THROAT_SPRAY = itemNameBlockItem("throat_spray", CobblemonBlocks.THROAT_SPRAY)
val THROAT_SPRAY = heldItem("throat_spray")
@JvmField
val TOXIC_ORB = itemNameBlockItem("toxic_orb", CobblemonBlocks.TOXIC_ORB)
val TOXIC_ORB = heldItem("toxic_orb")
@JvmField
val TWISTED_SPOON = heldItem("twisted_spoon")
@JvmField
@@ -1077,9 +1088,9 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val WEAKNESS_POLICY = itemNameBlockItem("weakness_policy", CobblemonBlocks.WEAKNESS_POLICY)
@JvmField
val WIDE_LENS = itemNameBlockItem("wide_lens", CobblemonBlocks.WIDE_LENS)
val WIDE_LENS = heldItem("wide_lens")
@JvmField
val ZOOM_LENS = itemNameBlockItem("zoom_lens", CobblemonBlocks.ZOOM_LENS)
val ZOOM_LENS = heldItem("zoom_lens")
@JvmField
val MENTAL_HERB = compostableHeldItem("mental_herb", null, 0.85F)
@JvmField
@@ -1091,19 +1102,19 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val WHITE_HERB = compostableHeldItem("white_herb", null, 0.85F)
@JvmField
val BRIGHT_POWDER = itemNameBlockItem("bright_powder", CobblemonBlocks.BRIGHT_POWDER)
val BRIGHT_POWDER = heldItem("bright_powder")
@JvmField
val METAL_POWDER = itemNameBlockItem("metal_powder", CobblemonBlocks.METAL_POWDER)
val METAL_POWDER = heldItem("metal_powder")
@JvmField
val QUICK_POWDER = itemNameBlockItem("quick_powder", CobblemonBlocks.QUICK_POWDER)
val QUICK_POWDER = heldItem("quick_powder")
@JvmField
val DAMP_ROCK = itemNameBlockItem("damp_rock", CobblemonBlocks.DAMP_ROCK)
val DAMP_ROCK = heldItem("damp_rock")
@JvmField
val HEAT_ROCK = itemNameBlockItem("heat_rock", CobblemonBlocks.HEAT_ROCK)
val HEAT_ROCK = heldItem("heat_rock")
@JvmField
val SMOOTH_ROCK = itemNameBlockItem("smooth_rock", CobblemonBlocks.SMOOTH_ROCK)
val SMOOTH_ROCK = heldItem("smooth_rock")
@JvmField
val ICY_ROCK = itemNameBlockItem("icy_rock", CobblemonBlocks.ICY_ROCK)
val ICY_ROCK = heldItem("icy_rock")
// Mulch
@JvmField
@@ -1594,16 +1605,7 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
private fun wearableItem(name: String, heldItemRemappedName: String? = null): CobblemonItem = create(
name,
WearableHatItem(name).also {
wearables.add(it)
if (heldItemRemappedName != null) {
CobblemonHeldItemManager.registerRemap(it, heldItemRemappedName)
}
}
)
private fun wearableBlockItem(name: String, block: Block, heldItemRemappedName: String? = null): WearableBlockItem = create(
name,
WearableBlockItem(name, block).also {
WearableItem(name).also {
wearables.add(it)
if (heldItemRemappedName != null) {
CobblemonHeldItemManager.registerRemap(it, heldItemRemappedName)
@@ -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))
@@ -425,7 +511,6 @@ object CobblemonNetwork {
list.add(PacketRegisterInfo(MovesRegistrySyncPacket.ID, MovesRegistrySyncPacket::decode, DataRegistrySyncPacketHandler()))
list.add(PacketRegisterInfo(BerryRegistrySyncPacket.ID, BerryRegistrySyncPacket::decode, DataRegistrySyncPacketHandler()))
list.add(PacketRegisterInfo(SpeciesRegistrySyncPacket.ID, SpeciesRegistrySyncPacket::decode, DataRegistrySyncPacketHandler()))
list.add(PacketRegisterInfo(SpeciesEvolutionSyncPacket.ID, SpeciesEvolutionSyncPacket::decode, DataRegistrySyncPacketHandler()))
list.add(PacketRegisterInfo(PropertiesCompletionRegistrySyncPacket.ID, PropertiesCompletionRegistrySyncPacket::decode, DataRegistrySyncPacketHandler()))
list.add(PacketRegisterInfo(StandardSpeciesFeatureSyncPacket.ID, StandardSpeciesFeatureSyncPacket::decode, DataRegistrySyncPacketHandler()))
list.add(PacketRegisterInfo(GlobalSpeciesFeatureSyncPacket.ID, GlobalSpeciesFeatureSyncPacket::decode, DataRegistrySyncPacketHandler()))
@@ -446,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))
@@ -475,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))
@@ -574,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))
@@ -641,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))
@@ -650,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
}
@@ -657,4 +790,4 @@ object CobblemonNetwork {
fun sendPacketToPlayer(player: ServerPlayer, packet: NetworkPacket<*>) {
Cobblemon.implementation.networkManager.sendPacketToPlayer(player, packet)
}
}
}
@@ -35,6 +35,7 @@ import com.cobblemon.mod.common.battles.ActiveBattlePokemon
import com.cobblemon.mod.common.battles.BattleCaptureAction
import com.cobblemon.mod.common.battles.BattleFormat
import com.cobblemon.mod.common.battles.BattleRegistry
import com.cobblemon.mod.common.battles.stadium.StadiumBattleSession
import com.cobblemon.mod.common.battles.BattleSide
import com.cobblemon.mod.common.battles.FleeAttemptActionResponse
import com.cobblemon.mod.common.battles.ForfeitActionResponse
@@ -80,14 +81,18 @@ import net.minecraft.world.item.ItemStack
open class PokemonBattle(
val format: BattleFormat,
val side1: BattleSide,
val side2: BattleSide
val side2: BattleSide,
initializeProgress: Boolean = true
) {
/** Whether logging will be silenced for this battle. */
var mute = !CobblemonBuildDetails.SNAPSHOT
val struct = this.asMoLangValue()
val runtime = MoLangRuntime().also { it.environment.query = struct }
val runtime = MoLangRuntime().also {
it.environment.query = struct
}
val onEndHandlers: MutableList<(PokemonBattle) -> Unit> = mutableListOf()
var stadiumSession: StadiumBattleSession? = null
val battlePartyStores = mutableListOf<PlayerPartyStore>()
@@ -96,6 +101,12 @@ open class PokemonBattle(
side2.battle = this
this.actors.forEach { actor ->
actor.battle = this
}
if (initializeProgress) resetEvolutionProgress()
}
internal fun resetEvolutionProgress() {
actors.forEach { actor ->
actor.pokemonList.forEach { battlePokemon ->
battlePokemon.effectedPokemon.evolutionProxy.current().progress()
.filterIsInstance<LastBattleCriticalHitsEvolutionProgress>()
@@ -237,7 +248,9 @@ open class PokemonBattle(
}
fun turn(newTurnNumber: Int) {
actors.forEach { it.turn() }
// Prepare all actors before notifying synchronous AI, or its first response can clear other actors' requests.
actors.forEach { it.prepareTurn() }
actors.forEach { it.fireChoiceRequest() }
// TODO: If a pokemon switches in the same turn another pokemon is KO'd it will not receive exp for the KO
for (side in sides) {
val opposite = side.getOppositeSide()
@@ -250,6 +263,7 @@ open class PokemonBattle(
}
fun end() {
if (ended) return
ended = true
val awardToFainted = Cobblemon.config.awardExperienceToFaintedPokemon
val awardOnLoss = Cobblemon.config.awardExperienceOnBattleLoss
@@ -464,7 +478,7 @@ open class PokemonBattle(
val message = battleLang("crash").red()
this.actors.filterIsInstance<PlayerBattleActor>().forEach { it.entity?.sendSystemMessage(message) }
this.saveBattleLog()
this.stop()
if (stadiumSession != null) BattleRegistry.abortStadiumBattle(battleId) else this.stop()
return
}
@@ -472,6 +486,7 @@ open class PokemonBattle(
ticks++
if (isPvW && !ended && dispatches.isEmpty()) checkFlee()
}
stadiumSession?.tick()
}
open fun checkFlee() {
@@ -507,6 +522,7 @@ open class PokemonBattle(
}
fun stop() {
if (ended) return
writeShowdownAction(">forcetie") // This will terminate the Showdown connection
end()
}
@@ -583,4 +599,4 @@ open class PokemonBattle(
queryStruct.addFunction("has_rule") { params -> DoubleValue(params.getString(0) in format.ruleSet) }
return queryStruct
}
}
}
@@ -11,8 +11,9 @@ package com.cobblemon.mod.common.api.battles.model.actor
import com.cobblemon.mod.common.Cobblemon.LOGGER
import com.cobblemon.mod.common.api.battles.model.ai.BattleAI
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.battles.DefaultActionResponse
import com.cobblemon.mod.common.battles.ForcePassActionResponse
import com.cobblemon.mod.common.battles.PassActionResponse
import com.cobblemon.mod.common.battles.ShowdownActionResponse
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.exception.IllegalActionChoiceException
import com.cobblemon.mod.common.net.messages.client.battle.BattleFaintPacket
@@ -49,36 +50,40 @@ abstract class AIBattleActor(
* Called when the AI is requested to make a choice.
*/
open fun onChoiceRequested() {
val req = request
if (req == null) {
responses.clear()
expectingPassActions.clear()
mustChoose = false
battle.checkForInputDispatch()
return
}
val passActions = expectingPassActions.toList()
try {
request?.let {
setActionResponses(it.iterate(this.activePokemon) { battleMon, moveset, forceSwitch ->
battleAI.choose(battleMon, battle, getSide(), moveset, forceSwitch)
})
pokemonList.forEach { pokemon ->
pokemon.willBeSwitchedIn = false
}
} ?: {
val response = mutableListOf<ShowdownActionResponse>()
repeat(activePokemon.size) {
response.add(PassActionResponse)
}
setActionResponses(response)
LOGGER.warn("AI requested choice, but no request was set. Returning PassActionResponses.")
}
setActionResponses(req.iterate(this.activePokemon) { battleMon, moveset, forceSwitch ->
battleAI.choose(battleMon, battle, getSide(), moveset, forceSwitch)
})
} catch (exception: IllegalActionChoiceException) {
// Validation may have consumed forced passes and accepted a prefix before rejecting a later slot.
responses.clear()
expectingPassActions.clear()
expectingPassActions.addAll(passActions)
if (req.forceSwitch.any { it }) {
LOGGER.error("AI could not choose replacements; using Showdown's default choices", exception)
setActionResponses(req.iterate(activePokemon) { _, _, _ -> DefaultActionResponse() })
return
}
LOGGER.error("AI was unable to choose an action, we're going to need to pass!")
exception.printStackTrace()
request?.let {
setActionResponses(it.iterate(this.activePokemon) { _, _ , _->
PassActionResponse
})
} ?: {
val response = mutableListOf<ShowdownActionResponse>()
repeat(activePokemon.size) {
response.add(PassActionResponse)
}
setActionResponses(response)
}
var forcedPasses = passActions.size
setActionResponses(req.iterate(this.activePokemon) { _, moveset, forceSwitch ->
if (!forceSwitch && moveset != null && forcedPasses > 0) {
forcedPasses--
ForcePassActionResponse()
} else PassActionResponse
})
} finally {
pokemonList.forEach { it.willBeSwitchedIn = false }
}
}
}
}
@@ -24,8 +24,9 @@ import com.cobblemon.mod.common.net.messages.client.battle.BattleMakeChoicePacke
import com.cobblemon.mod.common.net.messages.client.battle.BattleMessagePacket
import net.minecraft.network.chat.Component
import net.minecraft.network.chat.MutableComponent
import java.util.UUID
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.phys.Vec3
import java.util.UUID
abstract class BattleActor(
val uuid: UUID,
@@ -52,6 +53,8 @@ abstract class BattleActor(
var itemsUsed = mutableListOf<BagItem>()
/** For when battles start, it's the number of Pokémon that are still in the process of being sent out (animation wise) */
var stillSendingOutCount = 0
/** Positions follow active slot order: slot a is index 0. */
var stadiumSendOutPositions: List<Vec3>? = null
abstract val type: ActorType
@@ -71,21 +74,31 @@ abstract class BattleActor(
open fun isForPlayer(serverPlayerEntity: ServerPlayer) = serverPlayerEntity.uuid in getPlayerUUIDs()
open fun isForPokemon(pokemonEntity: PokemonEntity) = activePokemon.any { it.battlePokemon?.effectedPokemon?.entity == pokemonEntity }
fun turn() {
val request = request ?: return
responses.clear()
if (activePokemon.any { it.isAlive() }) {
mustChoose = true
sendUpdate(BattleMakeChoicePacket())
}
val requestActive = request.active
if (requestActive == null || requestActive.isEmpty() || request.wait) {
this.request = null
/** Prepare every actor before notifying any AI, since synchronous choices can dispatch immediately. */
fun prepareTurn() {
mustChoose = false
val req = request ?: run {
responses.clear()
expectingPassActions.clear()
return
}
responses.clear()
if (activePokemon.any { it.isAlive() }) {
mustChoose = true
}
val requestActive = req.active
if (req.wait || (requestActive.isNullOrEmpty() && req.forceSwitch.none { it })) {
this.request = null
expectingPassActions.clear()
mustChoose = false
} else if (req.forceSwitch.any { it }) {
mustChoose = true
}
}
/** Call only after every actor has been prepared for this turn. */
fun fireChoiceRequest() {
if (mustChoose) sendUpdate(BattleMakeChoicePacket())
}
fun postUpdate() {
@@ -162,4 +175,4 @@ abstract class BattleActor(
open fun win(otherWinners: List<BattleActor>, losers: List<BattleActor>) {}
open fun lose(winners: List<BattleActor>, otherLosers: List<BattleActor>) {}
}
}
@@ -0,0 +1,15 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.camera
/** Minecraft yaw and pitch, in degrees. */
data class CameraRotation(
val yaw: Float,
val pitch: Float
)
@@ -0,0 +1,34 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.camera.model
// Air bubbles have no separate vanilla call that Fabric can cancel.
enum class HudElement(val id: String) {
CROSSHAIR("crosshair"),
HOTBAR("hotbar"),
EXPERIENCE_BAR("experience_bar"),
PLAYER_HEALTH("player_health"),
ARMOR("armor"),
FOOD("food"),
EFFECTS("effects"),
BOSS_BAR("boss_bar"),
SCOREBOARD("scoreboard"),
CHAT("chat"),
PLAYER_LIST("player_list"),
SELECTED_ITEM_NAME("selected_item_name"),
COBBLEMON_OVERLAY("cobblemon_overlay");
companion object {
val ALL: Set<HudElement> = entries.toSet()
private val BY_ID = entries.associateBy { it.id }
fun fromId(id: String): HudElement? = BY_ID[id]
}
}
@@ -0,0 +1,27 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.camera.model
data class ParameterDefinition(
val type: ParameterType,
val description: String? = null,
val default: ParameterDefault? = null
)
enum class ParameterType {
NUMBER,
VEC3,
STRING
}
sealed class ParameterDefault {
data class NumberDefault(val value: Double) : ParameterDefault()
data class Vec3Default(val x: Double, val y: Double, val z: Double) : ParameterDefault()
data class StringDefault(val value: String) : ParameterDefault()
}
@@ -0,0 +1,126 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.camera.model
import com.bedrockk.molang.runtime.MoLangRuntime
import com.bedrockk.molang.runtime.value.MoValue
import com.cobblemon.mod.common.api.molang.ExpressionLike
import com.cobblemon.mod.common.util.asExpressionLike
import com.cobblemon.mod.common.util.resolve
import com.google.gson.JsonElement
import net.minecraft.world.phys.Vec3
data class PositionExpression(
val x: CoordinateValue,
val y: CoordinateValue,
val z: CoordinateValue
) {
fun toVec3(
context: Map<String, MoValue> = emptyMap(),
runtime: MoLangRuntime,
origin: Vec3 = Vec3.ZERO,
isRelative: Boolean = false
): Vec3 {
val base = Vec3(
x.evaluate(context, runtime),
y.evaluate(context, runtime),
z.evaluate(context, runtime)
)
return if (isRelative) base.add(origin) else base
}
fun isFinite(context: Map<String, MoValue>, runtime: MoLangRuntime): Boolean {
val vec = toVec3(context, runtime)
return vec.x.isFinite() && vec.y.isFinite() && vec.z.isFinite()
}
companion object {
fun fromJson(element: JsonElement): PositionExpression {
return when {
element.isJsonObject -> {
val obj = element.asJsonObject
PositionExpression(
x = deserializeCoordinate(obj.get("x")),
y = deserializeCoordinate(obj.get("y")),
z = deserializeCoordinate(obj.get("z"))
)
}
element.isJsonArray -> {
val arr = element.asJsonArray
if (arr.size() != 3) {
throw IllegalArgumentException("Position array must have exactly 3 elements [x, y, z], got ${arr.size()}")
}
PositionExpression(
x = deserializeCoordinate(arr[0]),
y = deserializeCoordinate(arr[1]),
z = deserializeCoordinate(arr[2])
)
}
else -> {
throw IllegalArgumentException("Position must be either an object {x, y, z} or array [x, y, z]")
}
}
}
private fun deserializeCoordinate(element: JsonElement?): CoordinateValue {
if (element == null) {
return CoordinateValue.Static(0.0)
}
return when {
element.isJsonPrimitive && element.asJsonPrimitive.isNumber -> {
CoordinateValue.Static(element.asDouble)
}
element.isJsonPrimitive && element.asJsonPrimitive.isString -> {
CoordinateValue.Dynamic(element.asString.asExpressionLike())
}
element.isJsonObject -> {
val obj = element.asJsonObject
val type = obj.get("type")?.asString
val expression = obj.get("expression")?.asString
when (type) {
"expression" -> {
if (expression == null) {
throw IllegalArgumentException("Expression coordinate missing 'expression' field")
}
CoordinateValue.Dynamic(expression.asExpressionLike())
}
else -> {
throw IllegalArgumentException("Unknown coordinate type: $type")
}
}
}
else -> {
throw IllegalArgumentException("Invalid coordinate value: $element")
}
}
}
}
}
sealed class CoordinateValue {
data class Static(val value: Double) : CoordinateValue() {
init { require(value.isFinite()) { "Static coordinate must be finite" } }
}
data class Dynamic(val expression: ExpressionLike) : CoordinateValue()
fun evaluate(
context: Map<String, MoValue>,
runtime: MoLangRuntime
): Double {
return when (this) {
is Static -> value
is Dynamic -> runtime.resolve(expression, context).asDouble().also {
require(it.isFinite()) { "Dynamic coordinate must evaluate to a finite value" }
}
}
}
}
@@ -0,0 +1,21 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.camera.result
import net.minecraft.network.chat.Component
data class ValidationError(
val path: String,
val message: Component,
val severity: Severity = Severity.ERROR
) {
enum class Severity { WARNING, ERROR }
override fun toString(): String = "[$severity] $path: ${message.string}"
}
@@ -0,0 +1,49 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cinematic.model
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.entity.Entity
sealed class ActorBinding {
abstract val kind: ActorKind
/** Visible player actors override server position and suppress movement packets. */
data class Player(
val player: ServerPlayer,
val fallbackPokemon: PokemonProperties? = null
) : ActorBinding() {
override val kind: ActorKind = ActorKind.PlayerTakeover
}
data class Real(val entity: Entity) : ActorBinding() {
override val kind: ActorKind = ActorKind.RealEntity
}
data class SynthPokemon(
val properties: PokemonProperties,
val aspects: Set<String> = emptySet()
) : ActorBinding() {
override val kind: ActorKind = ActorKind.SynthesizedPokemon
}
data class SynthBedrock(
val category: ResourceLocation,
val aspects: Set<String> = emptySet(),
val scale: Float = 1.0f
) : ActorBinding() {
override val kind: ActorKind = ActorKind.SynthesizedBedrock
init {
require(scale.isFinite() && scale > 0f) { "SynthBedrock scale must be positive, got $scale" }
}
}
}
@@ -0,0 +1,28 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cinematic.model
data class ActorDeclaration(
val id: String,
val kind: ActorKind
) {
init {
require(id.isNotBlank()) { "ActorDeclaration id must be non-blank" }
}
}
sealed class ActorKind {
object PlayerTakeover : ActorKind()
object RealEntity : ActorKind()
object SynthesizedPokemon : ActorKind()
object SynthesizedBedrock : ActorKind()
}
@@ -0,0 +1,27 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cinematic.model
import com.cobblemon.mod.common.api.cinematic.model.tracks.ActorTrack
/** Null visibility carries over between beats; tracks never carry over. */
data class ActorDirection(
val visible: Boolean? = null,
val startPose: StartPose? = null,
val startPoseMode: StartPoseMode? = null,
val tracks: List<ActorTrack> = emptyList()
) {
init {
if (startPose != null) {
require(visible != false) {
"ActorDirection.startPose declared but visible = false; hidden actors cannot have a start pose"
}
}
}
}
@@ -0,0 +1,33 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cinematic.model
import com.cobblemon.mod.common.api.camera.model.HudElement
/** Null duration leaves the beat unbounded; feeds without cuts are inactive. */
data class Beat(
val id: String,
val duration: Float? = null,
val isLooping: Boolean = false,
val speed: Float = 1.0f,
val actorDirections: Map<String, ActorDirection> = emptyMap(),
val cuts: Map<String, CutTrack> = emptyMap(),
val guiTrack: SceneGuiTrack? = null,
val renderPlayerBody: Boolean = true,
val hiddenHudElements: Set<HudElement> = emptySet(),
val cues: List<SceneCue> = emptyList()
) {
init {
require(id.isNotBlank()) { "Beat id must be non-blank" }
require(speed.isFinite() && speed > 0f) { "Beat speed must be positive, got $speed" }
duration?.let {
require(it.isFinite() && it >= 0f) { "Beat duration must be non-negative, got $it" }
}
}
}
@@ -0,0 +1,25 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cinematic.model
import com.cobblemon.mod.common.api.molang.ExpressionLike
/** Clamping at a beat's duration does not end the scene; completion requires an explicit transition. */
data class BeatTransition(
val sourceBeatId: String,
val targetBeatId: String,
val condition: ExpressionLike,
val transition: CutTransition = CutTransition.Blend(0.4f),
val endsScene: Boolean = false
) {
init {
require(sourceBeatId.isNotBlank()) { "BeatTransition sourceBeatId must be non-blank" }
require(endsScene || targetBeatId.isNotBlank()) { "BeatTransition targetBeatId must be non-blank" }
}
}
@@ -6,11 +6,8 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.item
package com.cobblemon.mod.common.api.cinematic.model
import net.minecraft.world.item.ItemNameBlockItem
import net.minecraft.world.level.block.Block
import net.minecraft.network.chat.Component
class WearableBlockItem(override val name: String, block: Block): ItemNameBlockItem(block, Properties()), WearableItem{
}
class CinematicValidationException(val reason: Component) : IllegalArgumentException(reason.string)
@@ -0,0 +1,41 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cinematic.model
data class CutTrack(
val entries: List<CutEntry>
) {
init {
require(entries.isNotEmpty()) { "CutTrack must have at least one entry" }
require(entries.zipWithNext().all { (a, b) -> a.time <= b.time }) {
"CutTrack entries must be sorted ascending by time"
}
}
}
data class CutEntry(
val time: Float,
val shotId: String,
val transition: CutTransition = CutTransition.Instant
) {
init {
require(time.isFinite() && time >= 0f) { "CutEntry time must be non-negative, got $time" }
require(shotId.isNotBlank()) { "CutEntry shotId must be non-blank" }
}
}
sealed class CutTransition {
object Instant : CutTransition()
data class Blend(val duration: Float) : CutTransition() {
init {
require(duration.isFinite() && duration > 0f) { "Blend duration must be positive, got $duration" }
}
}
}
@@ -0,0 +1,16 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cinematic.model
enum class DisconnectPolicy {
END_SCENE,
SUBSTITUTE,
HIDE_ACTOR,
FAIL
}
@@ -0,0 +1,48 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cinematic.model
data class FeedDeclaration(
val id: String,
val kind: FeedKind
) {
init {
require(id.isNotBlank()) { "FeedDeclaration id must be non-blank" }
}
companion object {
/** Bound for feed counts read off the wire, so a crafted packet can't drive a huge allocation. */
const val MAX_FEEDS = 64
}
}
sealed class FeedKind {
/** Occupies the full viewport. */
object Fullscreen : FeedKind()
/** Picture-in-picture overlay in a viewport corner. [size] is a fraction of viewport. */
data class Pip(val corner: Corner, val size: Float) : FeedKind() {
init {
require(size > 0f && size <= 1f) { "Pip size must be in (0, 1], got $size" }
}
}
/** Relative share of a paired split. */
data class Split(val side: Side, val size: Float) : FeedKind() {
init {
require(size > 0f && size <= 1f) { "Split size must be in (0, 1], got $size" }
}
}
data class Diagonal(val half: DiagonalHalf) : FeedKind()
enum class Corner { TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT }
enum class Side { LEFT, RIGHT, TOP, BOTTOM }
enum class DiagonalHalf { UPPER, LOWER }
}
@@ -0,0 +1,23 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cinematic.model
fun <T> orderedKeyframes(keyframes: List<T>, time: (T) -> Float): List<T> {
require(keyframes.isNotEmpty()) { "At least one keyframe is required" }
require(keyframes.all { time(it).isFinite() && time(it) >= 0f }) { "Keyframe times must be finite and nonnegative" }
val ordered = ArrayList<T>(keyframes.size)
for (keyframe in keyframes.sortedBy(time)) {
if (ordered.isNotEmpty() && time(ordered.last()) == time(keyframe)) {
ordered[ordered.lastIndex] = keyframe
} else {
ordered.add(keyframe)
}
}
return ordered
}
@@ -0,0 +1,231 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cinematic.model
import net.minecraft.network.chat.Component
import com.cobblemon.mod.common.api.cinematic.runtime.ActorFraming
import com.cobblemon.mod.common.api.camera.model.ParameterDefinition
import com.cobblemon.mod.common.api.camera.result.ValidationError
import com.cobblemon.mod.common.api.cinematic.runtime.ActorDependencyOrder
import com.cobblemon.mod.common.api.cinematic.model.sources.builtin.OrbitPosition
import com.cobblemon.mod.common.api.cinematic.model.sources.builtin.LookAt
import com.cobblemon.mod.common.api.cinematic.model.sources.builtin.LookAtTarget
data class Scene(
val id: String,
val parameters: Map<String, ParameterDefinition> = emptyMap(),
val actors: Map<String, ActorDeclaration> = emptyMap(),
val shots: Map<String, Shot> = emptyMap(),
val feeds: List<FeedDeclaration> = emptyList(),
val startBeatId: String,
val beats: Map<String, Beat>,
val transitions: List<BeatTransition> = emptyList(),
val version: Int = 2,
val damageImmunityDuringTakeover: Boolean = false,
val disconnectPolicy: DisconnectPolicy = DisconnectPolicy.END_SCENE
) {
init {
require(id.isNotBlank()) { "Scene id must be non-blank" }
require(startBeatId.isNotBlank()) { "Scene startBeatId must be non-blank" }
require(version == 2) { "Scene.version must be 2, got $version" }
}
fun getBeat(beatId: String): Beat? = beats[beatId]
fun getShot(shotId: String): Shot? = shots[shotId]
fun getActor(actorId: String): ActorDeclaration? = actors[actorId]
fun getFeed(feedId: String): FeedDeclaration? = feeds.firstOrNull { it.id == feedId }
fun getTransitionsFrom(beatId: String): List<BeatTransition> =
transitions.filter { it.sourceBeatId == beatId }
fun validate(): List<ValidationError> {
val errors = mutableListOf<ValidationError>()
if (startBeatId !in beats) {
errors.add(ValidationError(
path = "startBeatId",
message = Component.translatable("cobblemon.cinematic_editor.validation.start_missing", startBeatId)
))
}
actors.forEach { (key, actor) ->
if (actor.id != key) {
errors.add(ValidationError(
path = "actors[$key].id",
message = Component.translatable("cobblemon.cinematic_editor.validation.actor_id", key, actor.id)
))
}
}
shots.forEach { (key, shot) ->
val framingActors = (shot.position as? OrbitPosition)?.actorIds.orEmpty() +
listOfNotNull((shot.position as? OrbitPosition)?.bearingActorId) +
((shot.rotation as? LookAt)?.target as? LookAtTarget.Actors)?.actorIds.orEmpty()
framingActors.distinct().filter { it !in actors && it !in ActorFraming.groups }.forEach { actorId ->
errors.add(ValidationError(path = "shots[$key]", message = Component.translatable("cobblemon.cinematic_editor.validation.framing_actor", actorId)))
}
if (shot.id != key) {
errors.add(ValidationError(
path = "shots[$key].id",
message = Component.translatable("cobblemon.cinematic_editor.validation.shot_id", key, shot.id)
))
}
}
val feedIds = feeds.map { it.id }
val duplicateFeedIds = feedIds.groupingBy { it }.eachCount().filter { it.value > 1 }.keys
duplicateFeedIds.forEach { duplicateId ->
errors.add(ValidationError(
path = "feeds",
message = Component.translatable("cobblemon.cinematic_editor.validation.duplicate_feed", duplicateId)
))
}
val cueIds = mutableSetOf<String>()
beats.forEach { (beatKey, beat) ->
beat.actorDirections.forEach { (actorId, direction) ->
if (direction.tracks.any { it.trackType == "pathfinding" } &&
direction.tracks.count { it.trackType in setOf("pathfinding", "spline_movement", "keyframe_movement", "follow_actor", "attach", "pose_lock") } > 1) {
errors.add(ValidationError(
path = "beats[$beatKey].actorDirections[$actorId].tracks",
message = Component.translatable("cobblemon.cinematic_editor.validation.movement_conflict")
))
}
}
beat.cues.forEachIndexed { index, cue ->
if (!cueIds.add(cue.id)) errors.add(ValidationError(
path = "beats[$beatKey].cues[$index].id", message = Component.translatable("cobblemon.cinematic_editor.validation.duplicate_cue", cue.id)))
if (beat.duration != null && cue.time > beat.duration) errors.add(ValidationError(
path = "beats[$beatKey].cues[$index].time", message = Component.translatable("cobblemon.cinematic_editor.validation.late_cue", cue.id)))
}
try {
ActorDependencyOrder.resolve(actors.keys, beat)
} catch (error: CinematicValidationException) {
errors.add(ValidationError(path = "beats[$beatKey].actorDirections", message = error.reason))
}
if (beat.id != beatKey) {
errors.add(ValidationError(
path = "beats[$beatKey].id",
message = Component.translatable("cobblemon.cinematic_editor.validation.beat_id", beatKey, beat.id)
))
}
beat.actorDirections.keys.forEach { actorRef ->
if (actorRef !in actors) {
errors.add(ValidationError(
path = "beats[$beatKey].actorDirections[$actorRef]",
message = Component.translatable("cobblemon.cinematic_editor.validation.actor_missing", actorRef)
))
}
}
beat.cuts.forEach { (feedRef, cutTrack) ->
if (feedRef !in feedIds) {
errors.add(ValidationError(
path = "beats[$beatKey].cuts[$feedRef]",
message = Component.translatable("cobblemon.cinematic_editor.validation.feed_missing", feedRef)
))
}
cutTrack.entries.forEach { entry ->
if (entry.shotId !in shots) {
errors.add(ValidationError(
path = "beats[$beatKey].cuts[$feedRef]",
message = Component.translatable("cobblemon.cinematic_editor.validation.shot_missing", entry.shotId, entry.time)
))
}
}
}
}
transitions.forEachIndexed { index, transition ->
if (transition.sourceBeatId !in beats) {
errors.add(ValidationError(
path = "transitions[$index].sourceBeatId",
message = Component.translatable("cobblemon.cinematic_editor.validation.source_missing", transition.sourceBeatId)
))
}
if (!transition.endsScene && transition.targetBeatId !in beats) {
errors.add(ValidationError(
path = "transitions[$index].targetBeatId",
message = Component.translatable("cobblemon.cinematic_editor.validation.target_missing", transition.targetBeatId)
))
}
}
val graph = transitions.filter { !it.endsScene && it.sourceBeatId in beats && it.targetBeatId in beats }
.groupBy({ it.sourceBeatId }, { it.targetBeatId })
val reachable = findReachableBeats(graph)
beats.keys.forEach { beatId ->
if (beatId !in reachable) {
errors.add(ValidationError(
path = "beats[$beatId]",
message = Component.translatable("cobblemon.cinematic_editor.validation.unreachable", startBeatId),
severity = ValidationError.Severity.WARNING
))
}
}
val cycle = findCycle(graph)
if (cycle != null) {
errors.add(ValidationError(
path = "transitions",
message = Component.translatable("cobblemon.cinematic_editor.validation.cycle", cycle.joinToString(" -> ")),
severity = ValidationError.Severity.WARNING
))
}
return errors
}
private fun findReachableBeats(graph: Map<String, List<String>>): Set<String> {
val reachable = mutableSetOf<String>()
val queue = ArrayDeque<String>()
if (startBeatId in beats) {
queue.add(startBeatId)
reachable.add(startBeatId)
}
while (queue.isNotEmpty()) {
for (target in graph[queue.removeFirst()].orEmpty()) {
if (reachable.add(target)) queue.addLast(target)
}
}
return reachable
}
private fun findCycle(graph: Map<String, List<String>>): List<String>? {
val visited = mutableSetOf<String>()
val active = mutableMapOf<String, Int>()
val path = mutableListOf<String>()
val stack = ArrayDeque<Iterator<String>>()
fun enter(id: String) {
visited.add(id)
active[id] = path.size
path.add(id)
stack.addLast(graph[id].orEmpty().iterator())
}
for (id in beats.keys) {
if (id in visited) continue
enter(id)
while (stack.isNotEmpty()) {
val remaining = stack.last()
if (remaining.hasNext()) {
val target = remaining.next()
val cycleStart = active[target]
if (cycleStart != null) return path.subList(cycleStart, path.size) + target
if (target !in visited) enter(target)
} else {
stack.removeLast()
active.remove(path.removeAt(path.lastIndex))
}
}
}
return null
}
}
@@ -0,0 +1,17 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cinematic.model
/** A scene-unique, one-shot signal; loops and beat re-entry never fire it again. */
data class SceneCue(val id: String, val time: Float) {
init {
require(id.isNotBlank() && id.length <= 128) { "Cue id must contain 1 to 128 characters" }
require(time.isFinite() && time >= 0f) { "Cue time must be finite and non-negative" }
}
}
@@ -0,0 +1,42 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cinematic.model
data class SceneGuiTrack(
val actions: List<SceneGuiAction>
) {
init {
require(actions.zipWithNext().all { (a, b) -> a.time <= b.time }) {
"SceneGuiTrack actions must be sorted by time"
}
}
}
data class SceneGuiAction(
val time: Float,
val type: SceneGuiActionType,
val guiId: String,
val args: Map<String, SceneGuiArgValue> = emptyMap()
) {
init {
require(time.isFinite() && time >= 0f) { "SceneGuiAction time must be non-negative, got $time" }
}
}
enum class SceneGuiActionType {
OPEN,
CLOSE,
UPDATE_ARGS
}
sealed class SceneGuiArgValue {
data class Expression(val value: String) : SceneGuiArgValue()
data class StringLiteral(val value: String) : SceneGuiArgValue()
}
@@ -0,0 +1,30 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cinematic.model
import com.cobblemon.mod.common.api.cinematic.model.sources.FovSource
import com.cobblemon.mod.common.api.cinematic.model.sources.Lens
import com.cobblemon.mod.common.api.cinematic.model.sources.PositionSource
import com.cobblemon.mod.common.api.cinematic.model.sources.RollSource
import com.cobblemon.mod.common.api.cinematic.model.sources.RotationSource
import com.cobblemon.mod.common.api.cinematic.model.sources.ShotModifier
data class Shot(
val id: String,
val position: PositionSource,
val rotation: RotationSource,
val fov: FovSource? = null,
val roll: RollSource? = null,
val lens: Lens? = null,
val modifiers: List<ShotModifier> = emptyList()
) {
init {
require(id.isNotBlank()) { "Shot id must be non-blank" }
}
}
@@ -0,0 +1,34 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cinematic.model
import com.cobblemon.mod.common.api.camera.CameraRotation
import com.cobblemon.mod.common.api.camera.model.PositionExpression
data class StartPose(
val position: PositionExpression,
val rotation: CameraRotation,
val blendDurationSeconds: Float = 0.5f
) {
init {
require(rotation.yaw.isFinite() && rotation.pitch.isFinite()) { "Start pose rotation must be finite" }
require(blendDurationSeconds.isFinite() && blendDurationSeconds >= 0f) { "Start pose blend duration must be finite and nonnegative" }
}
}
/** Unset mode teleports newly visible actors and ignores the start pose for already visible actors. */
enum class StartPoseMode {
TELEPORT,
BLEND,
EXPECT,
IGNORE
}
@@ -0,0 +1,13 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cinematic.model.sources
abstract class FovSource {
abstract val type: String
}
@@ -0,0 +1,23 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cinematic.model.sources
data class Lens(
val aspectCrop: Float? = null,
val letterbox: Float = 0f
) {
init {
aspectCrop?.let {
require(it.isFinite() && it > 0f) { "Lens aspectCrop must be positive, got $it" }
}
require(letterbox >= 0f && letterbox <= 0.5f) {
"Lens letterbox must be in [0, 0.5], got $letterbox"
}
}
}
@@ -0,0 +1,13 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cinematic.model.sources
abstract class PositionSource {
abstract val type: String
}
@@ -0,0 +1,13 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cinematic.model.sources
abstract class RollSource {
abstract val type: String
}
@@ -0,0 +1,13 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cinematic.model.sources
abstract class RotationSource {
abstract val type: String
}
@@ -0,0 +1,13 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cinematic.model.sources
abstract class ShotModifier {
abstract val type: String
}
@@ -0,0 +1,43 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cinematic.model.sources.builtin
import com.cobblemon.mod.common.api.cinematic.model.sources.ShotModifier
import com.google.gson.JsonObject
import net.minecraft.world.phys.Vec3
data class ConstraintModifier(
val minX: Double? = null, val maxX: Double? = null,
val minY: Double? = null, val maxY: Double? = null,
val minZ: Double? = null, val maxZ: Double? = null
) : ShotModifier() {
override val type: String = TYPE
init {
require(listOfNotNull(minX, maxX, minY, maxY, minZ, maxZ).all { it.isFinite() }) { "Constraint bounds must be finite" }
require(minX == null || maxX == null || minX <= maxX) { "Minimum X must not exceed maximum X" }
require(minY == null || maxY == null || minY <= maxY) { "Minimum Y must not exceed maximum Y" }
require(minZ == null || maxZ == null || minZ <= maxZ) { "Minimum Z must not exceed maximum Z" }
}
fun apply(pos: Vec3): Vec3 = Vec3(
pos.x.let { if (minX != null && it < minX) minX else if (maxX != null && it > maxX) maxX else it },
pos.y.let { if (minY != null && it < minY) minY else if (maxY != null && it > maxY) maxY else it },
pos.z.let { if (minZ != null && it < minZ) minZ else if (maxZ != null && it > maxZ) maxZ else it }
)
companion object {
const val TYPE = "constraint"
fun fromJson(obj: JsonObject): ConstraintModifier = ConstraintModifier(
minX = obj.get("minX")?.asDouble, maxX = obj.get("maxX")?.asDouble,
minY = obj.get("minY")?.asDouble, maxY = obj.get("maxY")?.asDouble,
minZ = obj.get("minZ")?.asDouble, maxZ = obj.get("maxZ")?.asDouble
)
}
}
@@ -0,0 +1,56 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cinematic.model.sources.builtin
import kotlin.math.cos
import kotlin.math.sin
import com.cobblemon.mod.common.api.cinematic.model.sources.PositionSource
import com.google.gson.JsonObject
import net.minecraft.world.phys.Vec3
data class CranePosition(
val pivot: Vec3,
val armLength: Float,
val startAngleDeg: Float,
val angularVelocityDegPerSec: Float,
val height: Float = 0f
) : PositionSource() {
override val type: String = TYPE
init {
require(armLength.isFinite() && armLength > 0f) { "CranePosition armLength must be finite and positive" }
require(startAngleDeg.isFinite() && angularVelocityDegPerSec.isFinite() && height.isFinite()) {
"CranePosition angles, speed and height must be finite"
}
require(pivot.x.isFinite() && pivot.y.isFinite() && pivot.z.isFinite()) { "CranePosition pivot must be finite" }
}
fun evaluate(time: Float): Vec3 {
val angleRad = Math.toRadians(startAngleDeg + angularVelocityDegPerSec.toDouble() * time)
return Vec3(
pivot.x + armLength * cos(angleRad),
pivot.y + height,
pivot.z + armLength * sin(angleRad)
)
}
companion object {
const val TYPE = "crane"
fun fromJson(obj: JsonObject): CranePosition {
val p = obj.getAsJsonObject("pivot")
return CranePosition(
pivot = Vec3(p.get("x").asDouble, p.get("y").asDouble, p.get("z").asDouble),
armLength = obj.get("armLength").asFloat,
startAngleDeg = obj.get("startAngle")?.asFloat ?: 0f,
angularVelocityDegPerSec = obj.get("angularVelocity").asFloat,
height = obj.get("height")?.asFloat ?: 0f
)
}
}
}
@@ -0,0 +1,50 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cinematic.model.sources.builtin
import com.cobblemon.mod.common.api.cinematic.model.sources.PositionSource
import com.google.gson.JsonObject
import net.minecraft.world.phys.Vec3
data class DollyPosition(
val from: Vec3,
val to: Vec3,
val duration: Float
) : PositionSource() {
override val type: String = TYPE
init {
require(duration.isFinite() && duration > 0f) { "DollyPosition duration must be finite and positive" }
require(listOf(from.x, from.y, from.z, to.x, to.y, to.z).all { it.isFinite() }) {
"DollyPosition endpoints must be finite"
}
}
fun evaluate(time: Float): Vec3 {
val t = (time / duration).coerceIn(0f, 1f)
return Vec3(
from.x + (to.x - from.x) * t,
from.y + (to.y - from.y) * t,
from.z + (to.z - from.z) * t
)
}
companion object {
const val TYPE = "dolly"
fun fromJson(obj: JsonObject): DollyPosition {
val f = obj.getAsJsonObject("from")
val t = obj.getAsJsonObject("to")
return DollyPosition(
from = Vec3(f.get("x").asDouble, f.get("y").asDouble, f.get("z").asDouble),
to = Vec3(t.get("x").asDouble, t.get("y").asDouble, t.get("z").asDouble),
duration = obj.get("duration").asFloat
)
}
}
}
@@ -0,0 +1,60 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cinematic.model.sources.builtin
import com.bedrockk.molang.runtime.MoLangRuntime
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.cinematic.model.sources.FovSource
import com.cobblemon.mod.common.api.molang.ExpressionLike
import com.cobblemon.mod.common.util.asExpressionLike
import com.cobblemon.mod.common.util.resolveDouble
import com.google.gson.JsonObject
import java.util.concurrent.atomic.AtomicBoolean
data class ExpressionFov(
val expression: ExpressionLike,
val fallbackFov: Float = 70f
) : FovSource() {
override val type: String = "expression"
@Transient private val warningReported = AtomicBoolean()
init {
require(fallbackFov in 1f..179f) { "ExpressionFov fallbackFov must be in [1, 179]" }
}
fun evaluate(runtime: MoLangRuntime): Float {
return try {
// coerceIn does not filter NaN — its comparisons are false either way — so check first.
val raw = runtime.resolveDouble(expression).toFloat()
if (!raw.isFinite()) {
if (warningReported.compareAndSet(false, true)) {
Cobblemon.LOGGER.warn("[ExpressionFov] Non-finite result $raw; using fallback $fallbackFov")
}
fallbackFov
} else {
raw.coerceIn(1f, 179f)
}
} catch (e: Exception) {
if (warningReported.compareAndSet(false, true)) {
Cobblemon.LOGGER.warn("[ExpressionFov] Evaluation failed: ${e.message}; using fallback $fallbackFov", e)
}
fallbackFov
}
}
companion object {
const val TYPE = "expression"
fun fromJson(obj: JsonObject): ExpressionFov {
val exprStr = obj.get("expression")?.asString
?: throw IllegalArgumentException("ExpressionFov missing 'expression' field")
val fallback = obj.get("fallback")?.asFloat ?: 70f
return ExpressionFov(expression = exprStr.asExpressionLike(), fallbackFov = fallback)
}
}
}
@@ -0,0 +1,79 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cinematic.model.sources.builtin
import com.bedrockk.molang.runtime.MoLangRuntime
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.cinematic.model.sources.PositionSource
import com.cobblemon.mod.common.api.molang.ExpressionLike
import com.cobblemon.mod.common.util.asExpressionLike
import com.cobblemon.mod.common.util.resolveDouble
import com.google.gson.JsonObject
import net.minecraft.world.phys.Vec3
import java.util.concurrent.atomic.AtomicBoolean
data class ExpressionVec3Position(
val xExpression: ExpressionLike,
val yExpression: ExpressionLike,
val zExpression: ExpressionLike,
val fallback: Vec3 = Vec3.ZERO
) : PositionSource() {
override val type: String = "expression_vec3"
@Transient private val warningReported = AtomicBoolean()
init {
require(fallback.x.isFinite() && fallback.y.isFinite() && fallback.z.isFinite()) { "Expression position fallback must be finite" }
}
fun evaluate(runtime: MoLangRuntime?): Vec3 {
if (runtime == null) return fallback
return try {
val x = runtime.resolveDouble(xExpression)
val y = runtime.resolveDouble(yExpression)
val z = runtime.resolveDouble(zExpression)
// MoLang can return NaN or infinity without throwing, so validate before sending transforms.
if (!x.isFinite() || !y.isFinite() || !z.isFinite()) {
if (warningReported.compareAndSet(false, true)) {
Cobblemon.LOGGER.warn("[ExpressionVec3Position] Non-finite result ($x, $y, $z); using fallback $fallback")
}
fallback
} else {
Vec3(x, y, z)
}
} catch (e: Exception) {
if (warningReported.compareAndSet(false, true)) {
Cobblemon.LOGGER.warn("[ExpressionVec3Position] Evaluation failed: ${e.message}; using fallback $fallback", e)
}
fallback
}
}
companion object {
const val TYPE = "expression_vec3"
fun fromJson(obj: JsonObject): ExpressionVec3Position {
val x = obj.get("x")?.asString
?: throw IllegalArgumentException("ExpressionVec3Position missing 'x'")
val y = obj.get("y")?.asString
?: throw IllegalArgumentException("ExpressionVec3Position missing 'y'")
val z = obj.get("z")?.asString
?: throw IllegalArgumentException("ExpressionVec3Position missing 'z'")
val fb = obj.get("fallback")?.asJsonObject
val fallback = if (fb != null) {
Vec3(fb.get("x").asDouble, fb.get("y").asDouble, fb.get("z").asDouble)
} else Vec3.ZERO
return ExpressionVec3Position(
xExpression = x.asExpressionLike(),
yExpression = y.asExpressionLike(),
zExpression = z.asExpressionLike(),
fallback = fallback
)
}
}
}
@@ -0,0 +1,25 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cinematic.model.sources.builtin
import com.cobblemon.mod.common.api.cinematic.model.sources.FovSource
import com.google.gson.JsonObject
data class FixedFov(val fov: Float) : FovSource() {
override val type: String = "fixed"
init {
require(fov in 1f..179f) { "FixedFov must be in [1, 179], got $fov" }
}
companion object {
const val TYPE = "fixed"
fun fromJson(obj: JsonObject): FixedFov = FixedFov(obj.get("value").asFloat)
}
}
@@ -0,0 +1,29 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cinematic.model.sources.builtin
import com.cobblemon.mod.common.api.cinematic.model.sources.PositionSource
import com.google.gson.JsonObject
import net.minecraft.world.phys.Vec3
data class FixedPosition(val position: Vec3) : PositionSource() {
override val type: String = "fixed"
init {
require(position.x.isFinite() && position.y.isFinite() && position.z.isFinite()) { "Fixed position must be finite" }
}
companion object {
const val TYPE = "fixed"
fun fromJson(obj: JsonObject): FixedPosition {
val pos = obj.getAsJsonObject("position") ?: throw IllegalArgumentException("FixedPosition missing 'position'")
return FixedPosition(Vec3(pos.get("x").asDouble, pos.get("y").asDouble, pos.get("z").asDouble))
}
}
}
@@ -0,0 +1,25 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cinematic.model.sources.builtin
import com.cobblemon.mod.common.api.cinematic.model.sources.RollSource
import com.google.gson.JsonObject
data class FixedRoll(val roll: Float) : RollSource() {
override val type: String = "fixed"
init {
require(roll.isFinite()) { "Fixed roll must be finite" }
}
companion object {
const val TYPE = "fixed"
fun fromJson(obj: JsonObject): FixedRoll = FixedRoll(obj.get("value").asFloat)
}
}
@@ -0,0 +1,31 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cinematic.model.sources.builtin
import com.cobblemon.mod.common.api.camera.CameraRotation
import com.cobblemon.mod.common.api.cinematic.model.sources.RotationSource
import com.google.gson.JsonObject
data class FixedRotation(val rotation: CameraRotation) : RotationSource() {
override val type: String = "fixed"
init {
require(rotation.yaw.isFinite() && rotation.pitch.isFinite()) { "Fixed rotation must be finite" }
}
companion object {
const val TYPE = "fixed"
fun fromJson(obj: JsonObject): FixedRotation {
return FixedRotation(CameraRotation(
yaw = obj.get("yaw").asFloat,
pitch = obj.get("pitch").asFloat
))
}
}
}
@@ -0,0 +1,41 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cinematic.model.sources.builtin
import com.cobblemon.mod.common.api.cinematic.model.sources.PositionSource
import com.google.gson.JsonObject
import net.minecraft.world.phys.Vec3
/** Offset is world-space and ignores actor orientation. */
data class FollowActorPosition(
val actorId: String,
val offset: Vec3,
val fallback: Vec3 = Vec3.ZERO
) : PositionSource() {
override val type: String = TYPE
init {
require(listOf(offset.x, offset.y, offset.z, fallback.x, fallback.y, fallback.z).all { it.isFinite() }) {
"FollowActorPosition offset and fallback must be finite"
}
}
companion object {
const val TYPE = "follow_actor"
fun fromJson(obj: JsonObject): FollowActorPosition {
val o = obj.getAsJsonObject("offset")
val fObj = obj.getAsJsonObject("fallback")
return FollowActorPosition(
actorId = obj.get("actorId").asString,
offset = Vec3(o.get("x").asDouble, o.get("y").asDouble, o.get("z").asDouble),
fallback = if (fObj != null) Vec3(fObj.get("x").asDouble, fObj.get("y").asDouble, fObj.get("z").asDouble) else Vec3.ZERO
)
}
}
}
@@ -0,0 +1,53 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cinematic.model.sources.builtin
import com.cobblemon.mod.common.api.cinematic.model.sources.FovSource
import com.cobblemon.mod.common.api.cinematic.model.orderedKeyframes
import com.google.gson.JsonObject
data class KeyframeFov(val keyframes: List<Kf>) : FovSource() {
override val type: String = TYPE
data class Kf(val time: Float, val fov: Float)
init {
keyframes.forEach { require(it.fov in 1f..179f) { "Fov must be in [1, 179], got ${it.fov}" } }
}
@Transient private val playbackKeyframes = orderedKeyframes(keyframes) { it.time }
fun evaluate(time: Float): Float {
val keyframes = playbackKeyframes
if (keyframes.size == 1) return keyframes[0].fov
if (time <= keyframes.first().time) return keyframes.first().fov
if (time >= keyframes.last().time) return keyframes.last().fov
for (i in 0 until keyframes.size - 1) {
val a = keyframes[i]
val b = keyframes[i + 1]
if (time >= a.time && time <= b.time) {
val d = b.time - a.time
val t = if (d > 0f) (time - a.time) / d else 0f
return a.fov + (b.fov - a.fov) * t
}
}
return keyframes.last().fov
}
companion object {
const val TYPE = "keyframe"
fun fromJson(obj: JsonObject): KeyframeFov {
val kfs = obj.getAsJsonArray("keyframes").map { el ->
val k = el.asJsonObject
Kf(time = k.get("time").asFloat, fov = k.get("fov").asFloat)
}
return KeyframeFov(kfs)
}
}
}
@@ -0,0 +1,65 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cinematic.model.sources.builtin
import com.cobblemon.mod.common.api.cinematic.model.sources.PositionSource
import com.cobblemon.mod.common.api.cinematic.model.orderedKeyframes
import com.cobblemon.mod.common.api.tween.EasingRegistry
import com.google.gson.JsonObject
import net.minecraft.world.phys.Vec3
data class KeyframePosition(
val keyframes: List<Kf>,
val defaultEasing: String = "linear"
) : PositionSource() {
override val type: String = "keyframe"
init {
require(keyframes.all { it.position.x.isFinite() && it.position.y.isFinite() && it.position.z.isFinite() }) {
"Keyframe positions must be finite"
}
}
// Keep authoring indices intact; coincident keyframes use the last authored value during playback.
@Transient private val playbackKeyframes = orderedKeyframes(keyframes) { it.time }
data class Kf(val time: Float, val position: Vec3, val easing: String? = null)
fun evaluate(time: Float): Vec3 {
if (time <= playbackKeyframes.first().time) return playbackKeyframes.first().position
for (index in 1 until playbackKeyframes.size) {
val start = playbackKeyframes[index - 1]
val end = playbackKeyframes[index]
if (time <= end.time) {
val progress = ((time - start.time) / (end.time - start.time)).coerceIn(0f, 1f)
val eased = EasingRegistry.getEasingFunction(end.easing ?: defaultEasing)(progress)
return start.position.lerp(end.position, eased.toDouble())
}
}
return playbackKeyframes.last().position
}
companion object {
const val TYPE = "keyframe"
fun fromJson(obj: JsonObject): KeyframePosition {
val kfArray = obj.getAsJsonArray("keyframes") ?: throw IllegalArgumentException("KeyframePosition missing 'keyframes'")
val kfs = kfArray.map { el ->
val kfObj = el.asJsonObject
val pos = kfObj.getAsJsonObject("position")
Kf(
time = kfObj.get("time").asFloat,
position = Vec3(pos.get("x").asDouble, pos.get("y").asDouble, pos.get("z").asDouble),
easing = kfObj.get("easing")?.asString
)
}
val easing = obj.get("easing")?.asString ?: "linear"
return KeyframePosition(kfs, easing)
}
}
}
@@ -0,0 +1,53 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cinematic.model.sources.builtin
import com.cobblemon.mod.common.api.cinematic.model.sources.RollSource
import com.cobblemon.mod.common.api.cinematic.model.orderedKeyframes
import com.google.gson.JsonObject
data class KeyframeRoll(val keyframes: List<Kf>) : RollSource() {
override val type: String = TYPE
data class Kf(val time: Float, val roll: Float)
init {
require(keyframes.all { it.roll.isFinite() }) { "Keyframe roll must be finite" }
}
@Transient private val playbackKeyframes = orderedKeyframes(keyframes) { it.time }
fun evaluate(time: Float): Float {
val keyframes = playbackKeyframes
if (keyframes.size == 1) return keyframes[0].roll
if (time <= keyframes.first().time) return keyframes.first().roll
if (time >= keyframes.last().time) return keyframes.last().roll
for (i in 0 until keyframes.size - 1) {
val a = keyframes[i]
val b = keyframes[i + 1]
if (time >= a.time && time <= b.time) {
val d = b.time - a.time
val t = if (d > 0f) (time - a.time) / d else 0f
return (a.roll + (b.roll.toDouble() - a.roll) * t).toFloat()
}
}
return keyframes.last().roll
}
companion object {
const val TYPE = "keyframe"
fun fromJson(obj: JsonObject): KeyframeRoll {
val kfs = obj.getAsJsonArray("keyframes").map { el ->
val k = el.asJsonObject
Kf(time = k.get("time").asFloat, roll = k.get("roll").asFloat)
}
return KeyframeRoll(kfs)
}
}
}
@@ -0,0 +1,57 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cinematic.model.sources.builtin
import com.cobblemon.mod.common.api.camera.CameraRotation
import com.cobblemon.mod.common.api.cinematic.model.orderedKeyframes
import com.cobblemon.mod.common.api.cinematic.model.sources.RotationSource
import com.google.gson.JsonObject
data class KeyframeRotation(val keyframes: List<Kf>) : RotationSource() {
override val type: String = TYPE
data class Kf(val time: Float, val yaw: Float, val pitch: Float)
init {
require(keyframes.all { it.yaw.isFinite() && it.pitch.isFinite() }) { "Keyframe rotations must be finite" }
}
@Transient private val playbackKeyframes = orderedKeyframes(keyframes) { it.time }
fun evaluate(time: Float): CameraRotation {
val keyframes = playbackKeyframes
if (keyframes.size == 1) return CameraRotation(keyframes[0].yaw, keyframes[0].pitch)
if (time <= keyframes.first().time) return CameraRotation(keyframes.first().yaw, keyframes.first().pitch)
if (time >= keyframes.last().time) return CameraRotation(keyframes.last().yaw, keyframes.last().pitch)
for (i in 0 until keyframes.size - 1) {
val a = keyframes[i]
val b = keyframes[i + 1]
if (time >= a.time && time <= b.time) {
val d = b.time - a.time
val t = if (d > 0f) (time - a.time) / d else 0f
return CameraRotation(
(a.yaw + (b.yaw.toDouble() - a.yaw) * t).toFloat(),
(a.pitch + (b.pitch.toDouble() - a.pitch) * t).toFloat()
)
}
}
return CameraRotation(keyframes.last().yaw, keyframes.last().pitch)
}
companion object {
const val TYPE = "keyframe"
fun fromJson(obj: JsonObject): KeyframeRotation {
val kfs = obj.getAsJsonArray("keyframes").map { el ->
val k = el.asJsonObject
Kf(time = k.get("time").asFloat, yaw = k.get("yaw").asFloat, pitch = k.get("pitch").asFloat)
}
return KeyframeRotation(kfs)
}
}
}
@@ -0,0 +1,47 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cinematic.model.sources.builtin
import com.cobblemon.mod.common.api.camera.CameraRotation
import com.cobblemon.mod.common.api.cinematic.model.sources.RotationSource
import com.google.gson.JsonObject
import net.minecraft.world.phys.Vec3
import kotlin.math.atan2
import kotlin.math.sqrt
data class LookAlongPath(
val epsilon: Float = 1f / 60f
) : RotationSource() {
override val type: String = "look_along_path"
init {
require(epsilon.isFinite() && epsilon > 0f) { "LookAlongPath epsilon must be finite and positive" }
}
fun computeRotation(current: Vec3, ahead: Vec3): CameraRotation {
val dx = ahead.x - current.x
val dy = ahead.y - current.y
val dz = ahead.z - current.z
if (dx * dx + dy * dy + dz * dz < 1e-8) {
// Motion near zero — return forward-facing default to avoid NaN.
return CameraRotation(0f, 0f)
}
val horizontal = sqrt(dx * dx + dz * dz)
val yaw = Math.toDegrees(atan2(-dx, dz)).toFloat()
val pitch = Math.toDegrees(-atan2(dy, horizontal)).toFloat()
return CameraRotation(yaw = yaw, pitch = pitch)
}
companion object {
const val TYPE = "look_along_path"
fun fromJson(obj: JsonObject): LookAlongPath = LookAlongPath(
epsilon = obj.get("epsilon")?.asFloat ?: (1f / 60f)
)
}
}
@@ -0,0 +1,149 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cinematic.model.sources.builtin
import com.bedrockk.molang.runtime.MoLangRuntime
import com.cobblemon.mod.common.api.camera.CameraRotation
import com.cobblemon.mod.common.api.cinematic.model.sources.RotationSource
import com.cobblemon.mod.common.api.molang.ExpressionLike
import com.cobblemon.mod.common.util.asExpressionLike
import com.google.gson.JsonObject
import net.minecraft.world.phys.Vec3
import kotlin.math.atan2
import kotlin.math.sqrt
sealed class LookAtTarget {
data class Position(val pos: Vec3) : LookAtTarget() {
init {
require(pos.x.isFinite() && pos.y.isFinite() && pos.z.isFinite()) { "Look-at position must be finite" }
}
}
data class Actor(val actorId: String) : LookAtTarget() {
init { require(actorId.isNotBlank()) { "Look-at actor id must be non-blank" } }
}
data class Actors(val actorIds: List<String>) : LookAtTarget() {
init {
require(actorIds.size in 1..16)
require(actorIds.all(String::isNotBlank)) { "Look-at actor ids must be non-blank" }
}
}
data class Variable(val prefix: String, val fallback: Vec3 = Vec3.ZERO) : LookAtTarget() {
@Transient private val position = VariableVec3Position(prefix, fallback = fallback)
fun evaluate(runtime: MoLangRuntime?): Vec3 = position.evaluate(runtime)
}
data class Expression(
val xExpression: ExpressionLike,
val yExpression: ExpressionLike,
val zExpression: ExpressionLike,
val fallback: Vec3 = Vec3.ZERO
) : LookAtTarget() {
@Transient private val position = ExpressionVec3Position(xExpression, yExpression, zExpression, fallback)
fun evaluate(runtime: MoLangRuntime?): Vec3 = position.evaluate(runtime)
}
/** Bones resolve client-side; offsets are in world coordinates. */
data class ActorBone(
val actorId: String,
val boneName: String,
val offset: Vec3 = Vec3.ZERO,
val smooth: Boolean = true
) : LookAtTarget() {
init {
require(actorId.isNotBlank()) { "Look-at bone actor id must be non-blank" }
require(boneName.isNotBlank()) { "Look-at bone name must be non-blank" }
require(offset.x.isFinite() && offset.y.isFinite() && offset.z.isFinite()) { "Look-at bone offset must be finite" }
}
}
}
data class LookAt(
val target: LookAtTarget
) : RotationSource() {
override val type: String = "look_at"
fun computeRotation(cameraPos: Vec3, targetPos: Vec3): CameraRotation {
val dx = targetPos.x - cameraPos.x
val dy = targetPos.y - cameraPos.y
val dz = targetPos.z - cameraPos.z
val horizontal = sqrt(dx * dx + dz * dz)
val yaw = Math.toDegrees(atan2(-dx, dz)).toFloat()
val pitch = Math.toDegrees(-atan2(dy, horizontal)).toFloat()
return CameraRotation(yaw = yaw, pitch = pitch)
}
companion object {
const val TYPE = "look_at"
fun fromJson(obj: JsonObject): LookAt {
val targetObj = obj.getAsJsonObject("target")
?: throw IllegalArgumentException("LookAt missing 'target'")
val targetType = targetObj.get("type")?.asString ?: "position"
val target = when (targetType) {
"actors" -> LookAtTarget.Actors(targetObj.getAsJsonArray("actorIds").map { it.asString })
"position" -> LookAtTarget.Position(
Vec3(
targetObj.get("x").asDouble,
targetObj.get("y").asDouble,
targetObj.get("z").asDouble
)
)
"actor" -> LookAtTarget.Actor(
targetObj.get("actorId")?.asString
?: throw IllegalArgumentException("LookAt actor target missing 'actorId'")
)
"actor_bone" -> {
val actorId = targetObj.get("actorId")?.asString
?: throw IllegalArgumentException("LookAt actor_bone target missing 'actorId'")
val boneName = targetObj.get("bone")?.asString
?: throw IllegalArgumentException("LookAt actor_bone target missing 'bone'")
val off = targetObj.getAsJsonObject("offset")
val offset = if (off != null) {
Vec3(off.get("x").asDouble, off.get("y").asDouble, off.get("z").asDouble)
} else Vec3.ZERO
val smooth = targetObj.get("smooth")?.asBoolean ?: true
LookAtTarget.ActorBone(actorId, boneName, offset, smooth)
}
"variable" -> {
val prefix = targetObj.get("prefix")?.asString
?: throw IllegalArgumentException("LookAt variable target missing 'prefix'")
val fb = targetObj.getAsJsonObject("fallback")
val fallback = if (fb != null) {
Vec3(fb.get("x").asDouble, fb.get("y").asDouble, fb.get("z").asDouble)
} else Vec3.ZERO
LookAtTarget.Variable(prefix, fallback)
}
"expression" -> {
val x = targetObj.get("x")?.asString
?: throw IllegalArgumentException("LookAt expression target missing 'x'")
val y = targetObj.get("y")?.asString
?: throw IllegalArgumentException("LookAt expression target missing 'y'")
val z = targetObj.get("z")?.asString
?: throw IllegalArgumentException("LookAt expression target missing 'z'")
val fb = targetObj.getAsJsonObject("fallback")
val fallback = if (fb != null) {
Vec3(fb.get("x").asDouble, fb.get("y").asDouble, fb.get("z").asDouble)
} else Vec3.ZERO
LookAtTarget.Expression(
xExpression = x.asExpressionLike(),
yExpression = y.asExpressionLike(),
zExpression = z.asExpressionLike(),
fallback = fallback
)
}
else -> throw IllegalArgumentException("LookAt target unknown type '$targetType'")
}
return LookAt(target)
}
}
}
@@ -0,0 +1,43 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cinematic.model.sources.builtin
import com.cobblemon.mod.common.api.cinematic.model.sources.ShotModifier
import com.google.gson.JsonObject
import net.minecraft.world.phys.Vec3
data class OffsetModifier(
val positionOffset: Vec3 = Vec3.ZERO,
val yawOffset: Float = 0f,
val pitchOffset: Float = 0f,
val fovOffset: Float = 0f,
val rollOffset: Float = 0f
) : ShotModifier() {
override val type: String = TYPE
init {
require(positionOffset.x.isFinite() && positionOffset.y.isFinite() && positionOffset.z.isFinite()) { "Position offset must be finite" }
require(listOf(yawOffset, pitchOffset, fovOffset, rollOffset).all { it.isFinite() }) { "Camera offsets must be finite" }
}
companion object {
const val TYPE = "offset"
fun fromJson(obj: JsonObject): OffsetModifier {
val p = obj.getAsJsonObject("position")
val posOff = if (p != null) Vec3(p.get("x").asDouble, p.get("y").asDouble, p.get("z").asDouble) else Vec3.ZERO
return OffsetModifier(
positionOffset = posOff,
yawOffset = obj.get("yaw")?.asFloat ?: 0f,
pitchOffset = obj.get("pitch")?.asFloat ?: 0f,
fovOffset = obj.get("fov")?.asFloat ?: 0f,
rollOffset = obj.get("roll")?.asFloat ?: 0f
)
}
}
}
@@ -0,0 +1,98 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cinematic.model.sources.builtin
import com.cobblemon.mod.common.api.cinematic.model.sources.PositionSource
import com.google.gson.JsonObject
import net.minecraft.world.phys.Vec3
import net.minecraft.world.phys.AABB
import kotlin.math.max
import kotlin.math.sin
import kotlin.math.sqrt
import kotlin.math.cos
import kotlin.math.atan2
data class OrbitPosition(
val center: Vec3,
val radius: Float,
val angularVelocityDegPerSec: Float,
val startAngleDeg: Float = 0f,
val height: Float = 0f,
val actorIds: List<String> = emptyList(),
val padding: Float = 2f,
val framingFov: Float = 60f,
val bearingActorId: String? = null,
val avoidBlocks: Boolean = true,
val avoidanceSmoothing: Float = 0f,
val fitArena: Boolean = false,
val arenaWidth: Float? = null,
val arenaDepth: Float? = null,
val rememberProgress: Boolean = false
) : PositionSource() {
override val type: String = TYPE
init {
require(center.x.isFinite() && center.y.isFinite() && center.z.isFinite()) { "Orbit center must be finite" }
require(radius > 0f) { "OrbitPosition radius must be positive" }
require(listOf(radius, angularVelocityDegPerSec, startAngleDeg, height, padding, framingFov).all { it.isFinite() })
require(padding >= 0f && framingFov in 10f..120f && actorIds.size <= 16)
require(avoidanceSmoothing.isFinite() && avoidanceSmoothing in 0f..8f)
require(listOfNotNull(arenaWidth, arenaDepth).all { it.isFinite() && it in 2f..128f })
}
val hasArena get() = fitArena && arenaWidth != null && arenaDepth != null
fun focus(bounds: AABB?) = if (hasArena) center else bounds?.center ?: center
fun evaluate(time: Float, bounds: AABB? = null, lensFov: Float = framingFov, bearingPosition: Vec3? = null): Vec3 {
require(time.isFinite()) { "Orbit time must be finite" }
val focus = focus(bounds)
val bearing = bearingPosition?.subtract(focus)?.takeIf { it.horizontalDistanceSqr() > 0.000001 }
?.let { atan2(it.z, it.x) } ?: 0.0
val angleDeg = (startAngleDeg.toDouble() + angularVelocityDegPerSec.toDouble() * time.toDouble()) % 360.0
val angleRad = bearing + Math.toRadians(angleDeg)
if (hasArena) return Vec3(
focus.x + max(radius, arenaWidth!! / 2 + padding) * cos(angleRad),
focus.y + height,
focus.z + max(radius, arenaDepth!! / 2 + padding) * sin(angleRad))
val fitFov = lensFov.takeIf { it.isFinite() }?.coerceIn(1f, 179f) ?: framingFov
val fitRadius = bounds?.let {
sqrt(it.xsize * it.xsize + it.ysize * it.ysize + it.zsize * it.zsize) / 2 / sin(Math.toRadians(fitFov / 2.0)) + padding
} ?: radius.toDouble()
val distance = max(radius.toDouble(), fitRadius)
return Vec3(
focus.x + distance * cos(angleRad),
focus.y + height,
focus.z + distance * sin(angleRad)
)
}
companion object {
const val TYPE = "orbit"
fun fromJson(obj: JsonObject): OrbitPosition {
val c = obj.getAsJsonObject("center")
return OrbitPosition(
center = Vec3(c.get("x").asDouble, c.get("y").asDouble, c.get("z").asDouble),
radius = obj.get("radius").asFloat,
angularVelocityDegPerSec = obj.get("angularVelocity").asFloat,
startAngleDeg = obj.get("startAngle")?.asFloat ?: 0f,
height = obj.get("height")?.asFloat ?: 0f,
actorIds = obj.getAsJsonArray("actorIds")?.map { it.asString } ?: emptyList(),
padding = obj.get("padding")?.asFloat ?: 2f,
framingFov = obj.get("framingFov")?.asFloat ?: 60f,
bearingActorId = obj.get("bearingActorId")?.asString,
avoidBlocks = obj.get("avoidBlocks")?.asBoolean ?: true,
avoidanceSmoothing = obj.get("avoidanceSmoothing")?.asFloat ?: 0f,
fitArena = obj.get("fitArena")?.asBoolean ?: false,
arenaWidth = obj.get("arenaWidth")?.asFloat,
arenaDepth = obj.get("arenaDepth")?.asFloat,
rememberProgress = obj.get("rememberProgress")?.asBoolean ?: false
)
}
}
}
@@ -0,0 +1,44 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cinematic.model.sources.builtin
import kotlin.math.sin
import com.cobblemon.mod.common.api.cinematic.model.sources.RollSource
import com.google.gson.JsonObject
data class ShakeDerivedRoll(
val amplitudeDeg: Float,
val frequencyHz: Float = 8f,
val decaySecs: Float = 0f
) : RollSource() {
override val type: String = "shake"
init {
require(amplitudeDeg.isFinite() && amplitudeDeg >= 0f)
require(frequencyHz in 0f..60f)
require(decaySecs.isFinite() && decaySecs >= 0f)
}
fun evaluate(time: Float): Float {
val decay = if (decaySecs <= 0f) 1f else (1f - (time / decaySecs)).coerceAtLeast(0f)
if (decay <= 0f) return 0f
val t = time.toDouble() * frequencyHz
val n = (sin(1.0 * t) + sin(2.3 * t + 1.3) + sin(3.7 * t + 2.7)) / 3.0
return (n.toFloat() * amplitudeDeg * decay)
}
companion object {
const val TYPE = "shake"
fun fromJson(obj: JsonObject): ShakeDerivedRoll = ShakeDerivedRoll(
amplitudeDeg = obj.get("amplitude").asFloat,
frequencyHz = obj.get("frequency")?.asFloat ?: 8f,
decaySecs = obj.get("decay")?.asFloat ?: 0f
)
}
}
@@ -0,0 +1,53 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cinematic.model.sources.builtin
import kotlin.math.sin
import com.cobblemon.mod.common.api.cinematic.model.sources.ShotModifier
import com.google.gson.JsonObject
data class ShakeModifier(
val positionAmplitude: Float = 0f,
val rotationAmplitudeDeg: Float = 0f,
val frequencyHz: Float = 8f,
val decaySecs: Float = 0f
) : ShotModifier() {
override val type: String = TYPE
init {
require(positionAmplitude.isFinite() && positionAmplitude >= 0f) { "positionAmplitude must be >= 0" }
require(rotationAmplitudeDeg.isFinite() && rotationAmplitudeDeg >= 0f) { "rotationAmplitudeDeg must be >= 0" }
require(frequencyHz in 0f..60f) { "frequency must be in [0, 60] Hz" }
require(decaySecs.isFinite() && decaySecs >= 0f) { "decaySecs must be >= 0" }
}
fun noise3(time: Float): Triple<Float, Float, Float> {
val t = time.toDouble() * frequencyHz
fun s(a: Double, b: Double, c: Double) =
(sin(a * t) + sin(b * t + 1.3) + sin(c * t + 2.7)) / 3.0
return Triple(
s(1.0, 2.3, 3.7).toFloat(),
s(1.1, 2.7, 3.3).toFloat(),
s(0.9, 2.1, 4.1).toFloat()
)
}
fun decayFactor(time: Float): Float =
if (decaySecs <= 0f) 1f else (1f - (time / decaySecs)).coerceAtLeast(0f)
companion object {
const val TYPE = "shake"
fun fromJson(obj: JsonObject): ShakeModifier = ShakeModifier(
positionAmplitude = obj.get("positionAmplitude")?.asFloat ?: 0f,
rotationAmplitudeDeg = obj.get("rotationAmplitude")?.asFloat ?: 0f,
frequencyHz = obj.get("frequency")?.asFloat ?: 8f,
decaySecs = obj.get("decay")?.asFloat ?: 0f
)
}
}
@@ -0,0 +1,36 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cinematic.model.sources.builtin
import com.cobblemon.mod.common.api.cinematic.model.sources.ShotModifier
import com.google.gson.JsonObject
/** Factor 0 disables smoothing; factor 1 freezes the previous value. */
data class SmoothingModifier(
val positionFactor: Float = 0f,
val rotationFactor: Float = 0f,
val fovFactor: Float = 0f
) : ShotModifier() {
override val type: String = "smoothing"
init {
require(positionFactor in 0f..1f) { "positionFactor must be in [0, 1]" }
require(rotationFactor in 0f..1f) { "rotationFactor must be in [0, 1]" }
require(fovFactor in 0f..1f) { "fovFactor must be in [0, 1]" }
}
companion object {
const val TYPE = "smoothing"
fun fromJson(obj: JsonObject): SmoothingModifier = SmoothingModifier(
positionFactor = obj.get("position")?.asFloat ?: 0f,
rotationFactor = obj.get("rotation")?.asFloat ?: 0f,
fovFactor = obj.get("fov")?.asFloat ?: 0f
)
}
}
@@ -0,0 +1,43 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cinematic.model.sources.builtin
import com.cobblemon.mod.common.api.cinematic.model.tracks.builtin.CinematicSpline
import com.cobblemon.mod.common.api.cinematic.model.sources.PositionSource
import com.google.gson.JsonObject
import net.minecraft.world.phys.Vec3
data class SplinePosition(
val controlPoints: List<Vec3>,
val duration: Float
) : PositionSource() {
override val type: String = TYPE
init {
require(controlPoints.size >= 2) { "SplinePosition needs at least 2 control points" }
require(duration.isFinite() && duration > 0f) { "SplinePosition duration must be positive" }
}
@Transient private val path = CinematicSpline(controlPoints)
val pathLength: Double get() = path.length
fun evaluate(time: Float): Vec3 = path.position((time / duration).toDouble().coerceIn(0.0, 1.0))
companion object {
const val TYPE = "spline"
fun fromJson(obj: JsonObject): SplinePosition {
val points = obj.getAsJsonArray("controlPoints").map { el ->
val p = el.asJsonObject
Vec3(p.get("x").asDouble, p.get("y").asDouble, p.get("z").asDouble)
}
val duration = obj.get("duration").asFloat
return SplinePosition(points, duration)
}
}
}
@@ -0,0 +1,51 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cinematic.model.sources.builtin
import com.cobblemon.mod.common.api.cinematic.model.sources.PositionSource
import com.google.gson.JsonObject
import net.minecraft.world.phys.Vec3
/** The axis is not normalized; callers must supply a unit vector for predictable speed. */
data class TruckPosition(
val origin: Vec3,
val axis: Vec3,
val velocity: Float
) : PositionSource() {
override val type: String = TYPE
init {
require(velocity.isFinite()) { "TruckPosition velocity must be finite" }
require(listOf(origin.x, origin.y, origin.z, axis.x, axis.y, axis.z).all { it.isFinite() }) {
"TruckPosition origin and axis must be finite"
}
}
fun evaluate(time: Float): Vec3 {
val dist = velocity.toDouble() * time
return Vec3(
origin.x + axis.x * dist,
origin.y + axis.y * dist,
origin.z + axis.z * dist
)
}
companion object {
const val TYPE = "truck"
fun fromJson(obj: JsonObject): TruckPosition {
val o = obj.getAsJsonObject("origin")
val a = obj.getAsJsonObject("axis")
return TruckPosition(
origin = Vec3(o.get("x").asDouble, o.get("y").asDouble, o.get("z").asDouble),
axis = Vec3(a.get("x").asDouble, a.get("y").asDouble, a.get("z").asDouble),
velocity = obj.get("velocity").asFloat
)
}
}
}
@@ -0,0 +1,58 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cinematic.model.sources.builtin
import com.bedrockk.molang.runtime.MoLangRuntime
import com.cobblemon.mod.common.api.cinematic.model.sources.PositionSource
import com.google.gson.JsonObject
import net.minecraft.world.phys.Vec3
data class VariableVec3Position(
val prefix: String,
val offset: Vec3 = Vec3.ZERO,
val fallback: Vec3 = Vec3.ZERO
) : PositionSource() {
override val type: String = "variable_vec3"
init {
require(listOf(offset, fallback, fallback.add(offset)).all { it.x.isFinite() && it.y.isFinite() && it.z.isFinite() }) {
"Variable position offset and fallback must be finite"
}
}
fun evaluate(runtime: MoLangRuntime?): Vec3 {
if (runtime == null) return fallback.add(offset)
val vars = runtime.environment.variable.map
return runCatching {
val x = vars["${prefix}_x"]?.asDouble() ?: return fallback.add(offset)
val y = vars["${prefix}_y"]?.asDouble() ?: return fallback.add(offset)
val z = vars["${prefix}_z"]?.asDouble() ?: return fallback.add(offset)
Vec3(x + offset.x, y + offset.y, z + offset.z).takeIf {
it.x.isFinite() && it.y.isFinite() && it.z.isFinite()
} ?: fallback.add(offset)
}.getOrElse { fallback.add(offset) }
}
companion object {
const val TYPE = "variable_vec3"
fun fromJson(obj: JsonObject): VariableVec3Position {
val prefix = obj.get("prefix")?.asString
?: throw IllegalArgumentException("VariableVec3Position missing 'prefix'")
val off = obj.get("offset")?.asJsonObject
val offset = if (off != null) {
Vec3(off.get("x").asDouble, off.get("y").asDouble, off.get("z").asDouble)
} else Vec3.ZERO
val fb = obj.get("fallback")?.asJsonObject
val fallback = if (fb != null) {
Vec3(fb.get("x").asDouble, fb.get("y").asDouble, fb.get("z").asDouble)
} else Vec3.ZERO
return VariableVec3Position(prefix, offset, fallback)
}
}
}
@@ -0,0 +1,15 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cinematic.model.tracks
abstract class ActorTrack {
abstract val trackType: String
abstract val duration: Float
}
@@ -0,0 +1,37 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cinematic.model.tracks.builtin
import com.cobblemon.mod.common.api.cinematic.model.tracks.ActorTrack
import com.google.gson.JsonObject
data class AnimationTrack(
val cues: List<Cue>
) : ActorTrack() {
override val trackType: String = "animation"
override val duration: Float = cues.maxOfOrNull { it.time } ?: 0f
data class Cue(val time: Float, val animationName: String) {
init {
require(time.isFinite() && time >= 0f) { "Animation cue time must be finite and nonnegative" }
require(animationName.isNotBlank()) { "Animation name must be nonblank" }
}
}
companion object {
const val TYPE = "animation"
fun fromJson(obj: JsonObject): AnimationTrack {
val cues = obj.getAsJsonArray("cues").map { el ->
val cueObj = el.asJsonObject
Cue(time = cueObj.get("time").asFloat, animationName = cueObj.get("name").asString)
}
return AnimationTrack(cues)
}
}
}
@@ -0,0 +1,38 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cinematic.model.tracks.builtin
import com.cobblemon.mod.common.api.cinematic.model.tracks.ActorTrack
import com.google.gson.JsonObject
import net.minecraft.world.phys.Vec3
/** Position-only attachment; zero duration keeps evaluation active throughout the beat. */
data class AttachTrack(
val parentActorId: String,
val offset: Vec3 = Vec3.ZERO
) : ActorTrack() {
override val trackType: String = TYPE
override val duration: Float = 0f
init {
require(offset.x.isFinite() && offset.y.isFinite() && offset.z.isFinite()) { "Attachment offset must be finite" }
}
companion object {
const val TYPE = "attach"
fun fromJson(obj: JsonObject): AttachTrack {
val o = obj.get("offset")?.asJsonObject
val off = if (o != null) Vec3(o.get("x").asDouble, o.get("y").asDouble, o.get("z").asDouble) else Vec3.ZERO
return AttachTrack(
parentActorId = obj.get("parentActorId").asString,
offset = off
)
}
}
}
@@ -0,0 +1,79 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cinematic.model.tracks.builtin
import net.minecraft.world.phys.Vec3
import kotlin.math.sqrt
class CinematicSpline(controlPoints: List<Vec3>) {
private val points = controlPoints.fold(mutableListOf<Vec3>()) { result, point ->
require(point.x.isFinite() && point.y.isFinite() && point.z.isFinite()) { "Path points must be finite" }
if (result.isEmpty() || result.last().distanceToSqr(point) > 1e-12) result.add(point)
result
}.toList()
private val sampleCount = ((points.size - 1) * 64).coerceIn(1, 16384)
private val distances = DoubleArray(sampleCount + 1)
val length: Double get() = distances.last()
init {
require(points.isNotEmpty()) { "A path needs at least one point" }
var previous = parameterPosition(0.0)
for (i in 1..sampleCount) {
val position = parameterPosition(i.toDouble() / sampleCount)
distances[i] = distances[i - 1] + previous.distanceTo(position)
previous = position
}
}
fun position(progress: Double): Vec3 {
require(progress.isFinite()) { "Path progress must be finite" }
if (progress <= 0.0 || length <= 1e-9) return points.first()
if (progress >= 1.0) return points.last()
val distance = progress * length
var low = 0
var high = sampleCount
while (high - low > 1) {
val middle = (low + high) ushr 1
if (distances[middle] < distance) low = middle else high = middle
}
val span = distances[high] - distances[low]
val fraction = if (span > 1e-12) (distance - distances[low]) / span else 0.0
return parameterPosition((low + fraction) / sampleCount)
}
fun tangent(progress: Double): Vec3 {
val t = progress.coerceIn(0.0, 1.0)
val step = if (length > 0.0) (0.01 / length).coerceIn(1e-5, 0.01) else 0.01
val direction = position((t + step).coerceAtMost(1.0)).subtract(position((t - step).coerceAtLeast(0.0)))
return if (direction.lengthSqr() > 1e-12) direction.normalize() else Vec3.ZERO
}
private fun parameterPosition(progress: Double): Vec3 {
if (points.size == 1) return points.first()
if (points.size == 2) return points[0].lerp(points[1], progress)
val segment = progress.coerceIn(0.0, 1.0) * (points.size - 1)
val index = segment.toInt().coerceAtMost(points.size - 2)
val p1 = points[index]
val p2 = points[index + 1]
val p0 = points.getOrNull(index - 1) ?: p1.scale(2.0).subtract(p2)
val p3 = points.getOrNull(index + 2) ?: p2.scale(2.0).subtract(p1)
val t0 = 0.0
val t1 = sqrt(p0.distanceTo(p1))
val t2 = t1 + sqrt(p1.distanceTo(p2))
val t3 = t2 + sqrt(p2.distanceTo(p3))
val t = t1 + (segment - index) * (t2 - t1)
fun blend(a: Vec3, b: Vec3, start: Double, end: Double) = a.lerp(b, (t - start) / (end - start))
val a1 = blend(p0, p1, t0, t1)
val a2 = blend(p1, p2, t1, t2)
val a3 = blend(p2, p3, t2, t3)
val b1 = blend(a1, a2, t0, t2)
val b2 = blend(a2, a3, t1, t3)
return blend(b1, b2, t1, t2)
}
}
@@ -0,0 +1,40 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cinematic.model.tracks.builtin
import com.cobblemon.mod.common.api.cinematic.model.tracks.ActorTrack
import com.google.gson.JsonObject
import net.minecraft.world.phys.Vec3
/** Zero duration keeps follow evaluation active throughout the beat. */
data class FollowActor(
val targetActorId: String,
val offset: Vec3,
val lagSecs: Float = 0f
) : ActorTrack() {
override val trackType: String = TYPE
override val duration: Float = 0f
init {
require(offset.x.isFinite() && offset.y.isFinite() && offset.z.isFinite()) { "Follow offset must be finite" }
require(lagSecs.isFinite() && lagSecs >= 0f) { "FollowActor lagSecs must be non-negative" }
}
companion object {
const val TYPE = "follow_actor"
fun fromJson(obj: JsonObject): FollowActor {
val o = obj.getAsJsonObject("offset")
return FollowActor(
targetActorId = obj.get("targetActorId").asString,
offset = Vec3(o.get("x").asDouble, o.get("y").asDouble, o.get("z").asDouble),
lagSecs = obj.get("lag")?.asFloat ?: 0f
)
}
}
}
@@ -0,0 +1,69 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cinematic.model.tracks.builtin
import com.cobblemon.mod.common.api.cinematic.model.tracks.ActorTrack
import com.cobblemon.mod.common.api.cinematic.model.orderedKeyframes
import com.google.gson.JsonObject
import net.minecraft.world.phys.Vec3
/** Movement does not drive rotation; pair with a LookAtTrack to orient the actor. */
data class KeyframeMovement(
val keyframes: List<Kf>
) : ActorTrack() {
override val trackType: String = "keyframe_movement"
override val duration: Float = keyframes.maxOfOrNull { it.time } ?: 0f
init {
require(keyframes.all { it.position.x.isFinite() && it.position.y.isFinite() && it.position.z.isFinite() }) {
"Keyframe positions must be finite"
}
}
@Transient private val playbackKeyframes = orderedKeyframes(keyframes) { it.time }
data class Kf(val time: Float, val position: Vec3)
fun evaluate(time: Float): Vec3 {
val keyframes = playbackKeyframes
if (keyframes.size == 1) return keyframes[0].position
if (time <= keyframes.first().time) return keyframes.first().position
if (time >= keyframes.last().time) return keyframes.last().position
for (i in 0 until keyframes.size - 1) {
val a = keyframes[i]
val b = keyframes[i + 1]
if (time >= a.time && time <= b.time) {
val duration = b.time - a.time
val t = if (duration > 0f) (time - a.time) / duration else 0f
return Vec3(
a.position.x + (b.position.x - a.position.x) * t,
a.position.y + (b.position.y - a.position.y) * t,
a.position.z + (b.position.z - a.position.z) * t
)
}
}
return keyframes.last().position
}
companion object {
const val TYPE = "keyframe_movement"
fun fromJson(obj: JsonObject): KeyframeMovement {
val kfs = obj.getAsJsonArray("keyframes").map { el ->
val kfObj = el.asJsonObject
val pos = kfObj.getAsJsonObject("position")
Kf(
time = kfObj.get("time").asFloat,
position = Vec3(pos.get("x").asDouble, pos.get("y").asDouble, pos.get("z").asDouble)
)
}
return KeyframeMovement(kfs)
}
}
}
@@ -0,0 +1,49 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cinematic.model.tracks.builtin
import com.cobblemon.mod.common.api.cinematic.model.tracks.ActorTrack
import com.google.gson.JsonObject
import net.minecraft.world.phys.Vec3
import kotlin.math.atan2
import kotlin.math.sqrt
data class LookAtTrack(
val target: Vec3
) : ActorTrack() {
override val trackType: String = "look_at"
override val duration: Float = 0f
init {
require(target.x.isFinite() && target.y.isFinite() && target.z.isFinite()) { "Look-at target must be finite" }
}
fun computeYRot(fromPos: Vec3): Float {
val dx = target.x - fromPos.x
val dz = target.z - fromPos.z
return Math.toDegrees(atan2(-dx, dz)).toFloat()
}
fun computeXRot(fromPos: Vec3): Float {
val dx = target.x - fromPos.x
val dy = target.y - fromPos.y
val dz = target.z - fromPos.z
val horizontal = sqrt(dx * dx + dz * dz)
return Math.toDegrees(-atan2(dy, horizontal)).toFloat()
}
companion object {
const val TYPE = "look_at"
fun fromJson(obj: JsonObject): LookAtTrack {
val targetObj = obj.getAsJsonObject("target")
val target = Vec3(targetObj.get("x").asDouble, targetObj.get("y").asDouble, targetObj.get("z").asDouble)
return LookAtTrack(target)
}
}
}
@@ -0,0 +1,29 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cinematic.model.tracks.builtin
import com.cobblemon.mod.common.api.cinematic.model.tracks.ActorTrack
import com.cobblemon.mod.common.entity.PoseType
import com.google.gson.JsonObject
data class PoseLockTrack(
val pose: PoseType
) : ActorTrack() {
override val trackType: String = TYPE
override val duration: Float = 0f
companion object {
const val TYPE = "pose_lock"
fun fromJson(obj: JsonObject): PoseLockTrack {
val name = obj.get("pose").asString
val pose = PoseType.valueOf(name.uppercase())
return PoseLockTrack(pose)
}
}
}
@@ -0,0 +1,62 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cinematic.model.tracks.builtin
import kotlin.math.atan2
import kotlin.math.sqrt
import java.util.Locale
import com.cobblemon.mod.common.api.cinematic.model.tracks.ActorTrack
import com.google.gson.JsonObject
import net.minecraft.world.phys.Vec3
data class SplineMovement(
val controlPoints: List<Vec3>,
val durationSecs: Float,
val facing: PathFacing = PathFacing.GROUND,
val headingOffset: Float = 0f
) : ActorTrack() {
override val trackType: String = TYPE
override val duration: Float = durationSecs
init {
require(controlPoints.size >= 2) { "SplineMovement needs at least 2 control points" }
require(durationSecs.isFinite() && durationSecs > 0f) { "SplineMovement duration must be positive" }
require(headingOffset.isFinite()) { "SplineMovement heading offset must be finite" }
}
@Transient private val path = CinematicSpline(controlPoints)
val pathLength: Double get() = path.length
fun evaluate(time: Float): Vec3 = path.position((time / durationSecs).toDouble().coerceIn(0.0, 1.0))
fun facingAt(time: Float): Pair<Float, Float>? {
if (facing == PathFacing.FIXED) return null
val direction = path.tangent((time / durationSecs).toDouble())
if (direction.lengthSqr() < 1e-12) return null
val yaw = Math.toDegrees(atan2(-direction.x, direction.z)).toFloat() + headingOffset
val pitch = if (facing == PathFacing.FLIGHT) -Math.toDegrees(atan2(direction.y,
sqrt(direction.x * direction.x + direction.z * direction.z))).toFloat() else 0f
return yaw to pitch
}
companion object {
const val TYPE = "spline_movement"
fun fromJson(obj: JsonObject): SplineMovement {
val points = obj.getAsJsonArray("controlPoints").map { el ->
val p = el.asJsonObject
Vec3(p.get("x").asDouble, p.get("y").asDouble, p.get("z").asDouble)
}
val d = obj.get("duration").asFloat
val facing = obj.get("facing")?.asString?.let { PathFacing.valueOf(it.uppercase(Locale.ROOT)) } ?: PathFacing.GROUND
return SplineMovement(points, d, facing, obj.get("headingOffset")?.asFloat ?: 0f)
}
}
}
enum class PathFacing { FIXED, GROUND, FLIGHT }

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