Compare commits

..
Author SHA1 Message Date
MeAlam1 58cdf2e756 Added Debug Logging to hopefully find the issue 2026-08-06 18:26:43 +02:00
3647 changed files with 19495 additions and 186571 deletions
+495 -37
View File
@@ -1,60 +1,518 @@
# Changelog
## [1.8.1 (MONTH Xth, 2026)](#1-8-1)
## [1.8.0 (MONTH Xth, 2026)](#1-8-0)
### Additions
- Added config options to award experience to fainted Pokémon for opponents they helped defeat (`awardExperienceToFaintedPokemon`) and to still award experience for defeated enemies even after battle loss/forfeit (`awardExperienceOnBattleLoss`)
- Party Overlay Exp Gained Animation will now display the amount of EXP the Pokémon gained
- Added a toast to notify players of how they can scroll through their party. Did you know you can hold R and scroll instead of using arrow keys??!
- Added a new layer property that makes a layer scroll in a direction. "scrolling": {"speedU": 0.1, "speedV": 0.1}
- Added missing crops to Botany Pots integration
- Added a way to obtain Porygon by inserting the Upgrade or Dubious Disc into a data monitor
- Added a new `cobblemon:structure`, which is a feature without any restrictions allowing the Builders to place certain structures in much more specific manner.
- Added Configs for Infinite Use TMs and Unlocking all TMs
- Added Type Gem blocks
- Added Type Gem Clusters
- Added TMs
- Added TM Machine
- Added Deepslate Core Block
- Added /givetm Command
- Added advancement triggers for learning TMs: `cobblemon:has_learn_specific_tm` (per-TM), `cobblemon:has_learn_all_tm` (all TMs learned), and `cobblemon:tms_learned` (count-based), backed by a new `totalTMLearnedCount` on player advancement data.
- Added `cobblemon:catch_alpha_pokemon` advancement trigger and `totalAlphaCaptureCount` tracking for capturing Alpha Pokémon.
- Added Move Dex to Pokédex
- Added the Habitat Block, a spawner block for servers and adventure maps that controls spawning in an area.
- Added Disk Rack for storing disks and being an audio sequencer for Noteblocks
- Added intrinsic scaling to all Pokémon (+-5%). Some rendering cases, such as shoulder mounting, revert to 1.0 scaling to preserve animation accuracy.
- Added scaling to "baby" Pokémon (smaller (-20%) at level 1 → normally sized at level 10)
- Added conditional seats for riding. Seats can be gated by molang such as "is_alpha".
- Added Alpha Pokémon
- Added alpha scaling (alphas are larger than their normal counterparts)
- Alphas will always lead herds and will spawn in herds
- Alphas spawn with two random TM moves from their learnset
- Alpha Pokémon will always defend themselves from players that attack them
- Added alpha level matching and stat boost (based on level)
- Added pitch-down and reverb to alpha cries
- Added alpha Pokémon bait effect
- Added alpha eye rendering including eye overlay, bloom, eye trail, and particles.
- Added cobweb block slowness immunity for spider Pokémon
- Added 49 new habitat structures. 34 as typical Minecraft structures and 15 as structures that get placed as biome features to allow for more natural placement in generation. Structures placed as biome features will not be able to be located with the `/locate structure` command.
- Vanilla village house template pools have been adjusted to account for the new abandoned village structure habitat. Weights have changed within the JSON file for every piece to ensure too many habitats don't generate in a single village.
- Added 10 new ruin structures.
- rooted_arch_ruins & crumbling_arch_ruins have been transferred over to biome features, allowing for better placement in generation.
- All ruin structures that did not generate with a gimmighoul/gilded chest before will now generate with one.
- All ruin structures gilded chest loot tables have been updated to include a guaranteed TM not learned naturally by any Pokémon through their level-up moveset. Each ruin gets its own specific TM, so you can now go hunting ruins to complete your move dex!
- All fossil structures have been transferred over to biome features, allowing for better placement in generation.
- The prehistoric_underwater_fissure & prehistoric_submerged_impact fossil structures now use the biome tag `#cobblemon:is_temperate_ocean`.
- Gimmighoul towers now generate at ground level rather than sinking one block into the ground.
- A new shipwreck cove structure called the "magma_shipwreck_cove" has been added.
- Shipwreck coves now generate slightly more frequently.
- Added equipment loot tables for shipwreck cove enemies.
- Maps from abandoned fishing boats leading to shipwreck coves now will skip explored chunks, allowing the player to find more shipwreck coves easier.
- The lush shipwreck cove's phantom mob spawner has been replaced with breezes.
- The lush shipwreck cove structure itself has been slightly cleaned up to be less noisy with the blocks it is built out of.
- All shipwreck coves' spawners have been adjusted to give compatible mobs enchanted iron or diamond armour and weapons while in its ominous state. The armour given to the mobs will also use the automaton trim. We upped the difficulty because it was too easy now that your Pokémon can fight for you.
- One TM of the elemental hyper beams has been added as guaranteed loot to the main treasure chest in every shipwreck cove. (Magma cove gets blast burn, lush cove gets frenzy plant, and submerged cove gets hydro cannon)
- Added 7 new advancements
- Pressing the inventory key now closes the Pokémon interact wheel GUI
- Pokémon cries now play when clicking Pokémon in the summary screen.
- Pressing the Summary key while either the starter selection or summary screen are open now closes those screens
- Ice type Pokémon are now immune to Packed Snow
- Held item visibility can now be controlled by the held_item_visible command argument.
### New rideable Pokémon
- Crobat
- Skarmory
- Milotic
- Archeops
- Goodra
- Goodra (Hisuian Form)
- Drampa
- Duraludon
- Archaludon
### Pokémon Added
#### Generation 3
- Wurmple
- Silcoon
- Beautifly
- Cascoon
- Dustox
- Skitty
- Delcatty
- Seviper
- Zangoose
#### Generation 4
- Cherubi
- Cherrim
- Stunky
- Skuntank
#### Generation 5
- Audino
- Pawniard
- Bisharp
#### Generation 6
- Pancham (+ Cosmetic with Black Glasses)
- Pangoro (+ Cosmetic with Black Glasses)
#### Generation 7
- Rockruff
- Lycanroc
#### Generation 8
- Rolycoly
- Carkol
- Coalossal
- Runerigus
- Snom
- Frosmoth
- Indeedee
- Duraludon
- Applin
- Flapple
- Appletun
#### Generation 9
- Nymble
- Lokix
- Kingambit
- Dipplin
- Archaludon
- Hydrapple
#### Regional Forms
- Sandshrew (Alolan Form)
- Sandslash (Alolan Form)
- Yamask (Galarian Form)
- Avalugg (Hisuian Form)
### Model updates for the following Pokémon
- Sandshrew
- Sandslash
- Nidoking
- Zubat
- Golbat
- Crobat
- Ditto
- Porygon
- Porygon2
- Porygon-Z
- Skarmory
- Smoochum
- Jynx
- Popplio
- Brionne
- Primarina
- Fletchling
- Fletchinder
- Talonflame
- Sableye
- Mawile
- Goomy
- Sliggoo
- Goodra
- Nacli
- Naclstack
- Garganacl
- Bidoof
- Cranidos
- Chatot
- Yamask
- Cofagrigus
- Tyranitar
- Turtonator
- Golett
- Corsola
- Arbok (New Patterns: Angry, Cartoon, Clown, Comedic, Crow, Grin, Ink, Modern, Rain, Scribble, Silence, Sloth, Suspicious, Will)
- ... and Substitute
#### Model changes to the regional bias resource pack:
- Added Mime Jr. (Galar Bias)
- Added Bergmite (Hisui Bias)
- Changed models for every existing regional bias
### Animation updates for the following Pokémon
- Kakuna
- Grimer
- Nidoran♀ (+ New shoulder mount)
- Nidorina
- Nidoran♂ (+ New shoulder mount)
- Sandshrew
- Diglett
- Dugtrio
- Graveler
- Happiny
- Chansey
- Blissey
- Poliwhirl
- Poliwrath
- Zubat
- Vulpix
- Drowzee
- Hypno
- Noctowl
- Skarmory
- Skitty
- Flygon
- Shellos
- Riolu
- Lucario
- Purrloin
- Liepard
- Joltik
- Galvantula
- Nidoking
- Fletchling
- Fletchinder
- Talonflame
- Archeops
- Drampa
- Sewaddle
- Swadloon
- Leavanny
- Goomy
- Sliggoo
- Goodra
- Shedinja
- Skwovet
- Greedent
- Bidoof
- Cranidos
- Chatot
- Yamask
- Cofagrigus
- Luvdisc
- Staryu
- Starmie
- Togetic
- Rellor
- Rabsca
- Yungoos
- Gumshoos
- Cottonee
- Whimsicott
- Corsola
#### Added sounds to the following moves
- Absorb
- Aurora Beam
- Double Team
- Draco Meteor
- Fire Punch
- Fire Spin
- Flame Charge
- Flame Wheel
- Giga Drain
- Ice Punch
- Leech Life
- Magical Leaf
- Mega Drain
- Minimize
- Poison Gas
- Rock Throw
- Scary Face
- Seed bomb
- Smokescreen
- String Shot
- Tail Whip
### Changes
- Stat increase and decrease particles have been changed, with the addition of alpha boost particles.
- Dragonite's mailbag cosmetic can now also be obtained with a chest in the cosmetic slot
- Adjusted Croagunk's walkspeed
- Adjusted Aerodactyl's walkspeed
- Sharp Beak can now be crafted
- Metal Alloy can now be crafted
- Ability Capsules can now be brewed
- Using an Ability Capsule will now return an Empty Bottle
- The following items can now be obtained by brushing the following Pokémon:
- Pink Wool - Wigglytuff
- White Wool - Swablu, Altaria
- Shed Shell - Ekans, Arbok, Dratini, Dragonair, Dunsparce, Treecko, Grovyle, Sceptile, Seviper, Scraggy, Scrafty, Helioptile, Heliolisk, Salandit, Salazzle, Sobble, Drizzile, Inteleon, Silicoblra, Sandaconda, Toxel, Toxtricity, Orthworm, Dudunsparce
- Phantom Membrane - Dreepy, Drakloak, Dragapult
- The following items can now be obtained by evolving the following Pokémon into any of their evolutions:
- Turtle Scute - Squirtle, Wartortle, Turtwig, Grotle, Tirtouga, Chewtle
- Shed Shell - Metapod, Kakuna, Ekans, Paras, Venonat, Krabby, Scyther, Kabuto, Dratini, Dragonair, Ledyba, Spinarak, Yanma, Pineco, Dunsparce, GLigar, Pupitar, Treecko, Grovyle, Silcoon, Cascoon, Surskit, Nincada, Trapinch, Vibrava, Corphish, Anorith, Bagon, Shelgon, Kricketot, Burmy, Combee, Skorupi, Swadloon, Whirlipede, Dwebble, Scraggy, Karrablast, Joltik, Larvesta, Spewpa, Clauncher, Helioptile, Charjabug, Crabrawler, Cutiefly, Dewpider, Salandit, Wimpod, Poipole, Sobble, Drizzile, Dottler, Silicobra, Toxel, Sizzlipede, Snom, Tarountula, Nymble, Rellor
- Saccharine Leaves now drop Tart, Sweet, or Syrupy Apples, depending on their honey coating stage, instead of vanilla Apples
- Fixed the height of the Tumblestone Bud items when held in hand, placed on head, viewed in a GUI, and placed in an Item Frame
- Refactored all advancements, removing several deemed unnecessary. Due to this refactor, all previously obtained advancements will be reset besides the root (for picking a starter Pokémon). We are sorry for the inconvience, but it makes the tab cleaner and simplifies future advancements.
- Restructured Botany Pots integration folder structure
- Decrease the default pastured mon wander radius to 32.
- Factor in the weight of Pokémon when calculating pushing forces between hitboxes.
- Remade the Starter Selection Screen with new assets
- Refactored berry trees to be less impactful on TPS (more performant)
- Changed the bait effects of the following items: (Vanilla) Sweet & Glow Berries, Golden & Enchanted Golden Apples (Cobblemon) Custap, Eggant, Hopo & Micle Berries, Sweet Hearts
- Wild Pokémon now reliably have at least one offensive move if they can have offensive moves at that level.
- Data Monitor can accept Disks (TMs, Music Disks, Upgrade, Dubious Disk, etc.) to change what's being displayed on the screen
- Rider positioning is now client-driven (Remove poseOffsets and offset per seat/pose) in favour of sending real-time passenger position.
- Cleaned up seat data. Removed all now unused fields from seats in all species files. Also made locator seat names explicit in the species files (however, this doesn't break the legacy format)
- Marks can now provide aspects.
- Added new shiny and Alpha chance configs
- Apricorns now make a new sound when harvested
- Lowered the volume of harvesting berries
- Added alternate versions of the basic PC Wallpapers based on the original design of Cobblemon's PC, before public release
- Improved compatibility with various mods
- Farmer's Delight's cutting board can now be used to strip Apricorn and Saccharine's logs, or salvage their planks from various blocks. It can also separate their boats from chests
- Farmer's Delight's cutting board can be used to get two white dye from Pep-Up Flowers and two magenta dye from Medicinal Leeks
- Farmer's Delight's cutting board can separate Tumblestone Blocks back into Tumblestones
- Farmer's Delight's cutting board can salvage Type Gems from their Gem Blocks
- Farmer's Delight's Onions, Tomatoes, and Cabbages can be used to cook Open-Faced Sandwiches. Nature's Spirit's Olives also can be used
- Farmer's Delight's cutting board can be used on Hearty Grains to obtain rice and straw, similarly to Farmer's Delight's Rice Panicles
- Farmer's Delight's Apple Pies and Apple Ciders can now be made using Applin Apples, alongside the Vanilla counterpart
- Amendments allows for cooking Leek and Potato Stew in a Cauldron
- Any mods that utilise Moonlight's Soft Fluid system can store Leek and Potato Stew, Medicinal Brew, Antidote, Awakening, Burn Heal, Ice Heal, Paralyze Heal, Full Heal, Potion, Super Potion, Hyper Potion, Max Potion, Full Restore, Ability Capsules, Moomoo Milk, and Berry Juice in liquid form (such as Suplementaries' Jar or Goblet, or Cauldrons while using Amendments)
- Supplementaries' Hourglass can now use Bright Powder, Quick Powder, Metal Powder, Heal Powder, Silver Powder, and Remedy
- Berry Trees are now compatible with the Planter properly
- Saccharine Way Signs (added by Supplementaries automagically) now use a premade texture to prevent odd visual issues
- Supplementaries' Jar can now store various Cobblemon cookies, candies, and similar items
- Some of Supplementaries' items have been added to the `#cobblemon:held/container_held_items` item tag, including sacks, safes, and lunch baskets
- Experience Nuggets can be used in place of Sculk for making the EXP Candies if using Create
- Rage Candy Bar has a Create Mixing recipe
- All Cobblemon ores can now be crushed in Create's Crushing Wheels
- All Gem and Tumblestone Clusters can be crushed for automatic, low return, gems
- Hearty Grains can be milled like Farmer's Delight Rice Panicles if both Farmer's Delight and Create are installed
- Big Roots and Revival Herbs can be milled to their respective powders
- Pep-Up Flowers, Medicinal Leeks, and all 70 berries can be milled in Create's Millstone for additional dye drops
- Auspicious Armour and Upgrades can be Haunted using Create's crafting systems
- Applin Apples can be used to make Honeyed Apples in Create
- Golden Apples can now be crafted using the Applin Apples
- Prismarine Crystals and Amethyst Shards can now be used to craft Pokedexes
- Updated Pokédex Screens item tag to support tagged variants of Redstone and Glowstone Dust
- Sounds for interacting with various Cobblemon blocks now play from the centre of the block, rather than the corner
- The `/pc` command no longer plays PC activation sounds
### Fixes
- 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 their sleep animations
- Fixed Diglett line not having their faint animations
- Fixed Kingambit moving its legs while walking
- Fixed Blitzle's base scale being `1` rather than the intended `0.8`
- Fixed Magnezone's emissive layer for the shiny having the wrong name, thus displaying wrongly
- Fixed Litleo not blinking
- Fixed Omanyte and Omastar incorrectly using water animations on land.
- Fixed Pokédex layout for genderless variations
- Fixed odd stacking behaviour with the poke_bait item.
- Fixed a compatibility issue with World Game Rules which caused some log spam in specific cases.
- Fixed improper validation of Pokémon when performing a trade.
- Fixed an issue where particle like beam can disappear when you don't see the beginning of the beam, or you look away
- Fixed Bedrock particle materials (alpha/opaque/blend/add) sharing one render batch, which caused incorrect culling on mixed batches; each material now batches and draws with its own blend and cull state, and the alpha material renders as a true cutout
- Fixed an occasional crash on retrieving revived fossils
- Fixed brushing not working on Neoforge
- Fixed braking not working for submarines (whoops).
- Fixed strafing not having the same friction as forward movement (funny whoops).
- Fixed submarine's surface-water animations not playing when supposed to.
- Fixed submarine camera freaking out when looking up or down on the surface.
- Fixed pastured mons despawning and running away.
- Fixed Apricorn trees crashing when other mods bonemeal it
- Fixed the issue that caused Hover Behaviour rides without a ground style to be rendered useless upon touching the ground on a server.
- Removed the "inAir" penalty for block destruction speed when on a hover mount.
- Fixed horizontal and vertical collisions with hover mounts so they no longer accumulate or retain momentum
- Fixed NeoForge brewing stand unexpectedly crashing users when shift clicking items within its menu
- Fixed NPCs switching Pokémon very often during battles
- Fixed Campfire pot causing crashes under certain circumstances
- Fixed Type Gems not being consumed upon use
- Fixed Ancient Pokéball's to their respective modifiers
- Fixed a Server warning when recalling Pokémon
- Fixed sliding particles incorrectly handling block collision
- Fixed Full Heal not curing status conditions in battle
- Fixed a rare issue with battles locking up when you are forced to switch, with the switch menu appearing instantly.
- Fixed the combined effects of Pledge moves having broken Showdown message handling and localisation
- Fixed rare Pokémon being far too likely to spawn multiple times in a single spawn cycle. The dev told me that the exact bug is too complicated to explain.
- Resolved the niche issue where Structures would spawn in an incorrect location, creating very funky world gen.
- Fixed Galarian Ponyta missing its cry.
- Fixed various log warnings caused by redundant files and sound events.
- Traded Pokémon will now retain the original friendship level when traded back to its OT
- Clusters now display correctly on maps
- Fixed removing mulch from berry trees with a shovel not working.
- Fixed collecting honey from Saccharine Leaf blocks deleting Honey Bottles when the inventory was full
- Fixed Stripped Saccharine Wood recipe being missing
- Fixed CancelableSoundInstance causing lag by attempting to create sounds every tick when out of range of hearing
- Fixed Stat Reducing Aprijuice not being possible to apply on a max out Pokémon, and the possibility of going into negatives
- Fixed Exploit where you could take out the bait or drop your Pokérod with bait to avoid consuming bait, now the bobber will just disappear if you do so
- Fixed Apricorn and Saccharine boats incorrectly using chest boat models
- Fixed wild and trainer battle AI refusing to select moves with the `adjacentAlly` target type, even when it's their only option
- Fixed gilded chests being more accessible than intended
- Fixed fished up Pokémon not being added as "seen" to the Pokedex
- Fixed surprised Gimmighoul encounters not being added as "seen" to the Pokedex
- Fixed a crash involving berry trees that have been world edited.
- Fixed visual desync in multiplayer for players mounting Pokémon outside other players' loaded chunks
- Fixed Pokémon entities leveled by rare candy not getting stronger as Minecraft entities unless recalled.
- Fixed a freeze during game load that occurred when multiple mods tried to load Cobblemon API classes at the same time.
- Fixed random tick crashes from berry trees
- Lowered the volume of Blitzle and Zebstrika's cry
- Regional Foods will now prioritise being eaten instead of used for healing Pokémon, shift clicking will use it as healing
- Resolved Z-Fighting on Pokédex Models
- Restored Ninjask's ability to do barrel rolls
- Fixed Raichu (male) and Alolan Raichu (both genders) not displaying the correct Partner Cap when Pewter Crunchies are in the cosmetic slot.
- Fixed a crash caused by adding enchanted items to the Resurrection Machine
- Fixed a crash that could occur when starting a PvN battle with only fainted Pokémon.
- Fixed the Remember Riding Camera config not being respected when dismounting and resolved the incompatibility with the Auto Third Person mod
- Fixed Full Heal block not dropping Full Heals
- Fixed status cure items not returning an empty bottle when used
- Fixed Panpour consuming its held item when evolving with a Water Stone
- Fixed Smoked Tail Curry returning a Wooden Bowl when consumed despite it not being included in the recipe
- Fixed `/giveallpokemon` not respecting the min and max parameters
- Fixed the choose starter toast not disappearing when capturing a Pokémon instead of opening the starter selection screen
- Fixed an issue where certain Sinister Tea seasonings were only applying as instant effects and would not apply over time
- Long config option labels now scroll instead of overflowing
### Developer
- Added `PokeSnackSpawnerFactory` which allows influence registration on PokeSnack encounters
- Added `FishingSpawnerFactory` which allows influence registration on fishing encounters
- Added `BattleFleeAttemptEvent` fired from `PokemonBattle.checkFleeAttempt`, allowing developers to intercept and control flee attempts
- Tweaked the spawning selector interface to take a bucket function rather than a bucket directly.
- Added a new `cobblemon:height_range` processor type to allow developers to restrict structure-spawning ranges.
- Separated all the functions of MoLangFunctions.kt into their own files.
- Spawn buckets and their base weights are now provided by the spawner, with different base weights for world, fishing, snack, and habitat spawn buckets.
- Removed "SpawnBucket" class in favour of just storing maps of bucket names to weights.
- Fixed nicknames not being applied to Pokémon spawned from `spawn_pool_world` files.
- Added support for `profilePokedexScale`, `profilePokedexTranslation`, `profileSummaryScale`, and `profileSummaryTranslation` in poser files for finer control over profile displays.
- Fixed riding camera offsets defined on Pokémon forms not being applied
### Molang & Datapacks
- Added `moveset_builders` datapack folder for different styles of movesets.
- Added `party_pools` datapack folder for defining groups of Pokémon for complex NPC party definitions.
- Added `party_compositions` datapack folder for arranging parties on NPCs that rely on party pools.
- Added `composed_pool` NPC party provider type which combines a party pool with a party composition to create more complex NPC parties.
- Added basic chatter NPC Behaviour
- Added `q.item.is_enchanted()`
- Added `q.item.has_enchantment(minecraft:sharpness, 3)`
- Added `has_chosen_starter` & `get_starter_uuid` as available Molang functions
- Added `has_tm_move_unlocked`, `unlock_tm_move`, & `lock_tm_move` as available Molang functions for Players
- Added `marks`, `has_mark`, & `remove_marks` as available Molang functions for Pokémon
- Added Data Component support for species drops, eg `"components": {"minecraft:custom_model_data": 123}`
- Added the new `sortOrder` field to mark entries, (defaults to 0 if not specified) which controls the order in which marks are rendered on the HUD, with higher numbers being rendered on top of lower numbers
- Added optional `order` field to starter categories. It will allow explicitly sorting categories in the starter selection screen
- SpeciesAdditions append vs override now matches wiki documentation
- Added `weighted_choice` as a new feature type. It requires setting weights for each choice and allows developers to simplify the process of assigning random aspect choice based on marble chance
- Added `q.world.spawn_loot_table_items(loot_table_id, x, y, z)` which spawns the items from the loot table at the position. (Use type: "minecraft:chest" for the loot table.)
- Added `q.pokemon.entity` to access the entity of Pokémon.
- Flamethrower and Seismic Toss had their action effect JSON cleaned up
- Ride sounds can now be set as exclusive to passengers
- Ride sounds no longer play when submerged unless specified with a new setting
- NPC Trainers can now call a `command` animation when issuing a command in battle.
- Added a q.failed function to move action effects.
- Herd spawned Pokémon have their levels controlled differently. The `levelRange` property now acts as a clamp instead of a filter, with a new `herdLevelRange` property to do the filter behaviour.
- `q.sound` now accepts three parameters (sound event, volume and pitch) instead of just the sound event and pitch.
### Particles
#### New Move Animations
- Blizzard
- Disarming Voice
- Draco Meteor
- Dragon Breath
- Dragon Rage
- Drain Punch
- Fairy Wind
- Fire Fang
- Gust
- Horn Drill
- Hydro Pump
- Ice Fang
- Leafage
- Leaf Storm
- Light Screen
- Night Shade
- Play Rough
- Pyro Ball
- Reflect
- Rock Slide
- Sonic Boom
- Sucker Punch
- Supersonic
- Thunder Fang
#### Tweaks and Fixes (Moves)
- Confusion
- Dragon Claw
- Eruption
- Fire Punch
- Flame Burst
- Flame Charge
- Flamethrower
- Frost Breath
- Hypnosis
- Ice Punch
- Icy Wind
- Mud Sport
- Poison Fang
- Protect
- Psychic
- Quick Attack
- Rage Powder
- Razor Leaf
- Sand Attack
- Scratch
- Shadow Claw
- Sing
- Spore
- String Shot
- Tackle
- Tail Whip
- Thunder
- Thunder Punch
- Thunder Wave
- Water Gun
- Water Pulse
- Water Sport
#### Miscellaneous Particles
+ Added Wild Might particles for Alpha Pokémon
+ Updated stat increase and decrease particles
### Localization
- Updated translations for:
- Updated translations for:
- Japanese
- German
- Spanish
- Ecuadorian Spanish
- Portuguese
- Brazilian Portuguese
- Simplified Chinese
- Traditional Chinese
- Korean
-529
View File
@@ -1,529 +0,0 @@
# Changelog
## [1.8.0 - Make Your Move Update (September 6th, 2026)](#1-8-0)
#### I think the red eyes mean they're friendly!
### Additions
- Added Alpha Pokémon
- Alpha Pokémon are bigger, stronger, and more aggressive than regular Pokémon.
- They are also scarier.
- They always know a TM move and will adapt to be stronger than your party.
- Alphas will always lead herds and spawn in herds.
- Added alpha Pokémon attraction bait effect to Hopo Berries.
- Added intrinsic scaling to all Pokémon (anywhere from 5% smaller to 5% bigger than normal). Some cases, such as shoulder mounting, will not scale so that the shoulder animations don't look silly.
- Added scaling to "baby" Pokémon (10% smaller at level 1 → normally sized at level 10).
- Added TMs.
- Added TM Machine. TMs are made from elemental type gems, and their recipes are unlocked by having Pokémon that know the move.
- Added config options for making TMs infinite-use and instant unlocking of all TM recipes.
- Added Type Gem blocks as well as crystal clusters and deepslate core blocks that are used for growing elemental type gems.
- Added a new Starter Selection menu that looks way better.
- Added a "Move Dex" to the Pokédex to help identify what moves a Pokémon can learn and what Pokémon can get a move.
- Added the Habitat Block, a spawner block for servers and adventure maps that controls spawning in an area.
- Added 49 new habitat structures. In these habitats you will find rarer Pokémon and sometimes different Pokémon will appear on different days.
- Added a toast to notify players of how they can scroll through their party. Did you know you can hold R and scroll instead of using arrow keys? It's true!!!
- Added a way to obtain Porygon by inserting an Upgrade or Dubious Disc into a data monitor.
- Added Disk Rack for storing disks and being an audio sequencer for Noteblocks. What does that have to do with Pokémon? How about you mind your own business!
- Added missing crops to Botany Pots integration.
- Added conditional seats for riding. Seats can be gated by Molang such as "is_alpha".
- Added 10 new ruin structures.
- Added a new shipwreck cove structure called the Magma Shipwreck Cove.
- Added 7 new advancements.
- Pokémon cries now play when clicking Pokémon in the summary screen.
- Held item visibility can now be controlled by the `held_item_visible` command argument.
- Added config option to disable Pokémon spawning on a per-world basis.
- Added config options to award experience to fainted Pokémon for opponents they helped defeat (`awardExperienceToFaintedPokemon`) and to still award experience for defeated enemies even after battle loss/forfeit (`awardExperienceOnBattleLoss`).
- Added `/givetm` command.
- Added advancement triggers for learning TMs: `cobblemon:has_learn_specific_tm` (per-TM), `cobblemon:has_learn_all_tm` (all TMs learned), and `cobblemon:tms_learned` (count-based), backed by a new `totalTMLearnedCount` on player advancement data.
- Added `cobblemon:catch_alpha_pokemon` advancement trigger and `totalAlphaCaptureCount` stat for captured Alpha Pokémon.
### New rideable Pokémon
- Crobat
- Skarmory
- Milotic
- Archeops
- Goodra
- Goodra (Hisuian Form)
- Drampa
- Duraludon
- Archaludon
### Pokémon Added
#### Generation 3
- Wurmple
- Silcoon
- Beautifly
- Cascoon
- Dustox
- Skitty
- Delcatty
- Seviper
- Zangoose
#### Generation 4
- Cherubi
- Cherrim
- Stunky
- Skuntank
#### Generation 5
- Audino
- Pawniard
- Bisharp
#### Generation 6
- Pancham (+ Cosmetic with Black Glasses)
- Pangoro (+ Cosmetic with Black Glasses)
#### Generation 7
- Rockruff
- Lycanroc
#### Generation 8
- Rolycoly
- Carkol
- Coalossal
- Runerigus
- Snom
- Frosmoth
- Indeedee
- Duraludon
- Applin
- Flapple
- Appletun
#### Generation 9
- Nymble
- Lokix
- Kingambit
- Dipplin
- Archaludon
- Hydrapple
#### Regional Forms
- Sandshrew (Alolan Form)
- Sandslash (Alolan Form)
- Yamask (Galarian Form)
- Avalugg (Hisuian Form)
### Model updates for the following Pokémon
- Sandshrew
- Sandslash
- Nidoking
- Zubat
- Golbat
- Crobat
- Ditto
- Porygon
- Porygon2
- Porygon-Z
- Skarmory
- Smoochum
- Jynx
- Popplio
- Brionne
- Primarina
- Fletchling
- Fletchinder
- Talonflame
- Sableye
- Mawile
- Goomy
- Sliggoo
- Goodra
- Nacli
- Naclstack
- Garganacl
- Bidoof
- Cranidos
- Chatot
- Yamask
- Cofagrigus
- Tyranitar
- Turtonator
- Golett
- Corsola
- Arbok (New Patterns: Angry, Cartoon, Clown, Comedic, Crow, Grin, Ink, Modern, Rain, Scribble, Silence, Sloth, Suspicious, Will)
- ... and Substitute
#### Model changes to the regional bias resource pack:
- Added Mime Jr. (Galar Bias)
- Added Bergmite (Hisui Bias)
- Changed models for every existing regional bias
### Animation updates for the following Pokémon
- Kakuna
- Grimer
- Nidoran♀ (+ New shoulder mount)
- Nidorina
- Nidoran♂ (+ New shoulder mount)
- Sandshrew
- Diglett
- Dugtrio
- Graveler
- Happiny
- Chansey
- Blissey
- Poliwhirl
- Poliwrath
- Zubat
- Vulpix
- Drowzee
- Hypno
- Noctowl
- Skarmory
- Skitty
- Flygon
- Shellos
- Riolu
- Lucario
- Purrloin
- Liepard
- Joltik
- Galvantula
- Nidoking
- Fletchling
- Fletchinder
- Talonflame
- Archeops
- Drampa
- Sewaddle
- Swadloon
- Leavanny
- Goomy
- Sliggoo
- Goodra
- Shedinja
- Skwovet
- Greedent
- Bidoof
- Cranidos
- Chatot
- Yamask
- Cofagrigus
- Luvdisc
- Staryu
- Starmie
- Togetic
- Rellor
- Rabsca
- Yungoos
- Gumshoos
- Cottonee
- Whimsicott
- Corsola
#### Added sounds to the following moves
- Absorb
- Aurora Beam
- Double Team
- Draco Meteor
- Fire Punch
- Fire Spin
- Flame Charge
- Flame Wheel
- Giga Drain
- Ice Punch
- Leech Life
- Magical Leaf
- Mega Drain
- Minimize
- Poison Gas
- Rock Throw
- Scary Face
- Seed bomb
- Smokescreen
- String Shot
- Tail Whip
### Changes
- Removed the penalty for block destruction speed when on a hover mount, making them better for digging.
- Golden Apples can now be crafted using the Applin Apples.
- Prismarine Crystals and Amethyst Shards can now be used to craft Pokedexes.
- Updated Pokédex Screens item tag to support tagged variants of Redstone and Glowstone Dust.
- Sharp Beak can now be crafted using bones and Flying Gems.
- Metal Alloy can now be crafted using iron and Thunder Stones.
- Ability Capsules can now be brewed from Medicinal Brew and Dragon Breath.
- Using an Ability Capsule will now return an Empty Bottle.
- Decreased the default pastured wander radius to 32 blocks.
- Added alternate versions of the basic PC Wallpapers based on the original design of Cobblemon's PC, before public release.
- Rooted Arch Ruins, Crumbling Arch Ruins, and all fossil structures have been improved so that they generate more naturally in terrain.
- Gimmighoul towers now generate at ground-level rather than sinking one block into the ground.
- All ruin structures that did not generate with a gimmighoul/gilded chest before will now generate with one.
- All ruin structures gilded chest loot tables have been updated to include a guaranteed TM not learned naturally by any Pokémon through their level-up moveset. Each ruin gets its own specific TM, so you can now go hunting ruins to complete your move dex!
- The Prehistoric Underwater Fissure and Prehistoric Submerged Impact fossil structures now use the biome tag `#cobblemon:is_temperate_ocean`.
- The lush shipwreck cove's phantom mob spawner has been replaced with breezes.
- Shipwreck coves now generate slightly more frequently.
- Added equipment loot tables for shipwreck cove enemies.
- Maps from abandoned fishing boats leading to shipwreck coves now will skip explored chunks, allowing the player to find more shipwreck coves more easily.
- All shipwreck coves' spawners have been adjusted to give compatible mobs enchanted iron or diamond armour and weapons while in its ominous state. The armour given to the mobs will also use the automaton trim. We upped the difficulty because it was too easy now that your Pokémon can fight for you.
- One TM of the elemental hyper beams has been added as guaranteed loot to the main treasure chest in every shipwreck cove. (Magma Cove gets Blast Burn, Lush Cove gets Frenzy Plant, and Submerged Cove gets Hydro Cannon.)
- The following items can now be obtained by brushing the following Pokémon:
- Pink Wool - Wigglytuff
- White Wool - Swablu, Altaria
- Shed Shell - Ekans, Arbok, Dratini, Dragonair, Dunsparce, Treecko, Grovyle, Sceptile, Seviper, Scraggy, Scrafty, Helioptile, Heliolisk, Salandit, Salazzle, Sobble, Drizzile, Inteleon, Silicoblra, Sandaconda, Toxel, Toxtricity, Orthworm, Dudunsparce
- Phantom Membrane - Dreepy, Drakloak, Dragapult
- The following items can now be obtained by evolving the following Pokémon into any of their evolutions:
- Turtle Scute - Squirtle, Wartortle, Turtwig, Grotle, Tirtouga, Chewtle
- Shed Shell - Metapod, Kakuna, Ekans, Paras, Venonat, Krabby, Scyther, Kabuto, Dratini, Dragonair, Ledyba, Spinarak, Yanma, Pineco, Dunsparce, GLigar, Pupitar, Treecko, Grovyle, Silcoon, Cascoon, Surskit, Nincada, Trapinch, Vibrava, Corphish, Anorith, Bagon, Shelgon, Kricketot, Burmy, Combee, Skorupi, Swadloon, Whirlipede, Dwebble, Scraggy, Karrablast, Joltik, Larvesta, Spewpa, Clauncher, Helioptile, Charjabug, Crabrawler, Cutiefly, Dewpider, Salandit, Wimpod, Poipole, Sobble, Drizzile, Dottler, Silicobra, Toxel, Sizzlipede, Snom, Tarountula, Nymble, Rellor
- Saccharine Leaves now drop Tart, Sweet, or Syrupy Apples, depending on their honey coating stage, instead of vanilla Apples.
- Fixed the height of the Tumblestone Bud items when held in hand, placed on head, viewed in a GUI, and placed in an Item Frame.
- Refactored all advancements, removing several deemed unnecessary. Due to this cleanup, all previously obtained advancements will be reset besides the root (for picking a starter Pokémon). This sucks, and we're sorry. We had to do this eventually so better now than later.
- Forces between colliding hitboxes now factors in the weight of Pokémon.
- Improved berry tree performance on singleplayer and servers.
- Updated stat increase and decrease particle effects with the addition of alpha boost particles.
- Improved the performance of pasture blocks on singleplayer and servers.
- Changed the bait effects of the following items: (Vanilla) Sweet & Glow Berries, Golden & Enchanted Golden Apples (Cobblemon) Custap, Eggant, Hopo & Micle Berries, Sweet Hearts.
- Wild Pokémon now reliably have at least one offensive move if they can have offensive moves at that level. No more free lunches with Pokémon that can't even hurt you.
- Data Monitor can accept Disks (TMs, Music Disks, Upgrade, Dubious Disk, etc.) to change what's being displayed on the screen.
- The party overlay's experience-gained animation now displays the amount of EXP the Pokémon gained.
- Pressing the inventory key now closes the Pokémon interact wheel GUI.
- Updated the sound for harvesting Apricorns.
- Lowered the volume of harvesting berries.
- Improved compatibility with various mods
- Farmer's Delight's cutting board can now be used to strip Apricorn and Saccharine's logs, or salvage their planks from various blocks. It can also separate their boats from chests.
- Farmer's Delight's cutting board can be used to get two white dye from Pep-Up Flowers and two magenta dye from Medicinal Leeks.
- Farmer's Delight's cutting board can separate Tumblestone Blocks back into Tumblestones.
- Farmer's Delight's cutting board can salvage Type Gems from their Gem Blocks.
- Farmer's Delight's Onions, Tomatoes, and Cabbages can be used to cook Open-Faced Sandwiches. Nature's Spirit's Olives also can be used.
- Farmer's Delight's cutting board can be used on Hearty Grains to obtain rice and straw, similarly to Farmer's Delight's Rice Panicles.
- Farmer's Delight's Apple Pies and Apple Ciders can now be made using Applin Apples, alongside the Vanilla counterpart.
- Amendments allows for cooking Leek and Potato Stew in a Cauldron.
- Any mods that utilise Moonlight's Soft Fluid system can store Leek and Potato Stew, Medicinal Brew, Antidote, Awakening, Burn Heal, Ice Heal, Paralyze Heal, Full Heal, Potion, Super Potion, Hyper Potion, Max Potion, Full Restore, Ability Capsules, Moomoo Milk, and Berry Juice in liquid form (such as Suplementaries' Jar or Goblet, or Cauldrons while using Amendments).
- Supplementaries' Hourglass can now use Bright Powder, Quick Powder, Metal Powder, Heal Powder, Silver Powder, and Remedy.
- Berry Trees are now compatible with the Planter properly
- Saccharine Way Signs (added by Supplementaries automagically) now use a premade texture to prevent odd visual issues.
- Supplementaries' Jar can now store various Cobblemon cookies, candies, and similar items.
- Some of Supplementaries' items have been added to the `#cobblemon:held/container_held_items` item tag, including sacks, safes, and lunch baskets.
- Experience Nuggets can be used in place of Sculk for making the EXP Candies if using Create.
- Rage Candy Bar has a Create Mixing recipe.
- All Cobblemon ores can now be crushed in Create's Crushing Wheels. All Gem and Tumblestone Clusters can be crushed for automatic, low return, gems.
- Hearty Grains can be milled like Farmer's Delight Rice Panicles if both Farmer's Delight and Create are installed.
- Big Roots and Revival Herbs can be milled to their respective powders.
- Pep-Up Flowers, Medicinal Leeks, and all 70 berries can be milled in Create's Millstone for additional dye drops.
- Auspicious Armour and Upgrades can be Haunted using Create's crafting systems.
- Applin Apples can be used to make Honeyed Apples in Create.
- Sounds for interacting with various Cobblemon blocks now play from the centre of the block, rather than the corner. You probably didn't notice.
- Dragonite's mailbag cosmetic can now also be obtained with a chest in the cosmetic slot.
- Adjusted the walk speed for Croagunk and Aerodactyl.
- The `/pc` command no longer plays PC activation sounds
- Pressing the Summary key while either the starter selection or summary screen are open now closes those screens.
- Ice type Pokémon are now immune to Packed Snow.
- Added cobweb block slowness immunity for spider Pokémon
### Fixes
- Fixed rare Pokémon being far too likely to spawn multiple times in a single spawn cycle. The dev told me that the exact bug is too complicated to explain.
- Fixed Omanyte and Omastar incorrectly using water animations on land.
- Fixed Pokédex layout for genderless variations.
- Fixed odd stacking behaviour with Poké Baits.
- Fixed a compatibility issue with World Game Rules which caused some log spam in specific cases.
- Fixed improper validation of Pokémon when performing a trade.
- Fixed an issue where particles like beams can disappear when you don't see the beginning of the beam, or you look away.
- Fixed Bedrock particle materials sometimes being incorrectly invisible from some sides.
- Fixed an occasional crash on retrieving revived fossils.
- Fixed brushing not working on Neoforge. Again.
- Fixed braking not working for submarine-style mounts (whoops).
- Fixed strafing not having the same friction as forward movement (funny whoops).
- Fixed submarine-style mounts' surface-water animations not playing when supposed to.
- Fixed submarine-style mounts' camera freaking out when looking up or down on the surface.
- Fixed pastured Pokémon despawning and running away.
- Fixed Apricorn trees crashing when other mods bonemeal them.
- Fixed the issue that caused hover-style mounts without a ground style to be rendered useless upon touching the ground on a server.
- Fixed horizontal and vertical collisions with hover-style mounts so they no longer accumulate or retain momentum.
- Fixed NeoForge brewing stand unexpectedly crashing users when shift clicking items within its menu. Well, I suppose all crashes are unexpected. You know what I mean.
- Fixed NPCs switching Pokémon very often during battles.
- Fixed Campfire Pot causing crashes under certain circumstances.
- Fixed Type Gems not being consumed upon use in battles.
- Fixed several Ancient Pokéball's not using the correct capture multiplier.
- Fixed a server warning that could occur when recalling Pokémon.
- Fixed sliding particles incorrectly handling block collision.
- Fixed Full Heal not curing status conditions in battle. Literally it's one job.
- Fixed a rare issue with battles locking up when you are forced to switch, with the switch menu appearing instantly.
- Fixed the combined effects of Pledge moves having broken Showdown message handling and localisation.
- Fixed a very niche issue where structures would spawn in an incorrect location, creating very funky world gen.
- Fixed Galarian Ponyta missing its cry.
- Fixed various log warnings caused by redundant files and sound events.
- Traded Pokémon will now retain the original friendship level when traded back to its OT.
- Tumblestone Clusters now display correctly on maps.
- Fixed removing mulch from berry trees with a shovel not working. That was meant to be a thing.
- Fixed collecting honey from Saccharine Leaf blocks deleting Honey Bottles when the inventory was full.
- Fixed Stripped Saccharine Wood recipe being... not there.
- Fixed some performance issues with a kind of sound event causing lag by attempting to create sounds every tick when out of range of hearing.
- Fixed stat-reducing Aprijuice not being possible to apply on a maxed out Pokémon. Also fixed the possibility of going into negatives.
- Fixed exploit where you could take out the bait or drop your Pokérod with bait to avoid consuming bait, now the bobber will just disappear if you do so.
- Fixed Apricorn and Saccharine boats incorrectly using chest boat models.
- Fixed wild and trainer battle AI refusing to select moves with the `adjacentAlly` target type, even when it's their only option.
- Fixed gilded chests being more accessible than intended.
- Fixed fished up Pokémon not being added as "seen" to the Pokédex.
- Fixed surprised Gimmighoul encounters not being added as "seen" to the Pokédex.
- Fixed a crash involving berry trees that have been world edited.
- Fixed visual desync in multiplayer for players mounting Pokémon outside other players' loaded chunks.
- Fixed Pokémon entities leveled by rare candy not getting stronger as Minecraft entities unless recalled.
- Fixed a freeze during game load that occurred when multiple mods tried to load Cobblemon API classes at the same time.
- Lowered the volume of Blitzle and Zebstrika's cry (shut up!).
- Regional Foods will now prioritise being eaten instead of used for healing Pokémon - shift clicking will use it as healing.
- Resolved z-fighting (texture flickering) on Pokédex Models.
- Restored Ninjask's ability to do barrel rolls.
- Fixed Raichu (male) and Alolan Raichu (both genders) not displaying the correct Partner Cap when Pewter Crunchies are in the cosmetic slot.
- Fixed a crash caused by adding enchanted items to the Resurrection Machine.
- Fixed a crash that could occur when starting a PvN battle with only fainted Pokémon.
- Fixed the Remember Riding Camera config not being respected when dismounting and resolved the incompatibility with the Auto Third Person mod.
- Fixed Full Heal block not dropping Full Heals. Lmao.
- Fixed status cure items not returning an empty bottle when used.
- Fixed Panpour consuming its held item when evolving with a Water Stone.
- Fixed Smoked Tail Curry returning a Wooden Bowl when consumed despite it not being included in the recipe. Free bowls!
- Fixed `/giveallpokemon` not respecting the min and max arguments.
- Fixed the choose starter toast not disappearing when capturing a Pokémon instead of opening the starter selection screen.
- Fixed an issue where certain Sinister Tea seasonings were only applying as instant effects and would not apply over time.
- Long config option labels now scroll instead of overflowing in the configuration menu.
- Fixed some z-fighting and clipping on Dewott and Hisui-Bias Dewott.
- Eggant Berries can now be composted properly on NeoForge.
- Composting chances are now the same on NeoForge as on Fabric.
- Fixed problem where furnaces which had smelted Revival Herbs into Heal Powder in older versions would glitch out on trying to get the experience out of the block.
- Fixed the Battle AI not recognizing Surging Strikes as a multistrike move.
- Fixed Tentacool not using its swim animation when on water surface.
- Fixed Eevee using its left shoulder pose when in battle.
- Fixed Magikarp and Gyarados Jump Pattern resourcepacks replacing shiny textures of addons targeting Magikarp or Gyarados.
- Fixed Combee's pollinated texture overwriting the shiny and female textures.
- Fixed a colour mistake with Dracovish's shiny.
- Fixed Togedemaru's back spikes z-fighting.
- Fixed Sliggoo (+ Hisuian) evolving in the Nether and End where it can't actually rain.
- Corrected the scales, hitbox sizes, and eye heights for Ariados, Cetoddle, Durant, Galvantula, Klink, Lanturn, Lumineon, Mantine, Mantyke, Mudbray, Numel, Staravia, and Torkoal.
- Fixed Torkoal's walk speed not matching its animation. He will no longer move faster than Bunnelby and Buneary. This tortoise would've won.
- Fixed Lanturn T-posing due to missing ground walk and sleep animations.
- Fixed Oshawott and Dewott using their sleep animation in unintended scenarios.
- Fixed Dudunsparce and Maushold always having the same form instead of having the 1 in 100 chance of being different.
- Fixed bird mounts rapidly switching between their flying and gliding animations when flying at low speed.
- Fixed some battles being startable with fainted Pokémon in the initial sendout.
- Fixed random gender selection for forms with strict gender divides (such as Meowstic and Oinkologne) never choosing female (that's sexist).
- Fixed the "Press R to battle" prompt not disappearing after you finish your first battle, at least until you relogged.
- Fixed the composter block being listed as "composer" in the cosmetic items species feature
- Fixed Castform's forms being tagged as introduced in generation 4 instead of 3.
- Fixed Blitzle's base scale being `1` rather than the intended `0.8`.
- Fixed Magnezone's emissive layer for the shiny having the wrong name, thus displaying wrongly.
- Fixed Litleo not blinking.
### Developer
- Added `PokeSnackSpawnerFactory` which allows influence registration on PokeSnack encounters.
- Added `FishingSpawnerFactory` which allows influence registration on fishing encounters.
- Added `BattleFleeAttemptEvent` fired from `PokemonBattle.checkFleeAttempt`, allowing developers to intercept and control flee attempts.
- Tweaked the spawning selector interface to take a bucket function rather than a bucket directly.
- Added a new `cobblemon:height_range` processor type to allow developers to restrict structure-spawning ranges.
- Separated all the functions of MoLangFunctions.kt into their own files.
- Spawn buckets and their base weights are now provided by the spawner, with different base weights for world, fishing, snack, and habitat spawn buckets.
- Removed "SpawnBucket" class in favour of just storing maps of bucket names to weights.
- Fixed nicknames not being applied to Pokémon spawned from `spawn_pool_world` files.
- Added support for `profilePokedexScale`, `profilePokedexTranslation`, `profileSummaryScale`, and `profileSummaryTranslation` in poser files for finer control over profile displays.
- Fixed riding camera offsets defined on Pokémon forms not being applied.
- Marks can now provide aspects.
### Molang & Datapacks
- Added `moveset_builders` datapack folder for different styles of movesets.
- Added `party_pools` datapack folder for defining groups of Pokémon for complex NPC party definitions.
- Added `party_compositions` datapack folder for arranging parties on NPCs that rely on party pools.
- Added `composed_pool` NPC party provider type which combines a party pool with a party composition to create more complex NPC parties.
- Added basic chatter NPC Behaviour.
- Added `q.item.is_enchanted()`.
- Added `q.item.has_enchantment(minecraft:sharpness, 3)`.
- Added `has_chosen_starter` and `get_starter_uuid` as available Molang functions.
- Added `has_tm_move_unlocked`, `unlock_tm_move`, and `lock_tm_move` as available Molang functions for Players.
- Added `marks`, `has_mark`, and `remove_marks` as available Molang functions for Pokémon.
- Added Data Component support for species drops, eg `"components": {"minecraft:custom_model_data": 123}`
- Added the new `sortOrder` field to mark entries, (defaults to 0 if not specified) which controls the order in which marks are rendered on the HUD, with higher numbers being rendered on top of lower numbers.
- Added optional `order` field to starter categories. It will allow explicitly sorting categories in the starter selection screen.
- SpeciesAdditions append vs override now matches the wiki documentation.
- Added `weighted_choice` as a new feature type. It requires setting weights for each choice and allows developers to simplify the process of assigning random aspect choice based on marble chance.
- Added `q.world.spawn_loot_table_items(loot_table_id, x, y, z)` which spawns the items from the loot table at the position. Use type: `minecraft:chest` for the loot table.
- Added `q.pokemon.entity` to access the entity of Pokémon.
- Flamethrower and Seismic Toss had their action effect JSON cleaned up.
- Ride sounds can now be set as exclusive to passengers.
- Ride sounds no longer play when submerged unless specified with a new setting.
- NPC Trainers can now call a `command` animation when issuing a command in battle.
- Added a q.failed function to move action effects.
- Herd spawned Pokémon have their levels controlled differently. The `levelRange` property now acts as a pre-offset clamp instead of a filter, with a new `herdLevelRange` property to do the filter behaviour.
- `q.sound` now accepts three parameters (sound event, volume, and pitch) instead of just the sound event and pitch.
- Added a new layer property that makes a layer scroll in a direction. "scrolling": {"speedU": 0.1, "speedV": 0.1}
- Cleaned up seat data. Removed all now unused fields from seats in all species files. Also made locator seat names explicit in the species files (however, this doesn't break the legacy format)
### Particles
#### New Move Animations
- Blizzard
- Disarming Voice
- Draco Meteor
- Dragon Breath
- Dragon Rage
- Drain Punch
- Fairy Wind
- Fire Fang
- Gust
- Horn Drill
- Hydro Pump
- Ice Fang
- Leafage
- Leaf Storm
- Light Screen
- Night Shade
- Play Rough
- Pyro Ball
- Reflect
- Rock Slide
- Sonic Boom
- Sucker Punch
- Supersonic
- Thunder Fang
#### Tweaks and Fixes (Moves)
- Confusion
- Dragon Claw
- Eruption
- Fire Punch
- Flame Burst
- Flame Charge
- Flamethrower
- Frost Breath
- Hypnosis
- Ice Punch
- Icy Wind
- Mud Sport
- Poison Fang
- Protect
- Psychic
- Quick Attack
- Rage Powder
- Razor Leaf
- Sand Attack
- Scratch
- Shadow Claw
- Sing
- Spore
- String Shot
- Tackle
- Tail Whip
- Thunder
- Thunder Punch
- Thunder Wave
- Water Gun
- Water Pulse
- Water Sport
#### Miscellaneous Particles
- Added Wild Might particles for Alpha Pokémon.
- Updated stat increase and decrease particles.
### Localization
- Updated translations for:
- Japanese
- German
- Spanish
- Ecuadorian Spanish
- Portuguese
- Brazilian Portuguese
- Simplified Chinese
- Traditional Chinese
- Korean
- Russian
-7
View File
@@ -32,7 +32,6 @@ repositories {
maven(url = "https://api.modrinth.com/maven")
maven(url = "https://maven.neoforged.net/releases")
maven(url = "https://maven.gegy.dev")
mavenCentral()
mavenLocal()
}
@@ -41,7 +40,6 @@ dependencies {
modImplementation(libs.fabric.loader)
modApi(libs.molang)
// Integrations
compileOnlyApi(libs.jei.api)
modCompileOnly(libs.bundles.common.integrations.compileOnly) {
@@ -64,19 +62,14 @@ dependencies {
// Unit Testing
testImplementation(libs.bundles.unitTesting)
testImplementation(libs.graal.sdk)
testRuntimeOnly(libs.bundles.graal)
}
tasks.withType<Test> {
useJUnitPlatform()
// Static GL mocks must not load a graphics driver on headless CI.
systemProperty("org.lwjgl.opengl.explicitInit", "true")
testLogging {
setEvents(listOf("failed"))
setExceptionFormat("full")
}
System.getProperty("camera.benchmark")?.let { systemProperty("camera.benchmark", it) }
}
tasks.processResources {
@@ -1,153 +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.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);
}
}
}
}
@@ -1,155 +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.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;
}
}
@@ -1,14 +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.client.render.camera.accessor;
public interface CameraFeedRenderState {
boolean cobblemonRenderHand();
boolean cobblemonRenderBlockOutline();
}
@@ -1,24 +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.duck;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.Level;
import java.util.Optional;
/**
* Duck that facilitates getting a level key for a given level.
*
* @author dualspiral
* @since August 30th, 2026
*/
public interface MinecraftServerDuck {
Optional<ResourceKey<Level>> cobblemon$getLevelKey(Level level);
}
@@ -1,21 +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.duck;
import net.minecraft.resources.ResourceLocation;
/**
* Duck that facilitates getting a level key that we store on the level.
*
* @author dualspiral
* @since August 30th, 2026
*/
public interface ServerLevelDuck {
ResourceLocation cobblemon$getLevelResourceLocation();
}
@@ -0,0 +1,54 @@
/*
* 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;
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.multiplayer.ClientPacketListener;
import net.minecraft.network.protocol.game.ClientboundSetPassengersPacket;
import net.minecraft.world.entity.Entity;
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;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Objects;
@Mixin(ClientPacketListener.class)
public class ClientPacketListenerMixin {
@Shadow private ClientLevel level;
@Inject(method = "handleSetEntityPassengersPacket", at = @At(value = "INVOKE_ASSIGN", target = "Lnet/minecraft/world/entity/Entity;hasIndirectPassenger(Lnet/minecraft/world/entity/Entity;)Z", shift = At.Shift.BEFORE), cancellable = true)
private void cobblemon$handlePokemonPassengers(ClientboundSetPassengersPacket packet, CallbackInfo ci) {
Entity entity = this.level.getEntity(packet.getVehicle());
if (entity instanceof PokemonEntity pokemon) {
var passengers = new ArrayList<Entity>();
for (int passenger : packet.getPassengers()) {
passengers.add(this.level.getEntity(passenger));
}
pokemon.getOccupiedSeats().values().stream()
.filter(x -> !passengers.contains(x))
.filter(Objects::nonNull)
.forEach(Entity::stopRiding);
// Mount passengers not yet in the map
var currentPassengers = new ArrayList<>(pokemon.getOccupiedSeats().values());
passengers.stream()
.filter(x -> !currentPassengers.contains(x))
.filter(Objects::nonNull)
.forEach(x -> x.startRiding(pokemon, true));
ci.cancel();
}
}
}
@@ -0,0 +1,133 @@
/*
* 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;
import com.cobblemon.mod.common.mixin.accessor.SinglePoolElementAccessor;
import com.cobblemon.mod.common.world.structureprocessors.HeightRangeStructureProcessor;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.pools.*;
import net.minecraft.world.level.levelgen.structure.pools.alias.PoolAliasLookup;
import net.minecraft.world.level.levelgen.structure.templatesystem.LiquidSettings;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessor;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessorList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spongepowered.asm.mixin.Mixin;
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.CallbackInfoReturnable;
import java.util.Optional;
@Mixin(JigsawPlacement.class)
public class JigsawPlacementMixin {
@Unique
private static final Logger cobblemon$LOGGER = LoggerFactory.getLogger("Cobblemon/HeightRangeVeto");
@Inject(
method = "addPieces(Lnet/minecraft/world/level/levelgen/structure/Structure$GenerationContext;Lnet/minecraft/core/Holder;Ljava/util/Optional;ILnet/minecraft/core/BlockPos;ZLjava/util/Optional;ILnet/minecraft/world/level/levelgen/structure/pools/alias/PoolAliasLookup;Lnet/minecraft/world/level/levelgen/structure/pools/DimensionPadding;Lnet/minecraft/world/level/levelgen/structure/templatesystem/LiquidSettings;)Ljava/util/Optional;",
at = @At("RETURN"),
cancellable = true
)
private static void onAddPieces(
Structure.GenerationContext context,
Holder<StructureTemplatePool> startPool,
Optional<ResourceLocation> startJigsawName,
int maxDepth,
BlockPos pos,
boolean useExpansionHack,
Optional<Heightmap.Types> projectStartToHeightmap,
int maxDistanceFromCenter,
PoolAliasLookup aliasLookup,
DimensionPadding dimensionPadding,
LiquidSettings liquidSettings,
CallbackInfoReturnable<Optional<Structure.GenerationStub>> cir
) {
Optional<Structure.GenerationStub> result = cir.getReturnValue();
if (result.isEmpty()) {
return;
}
BlockPos spawnPos = result.get().position();
String poolId = startPool.unwrapKey().map(key -> key.location().toString()).orElse("unknown");
boolean canPlace = cobblemon$canStructurePlace(context, startPool, spawnPos);
cobblemon$LOGGER.info(
"[HeightRangeVeto] addPieces evaluated requestedPos={} spawnPos={} startPool={} -> canPlace={}",
pos, spawnPos, poolId, canPlace
);
if (canPlace) {
return;
}
cobblemon$LOGGER.info(
"[HeightRangeVeto] Vetoing structure start at {} (startPool={})",
spawnPos, poolId
);
cir.setReturnValue(Optional.empty());
}
@Unique
private static boolean cobblemon$canStructurePlace(
Structure.GenerationContext generationContext,
Holder<StructureTemplatePool> startPool,
BlockPos startPos
) {
for (StructurePoolElement element : startPool.value().templates) {
Optional<HeightRangeStructureProcessor> heightRangeProcessor = cobblemon$getHeightRangeProcessor(element);
if (heightRangeProcessor.isEmpty()) {
continue;
}
HeightRangeStructureProcessor processor = heightRangeProcessor.get();
boolean valid = processor.isValidPlacement(
generationContext.chunkGenerator(),
generationContext.heightAccessor(),
generationContext.randomState(),
startPos
);
cobblemon$LOGGER.info(
"[HeightRangeVeto] template={} pos={} valid={}",
element, startPos, valid
);
if (!valid) {
return false;
}
}
return true;
}
@Unique
private static Optional<HeightRangeStructureProcessor> cobblemon$getHeightRangeProcessor(StructurePoolElement element) {
if (!(element instanceof SinglePoolElement)) {
return Optional.empty();
}
StructureProcessorList processorList = ((SinglePoolElementAccessor) element).getProcessors().value();
for (StructureProcessor processor : processorList.list()) {
if (processor instanceof HeightRangeStructureProcessor heightRangeStructureProcessor) {
return Optional.of(heightRangeStructureProcessor);
}
}
return Optional.empty();
}
}
@@ -8,8 +8,6 @@
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;
@@ -134,16 +132,6 @@ 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,
@@ -1,40 +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;
import com.cobblemon.mod.common.duck.MinecraftServerDuck;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.Level;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import java.util.Map;
import java.util.Optional;
@Mixin(MinecraftServer.class)
public class MinecraftServerMixin implements MinecraftServerDuck {
@Shadow
@Final
private Map<ResourceKey<Level>, ServerLevel> levels;
@Override
public Optional<ResourceKey<Level>> cobblemon$getLevelKey(final Level level) {
return this.levels.entrySet()
.stream()
// Reference equality is intended here, the level we're handed is the same one as in the
// map so we don't need to check value semantics.
.filter(entry -> entry.getValue() == level)
.map(Map.Entry::getKey)
.findFirst();
}
}
@@ -40,7 +40,6 @@ import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.List;
import java.util.LinkedHashMap;
import java.util.Set;
import java.util.function.Consumer;
@@ -115,13 +114,15 @@ public abstract class ServerEntityMixin {
if (currentPassengers.equals(cobblemon$lastSeatPassengers)) return;
cobblemon$lastSeatPassengers = List.copyOf(currentPassengers);
var assignments = new LinkedHashMap<String, Integer>();
pokemon.getOccupiedSeats().forEach((seat, passenger) -> {
if (seat.getLocator() == null) return;
if (passenger == null) return;
assignments.put(seat.getLocator(), passenger.getId());
cobblemon$broadcast(new ClientboundSeatAssignmentPacket(
passenger.getId(),
pokemon.getId(),
seat.getLocator()
));
});
cobblemon$broadcast(new ClientboundSeatAssignmentPacket(pokemon.getId(), assignments));
}
private void cobblemon$broadcast(NetworkPacket<?> packet) {
@@ -8,69 +8,22 @@
package com.cobblemon.mod.common.mixin;
import com.cobblemon.mod.common.duck.MinecraftServerDuck;
import com.cobblemon.mod.common.duck.ServerLevelDuck;
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.RegistryAccess;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.util.profiling.ProfilerFiller;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.dimension.DimensionType;
import net.minecraft.world.level.storage.WritableLevelData;
import net.minecraft.world.phys.AABB;
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.CallbackInfoReturnable;
import java.util.List;
import java.util.Optional;
import java.util.function.Supplier;
@Mixin(ServerLevel.class)
public abstract class ServerLevelMixin extends Level implements ServerLevelDuck {
// Ignored by Mixin, fulfils Java contract
protected ServerLevelMixin(
WritableLevelData levelData,
ResourceKey<Level> dimension,
RegistryAccess registryAccess,
Holder<DimensionType> dimensionTypeRegistration,
Supplier<ProfilerFiller> profiler,
boolean isClientSide,
boolean isDebug,
long biomeZoomSeed,
int maxChainedNeighborUpdates
) {
super(levelData, dimension, registryAccess, dimensionTypeRegistration, profiler, isClientSide, isDebug, biomeZoomSeed, maxChainedNeighborUpdates);
}
@Shadow
public abstract MinecraftServer shadow$getServer();
/**
* Cached to speed up level world key checks every tick.
*/
private ResourceLocation cobblemon$levelResourceLocation;
@Override
public ResourceLocation cobblemon$getLevelResourceLocation() {
if (this.cobblemon$levelResourceLocation == null) {
this.cobblemon$levelResourceLocation = ((MinecraftServerDuck) this.shadow$getServer())
.cobblemon$getLevelKey(this)
.map(ResourceKey::location)
.orElse(null);
}
return this.cobblemon$levelResourceLocation;
}
public abstract class ServerLevelMixin {
@Inject(method = "findLightningRod", at = @At(value = "RETURN"), cancellable = true)
private void cobblemon$findLightningRod(BlockPos blockPos, CallbackInfoReturnable<Optional<BlockPos>> ci) {
@@ -15,18 +15,15 @@ import com.cobblemon.mod.common.api.riding.RidingStyle;
import com.cobblemon.mod.common.api.spawning.spawner.PlayerSpawner;
import com.cobblemon.mod.common.api.spawning.spawner.PlayerSpawnerFactory;
import com.cobblemon.mod.common.api.stats.CobblemonStats;
import com.cobblemon.mod.common.duck.ServerLevelDuck;
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity;
import com.cobblemon.mod.common.world.gamerules.CobblemonGameRules;
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.llamalad7.mixinextras.sugar.Local;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
@@ -36,13 +33,9 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(ServerPlayer.class)
public abstract class ServerPlayerMixin implements PlayerSpawnerAccessor {
@Shadow
public abstract ServerLevel serverLevel();
@Shadow
@Final
public MinecraftServer server;
@Unique
public PlayerSpawner cobblemon$spawner;
@@ -62,9 +55,12 @@ public abstract class ServerPlayerMixin implements PlayerSpawnerAccessor {
@Inject(method = "tick", at = @At("TAIL"))
private void cobblemon$tickSpawner(CallbackInfo ci) {
if (cobblemon$canSpawn()) {
getPlayerSpawner().tick();
if (!Cobblemon.config.getEnableSpawning()) {
return;
} else if (!serverLevel().getGameRules().getBoolean(CobblemonGameRules.DO_POKEMON_SPAWNING)) {
return;
}
getPlayerSpawner().tick();
}
@Inject(method = "rideTick", at = @At("HEAD"))
@@ -125,21 +121,4 @@ public abstract class ServerPlayerMixin implements PlayerSpawnerAccessor {
return entity;
}
/**
* Gets whether we should spawn Cobblemon entities here.
*
* @return true if we can spawn cobblemon in the current level,
* false otherwise
*/
@SuppressWarnings("resource")
@Unique
private boolean cobblemon$canSpawn() {
return Cobblemon.config.getEnableSpawning() &&
this.serverLevel().getGameRules().getBoolean(CobblemonGameRules.DO_POKEMON_SPAWNING) &&
// intentionally last as this is a more expensive check, even with all the caching
!Cobblemon.config.getWorldSpawningBlocklist().contains(
((ServerLevelDuck) this.serverLevel()).cobblemon$getLevelResourceLocation()
);
}
}
@@ -1,103 +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;
import com.cobblemon.mod.common.mixin.accessor.SinglePoolElementAccessor;
import com.cobblemon.mod.common.world.structureprocessors.HeightRangeStructureProcessor;
import com.llamalad7.mixinextras.sugar.Local;
import net.minecraft.core.Holder;
import net.minecraft.core.RegistryAccess;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeSource;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.RandomState;
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.pools.SinglePoolElement;
import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessor;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessorList;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
import org.spongepowered.asm.mixin.Mixin;
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.CallbackInfoReturnable;
import java.util.Optional;
import java.util.function.Predicate;
@Mixin(Structure.class)
public abstract class StructureMixin {
/**
* Discards a structure start when any of its pieces carries a {@link HeightRangeStructureProcessor}
* whose range the piece does not fit in.
* Crab Note: We have to make this has to happen here rather than in the processor itself. Terrain adaptation and {@code /locate}
* both read the start, so by the time processors run the terrain has already been generated and the
* structure is already on the map which previously were left behdind when we were trying to make some structures not generate via processor. Now
* Returning {@link StructureStart#INVALID_START} keeps the start from ever existing, which makes sure that weird generation isn't left behind.
*/
@Inject(
method = "generate",
at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/levelgen/structure/StructureStart;isValid()Z"),
cancellable = true
)
private void cobblemon$rejectStartsOutsideHeightRange(
RegistryAccess registryAccess, ChunkGenerator chunkGenerator, BiomeSource biomeSource,
RandomState randomState, StructureTemplateManager structureTemplateManager, long seed,
ChunkPos chunkPos, int references, LevelHeightAccessor heightAccessor,
Predicate<Holder<Biome>> validBiome,
CallbackInfoReturnable<StructureStart> cir,
@Local StructureStart structureStart
) {
for (StructurePiece piece : structureStart.getPieces()) {
if (!(piece instanceof PoolElementStructurePiece poolPiece)) {
continue;
}
Optional<HeightRangeStructureProcessor> heightRangeProcessor = cobblemon$getHeightRangeProcessor(poolPiece.getElement());
if (heightRangeProcessor.isEmpty()) {
continue;
}
boolean valid = heightRangeProcessor.get().isValidPlacement(
chunkGenerator,
heightAccessor,
randomState,
piece.getBoundingBox()
);
if (!valid) {
cir.setReturnValue(StructureStart.INVALID_START);
return;
}
}
}
@Unique
private static Optional<HeightRangeStructureProcessor> cobblemon$getHeightRangeProcessor(StructurePoolElement element) {
if (!(element instanceof SinglePoolElement)) {
return Optional.empty();
}
StructureProcessorList processorList = ((SinglePoolElementAccessor) element).getProcessors().value();
for (StructureProcessor processor : processorList.list()) {
if (processor instanceof HeightRangeStructureProcessor heightRangeStructureProcessor) {
return Optional.of(heightRangeStructureProcessor);
}
}
return Optional.empty();
}
}
@@ -11,12 +11,9 @@ 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);
@@ -1,19 +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.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);
}
@@ -1,41 +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.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);
}
@@ -1,20 +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.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,13 +9,10 @@
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")
@@ -25,7 +22,4 @@ public interface TrackedEntityAccessor {
@Accessor("serverEntity")
ServerEntity getServerEntity();
@Invoker("updatePlayers")
void cobblemon$updatePlayers(List<ServerPlayer> players);
}
}
@@ -1,47 +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.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();
}
}
}
}
@@ -1,36 +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.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();
}
}
}
@@ -1,127 +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.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,12 +8,8 @@
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;
@@ -23,15 +19,12 @@ 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 {
@@ -39,7 +32,6 @@ 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;
@@ -51,7 +43,6 @@ 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;
@@ -95,29 +86,4 @@ 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);
}
}
@@ -1,178 +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.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,142 +9,43 @@
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 abstract class GameRendererMixin implements CameraFeedRenderState {
@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;
public class GameRendererMixin {
@Unique
private Camera cobblemon$playerCamera;
long lastTime = -1;
@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) {
@Inject(
method = "render",
at = @At(value = "TAIL")
)
public void render(DeltaTracker counter, boolean tick, CallbackInfo ci) {
Minecraft client = Minecraft.getInstance();
if (client.level == null) {
cobblemon$restoreCamera();
return;
}
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();
}
}
// 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();
// Don't play scheduled animations when the game is paused
if (client.isPaused()) {
cobblemon$lastTime = newTime;
lastTime = newTime;
return;
}
if (cobblemon$lastTime != -1) {
float delta = (newTime - cobblemon$lastTime) / 1000F;
ClientTaskTracker.INSTANCE.update(delta);
if (lastTime != -1) {
ClientTaskTracker.INSTANCE.update((newTime - lastTime) / 1000F);
}
cobblemon$lastTime = newTime;
lastTime = newTime;
}
@ModifyArg(
@@ -159,12 +60,4 @@ public abstract class GameRendererMixin implements CameraFeedRenderState {
return (entityX) -> !(entity.getVehicle() == entityX && entityX instanceof PokemonEntity) && original.test(entityX);
}
@Unique
private void cobblemon$restoreCamera() {
if (cobblemon$usingCinematic) {
mainCamera = cobblemon$playerCamera;
cobblemon$playerCamera = null;
cobblemon$usingCinematic = false;
}
}
}
@@ -9,7 +9,6 @@
package com.cobblemon.mod.common.mixin.client;
import com.cobblemon.mod.common.OrientationControllable;
import com.cobblemon.mod.common.client.camera.ClientCameraController;
import com.cobblemon.mod.common.item.PokedexItem;
import com.mojang.blaze3d.vertex.PoseStack;
import net.minecraft.client.Minecraft;
@@ -29,11 +28,6 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
public class ItemInHandRendererMixin {
@Inject(method = "renderArmWithItem", at = @At(value = "HEAD"), cancellable = true)
private void cobblemon$renderArmWithItem(AbstractClientPlayer abstractClientPlayer, float f, float g, InteractionHand interactionHand, float h, ItemStack itemStack, float i, PoseStack poseStack, MultiBufferSource multiBufferSource, int j, CallbackInfo ci) {
if (ClientCameraController.shouldUseCinematicCamera()) {
ci.cancel();
return;
}
if (abstractClientPlayer.isUsingItem() && abstractClientPlayer.getUseItem().getItem() instanceof PokedexItem) {
ci.cancel();
}
@@ -8,14 +8,11 @@
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;
@@ -34,34 +31,11 @@ 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) {
@@ -1,30 +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.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();
}
}
@@ -1,24 +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.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,8 +9,6 @@
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;
@@ -27,8 +25,6 @@ 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;
@@ -39,35 +35,11 @@ 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 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;
}
public abstract class EntityMixin {
@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) {
@@ -20,7 +20,7 @@ import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
// There appears to be a data driven way to do this now, however I am not sure how it works since there is no wiki or anything so yolo
//This is a yucky hack but better than what we were doing before
@Mixin(BlockVariantSets.class)
public abstract class AdornRegisterInvoker {
@Final
@@ -8,11 +8,6 @@
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
@@ -81,8 +76,6 @@ 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.*
@@ -266,9 +259,6 @@ 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)
@@ -311,7 +301,6 @@ object Cobblemon {
HeldItemProvider.register(CobblemonHeldItemManager, Priority.LOWEST)
}
fun sendServerSettingsPacketToPlayer(player: ServerPlayer) {
ServerSettingsPacket(
this.config.preventCompletePartyDeposit,
@@ -364,8 +353,6 @@ object Cobblemon {
CustomPokemonProperty.register(NoAIProperty)
CustomPokemonProperty.register(HeldItemVisibleProperty)
SceneBuiltinSources.register()
CobblemonEvents.POKEMON_PROPERTY_INITIALISED.emit(Unit)
CallbackHandler.setup()
@@ -462,14 +449,7 @@ 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,10 +109,4 @@ object CobblemonBlockEntities : PlatformRegistry<Registry<BlockEntityType<*>>, R
"poke_snack",
BlockEntityType.Builder.of(::PokeSnackBlockEntity, CobblemonBlocks.POKE_SNACK, CobblemonBlocks.POKE_CAKE).build(null)
)
@JvmField
val STADIUM_BLOCK: BlockEntityType<StadiumBlockEntity> = this.create(
"stadium_block",
BlockEntityType.Builder.of(::StadiumBlockEntity, CobblemonBlocks.STADIUM_BLOCK).build(null)
)
}
@@ -682,9 +682,6 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<
@JvmField
val HEALING_MACHINE = create("healing_machine", HealingMachineBlock(BlockBehaviour.Properties.of().mapColor(MapColor.METAL).sound(SoundType.METAL).pushReaction(PushReaction.BLOCK).strength(2F).noOcclusion().lightLevel { if (it.getValue(HealingMachineBlock.CHARGE_LEVEL) >= HealingMachineBlock.MAX_CHARGE_LEVEL) 12 else 5 }))
@JvmField
val STADIUM_BLOCK = create("stadium_block", StadiumBlock(BlockBehaviour.Properties.of().mapColor(MapColor.METAL).sound(SoundType.METAL).strength(3.5F, 6.0F).noOcclusion().pushReaction(PushReaction.BLOCK)))
@JvmField
val PC = create("pc", PCBlock(BlockBehaviour.Properties.of().mapColor(MapColor.METAL).sound(SoundType.METAL).pushReaction(PushReaction.BLOCK).strength(2F).noOcclusion().lightLevel { if ((it.getValue(PCBlock.ON) as Boolean) && (it.getValue(PCBlock.PART) == PCBlock.PCPart.TOP)) 13 else 0 }))
@@ -15,11 +15,7 @@ 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)
@@ -65,6 +61,7 @@ object CobblemonCommands {
ChangeBoxWallpaperCommand.register(dispatcher)
RunMolangCommand.register(dispatcher)
RunMolangScriptCommand.register(dispatcher)
SpectateBattleCommand.register(dispatcher)
CobblemonConfigCommand.register(dispatcher)
UnlockPCBoxWallpaperCommand.register(dispatcher)
BehaviourEditCommand.register(dispatcher)
@@ -80,8 +77,5 @@ 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,7 +13,6 @@ 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
@@ -82,12 +81,6 @@ 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)
@@ -106,4 +99,4 @@ object CobblemonEntities : PlatformRegistry<Registry<EntityType<*>>, ResourceKey
consumer(POKEMON, PokemonEntity.createAttributes())
consumer(NPC, NPCEntity.createAttributes())
}
}
}
@@ -38,7 +38,6 @@ 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
@@ -75,9 +74,6 @@ 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
@@ -469,8 +465,6 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
val PASTURE = blockItem("pasture", CobblemonBlocks.PASTURE)
@JvmField
val DISPLAY_CASE = blockItem("display_case", CobblemonBlocks.DISPLAY_CASE)
@JvmField
val STADIUM_BLOCK = blockItem("stadium_block", CobblemonBlocks.STADIUM_BLOCK)
// @JvmField
// val INCENSE_SWEET = blockItem("incense_sweet", CobblemonBlocks.INCENSE_SWEET) // TODO after 1.7
@@ -9,7 +9,6 @@
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
@@ -17,17 +16,15 @@ 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,20 +17,6 @@ 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
@@ -46,10 +32,6 @@ 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
@@ -71,16 +53,6 @@ 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
@@ -100,30 +72,17 @@ 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
@@ -149,10 +108,6 @@ 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
@@ -202,8 +157,6 @@ 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
@@ -223,15 +176,10 @@ 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
@@ -286,16 +234,11 @@ 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
@@ -444,7 +387,6 @@ 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))
@@ -461,37 +403,9 @@ 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(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))
list.add(PacketRegisterInfo(BattleChallengeExpiredPacket.ID, BattleChallengeExpiredPacket::decode, BattleChallengeExpiredHandler))
list.add(PacketRegisterInfo(BattleReplacePokemonPacket.ID, BattleReplacePokemonPacket::decode, BattleReplacePokemonHandler))
list.add(PacketRegisterInfo(BattleTransformPokemonPacket.ID, BattleTransformPokemonPacket::decode, BattleTransformPokemonHandler))
// MultiBattleTeam Packets
list.add(PacketRegisterInfo(TeamRequestNotificationPacket.ID, TeamRequestNotificationPacket::decode, TeamRequestNotificationHandler))
@@ -531,11 +445,6 @@ 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))
@@ -565,25 +474,8 @@ 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(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))
list.add(PacketRegisterInfo(PokemonUnpasturedPacket.ID, PokemonUnpasturedPacket::decode, PokemonUnpasturedHandler))
list.add(PacketRegisterInfo(UpdatePastureConflictFlagPacket.ID, UpdatePastureConflictFlagPacket::decode, UpdatePastureConflictFlagHandler))
// Orientation
list.add(PacketRegisterInfo(ClientboundUpdateOrientationPacket.ID, ClientboundUpdateOrientationPacket::decode, ClientboundUpdateOrientationHandler))
@@ -681,7 +573,6 @@ 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))
@@ -749,29 +640,8 @@ 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(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))
list.add(PacketRegisterInfo(SetEntityBehaviourPacket.ID, SetEntityBehaviourPacket::decode, SetEntityBehaviourHandler))
list.add(PacketRegisterInfo(DamageOnCollisionPacket.ID, DamageOnCollisionPacket::decode, DamageOnCollisionPacketHandler))
// Habitat blocks
list.add(PacketRegisterInfo(SaveHabitatBlockSettingsPacket.ID, SaveHabitatBlockSettingsPacket::decode, SaveHabitatBlockSettingsHandler))
@@ -779,10 +649,6 @@ 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
}
@@ -790,4 +656,4 @@ object CobblemonNetwork {
fun sendPacketToPlayer(player: ServerPlayer, packet: NetworkPacket<*>) {
Cobblemon.implementation.networkManager.sendPacketToPlayer(player, packet)
}
}
}
@@ -35,7 +35,6 @@ 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
@@ -81,18 +80,14 @@ import net.minecraft.world.item.ItemStack
open class PokemonBattle(
val format: BattleFormat,
val side1: BattleSide,
val side2: BattleSide,
initializeProgress: Boolean = true
val side2: BattleSide
) {
/** 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>()
@@ -101,12 +96,6 @@ 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>()
@@ -144,7 +133,7 @@ open class PokemonBattle(
set(value) { field = value.coerceAtMost(Int.MAX_VALUE) } // go outside
/** The current duration of the battle in seconds. */
val time: Int get() = ticks / 20
val time: Int get() = ticks % 20
var dispatchResult = GO
val dispatches = ConcurrentLinkedDeque<BattleDispatch>()
@@ -248,9 +237,7 @@ open class PokemonBattle(
}
fun turn(newTurnNumber: Int) {
// Prepare all actors before notifying synchronous AI, or its first response can clear other actors' requests.
actors.forEach { it.prepareTurn() }
actors.forEach { it.fireChoiceRequest() }
actors.forEach { it.turn() }
// 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()
@@ -263,7 +250,6 @@ open class PokemonBattle(
}
fun end() {
if (ended) return
ended = true
val awardToFainted = Cobblemon.config.awardExperienceToFaintedPokemon
val awardOnLoss = Cobblemon.config.awardExperienceOnBattleLoss
@@ -478,7 +464,7 @@ open class PokemonBattle(
val message = battleLang("crash").red()
this.actors.filterIsInstance<PlayerBattleActor>().forEach { it.entity?.sendSystemMessage(message) }
this.saveBattleLog()
if (stadiumSession != null) BattleRegistry.abortStadiumBattle(battleId) else this.stop()
this.stop()
return
}
@@ -486,7 +472,6 @@ open class PokemonBattle(
ticks++
if (isPvW && !ended && dispatches.isEmpty()) checkFlee()
}
stadiumSession?.tick()
}
open fun checkFlee() {
@@ -522,7 +507,6 @@ open class PokemonBattle(
}
fun stop() {
if (ended) return
writeShowdownAction(">forcetie") // This will terminate the Showdown connection
end()
}
@@ -599,4 +583,4 @@ open class PokemonBattle(
queryStruct.addFunction("has_rule") { params -> DoubleValue(params.getString(0) in format.ruleSet) }
return queryStruct
}
}
}
@@ -11,9 +11,8 @@ 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
@@ -50,40 +49,36 @@ 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 {
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
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.")
}
} catch (exception: IllegalActionChoiceException) {
LOGGER.error("AI was unable to choose an action, we're going to need to pass!")
exception.printStackTrace()
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 }
request?.let {
setActionResponses(it.iterate(this.activePokemon) { _, _ , _->
PassActionResponse
})
} ?: {
val response = mutableListOf<ShowdownActionResponse>()
repeat(activePokemon.size) {
response.add(PassActionResponse)
}
setActionResponses(response)
}
}
}
}
}
@@ -24,9 +24,8 @@ 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 net.minecraft.server.level.ServerPlayer
import net.minecraft.world.phys.Vec3
import java.util.UUID
import net.minecraft.server.level.ServerPlayer
abstract class BattleActor(
val uuid: UUID,
@@ -53,8 +52,6 @@ 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
@@ -74,33 +71,23 @@ abstract class BattleActor(
open fun isForPlayer(serverPlayerEntity: ServerPlayer) = serverPlayerEntity.uuid in getPlayerUUIDs()
open fun isForPokemon(pokemonEntity: PokemonEntity) = activePokemon.any { it.battlePokemon?.effectedPokemon?.entity == pokemonEntity }
/** 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
}
fun turn() {
val request = request ?: return
responses.clear()
if (activePokemon.any { it.isAlive() }) {
mustChoose = true
sendUpdate(BattleMakeChoicePacket())
}
val requestActive = req.active
if (req.wait || (requestActive.isNullOrEmpty() && req.forceSwitch.none { it })) {
val requestActive = request.active
if (requestActive == null || requestActive.isEmpty() || request.wait) {
this.request = null
expectingPassActions.clear()
mustChoose = false
} else if (req.forceSwitch.any { it }) {
mustChoose = true
return
}
}
/** Call only after every actor has been prepared for this turn. */
fun fireChoiceRequest() {
if (mustChoose) sendUpdate(BattleMakeChoicePacket())
}
fun postUpdate() {
val request = request ?: return
val forceSwitchPokemon = request.forceSwitch.mapIndexedNotNull { index, b -> if (b) activePokemon[index] else null }
@@ -175,4 +162,4 @@ abstract class BattleActor(
open fun win(otherWinners: List<BattleActor>, losers: List<BattleActor>) {}
open fun lose(winners: List<BattleActor>, otherLosers: List<BattleActor>) {}
}
}
@@ -1,15 +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.api.camera
/** Minecraft yaw and pitch, in degrees. */
data class CameraRotation(
val yaw: Float,
val pitch: Float
)
@@ -1,34 +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.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]
}
}
@@ -1,27 +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.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()
}
@@ -1,126 +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.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" }
}
}
}
}
@@ -1,21 +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.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}"
}
@@ -1,49 +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.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" }
}
}
}
@@ -1,28 +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.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()
}
@@ -1,27 +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.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"
}
}
}
}
@@ -1,33 +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.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" }
}
}
}
@@ -1,25 +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.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" }
}
}
@@ -1,13 +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.api.cinematic.model
import net.minecraft.network.chat.Component
class CinematicValidationException(val reason: Component) : IllegalArgumentException(reason.string)
@@ -1,41 +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.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" }
}
}
}
@@ -1,16 +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.api.cinematic.model
enum class DisconnectPolicy {
END_SCENE,
SUBSTITUTE,
HIDE_ACTOR,
FAIL
}
@@ -1,48 +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.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 }
}
@@ -1,23 +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.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
}
@@ -1,231 +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.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
}
}
@@ -1,17 +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.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" }
}
}
@@ -1,42 +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.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()
}
@@ -1,30 +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.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" }
}
}
@@ -1,34 +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.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
}
@@ -1,13 +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.api.cinematic.model.sources
abstract class FovSource {
abstract val type: String
}
@@ -1,23 +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.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"
}
}
}
@@ -1,13 +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.api.cinematic.model.sources
abstract class PositionSource {
abstract val type: String
}
@@ -1,13 +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.api.cinematic.model.sources
abstract class RollSource {
abstract val type: String
}
@@ -1,13 +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.api.cinematic.model.sources
abstract class RotationSource {
abstract val type: String
}
@@ -1,13 +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.api.cinematic.model.sources
abstract class ShotModifier {
abstract val type: String
}
@@ -1,43 +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.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
)
}
}
@@ -1,56 +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.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
)
}
}
}
@@ -1,50 +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.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
)
}
}
}
@@ -1,60 +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.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)
}
}
}
@@ -1,79 +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.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
)
}
}
}
@@ -1,25 +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.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)
}
}
@@ -1,29 +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.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))
}
}
}
@@ -1,25 +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.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)
}
}
@@ -1,31 +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.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
))
}
}
}
@@ -1,41 +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.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
)
}
}
}
@@ -1,53 +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.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)
}
}
}
@@ -1,65 +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.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)
}
}
}
@@ -1,53 +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.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)
}
}
}
@@ -1,57 +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.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)
}
}
}
@@ -1,47 +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.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)
)
}
}
@@ -1,149 +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.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)
}
}
}
@@ -1,43 +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.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
)
}
}
}
@@ -1,98 +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.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
)
}
}
}
@@ -1,44 +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.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
)
}
}
@@ -1,53 +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.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
)
}
}
@@ -1,36 +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.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
)
}
}
@@ -1,43 +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.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)
}
}
}
@@ -1,51 +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.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
)
}
}
}
@@ -1,58 +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.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)
}
}
}
@@ -1,15 +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.api.cinematic.model.tracks
abstract class ActorTrack {
abstract val trackType: String
abstract val duration: Float
}
@@ -1,37 +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.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)
}
}
}
@@ -1,38 +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.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
)
}
}
}
@@ -1,79 +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.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)
}
}
@@ -1,40 +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.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
)
}
}
}
@@ -1,69 +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.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)
}
}
}

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