Compare commits

..
Author SHA1 Message Date
MeAlam1 58cdf2e756 Added Debug Logging to hopefully find the issue 2026-08-06 18:26:43 +02:00
3701 changed files with 24247 additions and 168906 deletions

No files matched your search

+489 -16
View File
@@ -1,45 +1,518 @@
# Changelog
## [1.8.2 (MONTH xth, 2026)](#1-8-2)
## [1.8.0 (MONTH Xth, 2026)](#1-8-0)
### Additions
- The following items can now be placed on the ground as blocks:
- King's Rock, Galarica Cuff, Unremarkable Teacup, Masterpiece Teacup, Chipped Pot, Cracked Pot, Sachet, Metal Powder, Quick Powder, Bright Powder, Icy Rock, Heat Rock, Smooth Rock, Damp Rock, Destiny Knot, Luminous Moss, Never-Melt Ice, Wide Lens, Scope Lens, and Zoom Lens.
- Iron Ball, Metal Coat, Throat Spray, HP Up, Protein, Iron, Carbos, Calcium, Zinc, PP Up, PP Max, Upgrade, Dubious Disc, and Loaded Dice.
- The following items can now be placed on various surfaces:
- Galarica Wreath, Magnet, Flame Orb, Life Orb, Toxic Orb, Light Ball, and Smoke Ball.
- The following items can now be placed as blocks, and have additional functionality:
- Ring Target, Metronome, and Eject Button.
- The following items can now be worn by Players and Pokémon:
- Galarica Wreath, Cleanse Tag, Spell Tag, Sharp Beak, Covert Cloak, Shell Helmet, and Never-Melt Ice.
- Wearable items can now be equipped by a player by holding it and right-clicking (or rebound use/interact)
- Wearable items that can be placed down are exempt from this behaviour.
- 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
- Correctly grants "They're Minerals!" advancement when planting a type gem
- Type Gem Blocks now have advancements to give you the recipe in the recipe book (the data for the advancements were inside the recipes somehow??)
- Corrected the TM Recipes for Charge Beam and Payday
- 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
-41
View File
@@ -1,41 +0,0 @@
# Changelog
## [1.8.1 (September 13th, 2026)](#1-8-1)
### Animation updates for the following Pokémon
- Flapple, Appletun, Dipplin, Hydrapple (placeholder animations only).
### Fixes
- Fixed a potential world generation stall caused by Type Gem clusters.
- Fixed Pokémon on shoulders not animating if they are visibly holding a held item.
- Fixed NPCs getting their own TM progress file.
- Fixed odd NeoForge crash due to a dependency being outdated. It will now correctly flag the dependency for being outdated.
- Fixed the TM Machine refusing move selections that the `unlockAllMoveDexMovesByDefault` config had made visible, leaving the move slot blank and the craft button unresponsive.
- Fixed the Beast Ball not applying its 0.1× penalty to Pokémon that are not Ultra Beasts.
- Fixed the Moon Ball reading the world's total elapsed time instead of the time of day, so its bonus applied during the day instead.
- Fixed the Love Ball never awarding its 2.5× opposite gender bonus, and matching species by display name instead of identifier.
- Fixed the Timer Ball being weaker than a regular Poké Ball on early turns and only reaching 4× on turn 14 instead of turn 10.
- Fixed the Luxury Ball only doubling Friendship gains of 1, leaving level up and berry gains unaffected.
- Fixed Rufflet, Braviary, and Hisuian Braviary flooding logs with errors about missing sound events.
- Fixed Pokémon property typos in Pangoro and Dedenne herd spawn files.
- Fixed unintentional watermelon cosmetic for Hisuian Electrode.
- Fixed Blastoise having an unintentional passenger seat.
- Fixed form typo in Rockruff-Dusk spawn, preventing it from being able to spawn.
- Fixed the `stat_compare` and `stat_equal` evolution requirements crashing the game when given `special_attack` or `special_defence`, and made them treat any non-permanent stat as a failed requirement instead of crashing.
- Fixed Rowap and Jacoba berries not correctly altering Pokémon sizes when used as bait.
- Fixed Ampharos' eyes not emitting particles.
- Fixed Alolan Raticate's cry not playing.
- Fixed several incorrect light-based spawning conditions for ghosts and bugs.
- Removed unused, placeholder habitat pools.
- Fixed Mantyke, Mantine, and Quaxly getting stuck in a recoil 'expression' during battles due to having an expression assigned as a recoil animation.
- Fixed Lucario's aura particles not emitting.
- Fixed Lycanroc's head movement.
- Added head movement to Archaludon.
- Fixed Duraludon line not having their sleep animations.
- Fixed Diglett line not having their faint animations.
- Fixed Kingambit moving its legs while walking.
### Localization
- Updated translations for:
- Brazilian Portuguese
- Simplified Chinese
- Japanese
-2
View File
@@ -66,8 +66,6 @@ dependencies {
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")
@@ -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();
}
}
@@ -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();
}
}
@@ -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();
}
}
@@ -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
@@ -22,8 +22,6 @@ import com.cobblemon.mod.common.api.habitats.HabitatPools
import com.cobblemon.mod.common.api.interaction.RequestManager
import com.cobblemon.mod.common.api.molang.MoLangLoadedFilesCache
import com.cobblemon.mod.common.api.molang.ObjectValue
import com.cobblemon.mod.common.api.molangui.MolangUiActionRegistry
import com.cobblemon.mod.common.api.molangui.MolangUiService
import com.cobblemon.mod.common.api.permission.PermissionValidator
import com.cobblemon.mod.common.api.pokeball.catching.calculators.CaptureCalculator
import com.cobblemon.mod.common.api.pokeball.catching.calculators.CaptureCalculators
@@ -71,7 +69,6 @@ import com.cobblemon.mod.common.api.storage.player.adapter.TMMoveNbtBackend
import com.cobblemon.mod.common.api.storage.player.factory.CachedPlayerDataStoreFactory
import com.cobblemon.mod.common.api.tags.CobblemonEntityTypeTags
import com.cobblemon.mod.common.api.tags.CobblemonItemTags
import com.cobblemon.mod.common.api.vending.VendingMachinePageHandler
import com.cobblemon.mod.common.battles.BagItems
import com.cobblemon.mod.common.battles.BattleFormat
import com.cobblemon.mod.common.battles.BattleRegistry
@@ -262,7 +259,6 @@ object Cobblemon {
playerDataManager.onPlayerDisconnect(it.player)
RequestManager.onLogoff(it.player)
serverPlayerStructs.remove(it.player.uuid)
MolangUiService.closeAllForPlayer(it.player.uuid)
}
PlatformEvents.PLAYER_DEATH.subscribe {
PCLinkManager.removeLink(it.player.uuid)
@@ -325,11 +321,6 @@ object Cobblemon {
// Start up the data provider.
CobblemonDataProvider.registerDefaults()
MolangUiActionRegistry.register(
VendingMachinePageHandler.ACTION,
VendingMachinePageHandler
)
SHINY_ASPECT.register()
GENDER_ASPECT.register()
COSMETIC_SLOT_ASPECT.register()
@@ -459,7 +450,6 @@ object Cobblemon {
}
PlatformEvents.SERVER_STOPPED.subscribe {
MolangUiService.closeAll()
storage.unregisterAll(it.server.registryAccess())
playerDataManager.saveAllStores()
playerDataManager.saveExecutor.shutdown()
@@ -16,8 +16,6 @@ import com.cobblemon.mod.common.block.MintBlock.MintType
import com.cobblemon.mod.common.block.campfirepot.CampfireBlock
import com.cobblemon.mod.common.block.campfirepot.CampfirePotBlock
import com.cobblemon.mod.common.block.chest.GildedChestBlock
import com.cobblemon.mod.common.block.decor.ActivatableDecorationBlock
import com.cobblemon.mod.common.block.decor.RingTargetBlock
import com.cobblemon.mod.common.block.general.BaleBlock
import com.cobblemon.mod.common.block.general.HorizontalRotationCarpetBlock
import com.cobblemon.mod.common.block.general.HorizontalRotationalBlock
@@ -622,88 +620,6 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<
@JvmField
val WEAKNESS_POLICY = create("weakness_policy", WallAttachedDirectionalShapeBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_PAPER_LARGE_SOUNDS).mapColor(MapColor.NONE).noOcclusion().noCollission(), 14, 12, 1))
@JvmField
val RING_TARGET = create("ring_target", RingTargetBlock(BlockBehaviour.Properties.of().sound(SoundType.WOOD).mapColor(MapColor.NONE).noOcclusion().strength(0.125f)))
@JvmField
val MAGNET = create("magnet", MagnetBlock(BlockBehaviour.Properties.of().sound(SoundType.METAL).mapColor(MapColor.NONE).noOcclusion().strength(0.075f)))
@JvmField
val GALARICA_WREATH = create("galarica_wreath", GalaricaWreathBlock(BlockBehaviour.Properties.of().sound(SoundType.AZALEA_LEAVES).mapColor(MapColor.NONE).noOcclusion().noCollission()))
@JvmField
val FLAME_ORB = create("flame_orb", OrbBlock(BlockBehaviour.Properties.of().sound(SoundType.GLASS).mapColor(MapColor.NONE).noOcclusion().lightLevel{15}))
@JvmField
val TOXIC_ORB = create("toxic_orb", OrbBlock(BlockBehaviour.Properties.of().sound(SoundType.GLASS).mapColor(MapColor.NONE).noOcclusion()))
@JvmField
val LIFE_ORB = create("life_orb", OrbBlock(BlockBehaviour.Properties.of().sound(SoundType.GLASS).mapColor(MapColor.NONE).noOcclusion().lightLevel{7}))
@JvmField
val LIGHT_BALL = create("light_ball", OrbBlock(BlockBehaviour.Properties.of().sound(SoundType.GLASS).mapColor(MapColor.NONE).noOcclusion().lightLevel{13}))
@JvmField
val SMOKE_BALL = create("smoke_ball", OrbBlock(BlockBehaviour.Properties.of().sound(SoundType.GLASS).mapColor(MapColor.NONE).noOcclusion()))
@JvmField
val KINGS_ROCK = create("kings_rock", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.TUMBLESTONE_BLOCK_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.CENTERED_6x))
@JvmField
val GALARICA_CUFF = create("galarica_cuff", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.AZALEA_LEAVES).mapColor(MapColor.NONE).noCollission().noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.FLATTENED_10x4))
@JvmField
val UNREMARKABLE_TEACUP = create("unremarkable_teacup", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.DECORATED_POT).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.TEACUP))
@JvmField
val MASTERPIECE_TEACUP = create("masterpiece_teacup", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.DECORATED_POT).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.TEACUP))
@JvmField
val CHIPPED_POT = create("chipped_pot", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.DECORATED_POT).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.TEAPOT))
@JvmField
val CRACKED_POT = create("cracked_pot", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.DECORATED_POT).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.TEAPOT))
@JvmField
val SACHET = create("sachet", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.WOOL).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.POUCH))
@JvmField
val METAL_POWDER = create("metal_powder", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.WOOL).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.POUCH))
@JvmField
val QUICK_POWDER = create("quick_powder", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.WOOL).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.POUCH))
@JvmField
val BRIGHT_POWDER = create("bright_powder", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.WOOL).mapColor(MapColor.NONE).noOcclusion().lightLevel{3}, DecorativeItemBlock.DecorativeItemBlockType.POUCH))
@JvmField
val NEVER_MELT_ICE = create("never_melt_ice", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.GLASS).mapColor(MapColor.NONE).noOcclusion().friction(0.989F), DecorativeItemBlock.DecorativeItemBlockType.NEVERMELT))
@JvmField
val SCOPE_LENS = create("scope_lens", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.COPPER).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.SCOPE))
@JvmField
val WIDE_LENS = create("wide_lens", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.COPPER).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.WIDE))
@JvmField
val ZOOM_LENS = create("zoom_lens", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.COPPER).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.ZOOM))
@JvmField
val ICY_ROCK = create("icy_rock", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.DEEPSLATE).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.FLATTENED_8x4))
@JvmField
val HEAT_ROCK = create("heat_rock", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.DEEPSLATE).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.FLATTENED_8x4))
@JvmField
val SMOOTH_ROCK = create("smooth_rock", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.DEEPSLATE).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.FLATTENED_8x4))
@JvmField
val DAMP_ROCK = create("damp_rock", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.DEEPSLATE).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.FLATTENED_8x6))
@JvmField
val DESTINY_KNOT = create("destiny_knot", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.WOOL).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.KNOT))
@JvmField
val LUMINOUS_MOSS = create("luminous_moss", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.MOSS).mapColor(MapColor.NONE).noOcclusion().lightLevel{5}, DecorativeItemBlock.DecorativeItemBlockType.CENTERED_8x))
@JvmField
val METRONOME = create("metronome", ActivatableDecorationBlock(BlockBehaviour.Properties.of().sound(SoundType.METAL).mapColor(MapColor.NONE).pushReaction(PushReaction.DESTROY).noOcclusion(), ActivatableDecorationBlock.ActivatableDecorationBlockType.METRONOME))
@JvmField
val EJECT_BUTTON = create("eject_button", EjectButtonBlock(BlockBehaviour.Properties.of().sound(SoundType.METAL).mapColor(MapColor.NONE).pushReaction(PushReaction.DESTROY).noOcclusion().strength(0.125f), 40))
@JvmField
val LOADED_DICE = create("loaded_dice", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.DICE))
@JvmField
val IRON_BALL = create("iron_ball", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.METAL).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.IRON_BALL))
@JvmField
val METAL_COAT = create("metal_coat", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.METAL).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.METAL_COAT))
@JvmField
val UPGRADE = create("upgrade", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.UPGRADE))
@JvmField
val DUBIOUS_DISC = create("dubious_disc", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.UPGRADE))
@JvmField
val THROAT_SPRAY = create("throat_spray", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.THROAT_SPRAY))
@JvmField
val FULL_HEAL = create("full_heal", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.STATUS_RESTORE))
@JvmField
@@ -754,23 +670,6 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<
@JvmField
val X_SPEED = create("x_${Stats.SPEED.identifier.path}", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.BATTLE_ITEM))
@JvmField
val HP_UP = create("hp_up", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.VITAMIN))
@JvmField
val PROTEIN = create("protein", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.VITAMIN))
@JvmField
val IRON = create("iron", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.VITAMIN))
@JvmField
val CARBOS = create("carbos", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.VITAMIN))
@JvmField
val CALCIUM = create("calcium", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.VITAMIN))
@JvmField
val ZINC = create("zinc", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.VITAMIN))
@JvmField
val PP_UP = create("pp_up", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.VITAMIN))
@JvmField
val PP_MAX = create("pp_max", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.VITAMIN))
@JvmField
val MONITOR = create("monitor", MonitorBlock(BlockBehaviour.Properties.of().mapColor(MapColor.METAL).sound(SoundType.METAL).pushReaction(PushReaction.BLOCK).requiresCorrectToolForDrops().strength(5.0F, 6.0F).lightLevel { if (it.getValue(MonitorBlock.SCREEN) != MonitorBlock.MonitorScreen.OFF) 13 else 0 }))
@JvmField
@@ -808,9 +707,6 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<
@JvmField
val DISPLAY_CASE = create("display_case", DisplayCaseBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.DISPLAY_CASE_SOUNDS).noOcclusion().pushReaction(PushReaction.BLOCK).mapColor(MapColor.STONE).strength(0.3F).requiresCorrectToolForDrops()))
@JvmField
val VENDING_MACHINE = create("vending_machine", VendingMachineBlock(BlockBehaviour.Properties.of().mapColor(MapColor.METAL).sound(SoundType.METAL).pushReaction(PushReaction.BLOCK).strength(2F).requiresCorrectToolForDrops()))
// TODO after 1.7
// @JvmField
// val INCENSE_SWEET = create("incense_sweet", SweetIncenseBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.DISPLAY_CASE_SOUNDS).noOcclusion().pushReaction(PushReaction.BLOCK).mapColor(MapColor.STONE).strength(0.3F)))
@@ -77,6 +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)
MolangUiCommand.register(dispatcher)
}
}
@@ -465,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 VENDING_MACHINE = blockItem("vending_machine", CobblemonBlocks.VENDING_MACHINE)
// @JvmField
// val INCENSE_SWEET = blockItem("incense_sweet", CobblemonBlocks.INCENSE_SWEET) // TODO after 1.7
@@ -474,9 +472,9 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
// Evolution items
@JvmField val LINK_CABLE = create("link_cable", LinkCableItem())
@JvmField val DRAGON_SCALE = noSettingsItem("dragon_scale")
@JvmField val METAL_COAT = itemNameBlockItem("metal_coat", CobblemonBlocks.METAL_COAT)
@JvmField val UPGRADE = itemNameBlockItem("upgrade", CobblemonBlocks.UPGRADE)
@JvmField val DUBIOUS_DISC = itemNameBlockItem("dubious_disc", CobblemonBlocks.DUBIOUS_DISC)
@JvmField val METAL_COAT = noSettingsItem("metal_coat")
@JvmField val UPGRADE = noSettingsItem("upgrade")
@JvmField val DUBIOUS_DISC = noSettingsItem("dubious_disc")
@JvmField val DEEP_SEA_SCALE = noSettingsItem("deep_sea_scale")
@JvmField val DEEP_SEA_TOOTH = noSettingsItem("deep_sea_tooth")
@JvmField val ELECTIRIZER = noSettingsItem("electirizer")
@@ -485,7 +483,7 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField val PROTECTOR = noSettingsItem("protector")
@JvmField val REAPER_CLOTH = noSettingsItem("reaper_cloth")
@JvmField val PRISM_SCALE = noSettingsItem("prism_scale")
@JvmField val SACHET = itemNameBlockItem("sachet", CobblemonBlocks.SACHET)
@JvmField val SACHET = noSettingsItem("sachet")
@JvmField val WHIPPED_DREAM = create("whipped_dream", foodItem(8, 0.3f))
@JvmField val STRAWBERRY_SWEET = create("strawberry_sweet", foodItem(6, 0.125f))
@JvmField val LOVE_SWEET = create("love_sweet", foodItem(6, 0.125f))
@@ -494,20 +492,22 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField val FLOWER_SWEET = create("flower_sweet", foodItem(6, 0.125f))
@JvmField val STAR_SWEET = create("star_sweet", foodItem(6, 0.125f))
@JvmField val RIBBON_SWEET = create("ribbon_sweet", foodItem(6, 0.125f))
@JvmField val CHIPPED_POT = itemNameBlockItem("chipped_pot", CobblemonBlocks.CHIPPED_POT)
@JvmField val CRACKED_POT = itemNameBlockItem("cracked_pot", CobblemonBlocks.CRACKED_POT)
@JvmField val MASTERPIECE_TEACUP = itemNameBlockItem("masterpiece_teacup", CobblemonBlocks.MASTERPIECE_TEACUP)
@JvmField val UNREMARKABLE_TEACUP = itemNameBlockItem("unremarkable_teacup", CobblemonBlocks.UNREMARKABLE_TEACUP)
@JvmField val CHIPPED_POT = noSettingsItem("chipped_pot")
@JvmField val CRACKED_POT = noSettingsItem("cracked_pot")
@JvmField val MASTERPIECE_TEACUP = noSettingsItem("masterpiece_teacup")
@JvmField val UNREMARKABLE_TEACUP = noSettingsItem("unremarkable_teacup")
@JvmField val SWEET_APPLE = compostableItem("sweet_apple", foodItem(4, 0.3f), 0.65f)
@JvmField val TART_APPLE = compostableItem("tart_apple", foodItem(4, 0.3f), 0.65f)
@JvmField val SYRUPY_APPLE = compostableItem("syrupy_apple", foodItem(4, 0.3f), 0.65f)
@JvmField val GALARICA_CUFF = itemNameBlockItem("galarica_cuff", CobblemonBlocks.GALARICA_CUFF)
@JvmField val GALARICA_CUFF = noSettingsItem("galarica_cuff")
@JvmField val GALARICA_WREATH = noSettingsItem("galarica_wreath")
@JvmField val BLACK_AUGURITE = noSettingsItem("black_augurite")
@JvmField val PEAT_BLOCK = noSettingsItem("peat_block")
@JvmField val RAZOR_CLAW = noSettingsItem("razor_claw")
@JvmField val RAZOR_FANG = noSettingsItem("razor_fang")
@JvmField val AUSPICIOUS_ARMOR = heldItem("auspicious_armor")
@JvmField val MALICIOUS_ARMOR = heldItem("malicious_armor")
@JvmField val SHELL_HELMET = heldItem("shell_helmet")
@JvmField val METAL_ALLOY = noSettingsItem("metal_alloy")
@JvmField val SCROLL_OF_DARKNESS = noSettingsItem("scroll_of_darkness")
@JvmField val SCROLL_OF_WATERS = noSettingsItem("scroll_of_waters")
@@ -651,17 +651,17 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val EXPERIENCE_CANDY_XL = candyItem("exp_candy_xl") { _, _ -> CandyItem.DEFAULT_XL_CANDY_YIELD }
@JvmField
val CALCIUM = create("calcium", VitaminItem(Stats.SPECIAL_ATTACK, CobblemonBlocks.CALCIUM))
val CALCIUM = create("calcium", VitaminItem(Stats.SPECIAL_ATTACK))
@JvmField
val CARBOS = create("carbos", VitaminItem(Stats.SPEED, CobblemonBlocks.CARBOS))
val CARBOS = create("carbos", VitaminItem(Stats.SPEED))
@JvmField
val HP_UP = create("hp_up", VitaminItem(Stats.HP, CobblemonBlocks.HP_UP))
val HP_UP = create("hp_up", VitaminItem(Stats.HP))
@JvmField
val IRON = create("iron", VitaminItem(Stats.DEFENCE, CobblemonBlocks.IRON))
val IRON = create("iron", VitaminItem(Stats.DEFENCE))
@JvmField
val PROTEIN = create("protein", VitaminItem(Stats.ATTACK, CobblemonBlocks.PROTEIN))
val PROTEIN = create("protein", VitaminItem(Stats.ATTACK))
@JvmField
val ZINC = create("zinc", VitaminItem(Stats.SPECIAL_DEFENCE, CobblemonBlocks.ZINC))
val ZINC = create("zinc", VitaminItem(Stats.SPECIAL_DEFENCE))
@JvmField
val HEALTH_MOCHI = create("health_mochi", MochiItem(Stats.HP))
@JvmField
@@ -743,9 +743,9 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val MAX_REVIVE = create("max_revive", ReviveItem(max = true))
@JvmField
val PP_UP = create("pp_up", PPUpItem(1, CobblemonBlocks.PP_UP))
val PP_UP = create("pp_up", PPUpItem(1))
@JvmField
val PP_MAX = create("pp_max", PPUpItem(3, CobblemonBlocks.PP_MAX))
val PP_MAX = create("pp_max", PPUpItem(3))
@JvmField
val RED_MINT_SEEDS = mintSeed("red", MintType.RED.getCropBlock())
@@ -922,18 +922,11 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField val CHOICE_SPECS = wearableItem("choice_specs")
@JvmField val EXP_SHARE = wearableItem("exp_share")
@JvmField val FOCUS_BAND = wearableItem("focus_band")
@JvmField val KINGS_ROCK = wearableBlockItem("kings_rock", CobblemonBlocks.KINGS_ROCK)
@JvmField val NEVER_MELT_ICE = wearableBlockItem("never_melt_ice", CobblemonBlocks.NEVER_MELT_ICE)
@JvmField val KINGS_ROCK = wearableItem("kings_rock")
@JvmField val MUSCLE_BAND = wearableItem("muscle_band")
@JvmField val ROCKY_HELMET = wearableItem("rocky_helmet")
@JvmField val SAFETY_GOGGLES = wearableItem("safety_goggles")
@JvmField val WISE_GLASSES = wearableItem("wise_glasses")
@JvmField val SHARP_BEAK = wearableItem("sharp_beak")
@JvmField val COVERT_CLOAK = wearableItem("covert_cloak")
@JvmField val SHELL_HELMET = wearableItem("shell_helmet")
@JvmField val GALARICA_WREATH = wearableBlockItem("galarica_wreath", CobblemonBlocks.GALARICA_WREATH)
@JvmField val CLEANSE_TAG = wearableBlockItem("cleanse_tag", CobblemonBlocks.CLEANSE_TAG)
@JvmField val SPELL_TAG = wearableBlockItem("spell_tag", CobblemonBlocks.SPELL_TAG)
// Held Items
@JvmField
@@ -961,13 +954,17 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val CHOICE_SCARF = heldItem("choice_scarf")
@JvmField
val CLEANSE_TAG = itemNameBlockItem("cleanse_tag", CobblemonBlocks.CLEANSE_TAG)
@JvmField
val CLEAR_AMULET = heldItem("clear_amulet")
@JvmField
val DESTINY_KNOT = itemNameBlockItem("destiny_knot", CobblemonBlocks.DESTINY_KNOT)
val COVERT_CLOAK = heldItem("covert_cloak")
@JvmField
val DESTINY_KNOT = heldItem("destiny_knot")
@JvmField
val DRAGON_FANG = heldItem("dragon_fang")
@JvmField
val EJECT_BUTTON = itemNameBlockItem("eject_button", CobblemonBlocks.EJECT_BUTTON)
val EJECT_BUTTON = heldItem("eject_button")
@JvmField
val EJECT_PACK = heldItem("eject_pack")
@JvmField
@@ -981,7 +978,7 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val FAIRY_FEATHER = heldItem("fairy_feather")
@JvmField
val FLAME_ORB = itemNameBlockItem("flame_orb", CobblemonBlocks.FLAME_ORB)
val FLAME_ORB = heldItem("flame_orb")
@JvmField
val FLOAT_STONE = heldItem("float_stone")
@JvmField
@@ -995,32 +992,34 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val HEAVY_DUTY_BOOTS = heldItem("heavy_duty_boots")
@JvmField
val IRON_BALL = itemNameBlockItem("iron_ball", CobblemonBlocks.IRON_BALL)
val IRON_BALL = heldItem("iron_ball")
@JvmField
val LAGGING_TAIL = heldItem("lagging_tail")
@JvmField
val LEFTOVERS = compostableHeldItem("leftovers", null, 0.50f)
@JvmField
val LIFE_ORB = itemNameBlockItem("life_orb", CobblemonBlocks.LIFE_ORB)
val LIFE_ORB = heldItem("life_orb")
@JvmField
val LIGHT_BALL = itemNameBlockItem("light_ball", CobblemonBlocks.LIGHT_BALL)
val LIGHT_BALL = heldItem("light_ball")
@JvmField
val LIGHT_CLAY = heldItem("light_clay")
@JvmField
val LOADED_DICE = itemNameBlockItem("loaded_dice", CobblemonBlocks.LOADED_DICE)
val LOADED_DICE = heldItem("loaded_dice")
@JvmField
val LUCKY_EGG = heldItem("lucky_egg")
@JvmField
val LUMINOUS_MOSS = itemNameBlockItem("luminous_moss", CobblemonBlocks.LUMINOUS_MOSS)
val LUMINOUS_MOSS = heldItem("luminous_moss")
@JvmField
val MAGNET = itemNameBlockItem("magnet", CobblemonBlocks.MAGNET)
val MAGNET = heldItem("magnet")
@JvmField
val METRONOME = itemNameBlockItem("metronome", CobblemonBlocks.METRONOME)
val METRONOME = heldItem("metronome")
@JvmField
val MIRACLE_SEED = heldItem("miracle_seed")
@JvmField
val MYSTIC_WATER = heldItem("mystic_water")
@JvmField
val NEVER_MELT_ICE = heldItem("never_melt_ice")
@JvmField
val POISON_BARB = heldItem("poison_barb")
@JvmField
val POWER_ANKLET = heldItem("power_anklet")
@@ -1045,11 +1044,13 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val RED_CARD = heldItem("red_card")
@JvmField
val RING_TARGET = itemNameBlockItem("ring_target", CobblemonBlocks.RING_TARGET)
val RING_TARGET = heldItem("ring_target")
@JvmField
val ROOM_SERVICE = heldItem("room_service")
@JvmField
val SCOPE_LENS = itemNameBlockItem("scope_lens", CobblemonBlocks.SCOPE_LENS)
val SCOPE_LENS = heldItem("scope_lens")
@JvmField
val SHARP_BEAK = heldItem("sharp_beak")
@JvmField
val SHED_SHELL = heldItem("shed_shell")
@JvmField
@@ -1061,7 +1062,9 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val SOFT_SAND = heldItem("soft_sand")
@JvmField
val SMOKE_BALL = itemNameBlockItem("smoke_ball", CobblemonBlocks.SMOKE_BALL)
val SPELL_TAG = itemNameBlockItem("spell_tag", CobblemonBlocks.SPELL_TAG)
@JvmField
val SMOKE_BALL = heldItem("smoke_ball")
@JvmField
val SOOTHE_BELL = heldItem("soothe_bell")
@JvmField
@@ -1069,9 +1072,9 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val TERRAIN_EXTENDER = heldItem("terrain_extender")
@JvmField
val THROAT_SPRAY = itemNameBlockItem("throat_spray", CobblemonBlocks.THROAT_SPRAY)
val THROAT_SPRAY = heldItem("throat_spray")
@JvmField
val TOXIC_ORB = itemNameBlockItem("toxic_orb", CobblemonBlocks.TOXIC_ORB)
val TOXIC_ORB = heldItem("toxic_orb")
@JvmField
val TWISTED_SPOON = heldItem("twisted_spoon")
@JvmField
@@ -1079,9 +1082,9 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val WEAKNESS_POLICY = itemNameBlockItem("weakness_policy", CobblemonBlocks.WEAKNESS_POLICY)
@JvmField
val WIDE_LENS = itemNameBlockItem("wide_lens", CobblemonBlocks.WIDE_LENS)
val WIDE_LENS = heldItem("wide_lens")
@JvmField
val ZOOM_LENS = itemNameBlockItem("zoom_lens", CobblemonBlocks.ZOOM_LENS)
val ZOOM_LENS = heldItem("zoom_lens")
@JvmField
val MENTAL_HERB = compostableHeldItem("mental_herb", null, 0.85F)
@JvmField
@@ -1093,19 +1096,19 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val WHITE_HERB = compostableHeldItem("white_herb", null, 0.85F)
@JvmField
val BRIGHT_POWDER = itemNameBlockItem("bright_powder", CobblemonBlocks.BRIGHT_POWDER)
val BRIGHT_POWDER = heldItem("bright_powder")
@JvmField
val METAL_POWDER = itemNameBlockItem("metal_powder", CobblemonBlocks.METAL_POWDER)
val METAL_POWDER = heldItem("metal_powder")
@JvmField
val QUICK_POWDER = itemNameBlockItem("quick_powder", CobblemonBlocks.QUICK_POWDER)
val QUICK_POWDER = heldItem("quick_powder")
@JvmField
val DAMP_ROCK = itemNameBlockItem("damp_rock", CobblemonBlocks.DAMP_ROCK)
val DAMP_ROCK = heldItem("damp_rock")
@JvmField
val HEAT_ROCK = itemNameBlockItem("heat_rock", CobblemonBlocks.HEAT_ROCK)
val HEAT_ROCK = heldItem("heat_rock")
@JvmField
val SMOOTH_ROCK = itemNameBlockItem("smooth_rock", CobblemonBlocks.SMOOTH_ROCK)
val SMOOTH_ROCK = heldItem("smooth_rock")
@JvmField
val ICY_ROCK = itemNameBlockItem("icy_rock", CobblemonBlocks.ICY_ROCK)
val ICY_ROCK = heldItem("icy_rock")
// Mulch
@JvmField
@@ -1596,16 +1599,7 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
private fun wearableItem(name: String, heldItemRemappedName: String? = null): CobblemonItem = create(
name,
WearableHatItem(name).also {
wearables.add(it)
if (heldItemRemappedName != null) {
CobblemonHeldItemManager.registerRemap(it, heldItemRemappedName)
}
}
)
private fun wearableBlockItem(name: String, block: Block, heldItemRemappedName: String? = null): WearableBlockItem = create(
name,
WearableBlockItem(name, block).also {
WearableItem(name).also {
wearables.add(it)
if (heldItemRemappedName != null) {
CobblemonHeldItemManager.registerRemap(it, heldItemRemappedName)
@@ -10,7 +10,6 @@ package com.cobblemon.mod.common
import com.cobblemon.mod.common.block.campfirepot.CookingPotMenu
import com.cobblemon.mod.common.api.molangui.container.MolangContainerMenu
import com.cobblemon.mod.common.block.tmmachine.TMMachineMenu
import com.cobblemon.mod.common.platform.PlatformRegistry
import net.minecraft.core.Registry
@@ -23,10 +22,9 @@ object CobblemonMenuType : PlatformRegistry<Registry<MenuType<*>>, ResourceKey<R
val COOKING_POT = MenuType.register("cooking_pot_menu", ::CookingPotMenu)
val TM_MACHINE = MenuType.register("tm_machine_menu", ::TMMachineMenu)
val MOLANG_UI = MenuType.register("molang_ui", ::MolangContainerMenu)
override val registry: Registry<MenuType<*>>
get() = BuiltInRegistries.MENU
override val resourceKey: ResourceKey<Registry<MenuType<*>>>
get() = Registries.MENU
}
}
@@ -32,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
@@ -87,10 +83,6 @@ import com.cobblemon.mod.common.net.messages.client.callback.OpenPartyMoveCallba
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
@@ -188,9 +180,6 @@ import com.cobblemon.mod.common.net.messages.server.dialogue.EscapeDialoguePacke
import com.cobblemon.mod.common.net.messages.server.dialogue.InputToDialoguePacket
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.MolangUiActionPacket
import com.cobblemon.mod.common.net.messages.server.molangui.MolangUiClosedPacket
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
@@ -250,9 +239,6 @@ import com.cobblemon.mod.common.net.serverhandling.dialogue.InputToDialogueHandl
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.MolangUiActionHandler
import com.cobblemon.mod.common.net.serverhandling.molangui.MolangUiClosedHandler
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
@@ -459,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))
@@ -662,10 +643,6 @@ object CobblemonNetwork {
list.add(PacketRegisterInfo(SetEntityBehaviourPacket.ID, SetEntityBehaviourPacket::decode, SetEntityBehaviourHandler))
list.add(PacketRegisterInfo(DamageOnCollisionPacket.ID, DamageOnCollisionPacket::decode, DamageOnCollisionPacketHandler))
list.add(PacketRegisterInfo(MolangUiActionPacket.ID, MolangUiActionPacket::decode, MolangUiActionHandler))
list.add(PacketRegisterInfo(MolangUiClosedPacket.ID, MolangUiClosedPacket::decode, MolangUiClosedHandler))
list.add(PacketRegisterInfo(MolangUiCompatibilityResponsePacket.ID, MolangUiCompatibilityResponsePacket::decode, MolangUiCompatibilityResponseHandler))
// Habitat blocks
list.add(PacketRegisterInfo(SaveHabitatBlockSettingsPacket.ID, SaveHabitatBlockSettingsPacket::decode, SaveHabitatBlockSettingsHandler))
@@ -679,4 +656,4 @@ object CobblemonNetwork {
fun sendPacketToPlayer(player: ServerPlayer, packet: NetworkPacket<*>) {
Cobblemon.implementation.networkManager.sendPacketToPlayer(player, packet)
}
}
}
@@ -133,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>()
@@ -21,8 +21,7 @@ import com.cobblemon.mod.common.pokemon.Pokemon
*/
data class FriendshipUpdatedEvent(
val pokemon: Pokemon,
val newFriendshipInitial: Int,
val earned: Boolean = false
val newFriendshipInitial: Int
) {
var newFriendship: Int = newFriendshipInitial
set(value) {
@@ -65,7 +65,7 @@ class HabitatSpawn {
/** The maximum light level that the Pokémon can spawn in. */
var maxLight: Int? = null
fun writeToNBT(nbt: CompoundTag) {
open fun writeToNBT(nbt: CompoundTag) {
nbt.putString(DataKeys.HABITAT_POOL_SPAWN_SPECIES, species.resourceIdentifier.toString())
nbt.putString(DataKeys.HABITAT_POOL_SPAWN_POSITION_TYPE, spawnablePositionType)
nbt.putFloat(DataKeys.HABITAT_POOL_SPAWN_WEIGHT, weight)
@@ -78,7 +78,7 @@ class HabitatSpawn {
nbt.putString(DataKeys.HABITAT_POOL_SPAWN_BUCKET, bucket)
}
fun readFromNBT(nbt: CompoundTag) {
open fun readFromNBT(nbt: CompoundTag) {
species = PokemonSpecies.getByIdentifier(nbt.getString(DataKeys.HABITAT_POOL_SPAWN_SPECIES).asIdentifierDefaultingNamespace())!!
spawnablePositionType = nbt.getString(DataKeys.HABITAT_POOL_SPAWN_POSITION_TYPE)
weight = nbt.getFloat(DataKeys.HABITAT_POOL_SPAWN_WEIGHT)
@@ -91,7 +91,7 @@ class HabitatSpawn {
bucket = if (nbt.contains(DataKeys.HABITAT_POOL_SPAWN_BUCKET)) nbt.getString(DataKeys.HABITAT_POOL_SPAWN_BUCKET) else "common"
}
fun encode(buffer: RegistryFriendlyByteBuf) {
open fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeResourceLocation(species.resourceIdentifier)
buffer.writeUtf(spawnablePositionType)
buffer.writeFloat(weight)
@@ -104,7 +104,7 @@ class HabitatSpawn {
buffer.writeUtf(bucket)
}
fun decode(buffer: RegistryFriendlyByteBuf) {
open fun decode(buffer: RegistryFriendlyByteBuf) {
species = PokemonSpecies.getByIdentifier(buffer.readResourceLocation())!!
spawnablePositionType = buffer.readUtf()
weight = buffer.readFloat()
@@ -118,7 +118,7 @@ class HabitatSpawn {
}
/** Creates the [PokemonSpawnDetail] that will be used in the spawner. This is done per habitat block. */
fun createSpawnDetail(habitatBlockEntity: HabitatBlockEntity): PokemonSpawnDetail? {
open fun createSpawnDetail(habitatBlockEntity: HabitatBlockEntity): PokemonSpawnDetail? {
val spawnDetail = PokemonSpawnDetail()
// If the habitat block specifies a level range, it restricts how and if this spawn exists
@@ -83,6 +83,12 @@ import net.minecraft.world.level.Level
import net.minecraft.world.level.biome.Biome
import net.minecraft.world.level.block.Block
import net.minecraft.world.level.dimension.DimensionType
import net.minecraft.world.level.levelgen.Heightmap
import net.minecraft.world.level.pathfinder.PathType
import net.minecraft.world.level.storage.loot.LootParams
import net.minecraft.world.level.storage.loot.parameters.LootContextParamSets
import net.minecraft.world.level.storage.loot.parameters.LootContextParams
import net.minecraft.world.phys.AABB
import net.minecraft.world.phys.Vec3
import java.util.function.Function
@@ -93,31 +99,25 @@ import java.util.function.Function
* @since October 2nd, 2023
*/
object MoLangFunctions {
fun ItemStack.asMoLangValue(registryAccess: RegistryAccess) =
ObjectValue(this)
.addStandardFunctions()
.addFunctions(ItemStackMoLangFunctions.attach(Pair(this, registryAccess)))
fun ItemStack.asMoLangValue(registryAccess: RegistryAccess) = ObjectValue(this)
.addStandardFunctions()
.addFunctions(ItemStackMoLangFunctions.attach(Pair(this, registryAccess)))
fun Holder<Biome>.asBiomeMoLangValue() =
asMoLangValue(Registries.BIOME)
.addFunctions(BiomeMoLangFunctions.attach(this))
fun Holder<Biome>.asBiomeMoLangValue() = asMoLangValue(Registries.BIOME)
.addFunctions(BiomeMoLangFunctions.attach(this))
fun Holder<Level>.asWorldMoLangValue() =
asMoLangValue(Registries.DIMENSION)
.addFunctions(WorldMoLangFunctions.attach(this))
fun Holder<Level>.asWorldMoLangValue() = asMoLangValue(Registries.DIMENSION)
.addFunctions(WorldMoLangFunctions.attach(this))
fun Holder<Block>.asBlockMoLangValue() =
asMoLangValue(Registries.BLOCK)
.addFunctions(BlockMoLangFunctions.attach(this))
fun Holder<Block>.asBlockMoLangValue() = asMoLangValue(Registries.BLOCK)
.addFunctions(BlockMoLangFunctions.attach(this))
fun MinecraftServer.asMoLangValue() =
ObjectValue(this)
.addStandardFunctions()
.addFunctions(ServerMoLangFunctions.attach(this))
fun MinecraftServer.asMoLangValue() = ObjectValue(this)
.addStandardFunctions()
.addFunctions(ServerMoLangFunctions.attach(this))
fun Holder<DimensionType>.asDimensionTypeMoLangValue() =
asMoLangValue(Registries.DIMENSION_TYPE)
.addFunctions(DimensionTypeMoLangFunctions.attach(this))
fun Holder<DimensionType>.asDimensionTypeMoLangValue() = asMoLangValue(Registries.DIMENSION_TYPE)
.addFunctions(DimensionTypeMoLangFunctions.attach(this))
fun Player.asMoLangValue(): ObjectValue<Player> {
if (this is ServerPlayer && uuid in Cobblemon.serverPlayerStructs) {
@@ -129,11 +129,10 @@ object MoLangFunctions {
}
}
val value =
ObjectValue(
obj = this,
stringify = { it.effectiveName().string },
)
val value = ObjectValue(
obj = this,
stringify = { it.effectiveName().string }
)
value.addFunctions(EntityMoLangFunctions.attach(this))
value.addFunctions(LivingEntityMoLangFunctions.attach(this))
@@ -154,63 +153,57 @@ object MoLangFunctions {
}
fun PartyStore.asMoLangValue(): ObjectValue<PartyStore> {
val value =
ObjectValue(
obj = this,
stringify = { it.toString() },
)
val value = ObjectValue(
obj = this,
stringify = { it.toString() }
)
value.addFunctions(PokemonStoreMoLangFunctions.attach(this))
value.addFunctions(PartyMoLangFunctions.attach(this))
return value
}
fun DropEntry.asMoLangValue(): ObjectValue<DropEntry> {
val value =
ObjectValue(
obj = this,
stringify = { it.toString() },
)
val value = ObjectValue(
obj = this,
stringify = { it.toString() }
)
value.addFunctions(DropEntryMoLangFunctions.attach(this))
return value
}
fun PokemonProperties.asMoLangValue(): ObjectValue<PokemonProperties> {
val value =
ObjectValue(
obj = this,
stringify = { it.asString() },
)
val value = ObjectValue(
obj = this,
stringify = { it.asString() }
)
value.addFunctions(PokemonPropertiesMoLangFunctions.attach(this))
return value
}
fun Evolution.asMoLangValue(): ObjectValue<Evolution> {
val value =
ObjectValue(
obj = this,
stringify = { it.toString() },
)
val value = ObjectValue(
obj = this,
stringify = { it.toString() }
)
value.addFunctions(EvolutionMoLangFunctions.attach(this))
return value
}
fun PCStore.asMoLangValue(): ObjectValue<PCStore> {
val value =
ObjectValue(
obj = this,
stringify = { it.toString() },
)
val value = ObjectValue(
obj = this,
stringify = { it.toString() }
)
value.addFunctions(PokemonStoreMoLangFunctions.attach(this))
value.addFunctions(PCMoLangFunctions.attach(this))
return value
}
fun NPCEntity.asMoLangValue(): ObjectValue<NPCEntity> {
val value =
ObjectValue(
obj = this,
stringify = { it.name.string },
)
val value = ObjectValue(
obj = this,
stringify = { it.name.string }
)
value.addFunctions(EntityMoLangFunctions.attach(this))
value.addFunctions(LivingEntityMoLangFunctions.attach(this))
value.addFunctions(NPCMoLangFunctions.attach(this))
@@ -218,11 +211,10 @@ object MoLangFunctions {
}
fun PokemonEntity.asMoLangValue(): ObjectValue<PokemonEntity> {
val value =
ObjectValue(
obj = this,
stringify = { it.pokemon.uuid.toString() },
)
val value = ObjectValue(
obj = this,
stringify = { it.pokemon.uuid.toString() }
)
value.addFunctions(EntityMoLangFunctions.attach(this))
value.addFunctions(LivingEntityMoLangFunctions.attach(this))
@@ -233,40 +225,36 @@ object MoLangFunctions {
}
fun PokemonBattle.asMoLangValue(): ObjectValue<PokemonBattle> {
val value =
ObjectValue(
obj = this,
stringify = { it.battleId.toString() },
)
val value = ObjectValue(
obj = this,
stringify = { it.battleId.toString() }
)
value.addFunctions(BattleMoLangFunctions.attach(this))
return value
}
fun BattleActor.asMoLangValue(): ObjectValue<BattleActor> {
val value =
ObjectValue(
obj = this,
stringify = { it.toString() },
)
val value = ObjectValue(
obj = this,
stringify = { it.toString() }
)
value.addFunctions(BattleActorMoLangFunctions.attach(this))
return value
}
fun SpawnablePosition.asMoLangValue(): ObjectValue<SpawnablePosition> {
val value =
ObjectValue(
obj = this,
stringify = { it.toString() },
)
val value = ObjectValue(
obj = this,
stringify = { it.toString() }
)
value.addFunctions(SpawnablePositionMoLangFunctions.attach(this))
return value
}
fun Vec3.asMoLangValue(): ObjectValue<Vec3> {
val value =
ObjectValue(
obj = this,
)
val value = ObjectValue(
obj = this
)
value.addFunction("x") { this.x }
value.addFunction("y") { this.y }
value.addFunction("z") { this.z }
@@ -277,66 +265,45 @@ object MoLangFunctions {
/**
* Different functions exist depending on the entity type, this tries to make the struct that's most specific to the type.
*/
fun Entity.asMostSpecificMoLangValue(): ObjectValue<out Entity> =
when (this) {
is Player -> {
asMoLangValue()
fun Entity.asMostSpecificMoLangValue(): ObjectValue<out Entity> {
return when (this) {
is Player -> asMoLangValue()
is PokemonEntity -> struct
is NPCEntity -> struct
is ItemEntity -> ObjectValue(this).also { item ->
item.addStandardFunctions()
item.addFunctions(EntityMoLangFunctions.attach(this))
item.addFunctions(ItemStackMoLangFunctions.attach(Pair(this.item, this.registryAccess())))
}
is PokemonEntity -> {
struct
}
is NPCEntity -> {
struct
}
is ItemEntity -> {
ObjectValue(this).also { item ->
item.addStandardFunctions()
item.addFunctions(EntityMoLangFunctions.attach(this))
item.addFunctions(ItemStackMoLangFunctions.attach(Pair(this.item, this.registryAccess())))
}
}
else -> {
ObjectValue(this).also { objectValue ->
objectValue.addStandardFunctions()
objectValue.addFunctions(EntityMoLangFunctions.attach(this))
if (this is LivingEntity) {
objectValue.addFunctions(LivingEntityMoLangFunctions.attach(this))
}
else -> ObjectValue(this).also { objectValue ->
objectValue.addStandardFunctions()
objectValue.addFunctions(EntityMoLangFunctions.attach(this))
if (this is LivingEntity) {
objectValue.addFunctions(LivingEntityMoLangFunctions.attach(this))
}
}
}
}
fun <T> Holder<T>.asMoLangValue(key: ResourceKey<Registry<T>>): ObjectValue<Holder<T>> {
val value =
ObjectValue(
obj = this,
stringify = {
it
.unwrapKey()
.get()
.location()
.toString()
},
)
value.functions["is_in"] =
Function put@{ params: MoParams ->
val tag = TagKey.create(key, ResourceLocation.parse(params.getString(0).replace("#", "")))
return@put DoubleValue(if (value.obj.`is`(tag)) 1.0 else 0.0)
}
value.functions["is_of"] =
Function put@{ params: MoParams ->
val identifier = ResourceLocation.parse(params.getString(0))
return@put DoubleValue(if (value.obj.`is`(identifier)) 1.0 else 0.0)
}
val value = ObjectValue(
obj = this,
stringify = { it.unwrapKey().get().location().toString() }
)
value.functions["is_in"] = Function put@{ params: MoParams ->
val tag = TagKey.create(key, ResourceLocation.parse(params.getString(0).replace("#", "")))
return@put DoubleValue(if (value.obj.`is`(tag)) 1.0 else 0.0)
}
value.functions["is_of"] = Function put@{ params: MoParams ->
val identifier = ResourceLocation.parse(params.getString(0))
return@put DoubleValue(if (value.obj.`is`(identifier)) 1.0 else 0.0)
}
return value
}
fun QueryStruct.addStandardFunctions(): QueryStruct {
addFunctions(GeneralMoLangFunctions.functions)
addFunctions(GeneralMoLangFunctions.holder)
return this
}
@@ -403,17 +370,23 @@ object MoLangFunctions {
return this
}
fun moLangFunctionMap(vararg functions: Pair<String, (MoParams) -> MoValue>): Map<String, (MoParams) -> MoValue> = functions.toMap()
fun moLangFunctionMap(
vararg functions: Pair<String, (MoParams) -> MoValue>
): Map<String, (MoParams) -> MoValue> {
return functions.toMap()
}
fun queryStructOf(vararg functions: Pair<String, (MoParams) -> MoValue>): QueryStruct =
QueryStruct(
fun queryStructOf(
vararg functions: Pair<String, (MoParams) -> MoValue>
): QueryStruct {
return QueryStruct(
hashMapOf<String, Function<MoParams, Any>>(
*functions
.map { (name, func) ->
name to Function<MoParams, Any> { params -> func(params) }
}.toTypedArray(),
),
*functions.map { (name, func) ->
name to Function<MoParams, Any> { params -> func(params) }
}.toTypedArray()
)
)
}
fun MoLangRuntime.setup(): MoLangRuntime {
environment.query.addStandardFunctions()
@@ -422,14 +395,8 @@ object MoLangFunctions {
fun writeMoValueToNBT(value: MoValue): Tag? {
return when (value) {
is DoubleValue -> {
DoubleTag.valueOf(value.value)
}
is StringValue -> {
StringTag.valueOf(value.value)
}
is DoubleValue -> DoubleTag.valueOf(value.value)
is StringValue -> StringTag.valueOf(value.value)
is ArrayStruct -> {
val list = value.map.values
val nbtList = ListTag()
@@ -446,22 +413,14 @@ object MoLangFunctions {
nbt
}
else -> {
null
}
else -> null
}
}
fun readMoValueFromNBT(nbt: Tag): MoValue =
when (nbt) {
is DoubleTag -> {
DoubleValue(nbt.asDouble)
}
is StringTag -> {
StringValue(nbt.asString)
}
fun readMoValueFromNBT(nbt: Tag): MoValue {
return when (nbt) {
is DoubleTag -> DoubleValue(nbt.asDouble)
is StringTag -> StringValue(nbt.asString)
is ListTag -> {
val array = ArrayStruct(hashMapOf())
var index = 0
@@ -482,12 +441,11 @@ object MoLangFunctions {
variable
}
else -> {
VariableStruct()
}
else -> VariableStruct()
}
}
}
// TODO: this one is unused. Should we move it to [MoLangFunctions] object?
fun Either<ResourceLocation, ExpressionLike>.runScript(runtime: MoLangRuntime) =
map({ CobblemonScripts.run(it, runtime) }, { it.resolve(runtime) })
map({ CobblemonScripts.run(it, runtime) }, { it.resolve(runtime) })
@@ -17,29 +17,30 @@ import com.cobblemon.mod.common.battles.actor.PlayerBattleActor
import com.cobblemon.mod.common.battles.actor.PokemonBattleActor
import com.cobblemon.mod.common.entity.npc.NPCBattleActor
object BattleActorMoLangFunctions : AbstractMoLangFunctionHolder<BattleActor>() {
override fun BattleActor.moLangFunctions(): MutableMap<String, (MoParams) -> Any> {
val actor = this
val functions = hashMapOf<String, (MoParams) -> Any>()
functions["is_npc"] = { DoubleValue(actor.type == ActorType.NPC) }
functions["is_player"] = { DoubleValue(actor.type == ActorType.PLAYER) }
functions["is_wild"] = { DoubleValue(actor.type == ActorType.WILD) }
val map = mutableMapOf<String, (MoParams) -> Any>(
"is_npc" to { DoubleValue(actor.type == ActorType.NPC) },
"is_player" to { DoubleValue(actor.type == ActorType.PLAYER) },
"is_wild" to { DoubleValue(actor.type == ActorType.WILD) }
)
when (actor) {
is NPCBattleActor -> {
functions["npc"] = { actor.entity.struct }
map["npc"] = { actor.entity.struct }
}
is PlayerBattleActor -> {
functions["player"] = { actor.entity?.asMoLangValue() ?: DoubleValue.ZERO }
map["player"] = { actor.entity?.asMoLangValue() ?: DoubleValue.ZERO }
}
is PokemonBattleActor -> {
functions["pokemon"] = { actor.entity?.asMoLangValue() ?: DoubleValue.ZERO }
map["pokemon"] = { actor.entity?.asMoLangValue() ?: DoubleValue.ZERO }
}
}
return functions
return map
}
}
@@ -16,19 +16,17 @@ import com.cobblemon.mod.common.util.getStringOrNull
object BattleMessageMoLangFunctions : AbstractMoLangFunctionHolder<BattleMessage>() {
override fun BattleMessage.moLangFunctions(): MutableMap<String, (MoParams) -> Any> {
val battleMessage = this
val functions = hashMapOf<String, (MoParams) -> Any>()
functions["has_argument_at"] = { params ->
val index = params.getInt(0)
val value = params.getStringOrNull(1) ?: ""
DoubleValue(value == battleMessage.argumentAt(index))
}
functions["has_argument"] = { params ->
val argumentName = params.getString(0)
val value = params.getStringOrNull(1) ?: ""
DoubleValue(value == battleMessage.optionalArgument(argumentName))
}
return functions
return mutableMapOf(
"has_argument_at" to { params ->
val index = params.getInt(0)
val value = params.getStringOrNull(1) ?: ""
DoubleValue(value == battleMessage.argumentAt(index))
},
"has_argument" to { params ->
val argumentName = params.getString(0)
val value = params.getStringOrNull(1) ?: ""
DoubleValue(value == battleMessage.optionalArgument(argumentName))
}
)
}
}
@@ -18,22 +18,20 @@ import java.util.*
object BattleMoLangFunctions : AbstractMoLangFunctionHolder<PokemonBattle>() {
override fun PokemonBattle.moLangFunctions(): MutableMap<String, (MoParams) -> Any> {
val battle = this
val functions = hashMapOf<String, (MoParams) -> Any>()
functions["environment"] = { battle.runtime.environment }
functions["battle_id"] = { StringValue(battle.battleId.toString()) }
functions["battle_type"] = { StringValue(battle.format.toString()) }
functions["is_pvn"] = { DoubleValue(battle.isPvN) }
functions["is_pvp"] = { DoubleValue(battle.isPvP) }
functions["is_pvw"] = { DoubleValue(battle.isPvW) }
functions["stop"] = { battle.stop() }
functions["actors"] = { battle.actors.toList().asArrayValue { it.struct } }
functions["get_actor"] = getActor@{ params ->
val uuid = UUID.fromString(params.getString(0))
val actor = battle.actors.find { it.uuid == uuid } ?: return@getActor DoubleValue.ZERO
actor.struct
}
return functions
return mutableMapOf(
"environment" to { battle.runtime.environment },
"battle_id" to { StringValue(battle.battleId.toString()) },
"battle_type" to { StringValue(battle.format.toString()) },
"is_pvn" to { DoubleValue(battle.isPvN) },
"is_pvp" to { DoubleValue(battle.isPvP) },
"is_pvw" to { DoubleValue(battle.isPvW) },
"stop" to { battle.stop() },
"actors" to { battle.actors.toList().asArrayValue { it.struct } },
"get_actor" to getActor@{ params ->
val uuid = UUID.fromString(params.getString(0))
val actor = battle.actors.find { it.uuid == uuid } ?: return@getActor DoubleValue.ZERO
actor.struct
}
)
}
}
@@ -12,11 +12,8 @@ import com.bedrockk.molang.runtime.MoParams
import net.minecraft.core.Holder
import net.minecraft.world.level.biome.Biome
object BiomeMoLangFunctions : AbstractMoLangFunctionHolder<Holder<Biome>>() {
object BiomeMoLangFunctions: AbstractMoLangFunctionHolder<Holder<Biome>>() {
override fun Holder<Biome>.moLangFunctions(): MutableMap<String, (MoParams) -> Any> {
val biome = this
val functions = hashMapOf<String, (MoParams) -> Any>()
return functions
return mutableMapOf()
}
}
@@ -12,11 +12,8 @@ import com.bedrockk.molang.runtime.MoParams
import net.minecraft.core.Holder
import net.minecraft.world.level.block.Block
object BlockMoLangFunctions : AbstractMoLangFunctionHolder<Holder<Block>>() {
object BlockMoLangFunctions: AbstractMoLangFunctionHolder<Holder<Block>>() {
override fun Holder<Block>.moLangFunctions(): MutableMap<String, (MoParams) -> Any> {
val block = this
val functions = hashMapOf<String, (MoParams) -> Any>()
return functions
return mutableMapOf()
}
}
@@ -14,9 +14,6 @@ import net.minecraft.world.level.dimension.DimensionType
object DimensionTypeMoLangFunctions : AbstractMoLangFunctionHolder<Holder<DimensionType>>() {
override fun Holder<DimensionType>.moLangFunctions(): MutableMap<String, (MoParams) -> Any> {
val dimension = this
val functions = hashMapOf<String, (MoParams) -> Any>()
return functions
return mutableMapOf()
}
}
@@ -18,16 +18,15 @@ import com.cobblemon.mod.common.util.getOrNull
object DropEntryMoLangFunctions : AbstractMoLangFunctionHolder<DropEntry>() {
override fun DropEntry.moLangFunctions(): MutableMap<String, (MoParams) -> Any> {
val drop = this
val functions = hashMapOf<String, (MoParams) -> Any>()
val map = mutableMapOf<String, (MoParams) -> Any>()
functions["percentage"] = { DoubleValue(drop.percentage.toDouble()) }
functions["quantity"] = { DoubleValue(drop.quantity.toDouble()) }
functions["max_selectable_times"] = { DoubleValue(drop.maxSelectableTimes.toDouble()) }
functions["can_drop"] = { params ->
map["percentage"] = { DoubleValue(drop.percentage.toDouble()) }
map["quantity"] = { DoubleValue(drop.quantity.toDouble()) }
map["max_selectable_times"] = { DoubleValue(drop.maxSelectableTimes.toDouble()) }
map["can_drop"] = { params ->
val pokemon = params.getOrNull<ObjectValue<Pokemon>>(0)?.obj
DoubleValue(if (drop.canDrop(pokemon)) 1.0 else 0.0)
}
return functions
return map
}
}
@@ -56,300 +56,258 @@ import kotlin.math.sqrt
object EntityMoLangFunctions : AbstractMoLangFunctionHolder<Entity>() {
override fun Entity.moLangFunctions(): MutableMap<String, (MoParams) -> Any> {
val entity = this
val functions = hashMapOf<String, (MoParams) -> Any>()
functions["uuid"] = { StringValue(entity.uuid.toString()) }
functions["name"] = { StringValue(entity.effectiveName().string) }
functions["yaw"] = { DoubleValue(entity.yRot.toDouble()) }
functions["pitch"] = { DoubleValue(entity.xRot.toDouble()) }
functions["x"] = { DoubleValue(entity.x) }
functions["y"] = { DoubleValue(entity.y) }
functions["z"] = { DoubleValue(entity.z) }
functions["velocity_x"] = { DoubleValue(entity.deltaMovement.x) }
functions["velocity_y"] = { DoubleValue(entity.deltaMovement.y) }
functions["velocity_z"] = { DoubleValue(entity.deltaMovement.z) }
functions["width"] = { DoubleValue(entity.boundingBox.xsize) }
functions["height"] = { DoubleValue(entity.boundingBox.ysize) }
functions["this_size"] = { DoubleValue(entity.boundingBox.run { if (xsize > ysize) xsize else ysize }) }
functions["this_width"] = { DoubleValue(entity.boundingBox.xsize) }
functions["this_height"] = { DoubleValue(entity.boundingBox.ysize) }
functions["id_modulo"] = { params -> DoubleValue(entity.uuid.hashCode() % params.getDouble(0)) }
functions["horizontal_velocity"] = { DoubleValue(entity.deltaMovement.horizontalDistance()) }
functions["vertical_velocity"] = { DoubleValue(entity.deltaMovement.y) }
functions["is_on_ground"] = { DoubleValue(entity.onGround()) }
functions["world"] = {
entity
.level()
.worldRegistry
.wrapAsHolder(entity.level())
.asWorldMoLangValue()
}
functions["biome"] = { entity.level().getBiome(entity.blockPosition()).asBiomeMoLangValue() }
functions["is_passenger"] = { DoubleValue(entity.isPassenger) }
functions["discard"] = { entity.discard() }
functions["is_sneaking"] = { DoubleValue(entity.isShiftKeyDown) }
functions["is_sprinting"] = { DoubleValue(entity.isSprinting) }
functions["is_in_water"] = { DoubleValue(entity.isUnderWater) }
functions["is_in_rain"] = { DoubleValue(entity.isInWaterOrRain && !entity.isInWater) }
functions["is_touching_water_or_rain"] = { DoubleValue(entity.isInWaterRainOrBubble) }
functions["is_touching_water"] = { DoubleValue(entity.isInWater) }
functions["is_underwater"] = { DoubleValue(entity.getIsSubmerged()) }
functions["is_in_lava"] = { DoubleValue(entity.isInLava) }
functions["is_on_fire"] = { DoubleValue(entity.isOnFire) }
functions["is_invisible"] = { DoubleValue(entity.isInvisible) }
functions["is_riding"] = { DoubleValue(entity.isPassenger) }
functions["set_name"] = { params ->
val name = params.getString(0)
entity.customName = name.text()
DoubleValue.ONE
}
functions["damage"] = { params ->
val amount = params.getDouble(0)
val source =
DamageSource(
entity
.level()
.registryAccess()
.registryOrThrow(Registries.DAMAGE_TYPE)
.getHolder(DamageTypes.GENERIC)
.get(),
val thisEntity = this
val map = mutableMapOf<String, (MoParams) -> Any>(
"uuid" to { StringValue(thisEntity.uuid.toString()) },
"name" to { _ -> StringValue(thisEntity.effectiveName().string) },
"yaw" to { _ -> DoubleValue(thisEntity.yRot.toDouble()) },
"pitch" to { _ -> DoubleValue(thisEntity.xRot.toDouble()) },
"x" to { _ -> DoubleValue(thisEntity.x) },
"y" to { _ -> DoubleValue(thisEntity.y) },
"z" to { _ -> DoubleValue(thisEntity.z) },
"velocity_x" to { _ -> DoubleValue(thisEntity.deltaMovement.x) },
"velocity_y" to { _ -> DoubleValue(thisEntity.deltaMovement.y) },
"velocity_z" to { _ -> DoubleValue(thisEntity.deltaMovement.z) },
"width" to { DoubleValue(thisEntity.boundingBox.xsize) },
"height" to { DoubleValue(thisEntity.boundingBox.ysize) },
"this_size" to { DoubleValue(thisEntity.boundingBox.run { if (xsize > ysize) xsize else ysize }) },
"this_width" to { DoubleValue(thisEntity.boundingBox.xsize) },
"this_height" to { DoubleValue(thisEntity.boundingBox.ysize) },
"id_modulo" to { params -> DoubleValue(thisEntity.uuid.hashCode() % params.getDouble(0)) },
"horizontal_velocity" to { _ -> DoubleValue(thisEntity.deltaMovement.horizontalDistance()) },
"vertical_velocity" to { DoubleValue(thisEntity.deltaMovement.y) },
"is_on_ground" to { _ -> DoubleValue(thisEntity.onGround()) },
"world" to { _ -> thisEntity.level().worldRegistry.wrapAsHolder(thisEntity.level()).asWorldMoLangValue() },
"biome" to { _ -> thisEntity.level().getBiome(thisEntity.blockPosition()).asBiomeMoLangValue() },
"is_passenger" to { DoubleValue(thisEntity.isPassenger) },
"discard" to { thisEntity.discard() },
"is_sneaking" to { _ -> DoubleValue(thisEntity.isShiftKeyDown) },
"is_sprinting" to { _ -> DoubleValue(thisEntity.isSprinting) },
"is_in_water" to { _ -> DoubleValue(thisEntity.isUnderWater) },
"is_in_rain" to { _ -> DoubleValue(thisEntity.isInWaterOrRain && !thisEntity.isInWater) },
"is_touching_water_or_rain" to { _ -> DoubleValue(thisEntity.isInWaterRainOrBubble) },
"is_touching_water" to { _ -> DoubleValue(thisEntity.isInWater) },
"is_underwater" to { DoubleValue(thisEntity.getIsSubmerged()) },
"is_in_lava" to { _ -> DoubleValue(thisEntity.isInLava) },
"is_on_fire" to { _ -> DoubleValue(thisEntity.isOnFire) },
"is_invisible" to { _ -> DoubleValue(thisEntity.isInvisible) },
"is_riding" to { _ -> DoubleValue(thisEntity.isPassenger) },
"set_name" to { params ->
val name = params.getString(0)
thisEntity.customName = name.text()
DoubleValue.ONE
},
"damage" to { params ->
val amount = params.getDouble(0)
val source = DamageSource(
thisEntity.level().registryAccess().registryOrThrow(Registries.DAMAGE_TYPE)
.getHolder(DamageTypes.GENERIC).get()
)
entity.hurt(source, amount.toFloat())
}
functions["distance_to_owner"] = distanceToOwner@{
if (entity !is TamableAnimal) {
return@distanceToOwner DoubleValue.ZERO
}
val owner = entity.owner ?: return@distanceToOwner DoubleValue.ZERO
DoubleValue(entity.distanceTo(owner))
}
functions["owner"] = owner@{
if (entity !is TamableAnimal) return@owner DoubleValue.ZERO
val owner = entity.owner
owner?.asMostSpecificMoLangValue() ?: DoubleValue.ZERO
}
functions["delta_movement"] = {
listOf(entity.deltaMovement.x, entity.deltaMovement.y, entity.deltaMovement.z)
.asArrayValue(::DoubleValue)
}
functions["tags"] = {
val tags = entity.tags
val array = ArrayStruct(hashMapOf())
tags.forEachIndexed { index, tag -> array.setDirectly("$index", StringValue(tag)) }
array
}
functions["add_tag"] = { params ->
val tag = params.getString(0)
entity.addTag(tag)
DoubleValue.ONE
}
functions["remove_tag"] = { params ->
val tag = params.getString(0)
entity.removeTag(tag)
DoubleValue.ONE
}
functions["has_tag"] = { params ->
val tag = params.getString(0)
DoubleValue(entity.tags.contains(tag))
}
functions["distance_to_pos"] = { params ->
val x = params.getDouble(0)
val y = params.getDouble(1)
val z = params.getDouble(2)
DoubleValue(sqrt(entity.distanceToSqr(Vec3(x, y, z))))
}
functions["type"] = {
entity
.registryAccess()
.registry(Registries.ENTITY_TYPE)
.get()
.getKey(entity.type)
?.toString()
?.let {
thisEntity.hurt(source, amount.toFloat())
},
"distance_to_owner" to distanceToOwner@{
if (thisEntity !is TamableAnimal) {
return@distanceToOwner DoubleValue.ZERO
}
val owner = thisEntity.owner ?: return@distanceToOwner DoubleValue.ZERO
DoubleValue(thisEntity.distanceTo(owner))
},
"owner" to owner@{
if (thisEntity !is TamableAnimal) return@owner DoubleValue.ZERO
val owner = thisEntity.owner
owner?.asMostSpecificMoLangValue() ?: DoubleValue.ZERO
},
"delta_movement" to {
listOf(thisEntity.deltaMovement.x, thisEntity.deltaMovement.y, thisEntity.deltaMovement.z)
.asArrayValue(::DoubleValue)
},
"tags" to {
val tags = thisEntity.tags
val array = ArrayStruct(hashMapOf())
tags.forEachIndexed { index, tag -> array.setDirectly("$index", StringValue(tag)) }
array
},
"add_tag" to { params ->
val tag = params.getString(0)
thisEntity.addTag(tag)
DoubleValue.ONE
},
"remove_tag" to { params ->
val tag = params.getString(0)
thisEntity.removeTag(tag)
DoubleValue.ONE
},
"has_tag" to { params ->
val tag = params.getString(0)
DoubleValue(thisEntity.tags.contains(tag))
},
"distance_to_pos" to { params ->
val x = params.getDouble(0)
val y = params.getDouble(1)
val z = params.getDouble(2)
DoubleValue(sqrt(thisEntity.distanceToSqr(Vec3(x, y, z))))
},
"type" to { _ ->
thisEntity.registryAccess().registry(Registries.ENTITY_TYPE).get().getKey(thisEntity.type)?.toString()?.let {
StringValue(it)
} ?: DoubleValue.ZERO
}
functions["find_nearby_block"] = findNearbyBlock@{ params ->
val input = params.getString(0)
val isTag = input.contains("#")
val type = input.replace("#", "").asIdentifierDefaultingNamespace(namespace = "minecraft")
val range = params.getDoubleOrNull(1) ?: 10
val blockPos =
entity
.level()
.getBlockStatesWithPos(
AABB.ofSize(
entity.position(),
range.toDouble(),
range.toDouble(),
range.toDouble(),
),
).filter { blockPosPair ->
blockPosPair.first.blockHolder.let {
if (isTag) {
it.`is`(
TagKey.create(
Registries.BLOCK,
type,
),
)
} else {
it.`is`(type)
}
}
}.minByOrNull { it.second.distSqr(entity.blockPosition()) }
?.second
if (blockPos != null) {
return@findNearbyBlock ArrayStruct(
mapOf(
"0" to DoubleValue(blockPos.x),
"1" to DoubleValue(blockPos.y),
"2" to DoubleValue(blockPos.z),
),
},
"find_nearby_block" to findNearbyBlock@{ params ->
val input = params.getString(0)
val isTag = input.contains("#")
val type = input.replace("#", "").asIdentifierDefaultingNamespace(namespace = "minecraft")
val range = params.getDoubleOrNull(1) ?: 10
val blockPos = thisEntity.level().getBlockStatesWithPos(
AABB.ofSize(
thisEntity.position(),
range.toDouble(),
range.toDouble(),
range.toDouble()
)
)
}
return@findNearbyBlock DoubleValue.ZERO
}
functions["get_nearby_entities"] = { params ->
val distance = params.getDouble(0)
val entities =
entity
.level()
.getEntities(entity, AABB.ofSize(entity.position(), distance, distance, distance))
.filter { blockPosPair ->
blockPosPair.first.blockHolder.let {
if (isTag) it.`is`(
TagKey.create(
Registries.BLOCK,
type
)
) else it.`is`(type)
}
}
.minByOrNull { it.second.distSqr(thisEntity.blockPosition()) }
?.second
if (blockPos != null) {
return@findNearbyBlock ArrayStruct(
mapOf(
"0" to DoubleValue(blockPos.x),
"1" to DoubleValue(blockPos.y),
"2" to DoubleValue(blockPos.z)
)
)
}
return@findNearbyBlock DoubleValue.ZERO
},
"get_nearby_entities" to { params ->
val distance = params.getDouble(0)
val entities =
thisEntity.level().getEntities(thisEntity, AABB.ofSize(thisEntity.position(), distance, distance, distance))
entities
.filterIsInstance<Entity>()
.map { it.asMostSpecificMoLangValue() }
.asArrayValue()
}
functions["is_standing_on_blocks"] = { params ->
val depth = params.getDouble(0).toInt()
val blockStrings: MutableSet<String> = mutableSetOf()
for (blockIndex in 1..<params.params.size) {
blockStrings.add(params.getString(blockIndex))
}
entities
.filterIsInstance<Entity>()
.map { it.asMostSpecificMoLangValue() }
.asArrayValue()
},
"is_standing_on_blocks" to { params ->
val depth = params.getDouble(0).toInt()
val blockStrings: MutableSet<String> = mutableSetOf()
for (blockIndex in 1..<params.params.size) {
blockStrings.add(params.getString(blockIndex))
}
if (entity.isStandingOn(blockStrings, depth)) DoubleValue.ONE else DoubleValue.ZERO
}
if (thisEntity.isStandingOn(blockStrings, depth)) DoubleValue.ONE else DoubleValue.ZERO
},
// q.entity.spawn_bedrock_particles(effect, locator, [player])
// sends to everyone nearby or just to the player if they're set.
// Locator is necessary even if unused on non-posables.
functions["spawn_bedrock_particles"] = { params ->
val particle = params.getString(0).asResource()
val locator = params.getString(1)
val player =
params.getOrNull<MoValue>(2)?.let {
// q.entity.spawn_bedrock_particles(effect, locator, [player])
// sends to everyone nearby or just to the player if they're set.
// Locator is necessary even if unused on non-posables.
"spawn_bedrock_particles" to { params ->
val particle = params.getString(0).asResource()
val locator = params.getString(1)
val player = params.getOrNull<MoValue>(2)?.let {
when (it) {
is StringValue -> entity.level().getPlayerByUUID(UUID.fromString(it.value))
is StringValue -> thisEntity.level().getPlayerByUUID(UUID.fromString(it.value))
is ObjectValue<*> -> it.obj
else -> null
}
} as? ServerPlayer
val packet = SpawnSnowstormEntityParticlePacket(particle, entity.id, listOf(locator))
if (player == null) {
packet.sendToPlayersAround(
entity.x,
entity.y,
entity.z,
64.0,
entity.level().dimension(),
)
} else {
packet.sendToPlayer(player)
val packet = SpawnSnowstormEntityParticlePacket(particle, thisEntity.id, listOf(locator))
if (player == null) {
packet.sendToPlayersAround(thisEntity.x, thisEntity.y, thisEntity.z, 64.0, thisEntity.level().dimension())
} else {
packet.sendToPlayer(player)
}
},
"make_intangible" to { params ->
val intangible = params.getBooleanOrNull(0) ?: true
thisEntity.noPhysics = intangible
DoubleValue.ONE
}
}
functions["make_intangible"] = { params ->
val intangible = params.getBooleanOrNull(0) ?: true
entity.noPhysics = intangible
DoubleValue.ONE
}
)
when (entity) {
is PosableEntity -> {
functions["play_animation"] = playAnimation@{ params ->
val animation = params.getString(0)
val packet = PlayPosableAnimationPacket(entity.id, setOf(animation), emptyList())
val target = params.getStringOrNull(1)
if (target != null) {
val targetPlayer =
if (target.asUUID != null) {
entity.level().getPlayerByUUID(target.asUUID!!) as ServerPlayer
} else if (entity.level() is ServerLevel) {
entity
.level()
.server!!
.playerList
.getPlayerByName(target)
} else {
null
}
if (targetPlayer != null) {
packet.sendToPlayer(targetPlayer)
return@playAnimation DoubleValue.ONE
} else {
return@playAnimation DoubleValue.ZERO
}
if (thisEntity is PosableEntity) {
map["play_animation"] = playAnimation@{ params ->
val animation = params.getString(0)
val packet = PlayPosableAnimationPacket(thisEntity.id, setOf(animation), emptyList())
val target = params.getStringOrNull(1)
if (target != null) {
val targetPlayer = if (target.asUUID != null) {
thisEntity.level().getPlayerByUUID(target.asUUID!!) as ServerPlayer
} else if (thisEntity.level() is ServerLevel) {
thisEntity.level().server!!.playerList.getPlayerByName(target)
} else {
packet.sendToPlayersAround(
entity.x,
entity.y,
entity.z,
64.0,
entity.level().dimension(),
)
null
}
if (targetPlayer != null) {
packet.sendToPlayer(targetPlayer)
return@playAnimation DoubleValue.ONE
}
}
functions["face"] = { params ->
ObjectValue(ReferenceDialogueFaceProvider(entity.id, params.getBooleanOrNull(0) != false))
}
}
is Schedulable -> {
functions["run_molang_after"] = { params ->
val expression = params.getString(0).asExpressionLike()
val delayInSeconds = params.getDouble(1).toFloat()
val runtime = MoLangRuntime()
runtime.environment.cloneFrom(params.environment)
entity.after(delayInSeconds) {
runtime.resolve(expression)
}
}
}
is MoLangScriptingEntity -> {
functions["add_callback"] = { params ->
val type = params.getString(0).asIdentifierDefaultingNamespace()
val callback = params.getString(1).asIdentifierDefaultingNamespace()
val allowDuplicates = params.getBooleanOrNull(2) ?: false
entity.callbacks.addCallback(type, callback, allowDuplicates)
}
functions["remove_callback"] = { params ->
val type = params.getString(0).asIdentifierDefaultingNamespace()
val callback = params.getString(1).asIdentifierDefaultingNamespace()
entity.callbacks.removeCallback(type, callback)
}
functions["has_callback"] = { params ->
val type = params.getString(0).asIdentifierDefaultingNamespace()
val callback = params.getString(1).asIdentifierDefaultingNamespace()
DoubleValue(callback in (entity.callbacks[type] ?: emptySet()))
}
functions["clear_callbacks"] = { params ->
val type = params.getStringOrNull(0)?.asIdentifierDefaultingNamespace()
if (type != null) {
entity.callbacks[type]?.clear()
} else {
entity.callbacks.clear()
return@playAnimation DoubleValue.ZERO
}
DoubleValue.ONE
} else {
packet.sendToPlayersAround(thisEntity.x, thisEntity.y, thisEntity.z, 64.0, thisEntity.level().dimension())
return@playAnimation DoubleValue.ONE
}
}
map["face"] = { params ->
ObjectValue(ReferenceDialogueFaceProvider(thisEntity.id, params.getBooleanOrNull(0) != false))
}
}
if (thisEntity is Schedulable) {
map["run_molang_after"] = { params ->
val expression = params.getString(0).asExpressionLike()
val delayInSeconds = params.getDouble(1).toFloat()
val runtime = MoLangRuntime()
runtime.environment.cloneFrom(params.environment)
thisEntity.after(delayInSeconds) {
runtime.resolve(expression)
}
}
}
return functions
if (thisEntity is MoLangScriptingEntity) {
map["add_callback"] = { params ->
val type = params.getString(0).asIdentifierDefaultingNamespace()
val callback = params.getString(1).asIdentifierDefaultingNamespace()
val allowDuplicates = params.getBooleanOrNull(2) ?: false
thisEntity.callbacks.addCallback(type, callback, allowDuplicates)
}
map["remove_callback"] = { params ->
val type = params.getString(0).asIdentifierDefaultingNamespace()
val callback = params.getString(1).asIdentifierDefaultingNamespace()
thisEntity.callbacks.removeCallback(type, callback)
}
map["has_callback"] = { params ->
val type = params.getString(0).asIdentifierDefaultingNamespace()
val callback = params.getString(1).asIdentifierDefaultingNamespace()
DoubleValue(callback in (thisEntity.callbacks[type] ?: emptySet()))
}
map["clear_callbacks"] = { params ->
val type = params.getStringOrNull(0)?.asIdentifierDefaultingNamespace()
if (type != null) {
thisEntity.callbacks[type]?.clear()
} else {
thisEntity.callbacks.clear()
}
DoubleValue.ONE
}
}
return map
}
}
@@ -19,26 +19,26 @@ import com.cobblemon.mod.common.pokemon.evolution.variants.TradeEvolution
object EvolutionMoLangFunctions : AbstractMoLangFunctionHolder<Evolution>() {
override fun Evolution.moLangFunctions(): MutableMap<String, (MoParams) -> Any> {
val evolution = this
val functions = hashMapOf<String, (MoParams) -> Any>()
val map = hashMapOf<String, (MoParams) -> Any>()
functions["result"] = { evolution.result.asMoLangValue() }
functions["is_optional"] = { DoubleValue(evolution.optional) }
functions["consumes_held_item"] = { DoubleValue(evolution.consumeHeldItem) }
map["result"] = { evolution.result.asMoLangValue() }
map["is_optional"] = { DoubleValue(evolution.optional) }
map["consumes_held_item"] = { DoubleValue(evolution.consumeHeldItem) }
when (evolution) {
is LevelUpEvolution -> {
functions["is_level_up"] = { DoubleValue.ONE }
map["is_level_up"] = { DoubleValue.ONE }
}
is TradeEvolution -> {
functions["is_trade"] = { DoubleValue.ONE }
map["is_trade"] = { DoubleValue.ONE }
}
is ItemInteractionEvolution -> {
functions["is_item"] = { DoubleValue.ONE }
map["is_item"] = { DoubleValue.ONE }
}
}
return functions
return map
}
}
@@ -42,104 +42,101 @@ import net.minecraft.world.item.ItemStack
import java.text.ParseException
object GeneralMoLangFunctions {
val functions: MutableMap<String, (MoParams) -> Any> = mutableMapOf()
init {
functions["format"] = { params ->
val value = params.get<MoValue>(0).asDouble()
val decimals = params.get<MoValue>(1).asDouble().toInt().coerceIn(0, 10)
StringValue(String.format("%.${decimals}f", value))
}
functions["print"] = { params ->
val holder: HashMap<String, (MoParams) -> Any> = hashMapOf(
"print" to { params ->
val message = params.get<MoValue>(0).asString()
LOGGER.info(message)
}
functions["delete_variable"] = { params ->
},
"delete_variable" to deleteVar@{ params ->
val struct = params.get<VariableStruct>(0)
val variable = params.getString(1)
struct.map.remove(variable)
return@deleteVar DoubleValue.ONE
},
"delete_variables" to { params ->
val struct = params.get<VariableStruct>(0)
struct.map.clear()
DoubleValue.ONE
}
functions["get_variable"] = { params ->
},
"get_variable" to { params ->
val struct = params.get<VariableStruct>(0)
val variable = params.getString(1)
struct.map[variable] ?: DoubleValue.ZERO
}
functions["set_variable"] = { params ->
},
"set_variable" to { params ->
val struct = params.get<VariableStruct>(0)
val variable = params.getString(1)
val value = params.get<MoValue>(2)
struct.map[variable] = value
value
}
functions["set_query"] = { params ->
},
"set_query" to { params ->
val variable = params.getString(0)
val value = params.get<MoValue>(1)
params.environment.query.addFunction(variable) { value }
value
}
functions["replace"] = { params ->
},
"replace" to { params ->
val text = params.getString(0)
val search = params.getString(1)
val replace = params.getString(2)
StringValue(text.replace(search, replace))
}
functions["is_included"] = { params ->
},
"is_included" to { params ->
val text = params.getString(0)
val search = params.getString(1)
DoubleValue(text.contains(search))
}
functions["to_lower"] = { params ->
},
"to_lower" to { params ->
StringValue(params.getString(0).lowercase())
}
functions["to_upper"] = { params ->
},
"to_upper" to { params ->
StringValue(params.getString(0).uppercase())
}
functions["string_length"] = { params ->
},
"string_length" to { params ->
DoubleValue(params.getString(0).length)
}
functions["split_string"] = { params ->
},
"split_string" to { params ->
val text = params.getString(0)
val delimiter = params.getString(1)
val parts = text.split(delimiter).map { StringValue(it) }
val struct = ArrayStruct(hashMapOf())
parts.forEachIndexed { index, moValue -> struct.setDirectly("$index", moValue) }
struct
}
functions["is_blank"] = { params ->
},
"is_blank" to { params ->
val arg = params.get<MoValue>(0)
DoubleValue(
(arg is StringValue && (arg.value.isBlank() || arg.value.toDoubleOrNull() == 0.0)) ||
(arg is DoubleValue && arg.value == 0.0),
)
}
functions["run_command"] = { params ->
DoubleValue((arg is StringValue && (arg.value.isBlank() || arg.value.toDoubleOrNull() == 0.0)) || (arg is DoubleValue && arg.value == 0.0))
},
"run_command" to { params ->
val command = params.getString(0)
try {
server()!!.commands.performPrefixedCommand(server()!!.createCommandSourceStack(), command)
} catch (_: NullPointerException) {
DoubleValue.ZERO
}
}
functions["is_int"] = { params -> DoubleValue(params.get<MoValue>(0).asString().isInt()) }
functions["is_number"] = { params ->
},
"is_int" to { params -> DoubleValue(params.get<MoValue>(0).asString().isInt()) },
"is_number" to { params ->
DoubleValue(
params.get<MoValue>(0).asString().toDoubleOrNull() != null,
params.get<MoValue>(0).asString().toDoubleOrNull() != null
)
}
functions["to_number"] = { params ->
},
"to_number" to { params ->
DoubleValue(
params.get<MoValue>(0).asString().toDoubleOrNull() ?: 0.0,
params.get<MoValue>(0).asString().toDoubleOrNull() ?: 0.0
)
}
functions["to_int"] = { params ->
},
"to_int" to { params ->
DoubleValue(
params.get<MoValue>(0).asString().toIntOrNull() ?: 0,
params.get<MoValue>(0).asString().toIntOrNull() ?: 0
)
}
functions["to_string"] = { params -> StringValue(params.get<MoValue>(0).asString()) }
functions["do_effect_walks"] = { DoubleValue(Cobblemon.config.walkingInBattleAnimations) }
functions["random"] = { params ->
},
"to_string" to { params -> StringValue(params.get<MoValue>(0).asString()) },
"do_effect_walks" to { _ ->
DoubleValue(Cobblemon.config.walkingInBattleAnimations)
},
"random" to { params ->
val options = mutableListOf<MoValue>()
var index = 0
while (params.contains(index)) {
@@ -147,35 +144,35 @@ object GeneralMoLangFunctions {
index++
}
options.random() // Can throw an exception if they specified no args. They'd be idiots though.
}
functions["curve"] = { params ->
},
"curve" to { params ->
val curveName = params.getString(0)
val curve =
WaveFunctions.functions[curveName] ?: throw IllegalArgumentException("Unknown curve: $curveName")
ObjectValue(curve)
}
functions["array"] = { params ->
},
"array" to { params ->
val values = params.params
val array = ArrayStruct(hashMapOf())
values.forEachIndexed { index, moValue -> array.setDirectly("$index", moValue) }
array
}
functions["length"] = { params ->
},
"length" to { params ->
val array = (params.getOrNull<MoValue>(0) as? VariableStruct)
if (array == null) {
DoubleValue.ZERO
} else {
DoubleValue(array.map.size.toDouble())
}
}
functions["append"] = { params ->
},
"append" to { params ->
val array = params.get<ArrayStruct>(0)
val value = params.get<MoValue>(1)
val nextIndex = array.map.size
array.setDirectly("$nextIndex", value)
array
}
functions["insert"] = { params ->
},
"insert" to { params ->
val array = params.get<ArrayStruct>(0)
val index = params.getInt(1)
val value = params.get<MoValue>(2)
@@ -190,40 +187,36 @@ object GeneralMoLangFunctions {
}
array.map[index.toString()] = value
array
}
functions["delete"] = { params ->
},
"delete" to { params ->
val array = params.get<ArrayStruct>(0)
val index = params.getInt(1)
if (index in 0 until array.map.size) {
array.map.remove(index.toString())
// Re-index the array to keep keys sequential in numerical order
val newMap = hashMapOf<String, MoValue>()
array.map.keys
.mapNotNull { it.toIntOrNull() }
array.map.keys.mapNotNull { it.toIntOrNull() }
.sorted()
.forEachIndexed { i, k -> newMap[i.toString()] = array.map[k.toString()]!! }
array.map.clear()
array.map.putAll(newMap)
}
array
}
functions["run_script"] = { params ->
},
"run_script" to { params ->
val runtime = MoLangRuntime()
runtime.environment.query = params.environment.query
runtime.environment.variable = params.environment.variable
val args = params.params.subList(1, params.params.size)
runtime.environment.context =
ContextStruct(
(
params.environment.context?.map
?: emptyMap()
) + args.mapIndexed { index, value -> "arg_${index + 1}" to value }.toMap(),
)
runtime.environment.context = ContextStruct(
(params.environment.context?.map
?: emptyMap()) + args.mapIndexed { index, value -> "arg_${index + 1}" to value }.toMap()
)
val script = params.getString(0).asIdentifierDefaultingNamespace()
// store the args in the
CobblemonScripts.run(script, runtime) ?: DoubleValue.ZERO
}
functions["run_molang"] = { params ->
},
"run_molang" to { params ->
val runtime = MoLangRuntime()
runtime.environment.query = params.environment.query
runtime.environment.variable = params.environment.variable
@@ -231,16 +224,17 @@ object GeneralMoLangFunctions {
val expression = params.getString(0).asExpressionLike()
val delayInSeconds = params.getDoubleOrNull(1)?.toFloat() ?: 0.0f
fun evaluate(): MoValue =
try {
fun evaluate(): MoValue {
return try {
runtime.resolve(expression)
} catch (ex: Exception) {
LOGGER.warn(
"Failed to evaluate MoLang expression${if (delayInSeconds > 0.0f) " (delayed)" else ""}: $expression",
ex,
ex
)
DoubleValue.ZERO
}
}
if (delayInSeconds > 0.0f) {
val tracker =
@@ -252,55 +246,55 @@ object GeneralMoLangFunctions {
} else {
evaluate()
}
}
functions["system_time_millis"] = { DoubleValue(System.currentTimeMillis()) }
},
"system_time_millis" to { _ ->
DoubleValue(System.currentTimeMillis())
},
// the rest of the world use dd/MM/yyyy grow up america (this comment was generated by copilot)
functions["date_local_time"] = {
"date_local_time" to { _ ->
val time = System.currentTimeMillis()
val date = java.util.Date(time)
val formatted = java.text.SimpleDateFormat("dd/MM/yyyy").format(date)
StringValue(formatted)
}
functions["date_of"] = { params ->
},
"date_of" to { params ->
val time = params.getDouble(0).toLong()
val date = java.util.Date(time)
val formatted = java.text.SimpleDateFormat("dd/MM/yyyy").format(date)
StringValue(formatted)
}
functions["date_is_after"] = { params ->
},
"date_is_after" to { params ->
val dateA = params.getString(0)
val dateB = params.getString(1)
val format = java.text.SimpleDateFormat("dd/MM/yyyy")
val a =
try {
format.parse(dateA)
} catch (_: ParseException) {
throw RuntimeException("Cannot parse date $dateA, expected it to be in dd/MM/yyyy format, other provided")
}
val b =
try {
format.parse(dateB)
} catch (_: ParseException) {
throw RuntimeException("Cannot parse date $dateB, expected it to be in dd/MM/yyyy format, other provided")
}
val a = try {
format.parse(dateA)
} catch (_: ParseException) {
throw RuntimeException("Cannot parse date ${dateA}, expected it to be in dd/MM/yyyy format, other provided")
}
val b = try {
format.parse(dateB)
} catch (_: ParseException) {
throw RuntimeException("Cannot parse date ${dateB}, expected it to be in dd/MM/yyyy format, other provided")
}
DoubleValue(a.after(b))
}
functions["get_move_from_id"] = { params ->
},
"get_move_from_id" to { params ->
val moveId = params.getString(0)
val moveTemplate = Moves.getByName(moveId)
moveTemplate?.struct ?: DoubleValue.ZERO
}
functions["create_simple_party_provider"] = {
},
"create_simple_party_provider" to { _ ->
val partyProvider = SimplePartyProvider()
partyProvider.struct
}
functions["create_pickup_item"] = { params ->
},
"create_pickup_item" to { params ->
val item = params.getOrNull<MoValue>(0)?.asString()?.let(ObtainableItemCondition::parseFromString)
val pickupPriority = params.getIntOrNull(1) ?: 0
val pickupItem = ObtainableItem(item = item, pickupPriority = pickupPriority)
pickupItem.struct
}
functions["create_itemstack"] = { params ->
},
"create_itemstack" to { params ->
val itemId = params.getString(0).asIdentifierDefaultingNamespace()
try {
val item = BuiltInRegistries.ITEM.get(itemId)
@@ -311,7 +305,7 @@ object GeneralMoLangFunctions {
LOGGER.error("Cannot create item stack from identifier: $itemId, item with that ID does not exist. Possible typo?")
DoubleValue.ZERO
}
}
functions["file"] = { MoLangLoadedFilesCache.struct }
}
},
"file" to { MoLangLoadedFilesCache.struct }
)
}
@@ -26,44 +26,43 @@ object ItemStackMoLangFunctions : AbstractMoLangFunctionHolder<Pair<ItemStack, R
val registryAccess = this.second
val itemRegistry = registryAccess.registryOrThrow(Registries.ITEM)
val holder = itemRegistry.wrapAsHolder(stack.item)
val functions = mutableMapOf<String, (MoParams) -> Any>()
functions["item"] = { holder.asMoLangValue(Registries.ITEM) }
functions["count"] = { DoubleValue(stack.count.toDouble()) }
functions["damage_value"] = { DoubleValue(stack.damageValue) }
functions["max_damage"] = { DoubleValue(stack.maxDamage) }
functions["is_empty"] = { DoubleValue(stack.isEmpty) }
functions["shrink"] = { params -> stack.shrink(params.getInt(0)) }
functions["grow"] = { params -> stack.grow(params.getInt(0)) }
functions["is_of"] = { params ->
val map = hashMapOf<String, (MoParams) -> Any>()
map["item"] = { _ -> holder.asMoLangValue(Registries.ITEM) }
map["count"] = { _ -> DoubleValue(stack.count.toDouble()) }
map["damage_value"] = { _ -> DoubleValue(stack.damageValue) }
map["max_damage"] = { _ -> DoubleValue(stack.maxDamage) }
map["is_empty"] = { _ -> DoubleValue(stack.isEmpty) }
map["shrink"] = { params -> stack.shrink(params.getInt(0)) }
map["grow"] = { params -> stack.grow(params.getInt(0)) }
map["is_of"] = { params ->
DoubleValue(
holder.`is`(
params.getString(0).asIdentifierDefaultingNamespace(),
),
params.getString(0).asIdentifierDefaultingNamespace()
)
)
}
functions["is_in"] = { params ->
map["is_in"] = { params ->
DoubleValue(
holder.`is`(
TagKey.create(
Registries.ITEM,
params.getString(0).replace("#", "").asIdentifierDefaultingNamespace(),
),
),
params.getString(0).replace("#", "").asIdentifierDefaultingNamespace()
)
)
)
}
functions["is_food"] = { DoubleValue(stack.has(DataComponents.FOOD)) }
functions["is_enchanted"] = { DoubleValue(stack.hasFoil()) }
functions["has_enchantment"] = hasEnchantment@{ params ->
map["is_food"] = { _ -> DoubleValue(stack.has(DataComponents.FOOD)) }
map["is_enchanted"] = { _ -> DoubleValue(stack.hasFoil()) }
map["has_enchantment"] = put@{ params ->
val enchantmentId = params.getString(0).asIdentifierDefaultingNamespace()
val minLevel = params.getIntOrNull(1) ?: 1
val enchantmentRegistry = registryAccess.registryOrThrow(Registries.ENCHANTMENT)
val holder =
enchantmentRegistry.getHolder(enchantmentId).orElse(null) ?: return@hasEnchantment DoubleValue.ZERO
enchantmentRegistry.getHolder(enchantmentId).orElse(null) ?: return@put DoubleValue.ZERO
val level = EnchantmentHelper.getItemEnchantmentLevel(holder, stack)
DoubleValue(level >= minLevel)
return@put DoubleValue(level >= minLevel)
}
return functions
return map
}
}
@@ -53,27 +53,26 @@ import java.util.*
object LivingEntityMoLangFunctions : AbstractMoLangFunctionHolder<LivingEntity>() {
override fun LivingEntity.moLangFunctions(): MutableMap<String, (MoParams) -> Any> {
val entity = this
val functions = hashMapOf<String, (MoParams) -> Any>()
functions["is_player"] = { DoubleValue(entity is Player) }
functions["is_npc"] = { DoubleValue(entity is NPCEntity) }
functions["is_mob"] = { DoubleValue(entity is Mob) }
functions["is_pokemon"] = { DoubleValue(entity is PokemonEntity) }
functions["is_animal"] = { DoubleValue(entity is Animal) }
functions["is_tamable"] = { DoubleValue(entity is TamableAnimal) }
functions["is_tamed"] = { DoubleValue(entity is TamableAnimal && entity.isTame) }
functions["is_hostile"] = { DoubleValue(entity is Monster) }
functions["is_baby"] = { DoubleValue(entity.isBaby) }
functions["is_adult"] = { DoubleValue(!entity.isBaby) }
functions["remove_effect"] = removeEffect@{ params ->
val map = mutableMapOf<String, (MoParams) -> Any>()
map["is_player"] = { _ -> DoubleValue(entity is Player) }
map["is_npc"] = { _ -> DoubleValue(entity is NPCEntity) }
map["is_mob"] = { _ -> DoubleValue(entity is Mob) }
map["is_pokemon"] = { _ -> DoubleValue(entity is PokemonEntity) }
map["is_animal"] = { _ -> DoubleValue(entity is Animal) }
map["is_tamable"] = { _ -> DoubleValue(entity is TamableAnimal) }
map["is_tamed"] = { _ -> DoubleValue(entity is TamableAnimal && entity.isTame) }
map["is_hostile"] = { _ -> DoubleValue(entity is Monster) }
map["is_baby"] = { _ -> DoubleValue(entity.isBaby) }
map["is_adult"] = { _ -> DoubleValue(!entity.isBaby) }
map["remove_effect"] = put@{ params ->
val effectId = params.getString(0).asIdentifierDefaultingNamespace()
val effectHolder = BuiltInRegistries.MOB_EFFECT.getHolder(effectId).orElse(null)
if (effectHolder != null) {
entity.removeEffect(effectHolder)
return@removeEffect DoubleValue.ONE
return@put DoubleValue.ONE
}
}
functions["add_effect"] = addEffect@{ params ->
map["add_effect"] = put@{ params ->
val effectId = params.getString(0).asIdentifierDefaultingNamespace()
val duration = params.getInt(1)
val amplifier = params.getIntOrNull(2) ?: 0
@@ -82,37 +81,36 @@ object LivingEntityMoLangFunctions : AbstractMoLangFunctionHolder<LivingEntity>(
val effectHolder = BuiltInRegistries.MOB_EFFECT.getHolder(effectId).orElse(null)
if (effectHolder != null) {
entity.addEffect(MobEffectInstance(effectHolder, duration, amplifier, ambient, visible))
return@addEffect DoubleValue.ONE
return@put DoubleValue.ONE
}
}
functions["has_effect"] = { params ->
map["has_effect"] = put@{ params ->
val effectId = params.getString(0).asIdentifierDefaultingNamespace()
val effectHolder = BuiltInRegistries.MOB_EFFECT.getHolder(effectId).orElse(null)
DoubleValue(effectHolder != null && entity.hasEffect(effectHolder))
return@put DoubleValue(if (effectHolder != null) entity.hasEffect(effectHolder) else false)
}
functions["heal"] = { params ->
map["heal"] = { params ->
val amount = params.getDouble(0)
entity.heal(amount.toFloat())
}
functions["is_looking_at"] = isLookingAt@{ params ->
map["is_looking_at"] = put@{ params ->
val targetEntity = params.get<MoValue>(0)
val maxDistance = params.getDoubleOrNull(1)?.toFloat() ?: 2.0F
val entity =
if (targetEntity is ObjectValue<*> && targetEntity.obj is Entity) {
targetEntity.obj as Entity
} else {
return@isLookingAt DoubleValue.ZERO
}
val entity = if (targetEntity is ObjectValue<*> && targetEntity.obj is Entity) {
targetEntity.obj as Entity
} else {
return@put DoubleValue.ZERO
}
DoubleValue(entity.isLookingAt(entity, maxDistance))
return@put DoubleValue(entity.isLookingAt(entity, maxDistance))
}
functions["is_living_entity"] = { DoubleValue.ONE }
functions["is_flying"] = { DoubleValue(entity.isFallFlying) }
functions["is_sleeping"] = { DoubleValue(entity.isSleeping) }
functions["health"] = { DoubleValue(entity.health) }
functions["max_health"] = { DoubleValue(entity.maxHealth) }
functions["look_at_position"] = { params ->
map["is_living_entity"] = { DoubleValue.ONE }
map["is_flying"] = { _ -> DoubleValue(entity.isFallFlying) }
map["is_sleeping"] = { _ -> DoubleValue(entity.isSleeping) }
map["health"] = { _ -> DoubleValue(entity.health) }
map["max_health"] = { _ -> DoubleValue(entity.maxHealth) }
map["look_at_position"] = { params ->
val x = params.getDouble(0)
val y = params.getDouble(1)
val z = params.getDouble(2)
@@ -122,174 +120,159 @@ object LivingEntityMoLangFunctions : AbstractMoLangFunctionHolder<LivingEntity>(
brain.setMemoryWithExpiry(
MemoryModuleType.LOOK_TARGET,
CobblemonBlockPosTracker(Vec3(x, y, z), flags.toSet()),
duration.toLong(),
duration.toLong()
)
}
functions["get_wander_control_memory"] = {
val value =
entity.brain.getMemorySafely(CobblemonMemories.WANDER_CONTROL).orElse(null)
?: CobblemonWanderControl()
map["get_wander_control_memory"] = put@{
val value = entity.brain.getMemorySafely(CobblemonMemories.WANDER_CONTROL).orElse(null)
?: CobblemonWanderControl()
if (
!entity.brain.checkMemory(CobblemonMemories.WANDER_CONTROL, MemoryStatus.VALUE_PRESENT) &&
entity.brain.checkMemory(CobblemonMemories.WANDER_CONTROL, MemoryStatus.REGISTERED)
!entity.brain.checkMemory(CobblemonMemories.WANDER_CONTROL, MemoryStatus.VALUE_PRESENT)
&& entity.brain.checkMemory(CobblemonMemories.WANDER_CONTROL, MemoryStatus.REGISTERED)
) {
entity.brain.setMemory(CobblemonMemories.WANDER_CONTROL, value)
}
value.struct
return@put value.struct
}
functions["get_position_memory"] = getPosMemory@{ params ->
map["get_position_memory"] = put@{ params ->
val id = params.getString(0).asIdentifierDefaultingNamespace()
val memoryType = BuiltInRegistries.MEMORY_MODULE_TYPE.get(id)
if (entity.brain.checkMemory(memoryType, MemoryStatus.VALUE_PRESENT)) {
return@getPosMemory when (val memory = entity.brain.getMemory(memoryType).get()) {
is Vec3i -> {
VariableStruct(
mapOf(
"x" to DoubleValue(memory.x),
"y" to DoubleValue(memory.y),
"z" to DoubleValue(memory.z),
),
return@put when (val memory = entity.brain.getMemory(memoryType).get()) {
is Vec3i -> VariableStruct(
mapOf(
"x" to DoubleValue(memory.x),
"y" to DoubleValue(memory.y),
"z" to DoubleValue(memory.z)
)
}
)
is Position -> {
VariableStruct(
mapOf(
"x" to DoubleValue(memory.x()),
"y" to DoubleValue(memory.y()),
"z" to DoubleValue(memory.z()),
),
is Position -> VariableStruct(
mapOf(
"x" to DoubleValue(memory.x()),
"y" to DoubleValue(memory.y()),
"z" to DoubleValue(memory.z())
)
}
)
else -> {
DoubleValue.ZERO
}
else -> DoubleValue.ZERO
}
} else {
return@put DoubleValue.ZERO
}
DoubleValue.ZERO
}
functions["set_uuid_memory"] = setUuidMemory@{ params ->
val memory =
params
.getString(0)
.asIdentifierDefaultingNamespace()
.let(BuiltInRegistries.MEMORY_MODULE_TYPE::get) as MemoryModuleType<UUID>
val uuid = params.getString(1).asUUID ?: return@setUuidMemory DoubleValue.ZERO
map["set_uuid_memory"] = put@{ params ->
val memory = params.getString(0).asIdentifierDefaultingNamespace()
.let(BuiltInRegistries.MEMORY_MODULE_TYPE::get) as MemoryModuleType<UUID>
val uuid = params.getString(1).asUUID ?: return@put DoubleValue.ZERO
val expiry = params.getIntOrNull(2) ?: -1
if (expiry != -1) {
entity.brain.setMemoryWithExpiry(memory, uuid, expiry.toLong())
} else {
entity.brain.setMemory(memory, uuid)
}
DoubleValue.ONE
return@put DoubleValue.ONE
}
functions["erase_memory"] = { params ->
val memories =
params.params
.map { it.asString().asIdentifierDefaultingNamespace() }
.map(BuiltInRegistries.MEMORY_MODULE_TYPE::get)
map["erase_memory"] = put@{ params ->
val memories = params.params.map { it.asString().asIdentifierDefaultingNamespace() }
.map(BuiltInRegistries.MEMORY_MODULE_TYPE::get)
memories.forEach { entity.brain.eraseMemory(it) }
DoubleValue.ONE
return@put DoubleValue.ONE
}
functions["has_memory_value"] = hasMemoryValue@{ params ->
map["has_memory_value"] = put@{ params ->
for (param in params.params) {
if (!entity.hasMemoryFromString(param.asString())) {
return@hasMemoryValue DoubleValue.ZERO
return@put DoubleValue.ZERO
}
}
DoubleValue.ONE
return@put DoubleValue.ONE
}
functions["lacks_memory_value"] = lacksMemoryValue@{ params ->
map["lacks_memory_value"] = put@{ params ->
for (param in params.params) {
if (entity.hasMemoryFromString(param.asString())) {
return@lacksMemoryValue DoubleValue.ZERO
return@put DoubleValue.ZERO
}
}
DoubleValue.ONE
return@put DoubleValue.ONE
}
functions["is_doing_activity"] = isDoingActivity@{ params ->
map["is_doing_activity"] = put@{ params ->
for (param in params.params) {
if (entity.isDoingActivityFromString(param.asString())) {
return@isDoingActivity DoubleValue.ONE
return@put DoubleValue.ONE
}
}
DoubleValue.ZERO
return@put DoubleValue.ZERO
}
functions["can_see"] = canSee@{ params ->
map["can_see"] = put@{ params ->
val target = params.get<MoValue>(0)
val range = params.getDoubleOrNull(1) ?: 32.0
if (target is ObjectValue<*>) {
val targetEntity = target.obj as? LivingEntity ?: return@canSee DoubleValue.ZERO
val targetEntity = target.obj as? LivingEntity ?: return@put DoubleValue.ZERO
val vector = targetEntity.eyePosition.subtract(entity.eyePosition).normalize()
val trace =
entity.traceEntityCollision(
maxDistance = range.toFloat(),
stepDistance = 0.1F,
direction = vector,
entityClass = targetEntity.javaClass,
collideBlock = ClipContext.Fluid.NONE,
val trace = entity.traceEntityCollision(
maxDistance = range.toFloat(),
stepDistance = 0.1F,
direction = vector,
entityClass = targetEntity.javaClass,
collideBlock = ClipContext.Fluid.NONE
)
return@put DoubleValue(targetEntity in (trace?.entities ?: emptyList()))
} else {
return@put DoubleValue.ZERO
}
}
if (entity is PathfinderMob) {
map["walk_to"] = { params ->
val x = params.getDouble(0)
val y = params.getDouble(1)
val z = params.getDouble(2)
val speedMultiplier = params.getDoubleOrNull(3) ?: 0.35
if (entity.brain.checkMemory(MemoryModuleType.WALK_TARGET, MemoryStatus.REGISTERED)) {
entity.brain.setMemory(
MemoryModuleType.WALK_TARGET,
WalkTarget(Vec3(x, y, z), speedMultiplier.toFloat(), 1)
)
return@canSee DoubleValue(targetEntity in (trace?.entities ?: emptyList()))
}
DoubleValue.ZERO
}
when (entity) {
is PathfinderMob -> {
functions["walk_to"] = { params ->
val x = params.getDouble(0)
val y = params.getDouble(1)
val z = params.getDouble(2)
val speedMultiplier = params.getDoubleOrNull(3) ?: 0.35
if (entity.brain.checkMemory(MemoryModuleType.WALK_TARGET, MemoryStatus.REGISTERED)) {
entity.brain.setMemory(
MemoryModuleType.WALK_TARGET,
WalkTarget(Vec3(x, y, z), speedMultiplier.toFloat(), 1),
)
entity.brain.setMemory(
MemoryModuleType.LOOK_TARGET,
BlockPosTracker(Vec3(x, y + entity.eyeHeight, z)),
)
} else {
entity.navigation.moveTo(x, y, z, speedMultiplier)
entity.lookControl.setLookAt(Vec3(x, y + entity.eyeHeight, z))
}
}
functions["has_walk_target"] = {
DoubleValue(entity.brain.getMemorySafely(MemoryModuleType.WALK_TARGET).isPresent || entity.isPathFinding)
}
functions["set_can_float"] = { params ->
val canFloat = params.getBooleanOrNull(0) != false
entity.navigation.setCanFloat(canFloat)
DoubleValue.ONE
}
functions["get_pathfinding_malus"] = getPfMalus@{ params ->
val type = PathType.entries.find { it.name == params.getString(0).uppercase() }
if (type != null) {
return@getPfMalus DoubleValue(entity.getPathfindingMalus(type))
}
Cobblemon.LOGGER.error("Cannot get pathfinding type, unknown value: ${params.getString(0)}")
DoubleValue.ZERO
}
functions["set_pathfinding_malus"] = setPfMalus@{ params ->
val type = PathType.entries.find { it.name == params.getString(0).uppercase() }
val malus = params.getDouble(1).toFloat()
if (type != null) {
entity.setPathfindingMalus(type, malus)
return@setPfMalus DoubleValue.ONE
}
Cobblemon.LOGGER.error("Cannot set pathfinding type, unknown value: ${params.getString(0)}")
DoubleValue.ZERO
entity.brain.setMemory(
MemoryModuleType.LOOK_TARGET,
BlockPosTracker(Vec3(x, y + entity.eyeHeight, z))
)
} else {
entity.navigation.moveTo(x, y, z, speedMultiplier)
entity.lookControl.setLookAt(Vec3(x, y + entity.eyeHeight, z))
}
}
}
return functions
map["has_walk_target"] = { _ ->
DoubleValue(entity.brain.getMemorySafely(MemoryModuleType.WALK_TARGET).isPresent || entity.isPathFinding)
}
map["set_can_float"] = { params ->
val canFloat = params.getBooleanOrNull(0) != false
entity.navigation.setCanFloat(canFloat)
DoubleValue.ONE
}
map["get_pathfinding_malus"] = put@{ params ->
val type = PathType.entries.find { it.name == params.getString(0).uppercase() }
if (type != null) {
return@put DoubleValue(entity.getPathfindingMalus(type))
}
Cobblemon.LOGGER.error("Unknown pathfinding type: ${params.getString(0)}")
return@put DoubleValue.ZERO
}
map["set_pathfinding_malus"] = put@{ params ->
val type = PathType.entries.find { it.name == params.getString(0).uppercase() }
val malus = params.getDouble(1).toFloat()
if (type != null) {
entity.setPathfindingMalus(type, malus)
return@put DoubleValue.ONE
}
Cobblemon.LOGGER.error("Unknown pathfinding type: ${params.getString(0)}")
return@put DoubleValue.ZERO
}
}
return map
}
}
@@ -50,23 +50,18 @@ import kotlin.collections.any
object NPCMoLangFunctions : AbstractMoLangFunctionHolder<NPCEntity>() {
override fun NPCEntity.moLangFunctions(): MutableMap<String, (MoParams) -> Any> {
val npc = this
val functions = hashMapOf<String, (MoParams) -> Any>()
functions["class"] = { StringValue(npc.npc.id.toString()) }
functions["name"] = { StringValue(npc.name.string) }
functions["level"] = { DoubleValue(npc.level) }
functions["has_aspect"] = { params -> DoubleValue(npc.aspects.contains(params.getString(0))) }
functions["in_battle"] = { DoubleValue(npc.isInBattle()) }
functions["battles"] = {
ArrayStruct(
npc.battleIds
.mapNotNull { BattleRegistry.getBattle(it)?.struct }
.mapIndexed { index, value -> "$index" to value }
.toMap(),
)
val map = hashMapOf<String, (MoParams) -> Any>()
map["class"] = { StringValue(npc.npc.id.toString()) }
map["name"] = { StringValue(npc.name.string) }
map["level"] = { DoubleValue(npc.level) }
map["has_aspect"] = { params -> DoubleValue(npc.aspects.contains(params.getString(0))) }
map["in_battle"] = { DoubleValue(npc.isInBattle()) }
map["battles"] = {
ArrayStruct(npc.battleIds.mapNotNull { BattleRegistry.getBattle(it)?.struct }
.mapIndexed { index, value -> "$index" to value }.toMap())
}
functions["stop_battles"] = { npc.battleIds.forEach { BattleRegistry.getBattle(it)?.stop() } }
functions["run_script_on_client"] = { params ->
map["stop_battles"] = { _ -> npc.battleIds.forEach { BattleRegistry.getBattle(it)?.stop() } }
map["run_script_on_client"] = { params ->
val world = npc.level()
if (world is ServerLevel) {
val script = params.getString(0)
@@ -75,142 +70,141 @@ object NPCMoLangFunctions : AbstractMoLangFunctionHolder<NPCEntity>() {
}
Unit
}
functions["run_script"] = { params ->
map["run_script"] = { params ->
val script = params.getString(0).asIdentifierDefaultingNamespace()
val runtime = MoLangRuntime()
runtime.environment.cloneFrom(params.environment)
CobblemonScripts.run(script, runtime) ?: DoubleValue(0)
}
functions["set_movable"] = { params ->
map["set_movable"] = put@{ params ->
val movable = params.getBooleanOrNull(0) != false
npc.isMovable = movable
DoubleValue.ONE
return@put DoubleValue.ONE
}
functions["set_invulnerable"] = { params ->
map["set_invulnerable"] = put@{ params ->
val invulnerable = params.getBooleanOrNull(0) != false
npc.isInvulnerable = invulnerable
DoubleValue.ONE
return@put DoubleValue.ONE
}
functions["set_leashable"] = { params ->
map["set_leashable"] = put@{ params ->
val leashable = params.getBooleanOrNull(0) != false
npc.isLeashable = leashable
DoubleValue.ONE
return@put DoubleValue.ONE
}
functions["set_allow_projectile_hits"] = { params ->
map["set_allow_projectile_hits"] = put@{ params ->
val allowProjectileHits = params.getBooleanOrNull(0) != false
npc.allowProjectileHits = allowProjectileHits
DoubleValue.ONE
return@put DoubleValue.ONE
}
functions["set_name_tag_visible"] = { params ->
map["set_name_tag_visible"] = put@{ params ->
val nameTagVisible = params.getBooleanOrNull(0) != false
npc.hideNameTag = !nameTagVisible
DoubleValue.ONE
return@put DoubleValue.ONE
}
functions["unset_interaction"] = {
map["unset_interaction"] = put@{
npc.interaction = null
DoubleValue.ONE
return@put DoubleValue.ONE
}
functions["set_dialogue_interaction"] = { params ->
map["set_dialogue_interaction"] = put@{ params ->
val dialogue = params.getString(0).asIdentifierDefaultingNamespace()
npc.interaction =
DialogueNPCInteractionConfiguration().also {
it.dialogue = dialogue
}
DoubleValue.ONE
npc.interaction = DialogueNPCInteractionConfiguration().also {
it.dialogue = dialogue
}
return@put DoubleValue.ONE
}
functions["set_script_interaction"] = { params ->
map["set_script_interaction"] = put@{ params ->
val script = params.getString(0).asIdentifierDefaultingNamespace()
npc.interaction =
ScriptNPCInteractionConfiguration().also {
it.script = script
}
DoubleValue.ONE
npc.interaction = ScriptNPCInteractionConfiguration().also {
it.script = script
}
return@put DoubleValue.ONE
}
functions["set_player_texture"] = setPlayerTexture@{ params ->
map["set_player_texture"] = put@{ params ->
val username = params.getString(0)
// Re-applying it would be unnecessarily laggy.
if (username == npc.data.map["player_texture_username"]?.asString()) {
return@setPlayerTexture DoubleValue.ZERO
return@put DoubleValue.ZERO
}
npc.loadTextureFromGameProfileName(username)
DoubleValue.ONE
return@put DoubleValue.ONE
}
functions["unset_player_texture"] = {
map["unset_player_texture"] = put@{
npc.unloadTexture()
DoubleValue.ONE
return@put DoubleValue.ONE
}
functions["set_resource_identifier"] = { params ->
map["set_resource_identifier"] = put@{ params ->
val identifier = params.getStringOrNull(0)?.asIdentifierDefaultingNamespace()
npc.forcedResourceIdentifier = identifier
DoubleValue.ONE
return@put DoubleValue.ONE
}
functions["unset_resource_identifier"] = {
map["unset_resource_identifier"] = put@{
npc.forcedResourceIdentifier = null
DoubleValue.ONE
return@put DoubleValue.ONE
}
functions["set_class"] = setClass@{ params ->
map["set_class"] = put@{ params ->
val identifier = params.getString(0).asIdentifierDefaultingNamespace()
val npcClass = NPCClasses.getByIdentifier(identifier)
if (npcClass != null) {
npc.npc = npcClass
return@setClass DoubleValue.ONE
return@put DoubleValue.ONE
} else {
Cobblemon.LOGGER.error("Unknown NPC class: $identifier")
return@put DoubleValue.ZERO
}
Cobblemon.LOGGER.error("Unknown NPC class: $identifier")
DoubleValue.ZERO
}
functions["set_render_scale"] = { params ->
map["set_render_scale"] = put@{ params ->
val scale = params.getDouble(0)
npc.renderScale = scale.toFloat()
DoubleValue.ONE
return@put DoubleValue.ONE
}
functions["render_scale"] = { DoubleValue(npc.renderScale) }
functions["set_hitbox_scale"] = { params ->
map["render_scale"] = { _ -> DoubleValue(npc.renderScale) }
map["set_hitbox_scale"] = put@{ params ->
val scale = params.getDouble(0).toFloat()
npc.hitboxScale = scale
npc.refreshDimensions()
DoubleValue.ONE
return@put DoubleValue.ONE
}
functions["hitbox_scale"] = { DoubleValue(npc.hitboxScale) }
functions["set_hitbox"] = setHitbox@{ params ->
map["hitbox_scale"] = { _ -> DoubleValue(npc.hitboxScale) }
map["set_hitbox"] = put@{ params ->
if (params.params.isEmpty()) {
npc.hitbox = null
return@setHitbox DoubleValue.ONE
return@put DoubleValue.ONE
}
val width = params.getDouble(0).toFloat()
val height = params.getDouble(1).toFloat()
val eyeHeight = params.getDoubleOrNull(2)?.toFloat() ?: (height * 0.85F)
npc.hitbox = EntityDimensions.scalable(width, height).withEyeHeight(eyeHeight)
DoubleValue.ONE
return@put DoubleValue.ONE
}
functions["unset_hitbox"] = {
map["unset_hitbox"] = put@{
npc.hitbox = null
DoubleValue.ONE
return@put DoubleValue.ONE
}
functions["aspects"] = {
map["aspects"] = put@{
val aspects = npc.aspects
aspects.asArrayValue { StringValue(it) }
return@put aspects.asArrayValue { StringValue(it) }
}
functions["add_aspect"] = { params ->
map["add_aspect"] = put@{ params ->
val aspects = params.params.map { it.asString() }
npc.appliedAspects.addAll(aspects)
npc.updateAspects()
DoubleValue.ONE
return@put DoubleValue.ONE
}
functions["remove_aspect"] = { params ->
map["remove_aspect"] = put@{ params ->
val aspects = params.params.map { it.asString() }
npc.appliedAspects.removeAll(aspects.toSet())
npc.updateAspects()
DoubleValue.ONE
return@put DoubleValue.ONE
}
functions["party"] = party@{
val party = npc.party ?: return@party DoubleValue.ZERO
party.asMoLangValue()
map["party"] = put@{
val party = npc.party ?: return@put DoubleValue.ZERO
return@put party.asMoLangValue()
}
functions["create_npc_party"] = {
map["create_npc_party"] = put@{
val party = NPCPartyStore(npc)
party.asMoLangValue()
return@put party.asMoLangValue()
}
functions["set_npc_party"] = { params ->
map["set_npc_party"] = { params ->
val party = params.get<MoValue>(0)
if (party is ObjectValue<*>) {
npc.party = party.obj as NPCPartyStore
@@ -219,19 +213,18 @@ object NPCMoLangFunctions : AbstractMoLangFunctionHolder<NPCEntity>() {
}
DoubleValue.ONE
}
functions["run_action_effect"] = runActionEffect@{ params ->
map["run_action_effect"] = put@{ params ->
val runtime = MoLangRuntime().setup()
runtime.environment.cloneFrom(params.environment)
runtime.withNPCValue(value = npc)
val actionEffect = ActionEffects.actionEffects[params.getString(0).asIdentifierDefaultingNamespace()]
if (actionEffect != null) {
val context =
ActionEffectContext(
actionEffect = actionEffect,
providers = mutableListOf(NPCProvider(npc)),
runtime = runtime,
level = npc.level(),
)
val context = ActionEffectContext(
actionEffect = actionEffect,
providers = mutableListOf(NPCProvider(npc)),
runtime = runtime,
level = npc.level()
)
npc.actionEffect = context
npc.brain.setMemory(CobblemonMemories.ACTIVE_ACTION_EFFECT, context)
npc.brain.setActiveActivityIfPossible(CobblemonActivities.ACTION_EFFECT)
@@ -243,40 +236,37 @@ object NPCMoLangFunctions : AbstractMoLangFunctionHolder<NPCEntity>() {
}
}
return@runActionEffect DoubleValue.ONE
return@put DoubleValue.ONE
}
DoubleValue.ZERO
return@put DoubleValue.ZERO
}
functions["put_pokemon_in_healer"] = putPokeInHealer@{ params ->
val healer =
npc
.level()
.getBlockEntity(params.get<ArrayStruct>(0).asBlockPos(), CobblemonBlockEntities.HEALING_MACHINE)
.orElse(null) ?: return@putPokeInHealer DoubleValue.ZERO
val party = npc.party ?: return@putPokeInHealer DoubleValue.ZERO
map["put_pokemon_in_healer"] = put@{ params ->
val healer = npc.level()
.getBlockEntity(params.get<ArrayStruct>(0).asBlockPos(), CobblemonBlockEntities.HEALING_MACHINE)
.orElse(null) ?: return@put DoubleValue.ZERO
val party = npc.party ?: return@put DoubleValue.ZERO
if (healer.canHeal(party)) {
healer.activate(npc.uuid, party)
return@putPokeInHealer DoubleValue.ONE
return@put DoubleValue.ONE
} else {
return@put DoubleValue.ZERO
}
DoubleValue.ZERO
}
functions["look_at_position"] = { params ->
map["look_at_position"] = { params ->
val pos = Vec3(params.getDouble(0), params.getDouble(1), params.getDouble(2))
npc.lookAt(EntityAnchorArgument.Anchor.EYES, pos)
}
functions["environment"] = { npc.runtime.environment }
functions["party"] = { npc.party?.struct ?: DoubleValue.ZERO }
functions["has_party"] = { DoubleValue(npc.party != null) }
functions["is_npc"] = { DoubleValue.ONE }
functions["can_battle"] = {
DoubleValue(npc.party?.any { it.currentHealth > 0 } == true || npc.npc.party?.isStatic == false)
}
functions["set_battle_theme"] = { params ->
map["environment"] = { _ -> npc.runtime.environment }
map["party"] = { npc.party?.struct ?: DoubleValue.ZERO }
map["has_party"] = { DoubleValue(npc.party != null) }
map["is_npc"] = { DoubleValue.ONE }
map["can_battle"] =
{ DoubleValue(npc.party?.any { it.currentHealth > 0 } == true || npc.npc.party?.isStatic == false) }
map["set_battle_theme"] = put@{ params ->
val soundId = params.getString(0).asResource()
npc.npc.battleTheme = soundId
DoubleValue.ONE
return@put DoubleValue.ONE
}
return functions
return map
}
}
@@ -23,40 +23,40 @@ import com.cobblemon.mod.common.util.getBooleanOrNull
object PCMoLangFunctions : AbstractMoLangFunctionHolder<PCStore>() {
override fun PCStore.moLangFunctions(): MutableMap<String, (MoParams) -> Any> {
val pc = this
val functions = hashMapOf<String, (MoParams) -> Any>()
val map = mutableMapOf<String, (MoParams) -> Any>()
functions["get_pokemon"] = getPokemon@{ params ->
map["get_pokemon"] = getPokemon@{ params ->
val box = params.getInt(0)
val slot = params.getInt(1)
val pokemon = pc[PCPosition(box, slot)] ?: return@getPokemon DoubleValue.ZERO
pokemon.struct
return@getPokemon pokemon.struct
}
functions["set_pokemon"] = { params ->
map["set_pokemon"] = { params ->
val box = params.getInt(0)
val slot = params.getInt(1)
val pokemon = params.get<ObjectValue<Pokemon>>(2).obj
pc[PCPosition(box, slot)] = pokemon
DoubleValue.ONE
}
functions["resize"] = { params ->
map["resize"] = { params ->
val newSize = params.getInt(0)
val lockNewSize = params.getBooleanOrNull(1) == true
pc.resize(newSize, lockNewSize)
DoubleValue.ONE
}
functions["get_box_count"] = { DoubleValue(pc.boxes.size.toDouble()) }
functions["has_unlocked_wallpaper"] = { params ->
map["get_box_count"] = { DoubleValue(pc.boxes.size.toDouble()) }
map["has_unlocked_wallpaper"] = { params ->
val wallpaper = params.getString(0).asIdentifierDefaultingNamespace()
DoubleValue(pc.unlockedWallpapers.contains(wallpaper))
}
functions["get_unlocked_wallpapers"] = { pc.unlockedWallpapers.asArrayValue { StringValue(it.toString()) } }
functions["unlock_wallpaper"] = unlockWallpaper@{
map["get_unlocked_wallpapers"] = { pc.unlockedWallpapers.asArrayValue { StringValue(it.toString()) } }
map["unlock_wallpaper"] = unlockWallpaper@{
val wallpaper = it.getString(0).asIdentifierDefaultingNamespace()
val playSound = it.getBooleanOrNull(1) != false
CobblemonUnlockableWallpapers.unlockableWallpapers[wallpaper] ?: return@unlockWallpaper DoubleValue.ZERO
DoubleValue(pc.unlockWallpaper(wallpaper, playSound))
return@unlockWallpaper DoubleValue(pc.unlockWallpaper(wallpaper, playSound))
}
return functions
return map
}
}
@@ -14,23 +14,21 @@ import com.cobblemon.mod.common.api.molang.ObjectValue
import com.cobblemon.mod.common.api.storage.party.PartyStore
import com.cobblemon.mod.common.pokemon.Pokemon
object PartyMoLangFunctions : AbstractMoLangFunctionHolder<PartyStore>() {
object PartyMoLangFunctions: AbstractMoLangFunctionHolder<PartyStore>() {
override fun PartyStore.moLangFunctions(): MutableMap<String, (MoParams) -> Any> {
val party = this
val functions = hashMapOf<String, (MoParams) -> Any>()
functions["get_pokemon"] = getPokemon@{ params ->
val map = hashMapOf<String, (MoParams) -> Any>()
map["get_pokemon"] = getPokemon@{ params ->
val index = params.getInt(0)
val pokemon = party.get(index) ?: return@getPokemon DoubleValue.ZERO
pokemon.struct
}
functions["set_pokemon"] = { params ->
map["set_pokemon"] = { params ->
val index = params.getInt(0)
val pokemon = params.get<ObjectValue<Pokemon>>(1).obj
party.set(index, pokemon)
DoubleValue.ONE
}
return functions
return map
}
}
@@ -17,12 +17,12 @@ import com.bedrockk.molang.runtime.value.MoValue
import com.bedrockk.molang.runtime.value.StringValue
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.CobblemonBlockEntities
import com.cobblemon.mod.common.api.moves.Moves
import com.cobblemon.mod.common.api.tms.TechnicalMachines
import com.cobblemon.mod.common.api.dialogue.PlayerDialogueFaceProvider
import com.cobblemon.mod.common.api.molang.MoLangFunctions.asMoLangValue
import com.cobblemon.mod.common.api.molang.ObjectValue
import com.cobblemon.mod.common.api.moves.Moves
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.api.tms.TechnicalMachines
import com.cobblemon.mod.common.battles.BattleBuilder
import com.cobblemon.mod.common.battles.BattleFormat
import com.cobblemon.mod.common.entity.npc.NPCEntity
@@ -52,389 +52,355 @@ import net.minecraft.core.registries.Registries
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import net.minecraft.sounds.SoundSource
import net.minecraft.stats.Stats
import net.minecraft.world.entity.player.Player
import net.minecraft.world.item.ItemStack
import net.minecraft.world.level.GameType
import kotlin.collections.iterator
object PlayerMoLangFunctions : AbstractMoLangFunctionHolder<Player>() {
override fun Player.moLangFunctions(): MutableMap<String, (MoParams) -> Any> {
val player = this
val functions = hashMapOf<String, (MoParams) -> Any>()
functions["username"] = { StringValue(player.gameProfile.name) }
functions["uuid"] = { StringValue(player.gameProfile.id.toString()) }
functions["main_held_item"] = { player.mainHandItem.asMoLangValue(player.registryAccess()) }
functions["off_held_item"] = { player.offhandItem.asMoLangValue(player.registryAccess()) }
functions["inventory"] = {
val map = mutableMapOf<String, (MoParams) -> Any>()
map["username"] = { _ -> StringValue(player.gameProfile.name) }
map["uuid"] = { _ -> StringValue(player.gameProfile.id.toString()) }
map["main_held_item"] = { _ -> player.mainHandItem.asMoLangValue(player.registryAccess()) }
map["off_held_item"] = { _ -> player.offhandItem.asMoLangValue(player.registryAccess()) }
map["inventory"] = put@{ _ ->
val inventory = player.inventory
val items = ArrayStruct(hashMapOf())
for (i in 0 until inventory.containerSize) {
items.setDirectly("$i", inventory.getItem(i).asMoLangValue(player.registryAccess()))
}
items
return@put items
}
functions["has_inventory_space"] = {
map["has_inventory_space"] = put@{ _ ->
val inventory = player.inventory
if (inventory.freeSlot != -1) DoubleValue.ONE else DoubleValue.ZERO
return@put if (inventory.getFreeSlot() != -1) DoubleValue.ONE else DoubleValue.ZERO
}
functions["set_inventory_slot"] = setInvSlot@{ params ->
map["set_inventory_slot"] = put@{ params ->
val slot = params.getInt(0)
val inventory = player.inventory
if (slot !in 0 until inventory.containerSize) return@setInvSlot DoubleValue.ZERO
if (slot !in 0 until inventory.containerSize) return@put DoubleValue.ZERO
val value = params.getOrNull<MoValue>(1) ?: return@setInvSlot DoubleValue.ZERO
val value = params.getOrNull<MoValue>(1) ?: return@put DoubleValue.ZERO
val stack: ItemStack? =
when (value) {
is ObjectValue<*> -> {
value.obj as? ItemStack
}
is StringValue -> {
val id = ResourceLocation.parse(value.value)
val item =
player.registryAccess().registryOrThrow(Registries.ITEM).get(id)
?: return@setInvSlot DoubleValue.ZERO
ItemStack(item)
}
else -> {
null
}
val stack: ItemStack? = when (value) {
is ObjectValue<*> -> value.obj as? ItemStack
is StringValue -> {
val id = ResourceLocation.parse(value.value)
val item = player.registryAccess().registryOrThrow(Registries.ITEM).get(id)
?: return@put DoubleValue.ZERO
ItemStack(item)
}
if (stack == null) return@setInvSlot DoubleValue.ZERO
else -> null
}
if (stack == null) return@put DoubleValue.ZERO
inventory.setItem(slot, stack)
DoubleValue.ONE
return@put DoubleValue.ONE
}
functions["give_item"] = giveItem@{ params ->
map["give_item"] = put@{ params ->
val inventory = player.inventory
val freeSlot = inventory.freeSlot
val value = params.getOrNull<MoValue>(0) ?: return@giveItem DoubleValue.ZERO
val freeSlot = inventory.getFreeSlot()
val value = params.getOrNull<MoValue>(0) ?: return@put DoubleValue.ZERO
val dropIfFull = params.getBooleanOrNull(1) ?: false
val stack: ItemStack? =
when (value) {
is ObjectValue<*> -> {
value.obj as? ItemStack
}
is StringValue -> {
val id = ResourceLocation.parse(value.value)
val item =
player.registryAccess().registryOrThrow(Registries.ITEM).get(id)
?: return@giveItem DoubleValue.ZERO
ItemStack(item)
}
else -> {
null
}
val stack: ItemStack? = when (value) {
is ObjectValue<*> -> value.obj as? ItemStack
is StringValue -> {
val id = ResourceLocation.parse(value.value)
val item = player.registryAccess().registryOrThrow(Registries.ITEM).get(id)
?: return@put DoubleValue.ZERO
ItemStack(item)
}
if (stack == null) return@giveItem DoubleValue.ZERO
else -> null
}
if (stack == null) return@put DoubleValue.ZERO
if (freeSlot == -1) {
if (dropIfFull) {
player.drop(stack, false)
return@giveItem DoubleValue.ONE
return@put DoubleValue.ONE
} else {
return@giveItem DoubleValue.ZERO
return@put DoubleValue.ZERO
}
}
inventory.setItem(freeSlot, stack)
DoubleValue.ONE
return@put DoubleValue.ONE
}
functions["face"] = { params ->
map["face"] = { params ->
ObjectValue(
PlayerDialogueFaceProvider(
player.uuid,
params.getBooleanOrNull(0) != false,
),
params.getBooleanOrNull(0) != false
)
)
}
functions["swing_hand"] = { player.swing(player.usedItemHand) }
functions["food_level"] = { DoubleValue(player.foodData.foodLevel) }
functions["saturation_level"] = { DoubleValue(player.foodData.saturationLevel) }
functions["tell"] = { params ->
map["swing_hand"] = { _ -> player.swing(player.usedItemHand) }
map["food_level"] = { _ -> DoubleValue(player.foodData.foodLevel) }
map["saturation_level"] = { _ -> DoubleValue(player.foodData.saturationLevel) }
map["tell"] = { params ->
val message = params.getString(0).text()
val overlay = params.getBooleanOrNull(1) == true
player.displayClientMessage(message, overlay)
}
functions["teleport"] = { params ->
map["teleport"] = { params ->
val x = params.getDouble(0)
val y = params.getDouble(1)
val z = params.getDouble(2)
val playParticleOptions = params.getBooleanOrNull(3) ?: false
player.randomTeleport(x, y, z, playParticleOptions)
val playParticleOptionss = params.getBooleanOrNull(3) ?: false
player.randomTeleport(x, y, z, playParticleOptionss)
}
functions["heal"] = { params ->
map["heal"] = { params ->
val amount = params.getDoubleOrNull(0) ?: player.maxHealth
player.heal(amount.toFloat())
}
functions["environment"] = {
map["environment"] = {
val environment = MoLangEnvironment()
environment.query = player.asMoLangValue()
environment
}
functions["is_player"] = { DoubleValue.ONE }
functions["riding_pokemon"] = ridingPokemon@{
map["is_player"] = { DoubleValue.ONE }
map["riding_pokemon"] = put@{
val vehicle = player.vehicle
if (vehicle is PokemonEntity) {
return@ridingPokemon vehicle.struct
return@put vehicle.struct
} else {
return@put DoubleValue.ZERO
}
DoubleValue.ZERO
}
when (player) {
is ServerPlayer -> {
functions["seen_credits"] = { DoubleValue(player.seenCredits) }
functions["is_in_dialogue"] = { DoubleValue(player.isInDialogue) }
functions["active_dialogue"] = activeDialogue@{
if (player.isInDialogue) {
return@activeDialogue player.activeDialogue?.toMoLangStruct() ?: DoubleValue.ZERO
}
if (player is ServerPlayer) {
map["seen_credits"] = { _ ->
DoubleValue(player.seenCredits)
}
map["is_in_dialogue"] = { _ ->
DoubleValue(player.isInDialogue)
}
map["active_dialogue"] = put@{ _ ->
if (player.isInDialogue) {
return@put player.activeDialogue?.toMoLangStruct() ?: DoubleValue.ZERO
} else {
DoubleValue.ZERO
}
functions["is_spectator"] = { DoubleValue(player.isSpectator) }
functions["is_creative"] = { DoubleValue(player.isCreative) }
functions["is_survival"] = { DoubleValue(player.gameMode.isSurvival) }
functions["is_adventure"] = { DoubleValue(player.gameMode.gameModeForPlayer == GameType.ADVENTURE) }
functions["run_command"] = { params ->
val command = params.getString(0)
player.server.commands.performPrefixedCommand(player.createCommandSourceStack(), command)
}
functions["set_battle_theme"] = { params ->
val soundId = params.getString(0).asResource()
Cobblemon.playerDataManager.getGenericData(player).battleTheme = soundId
DoubleValue.ONE
}
functions["battle_music"] = battleMusic@{ params ->
val soundId = params.getString(0).asResource()
val volume = params.getDoubleOrNull(1)?.toFloat() ?: 1.0f
val pitch = params.getDoubleOrNull(2)?.toFloat() ?: 1.0f
val restart = params.getBooleanOrNull(3) ?: true
}
map["is_spectator"] = { DoubleValue(player.isSpectator) }
map["is_creative"] = { DoubleValue(player.isCreative) }
map["is_survival"] = { DoubleValue(player.gameMode.isSurvival) }
map["is_adventure"] = { DoubleValue(player.gameMode.gameModeForPlayer == GameType.ADVENTURE) }
map["run_command"] = { params ->
val command = params.getString(0)
player.server.commands.performPrefixedCommand(player.createCommandSourceStack(), command)
}
map["set_battle_theme"] = put@{ params ->
val soundId = params.getString(0).asResource()
Cobblemon.playerDataManager.getGenericData(player).battleTheme = soundId
return@put DoubleValue.ONE
}
map["battle_music"] = put@{ params ->
val soundId = params.getString(0).asResource()
val volume = params.getDoubleOrNull(1)?.toFloat() ?: 1.0f
val pitch = params.getDoubleOrNull(2)?.toFloat() ?: 1.0f
val restart = params.getBooleanOrNull(3) ?: true
if (soundId != null) {
val packet = BattleMusicPacket(soundId, volume, pitch, restart)
packet.sendToPlayer(player)
return@battleMusic DoubleValue.ONE
}
DoubleValue.ZERO
}
functions["stop_battle_music"] = {
val packet = BattleMusicPacket(null)
if (soundId != null) {
val packet = BattleMusicPacket(soundId, volume, pitch, restart)
packet.sendToPlayer(player)
DoubleValue.ONE
return@put DoubleValue.ONE
} else {
return@put DoubleValue.ZERO
}
functions["play_sound_on_server"] = { params ->
val sound = params.getString(0).asResource()
val soundSource = SoundSource.valueOf(params.getString(1).uppercase())
val volume = params.getDoubleOrNull(2)?.toFloat() ?: 1.0f
val pitch = params.getDoubleOrNull(3)?.toFloat() ?: 1.0f
}
map["stop_battle_music"] = put@{ _ ->
val packet = BattleMusicPacket(null)
packet.sendToPlayer(player)
return@put DoubleValue.ONE
}
map["play_sound_on_server"] = { params ->
val sound = params.getString(0).asResource()
val soundSource = SoundSource.valueOf(params.getString(1).uppercase())
val volume = params.getDoubleOrNull(2)?.toFloat() ?: 1.0f
val pitch = params.getDoubleOrNull(3)?.toFloat() ?: 1.0f
val packet =
UnvalidatedPlaySoundS2CPacket(
sound,
soundSource,
player.x,
player.y,
player.z,
volume,
pitch,
)
packet.sendToPlayer(player)
val packet =
UnvalidatedPlaySoundS2CPacket(sound, soundSource, player.x, player.y, player.z, volume, pitch)
packet.sendToPlayer(player)
}
map["is_party_at_full_health"] = { _ ->
DoubleValue(player.party().none(Pokemon::canBeHealed))
}
map["can_heal_at_healer"] = put@{ params ->
val pos = params.get<ArrayStruct>(0).asBlockPos()
val healer = player.level().getBlockEntity(pos, CobblemonBlockEntities.HEALING_MACHINE).orElse(null)
?: return@put DoubleValue.ZERO
val party = player.party()
return@put DoubleValue(healer.canHeal(party))
}
map["put_pokemon_in_healer"] = put@{ params ->
val healer = player.level()
.getBlockEntity(params.get<ArrayStruct>(0).asBlockPos(), CobblemonBlockEntities.HEALING_MACHINE)
.orElse(null) ?: return@put DoubleValue.ZERO
val party = player.party()
if (healer.canHeal(party)) {
healer.activate(player.uuid, party)
return@put DoubleValue.ONE
} else {
return@put DoubleValue.ZERO
}
functions["is_party_at_full_health"] = { DoubleValue(player.party().none(Pokemon::canBeHealed)) }
functions["can_heal_at_healer"] = canHealAtHealer@{ params ->
val pos = params.get<ArrayStruct>(0).asBlockPos()
val healer =
player.level().getBlockEntity(pos, CobblemonBlockEntities.HEALING_MACHINE).orElse(null)
?: return@canHealAtHealer DoubleValue.ZERO
val party = player.party()
DoubleValue(healer.canHeal(party))
}
functions["put_pokemon_in_healer"] = putPokemonAtHealer@{ params ->
val healer =
player
.level()
.getBlockEntity(
params.get<ArrayStruct>(0).asBlockPos(),
CobblemonBlockEntities.HEALING_MACHINE,
).orElse(null) ?: return@putPokemonAtHealer DoubleValue.ZERO
val party = player.party()
if (healer.canHeal(party)) {
healer.activate(player.uuid, party)
return@putPokemonAtHealer DoubleValue.ONE
}
DoubleValue.ZERO
}
functions["party"] = { player.party().struct }
functions["pc"] = { player.pc().struct }
functions["has_permission"] = { params ->
DoubleValue(
Cobblemon.permissionValidator.hasPermission(
player,
params.getString(0),
params.getIntOrNull(1) ?: 4,
),
}
map["party"] = { player.party().struct }
map["pc"] = { player.pc().struct }
map["has_permission"] = { params ->
DoubleValue(
Cobblemon.permissionValidator.hasPermission(
player,
params.getString(0),
params.getIntOrNull(1) ?: 4
)
}
functions["data"] = { params -> Cobblemon.molangData.load(player.uuid, params.getStringOrNull(0)) }
functions["save_data"] = { params -> Cobblemon.molangData.save(player.uuid, params.getStringOrNull(0)) }
functions["in_battle"] = { DoubleValue(player.isInBattle()) }
functions["battle"] = { player.getBattleState()?.first?.struct ?: DoubleValue.ZERO }
functions["get_npc_data"] = getNpcData@{ params ->
val npcId =
((params.get<MoValue>(0) as? ObjectValue<*>)?.obj as? NPCEntity)?.stringUUID
?: params.getString(0)
val data = Cobblemon.molangData.load(player.uuid, params.getStringOrNull(1))
if (data.map.containsKey(npcId)) {
return@getNpcData data.map[npcId]!!
}
)
}
map["data"] = { params -> Cobblemon.molangData.load(player.uuid, params.getStringOrNull(0)) }
map["save_data"] = { params -> Cobblemon.molangData.save(player.uuid, params.getStringOrNull(0)) }
map["in_battle"] = { DoubleValue(player.isInBattle()) }
map["battle"] = { player.getBattleState()?.first?.struct ?: DoubleValue.ZERO }
map["get_npc_data"] = put@{ params ->
val npcId = ((params.get<MoValue>(0) as? ObjectValue<*>)?.obj as? NPCEntity)?.stringUUID
?: params.getString(0)
val data = Cobblemon.molangData.load(player.uuid, params.getStringOrNull(1))
if (data.map.containsKey(npcId)) {
return@put data.map[npcId]!!
} else {
val vars = VariableStruct()
data.map[npcId] = vars
vars
}
functions["get_npc_variable"] = getNpcVariable@{ params ->
val npcId =
((params.get<MoValue>(0) as? ObjectValue<*>)?.obj as? NPCEntity)?.stringUUID
?: params.getString(0)
val variable = params.getString(1)
val data = Cobblemon.molangData.load(player.uuid, params.getStringOrNull(2))
if (data.map.containsKey(npcId)) {
return@getNpcVariable (data.map[npcId] as VariableStruct).map[variable] ?: DoubleValue.ZERO
}
DoubleValue.ZERO
}
functions["set_npc_variable"] = { params ->
val npcId =
((params.get<MoValue>(0) as? ObjectValue<*>)?.obj as? NPCEntity)?.stringUUID
?: params.getString(0)
val variable = params.getString(1)
val value = params.get<MoValue>(2)
val saveAfterwards = params.getBooleanOrNull(3) != false
val path = params.getStringOrNull(4)
val data = Cobblemon.molangData.load(player.uuid, path)
val npcData = data.map.getOrPut(npcId) { VariableStruct() } as VariableStruct
npcData.map[variable] = value
if (saveAfterwards) {
Cobblemon.molangData.save(player.uuid, path)
}
DoubleValue.ONE
}
functions["pokedex"] = { player.pokedex().struct }
functions["has_advancement"] = hasAdvancement@{ params ->
val requiredAdvancement = ResourceLocation.parse(params.getString(0))
player.advancements.progress.forEach { (holder, progress) ->
if (holder.id == requiredAdvancement && progress.isDone) {
return@hasAdvancement DoubleValue.ONE
}
}
DoubleValue.ZERO
}
functions["has_tm_move_unlocked"] = hasTmMoveUnlocked@{ params ->
val move = Moves.getByName(params.getString(0)) ?: return@hasTmMoveUnlocked DoubleValue.ZERO
val tm = TechnicalMachines.moveToTM[move] ?: return@hasTmMoveUnlocked DoubleValue.ZERO
DoubleValue(if (player.tmList().learnedTMs.contains(tm.id)) 1.0 else 0.0)
}
functions["unlock_tm_move"] = unlockTmMove@{ params ->
val move = Moves.getByName(params.getString(0)) ?: return@unlockTmMove DoubleValue.ZERO
val tm = TechnicalMachines.moveToTM[move] ?: return@unlockTmMove DoubleValue.ZERO
player.tmList().syncTMFromMove(move)
DoubleValue(if (player.tmList().learnedTMs.contains(tm.id)) 1.0 else 0.0)
}
functions["lock_tm_move"] = lockTmMove@{ params ->
val move = Moves.getByName(params.getString(0)) ?: return@lockTmMove DoubleValue.ZERO
val tm = TechnicalMachines.moveToTM[move] ?: return@lockTmMove DoubleValue.ZERO
player.tmList().syncTMLockFromMove(move)
DoubleValue(if (player.tmList().learnedTMs.contains(tm.id)) 0.0 else 1.0)
}
functions["start_battle"] = startBattle@{ params ->
val opponentValue = params.get<MoValue>(0)
val opponent =
if (opponentValue is ObjectValue<*>) {
opponentValue.obj as ServerPlayer
} else {
val paramString = opponentValue.asString()
val playerUUID = paramString.asUUID
if (playerUUID != null) {
server()?.playerList?.getPlayer(playerUUID) ?: return@startBattle DoubleValue.ZERO
} else {
server()?.playerList?.getPlayerByName(paramString)
?: return@startBattle DoubleValue.ZERO
}
}
val format =
params
.getStringOrNull(1)
?.let(BattleFormat::fromFormatIdentifier)
?: BattleFormat.GEN_9_SINGLES
val setLevel = params.getIntOrNull(2) ?: -1
format.adjustLevel = setLevel
val rules =
params
.getStringOrNull(5)
?.split(",")
?.toSet()
?: emptySet()
val modifiedBattleFormat =
BattleFormat.setBattleRules(
battleFormat = format,
rules = rules,
)
val cloneParties = (setLevel != -1) || (params.getBooleanOrNull(3) ?: false)
val healFirst = params.getBooleanOrNull(4) ?: false
val battleStartResult =
BattleBuilder.pvp1v1(
player1 = player,
player2 = opponent,
battleFormat = modifiedBattleFormat,
cloneParties = cloneParties,
healFirst = healFirst,
)
var returnValue: MoValue = DoubleValue.ZERO
battleStartResult.ifSuccessful { returnValue = it.struct }
returnValue
}
functions["get_custom_stat"] = getCustomStat@{ params ->
val statName = params.getString(0)
val resourceLocation = ResourceLocation.tryParse(statName) ?: return@getCustomStat DoubleValue.ZERO
val statResourceLocation =
BuiltInRegistries.CUSTOM_STAT.get(resourceLocation) ?: return@getCustomStat DoubleValue.ZERO
val exists = Stats.CUSTOM.contains(statResourceLocation)
if (!exists) return@getCustomStat DoubleValue.ZERO
val stat = Stats.CUSTOM.get(statResourceLocation)
val value = player.stats.getValue(stat)
DoubleValue(value)
}
functions["get_starter_uuid"] = {
Cobblemon.playerDataManager.getGenericData(player).starterUUID?.let {
StringValue(
it.toString(),
)
} ?: DoubleValue.ZERO
}
functions["has_chosen_starter"] = {
DoubleValue(Cobblemon.playerDataManager.getGenericData(player).starterSelected)
return@put vars
}
}
map["get_npc_variable"] = put@{ params ->
val npcId = ((params.get<MoValue>(0) as? ObjectValue<*>)?.obj as? NPCEntity)?.stringUUID
?: params.getString(0)
val variable = params.getString(1)
val data = Cobblemon.molangData.load(player.uuid, params.getStringOrNull(2))
if (data.map.containsKey(npcId)) {
return@put (data.map[npcId] as VariableStruct).map[variable] ?: DoubleValue.ZERO
} else {
return@put DoubleValue.ZERO
}
}
map["set_npc_variable"] = put@{ params ->
val npcId = ((params.get<MoValue>(0) as? ObjectValue<*>)?.obj as? NPCEntity)?.stringUUID
?: params.getString(0)
val variable = params.getString(1)
val value = params.get<MoValue>(2)
val saveAfterwards = params.getBooleanOrNull(3) != false
val path = params.getStringOrNull(4)
val data = Cobblemon.molangData.load(player.uuid, path)
val npcData = data.map.getOrPut(npcId) { VariableStruct() } as VariableStruct
npcData.map[variable] = value
if (saveAfterwards) {
Cobblemon.molangData.save(player.uuid, path)
}
return@put DoubleValue.ONE
}
map["pokedex"] = { player.pokedex().struct }
map["has_advancement"] = put@{ params ->
val requiredAdvancement = ResourceLocation.parse(params.getString(0))
for (entry in player.advancements.progress) {
if (entry.key.id == requiredAdvancement && entry.value.isDone) {
return@put DoubleValue.ONE
}
}
return@put DoubleValue.ZERO
}
map["has_tm_move_unlocked"] = put@{ params ->
val move = Moves.getByName(params.getString(0)) ?: return@put DoubleValue.ZERO
val tm = TechnicalMachines.moveToTM[move] ?: return@put DoubleValue.ZERO
DoubleValue(if (player.tmList().learnedTMs.contains(tm.id)) 1.0 else 0.0)
}
map["unlock_tm_move"] = put@{ params ->
val move = Moves.getByName(params.getString(0)) ?: return@put DoubleValue.ZERO
val tm = TechnicalMachines.moveToTM[move] ?: return@put DoubleValue.ZERO
player.tmList().syncTMFromMove(move)
DoubleValue(if (player.tmList().learnedTMs.contains(tm.id)) 1.0 else 0.0)
}
map["lock_tm_move"] = put@{ params ->
val move = Moves.getByName(params.getString(0)) ?: return@put DoubleValue.ZERO
val tm = TechnicalMachines.moveToTM[move] ?: return@put DoubleValue.ZERO
player.tmList().syncTMLockFromMove(move)
DoubleValue(if (player.tmList().learnedTMs.contains(tm.id)) 0.0 else 1.0)
}
map["start_battle"] = put@{ params ->
val opponentValue = params.get<MoValue>(0)
val opponent = if (opponentValue is ObjectValue<*>) {
opponentValue.obj as ServerPlayer
} else {
val paramString = opponentValue.asString()
val playerUUID = paramString.asUUID
if (playerUUID != null) {
server()?.playerList?.getPlayer(playerUUID) ?: return@put DoubleValue.ZERO
} else {
server()?.playerList?.getPlayerByName(paramString) ?: return@put DoubleValue.ZERO
}
}
val format = params.getStringOrNull(1)
?.let(BattleFormat::fromFormatIdentifier)
?: BattleFormat.GEN_9_SINGLES
val setLevel = params.getIntOrNull(2) ?: -1
format.adjustLevel = setLevel
val rules = params.getStringOrNull(5)
?.split(",")
?.toSet()
?: emptySet()
val modifiedBattleFormat = BattleFormat.setBattleRules(
battleFormat = format,
rules = rules
)
val cloneParties = (setLevel != -1) || (params.getBooleanOrNull(3) ?: false)
val healFirst = params.getBooleanOrNull(4) ?: false
val battleStartResult = BattleBuilder.pvp1v1(
player1 = player,
player2 = opponent,
battleFormat = modifiedBattleFormat,
cloneParties = cloneParties,
healFirst = healFirst
)
var returnValue: MoValue = DoubleValue.ZERO
battleStartResult.ifSuccessful { returnValue = it.struct }
return@put returnValue
}
map["get_custom_stat"] = put@{ params ->
val statName = params.getString(0)
val resourceLocation = ResourceLocation.tryParse(statName) ?: return@put DoubleValue.ZERO
val statResourceLocation =
BuiltInRegistries.CUSTOM_STAT.get(resourceLocation) ?: return@put DoubleValue.ZERO
val exists = net.minecraft.stats.Stats.CUSTOM.contains(statResourceLocation)
if (!exists) return@put DoubleValue.ZERO
val stat = net.minecraft.stats.Stats.CUSTOM.get(statResourceLocation)
val value = player.stats.getValue(stat)
DoubleValue(value)
}
map["get_starter_uuid"] = { _ ->
Cobblemon.playerDataManager.getGenericData(player).starterUUID?.let {
StringValue(
it.toString()
)
} ?: DoubleValue.ZERO
}
map["has_chosen_starter"] = { _ -> DoubleValue(Cobblemon.playerDataManager.getGenericData(player).starterSelected) }
}
return functions
return map
}
}
@@ -27,69 +27,57 @@ import com.cobblemon.mod.common.util.getStringOrNull
object PokedexMoLangFunctions : AbstractMoLangFunctionHolder<AbstractPokedexManager>() {
override fun AbstractPokedexManager.moLangFunctions(): MutableMap<String, (MoParams) -> Any> {
val pokedex = this
val functions = hashMapOf<String, (MoParams) -> Any>()
functions["get_species_record"] = { params ->
val map = mutableMapOf<String, (MoParams) -> Any>()
map["get_species_record"] = { params ->
val speciesId = params.getString(0).asIdentifierDefaultingNamespace()
pokedex.speciesRecords[speciesId]?.struct ?: QueryStruct(hashMapOf())
}
functions["has_seen"] = hasSeen@{ params ->
map["has_seen"] = put@{ params ->
val speciesId = params.getString(0).asIdentifierDefaultingNamespace()
val formName =
params.getStringOrNull(1)
?: return@hasSeen DoubleValue(
pokedex.getHighestKnowledgeForSpecies(speciesId).ordinal >= PokedexEntryProgress.SEEN.ordinal,
)
val formName = params.getStringOrNull(1)
DoubleValue(
(
pokedex
.getSpeciesRecord(speciesId)
?.getFormRecord(formName)
?.knowledge
?.ordinal
?: 0
) >= PokedexEntryProgress.SEEN.ordinal,
)
}
functions["has_caught"] = hasCaught@{ params ->
val speciesId = params.getString(0).asIdentifierDefaultingNamespace()
val formName =
params.getStringOrNull(1) ?: return@hasCaught DoubleValue(
pokedex.getHighestKnowledgeForSpecies(
speciesId,
) == PokedexEntryProgress.OWNED,
if (formName == null) {
return@put DoubleValue(pokedex.getHighestKnowledgeForSpecies(speciesId).ordinal >= PokedexEntryProgress.SEEN.ordinal)
} else {
return@put DoubleValue(
(pokedex.getSpeciesRecord(speciesId)?.getFormRecord(formName)?.knowledge?.ordinal
?: 0) >= PokedexEntryProgress.SEEN.ordinal
)
DoubleValue(
pokedex
.getSpeciesRecord(speciesId)
?.getFormRecord(formName)
?.knowledge == PokedexEntryProgress.OWNED,
)
}
functions["caught_count"] = { DoubleValue(pokedex.getGlobalCalculatedValue(CaughtCount)) }
functions["seen_count"] = { DoubleValue(pokedex.getGlobalCalculatedValue(SeenCount)) }
functions["caught_percent"] = { DoubleValue(pokedex.getGlobalCalculatedValue(CaughtPercent)) }
functions["seen_percent"] = { DoubleValue(pokedex.getGlobalCalculatedValue(SeenPercent)) }
when (pokedex) {
is PokedexManager -> {
functions["player_id"] = { StringValue(pokedex.uuid.toString()) }
functions["see"] = { params ->
val pokemon = params.get<ObjectValue<Pokemon>>(0).obj
pokedex.encounter(pokemon)
DoubleValue.ONE
}
functions["catch"] = { params ->
val pokemon = params.get<ObjectValue<Pokemon>>(0).obj
pokedex.obtain(pokemon)
DoubleValue.ONE
}
}
}
return functions
map["has_caught"] = put@{ params ->
val speciesId = params.getString(0).asIdentifierDefaultingNamespace()
val formName = params.getStringOrNull(1)
if (formName == null) {
return@put DoubleValue(pokedex.getHighestKnowledgeForSpecies(speciesId) == PokedexEntryProgress.OWNED)
} else {
return@put DoubleValue(
pokedex.getSpeciesRecord(speciesId)
?.getFormRecord(formName)?.knowledge == PokedexEntryProgress.OWNED
)
}
}
map["caught_count"] = { DoubleValue(pokedex.getGlobalCalculatedValue(CaughtCount)) }
map["seen_count"] = { DoubleValue(pokedex.getGlobalCalculatedValue(SeenCount)) }
map["caught_percent"] = { DoubleValue(pokedex.getGlobalCalculatedValue(CaughtPercent)) }
map["seen_percent"] = { DoubleValue(pokedex.getGlobalCalculatedValue(SeenPercent)) }
if (pokedex is PokedexManager) {
map["player_id"] = { StringValue(pokedex.uuid.toString()) }
map["see"] = put@{ params ->
val pokemon = params.get<ObjectValue<Pokemon>>(0).obj
pokedex.encounter(pokemon)
return@put DoubleValue.ONE
}
map["catch"] = put@{ params ->
val pokemon = params.get<ObjectValue<Pokemon>>(0).obj
pokedex.obtain(pokemon)
return@put DoubleValue.ONE
}
}
return map
}
}
@@ -32,113 +32,100 @@ import com.cobblemon.mod.common.util.withQueryValue
object PokemonEntityMoLangFunctions : AbstractMoLangFunctionHolder<PokemonEntity>() {
override fun PokemonEntity.moLangFunctions(): MutableMap<String, (MoParams) -> Any> {
val pokemonEntity = this
val functions = hashMapOf<String, (MoParams) -> Any>()
functions["is_busy"] = { DoubleValue(pokemonEntity.isBusy) }
functions["in_battle"] = { DoubleValue(pokemonEntity.isBattling) }
functions["is_moving"] =
{ DoubleValue((pokemonEntity.moveControl as? PokemonMoveControl)?.hasWanted() == true) }
functions["is_flying"] = { DoubleValue(pokemonEntity.getBehaviourFlag(PokemonBehaviourFlag.FLYING)) }
functions["get_riding_state"] = { params ->
val map = mutableMapOf<String, (MoParams) -> Any>()
map["is_busy"] = { DoubleValue(pokemonEntity.isBusy) }
map["in_battle"] = { DoubleValue(pokemonEntity.isBattling) }
map["is_moving"] = { DoubleValue((pokemonEntity.moveControl as? PokemonMoveControl)?.hasWanted() == true) }
map["is_flying"] = { DoubleValue(pokemonEntity.getBehaviourFlag(PokemonBehaviourFlag.FLYING)) }
map["get_riding_state"] = { params ->
val name = params.getStringOrNull(0)
pokemonEntity.ifRidingAvailableSupply(DoubleValue.ZERO) { behaviour, settings, state ->
val moValue = behaviour.asMoLangValue(settings, state, pokemonEntity)
return@ifRidingAvailableSupply moValue.functions[name]?.apply(MoParams.EMPTY) as? MoValue ?: moValue
}
}
functions["is_gliding"] = {
map["is_gliding"] = {
pokemonEntity.ifRidingAvailableSupply(DoubleValue.ZERO) { behaviour, settings, state ->
val moValue = behaviour.asMoLangValue(settings, state, pokemonEntity)
return@ifRidingAvailableSupply moValue.functions["gliding"]?.apply(MoParams.EMPTY) as? MoValue
?: DoubleValue.ZERO
}
}
functions["is_sprinting"] = {
map["is_sprinting"] = {
pokemonEntity.ifRidingAvailableSupply(DoubleValue.ZERO) { behaviour, settings, state ->
val moValue = behaviour.asMoLangValue(settings, state, pokemonEntity)
return@ifRidingAvailableSupply moValue.functions["sprinting"]?.apply(MoParams.EMPTY) as? MoValue
?: DoubleValue.ZERO
}
}
functions["is_drifting"] = {
map["is_drifting"] = {
pokemonEntity.ifRidingAvailableSupply(DoubleValue.ZERO) { behaviour, settings, state ->
val moValue = behaviour.asMoLangValue(settings, state, pokemonEntity)
return@ifRidingAvailableSupply moValue.functions["drifting"]?.apply(MoParams.EMPTY) as? MoValue
?: DoubleValue.ZERO
}
}
functions["is_powered_drifting"] = {
map["is_powered_drifting"] = {
pokemonEntity.ifRidingAvailableSupply(DoubleValue.ZERO) { behaviour, settings, state ->
val moValue = behaviour.asMoLangValue(settings, state, pokemonEntity)
return@ifRidingAvailableSupply moValue.functions["powered_drifting"]?.apply(MoParams.EMPTY) as? MoValue
?: DoubleValue.ZERO
}
}
functions["in_air"] = {
map["in_air"] = {
pokemonEntity.ifRidingAvailableSupply(DoubleValue.ZERO) { behaviour, settings, state ->
val moValue = behaviour.asMoLangValue(settings, state, pokemonEntity)
return@ifRidingAvailableSupply moValue.functions["in_air"]?.apply(MoParams.EMPTY) as? MoValue
?: DoubleValue.ZERO
}
}
functions["is_wild"] = { DoubleValue(pokemonEntity.ownerUUID == null) }
functions["is_in_party"] = {
DoubleValue(
pokemonEntity.pokemon.storeCoordinates
.get()
?.store is PartyStore,
)
map["is_wild"] = { DoubleValue(pokemonEntity.ownerUUID == null) }
map["is_in_party"] = { DoubleValue(pokemonEntity.pokemon.storeCoordinates.get()?.store is PartyStore) }
map["is_ridden"] = { DoubleValue(pokemonEntity.hasControllingPassenger()) }
map["has_aspect"] = { DoubleValue(it.getString(0) in pokemonEntity.aspects) }
map["is_pokemon"] = { DoubleValue.ONE }
map["is_holding_item"] = {
DoubleValue(!pokemonEntity.entityData.get(PokemonEntity.SHOWN_HELD_ITEM).let {
it.isEmpty || it.`is`(CobblemonItemTags.WEARABLE_HAT_ITEMS) || it.`is`(CobblemonItemTags.WEARABLE_FACE_ITEMS)
})
}
functions["is_ridden"] = { DoubleValue(pokemonEntity.hasControllingPassenger()) }
functions["has_aspect"] = { DoubleValue(it.getString(0) in pokemonEntity.aspects) }
functions["is_pokemon"] = { DoubleValue.ONE }
functions["is_holding_item"] = {
DoubleValue(
!pokemonEntity.entityData.get(PokemonEntity.SHOWN_HELD_ITEM).let {
it.isEmpty || it.`is`(CobblemonItemTags.WEARABLE_HAT_ITEMS) || it.`is`(CobblemonItemTags.WEARABLE_FACE_ITEMS)
},
)
map["riding_style"] = {
StringValue(pokemonEntity.ifRidingAvailableSupply("") { behaviour, settings, state ->
behaviour.getRidingStyle(settings, state).name
})
}
functions["riding_style"] = {
StringValue(
pokemonEntity.ifRidingAvailableSupply("") { behaviour, settings, state ->
behaviour.getRidingStyle(settings, state).name
},
)
}
functions["is_wearing_hat"] = {
map["is_wearing_hat"] = {
DoubleValue(
pokemonEntity.entityData
.get(PokemonEntity.SHOWN_HELD_ITEM)
.`is`(CobblemonItemTags.WEARABLE_HAT_ITEMS),
.`is`(CobblemonItemTags.WEARABLE_HAT_ITEMS)
)
}
functions["is_wearing_face"] = {
map["is_wearing_face"] = {
DoubleValue(
pokemonEntity.entityData
.get(PokemonEntity.SHOWN_HELD_ITEM)
.`is`(CobblemonItemTags.WEARABLE_FACE_ITEMS),
.`is`(CobblemonItemTags.WEARABLE_FACE_ITEMS)
)
}
functions["is_pastured"] = {
map["is_pastured"] = {
DoubleValue((pokemonEntity.tethering != null))
}
functions["pasture_conflict_enabled"] = {
map["pasture_conflict_enabled"] = {
DoubleValue(pokemonEntity.getBehaviourFlag(PokemonBehaviourFlag.PASTURE_CONFLICT))
}
functions["run_action_effect"] = runActionEffect@{ params ->
map["run_action_effect"] = runActionEffect@{ params ->
val runtime = MoLangRuntime().setup()
runtime.environment.cloneFrom(params.environment)
runtime.withQueryValue("entity", pokemonEntity.struct)
val actionEffect = ActionEffects.actionEffects[params.getString(0).asIdentifierDefaultingNamespace()]
if (actionEffect != null) {
val context =
ActionEffectContext(
actionEffect = actionEffect,
providers = mutableListOf(TargetsProvider(pokemonEntity)),
runtime = runtime,
level = pokemonEntity.level(),
)
val context = ActionEffectContext(
actionEffect = actionEffect,
providers = mutableListOf(TargetsProvider(pokemonEntity)),
runtime = runtime,
level = pokemonEntity.level()
)
// This was taken from NPC run_action_effect but this variable isn't used, so I just commented it out.
// pokemonEntity.actionEffect = context
pokemonEntity.brain.setMemory(CobblemonMemories.ACTIVE_ACTION_EFFECT, context)
@@ -148,16 +135,14 @@ object PokemonEntityMoLangFunctions : AbstractMoLangFunctionHolder<PokemonEntity
pokemonEntity.brain.getMemory(CobblemonMemories.ACTIVE_ACTION_EFFECT).orElse(null)
if (pokemonActionEffect == context && pokemonEntity.brain.isActive(CobblemonActivities.ACTION_EFFECT)) {
pokemonEntity.brain.eraseMemory(CobblemonMemories.ACTIVE_ACTION_EFFECT)
// pokemonEntity.actionEffect = null
//pokemonEntity.actionEffect = null
}
}
return@runActionEffect DoubleValue.ONE
}
DoubleValue.ZERO
return@runActionEffect DoubleValue.ZERO
}
return functions
return map
}
}
@@ -33,214 +33,218 @@ import com.cobblemon.mod.common.util.getDoubleOrNull
import com.cobblemon.mod.common.util.getIntOrNull
import com.cobblemon.mod.common.util.server
import com.cobblemon.mod.common.util.toProperties
import net.minecraft.resources.ResourceLocation
import kotlin.random.Random
import net.minecraft.resources.ResourceLocation
object PokemonMoLangFunctions : AbstractMoLangFunctionHolder<Pokemon>() {
override fun Pokemon.moLangFunctions(): MutableMap<String, (MoParams) -> Any> {
val pokemon = this
val functions = hashMapOf<String, (MoParams) -> Any>()
functions["id"] = { StringValue(pokemon.uuid.toString()) }
functions["nickname"] = { StringValue(pokemon.nickname.toString()) }
functions["level"] = { DoubleValue(pokemon.level.toDouble()) }
functions["max_hp"] = { DoubleValue(pokemon.maxHealth.toDouble()) }
functions["current_hp"] = { DoubleValue(pokemon.currentHealth.toDouble()) }
functions["friendship"] = { DoubleValue(pokemon.friendship.toDouble()) }
functions["max_fullness"] = { DoubleValue(pokemon.getMaxFullness().toDouble()) }
functions["fullness"] = { DoubleValue(pokemon.currentFullness.toDouble()) }
functions["lose_fullness"] = { params ->
val map = mutableMapOf<String, (MoParams) -> Any>()
map["id"] = { StringValue(pokemon.uuid.toString()) }
map["nickname"] = { StringValue(pokemon.nickname.toString()) }
map["level"] = { DoubleValue(pokemon.level.toDouble()) }
map["max_hp"] = { DoubleValue(pokemon.maxHealth.toDouble()) }
map["current_hp"] = { DoubleValue(pokemon.currentHealth.toDouble()) }
map["friendship"] = { DoubleValue(pokemon.friendship.toDouble()) }
map["max_fullness"] = { DoubleValue(pokemon.getMaxFullness().toDouble()) }
map["fullness"] = { DoubleValue(pokemon.currentFullness.toDouble()) }
map["lose_fullness"] = { params ->
val amount = params.getDouble(0)
pokemon.loseFullness(amount.toInt())
}
functions["feed_pokemon"] = { params ->
map["feed_pokemon"] = { params ->
val amount = params.getDouble(0)
val playSound = params.getBooleanOrNull(1) ?: true
pokemon.feedPokemon(amount.toInt(), playSound)
}
functions["behaviour"] = { pokemon.form.behaviour.struct }
functions["behavior"] = { pokemon.form.behaviour.struct } // Inferior
functions["pokeball"] = { StringValue(pokemon.caughtBall.toString()) }
functions["ability"] = { StringValue(pokemon.ability.name) }
functions["has_learned"] = hasLearned@{ params ->
map["behaviour"] = { pokemon.form.behaviour.struct }
map["behavior"] = { pokemon.form.behaviour.struct } // Inferior
map["pokeball"] = { StringValue(pokemon.caughtBall.toString()) }
map["ability"] = { StringValue(pokemon.ability.name) }
map["has_learned"] = put@{ params ->
val moveName = params.getString(0)
val move = pokemon.allAccessibleMoves.find { it.name == moveName }
if (move != null) return@hasLearned DoubleValue.ONE
DoubleValue.ZERO
if (move != null) {
return@put DoubleValue.ONE
} else {
return@put DoubleValue.ZERO
}
}
functions["moveset"] = {
map["moveset"] = {
val struct = QueryStruct(hashMapOf())
for ((index, move) in pokemon.moveSet.withIndex()) {
struct.addFunction(index.toString()) { move.struct }
}
struct
}
functions["evs"] = {
map["evs"] = {
val struct = QueryStruct(hashMapOf())
for (stat in Stats.PERMANENT) {
struct.addFunction(stat.showdownId) { DoubleValue(pokemon.evs.getOrDefault(stat).toDouble()) }
}
struct
}
functions["ivs"] = {
map["ivs"] = {
val struct = QueryStruct(hashMapOf())
for (stat in Stats.PERMANENT) {
struct.addFunction(stat.showdownId) { DoubleValue(pokemon.ivs.getOrDefault(stat).toDouble()) }
}
struct
}
functions["hyper_trained_ivs"] = {
map["hyper_trained_ivs"] = {
val struct = QueryStruct(hashMapOf())
for (stat in Stats.PERMANENT) {
struct.addFunction(stat.showdownId) { DoubleValue(pokemon.ivs.hyperTrainedIVs[stat] ?: -1.0) }
}
struct
}
functions["ride_boosts"] = {
map["ride_boosts"] = {
val struct = QueryStruct(hashMapOf())
for (stat in RidingStat.entries) {
struct.addFunction(stat.name.lowercase()) { DoubleValue(pokemon.getRideBoost(stat)) }
}
struct
}
functions["natdex_number"] = {
map["natdex_number"] = {
DoubleValue(pokemon.species.nationalPokedexNumber.toDouble())
}
functions["types"] = {
pokemon.form.types
.map { it.showdownId }
.asArrayValue(::StringValue)
map["types"] = {
pokemon.form.types.map { it.showdownId }.asArrayValue(::StringValue)
}
functions["gender_ratio"] = {
map["gender_ratio"] = {
DoubleValue(pokemon.form.maleRatio.toDouble())
}
functions["ev_yield"] = {
map["ev_yield"] = {
val struct = QueryStruct(hashMapOf())
for (stat in Stats.PERMANENT) {
struct.addFunction(stat.showdownId) { DoubleValue(pokemon.form.evYield[stat]?.toDouble()) }
}
struct
}
functions["base_stats"] = {
map["base_stats"] = {
val struct = QueryStruct(hashMapOf())
for (stat in Stats.PERMANENT) {
struct.addFunction(stat.showdownId) { DoubleValue(pokemon.form.baseStats[stat]?.toDouble()) }
}
struct
}
functions["catch_rate"] = {
map["catch_rate"] = {
DoubleValue(pokemon.form.catchRate.toDouble())
}
functions["base_experience_yield"] = {
map["base_experience_yield"] = {
DoubleValue(pokemon.form.baseExperienceYield.toDouble())
}
functions["drops"] = {
map["drops"] = {
val struct = QueryStruct(hashMapOf())
for ((index, drop) in pokemon.form.drops.entries
.withIndex()) {
for ((index, drop) in pokemon.form.drops.entries.withIndex()) {
struct.addFunction(index.toString()) { drop }
}
struct
}
functions["tm_learnset"] = {
map["tm_learnset"] = {
val struct = QueryStruct(hashMapOf())
for ((index, move) in pokemon.form.moves.tmMoves
.withIndex()) {
for ((index, move) in pokemon.form.moves.tmMoves.withIndex()) {
struct.addFunction(index.toString()) { move.struct }
}
struct
}
functions["egg_learnset"] = {
map["egg_learnset"] = {
val struct = QueryStruct(hashMapOf())
for ((index, move) in pokemon.form.moves.eggMoves
.withIndex()) {
for ((index, move) in pokemon.form.moves.eggMoves.withIndex()) {
struct.addFunction(index.toString()) { move.struct }
}
struct
}
functions["tutor_learnset"] = {
map["tutor_learnset"] = {
val struct = QueryStruct(hashMapOf())
for ((index, move) in pokemon.form.moves.tutorMoves
.withIndex()) {
for ((index, move) in pokemon.form.moves.tutorMoves.withIndex()) {
struct.addFunction(index.toString()) { move.struct }
}
struct
}
functions["level_learnset"] = {
map["level_learnset"] = {
val struct = QueryStruct(hashMapOf())
for ((index, move) in pokemon.form.moves.levelUpMoves) {
struct.addFunction(index.toString()) { move }
}
struct
}
functions["ability_pool"] = {
map["ability_pool"] = {
val struct = QueryStruct(hashMapOf())
for ((index, ability) in pokemon.form.abilities.withIndex()) {
struct.addFunction(index.toString()) { StringValue(ability.toString()) }
}
struct
}
functions["egg_groups"] = {
map["egg_groups"] = {
val struct = QueryStruct(hashMapOf())
for ((index, group) in pokemon.form.eggGroups.withIndex()) {
struct.addFunction(index.toString()) { StringValue(group.toString()) }
}
struct
}
functions["egg_cycles"] = {
map["egg_cycles"] = {
DoubleValue(pokemon.species.eggCycles.toDouble())
}
functions["labels"] = {
map["labels"] = {
val struct = QueryStruct(hashMapOf())
for ((index, label) in pokemon.form.labels.withIndex()) {
struct.addFunction(index.toString()) { StringValue(label) }
}
struct
}
functions["aspects"] = {
map["aspects"] = {
val aspects = pokemon.aspects
aspects.asArrayValue { StringValue(it) }
}
functions["form_aspects"] = {
map["form_aspects"] = {
val aspects = pokemon.form.aspects
aspects.asArrayValue { StringValue(it) }
}
functions["form_name"] = {
map["form_name"] = {
StringValue(pokemon.form.name)
}
// Yes, this is the lazy call for a single hardcoded pre-evolution.
// Lol. Lmao, even.
// TO-DO: Subscribe to [PokemonSpecies.observable].
functions["pre_evolution"] = { pokemon.species.preEvolution ?: DoubleValue.ZERO }
functions["nature"] = { StringValue(pokemon.nature.toString()) }
functions["is_wild"] = { DoubleValue(pokemon.entity?.let { it.ownerUUID == null } == true) }
functions["is_shiny"] = { DoubleValue(pokemon.shiny) }
functions["is_in_party"] = { DoubleValue(pokemon.storeCoordinates.get()?.store is PartyStore) }
functions["species"] = { pokemon.species.struct }
functions["form"] = { StringValue(pokemon.form.name) }
functions["weight"] = { DoubleValue(pokemon.species.weight.toDouble()) }
functions["matches"] = { params -> DoubleValue(params.getString(0).toProperties().matches(pokemon)) }
functions["apply"] = { params ->
// Gito: surely this is just a typo, right? not touching the function name just in case
map["prevolution"] = preEvolution@{
val preEvolution = pokemon.species.preEvolution ?: return@preEvolution DoubleValue.ZERO
return@preEvolution preEvolution
}
map["nature"] = { StringValue(pokemon.nature.toString()) }
map["is_wild"] = { DoubleValue(pokemon.entity?.let { it.ownerUUID == null } == true) }
map["is_shiny"] = { DoubleValue(pokemon.shiny) }
map["is_in_party"] = { DoubleValue(pokemon.storeCoordinates.get()?.store is PartyStore) }
map["species"] = { pokemon.species.struct }
map["form"] = { StringValue(pokemon.form.name) }
map["weight"] = { DoubleValue(pokemon.species.weight.toDouble()) }
map["matches"] = { params -> DoubleValue(params.getString(0).toProperties().matches(pokemon)) }
map["apply"] = { params ->
params.getString(0).toProperties().apply(pokemon)
DoubleValue.ONE
}
functions["owner"] = { pokemon.getOwnerPlayer()?.asMoLangValue() ?: DoubleValue.ZERO }
functions["held_item"] = { pokemon.heldItem().asMoLangValue(server()!!.registryAccess()) }
functions["remove_held_item"] = { pokemon.removeHeldItem() }
functions["add_aspects"] = { params ->
map["owner"] = { pokemon.getOwnerPlayer()?.asMoLangValue() ?: DoubleValue.ZERO }
map["held_item"] = { pokemon.heldItem().asMoLangValue(server()!!.registryAccess()) }
map["remove_held_item"] = { _ ->
pokemon.removeHeldItem()
}
map["add_aspects"] = { params ->
for (aspect in params.params) pokemon.forcedAspects += aspect.asString()
pokemon.updateAspects()
}
functions["remove_aspects"] = { params ->
map["remove_aspects"] = { params ->
for (aspect in params.params) pokemon.forcedAspects -= aspect.asString()
pokemon.updateAspects()
}
functions["cosmetic_item"] = {
map["cosmetic_item"] = {
pokemon.cosmeticItem().asMoLangValue(server()!!.registryAccess())
}
functions["remove_cosmetic_item"] = { pokemon.removeCosmeticItem() }
functions["marks"] = { pokemon.marks.asArrayValue { StringValue(it.identifier.toString()) } }
functions["has_mark"] = { params ->
map["remove_cosmetic_item"] = { _ ->
pokemon.removeCosmeticItem()
}
map["marks"] = { _ -> pokemon.marks.asArrayValue { StringValue(it.identifier.toString()) } }
map["has_mark"] = { params ->
var hasMark = false
val identifier = params.getString(0).asIdentifierDefaultingNamespace()
val mark = Marks.getByIdentifier(identifier)
@@ -250,7 +254,7 @@ object PokemonMoLangFunctions : AbstractMoLangFunctionHolder<Pokemon>() {
DoubleValue(hasMark)
}
functions["remove_marks"] = { params ->
map["remove_marks"] = { params ->
var removedMark = false
for (param in params.params) {
val identifier = param.asString().asIdentifierDefaultingNamespace()
@@ -263,7 +267,7 @@ object PokemonMoLangFunctions : AbstractMoLangFunctionHolder<Pokemon>() {
DoubleValue(removedMark)
}
functions["add_marks"] = { params ->
map["add_marks"] = { params ->
var appliedMark = false
for (param in params.params) {
val identifier = param.asString().asIdentifierDefaultingNamespace()
@@ -276,7 +280,7 @@ object PokemonMoLangFunctions : AbstractMoLangFunctionHolder<Pokemon>() {
DoubleValue(appliedMark)
}
functions["add_marks_with_chance"] = { params ->
map["add_marks_with_chance"] = { params ->
var appliedMark = false
for (param in params.params) {
val identifier = param.asString().asIdentifierDefaultingNamespace()
@@ -294,15 +298,17 @@ object PokemonMoLangFunctions : AbstractMoLangFunctionHolder<Pokemon>() {
DoubleValue(appliedMark)
}
functions["add_potential_marks"] = { params ->
map["add_potential_marks"] = { params ->
for (param in params.params) {
val identifier = param.asString().asIdentifierDefaultingNamespace()
val mark = Marks.getByIdentifier(identifier)
if (mark != null) pokemon.addPotentialMark(mark)
}
}
functions["apply_potential_marks"] = { DoubleValue(pokemon.applyPotentialMarks()) }
functions["hyper_train_iv"] = hyperTrainIv@{ params ->
map["apply_potential_marks"] = put@{
return@put DoubleValue(pokemon.applyPotentialMarks())
}
map["hyper_train_iv"] = hyperTrainIv@{ params ->
val statId = params.getString(0)
val stat = Stats.getStat(statId)
val value = params.getIntOrNull(1) ?: IVs.MAX_VALUE
@@ -315,12 +321,12 @@ object PokemonMoLangFunctions : AbstractMoLangFunctionHolder<Pokemon>() {
return@hyperTrainIv DoubleValue.ZERO
}
}
functions["add_exp"] = { params ->
map["add_exp"] = { params ->
val exp = params.getDouble(0).toInt()
pokemon.addExperience(SidemodExperienceSource("molang"), exp)
DoubleValue.ONE
}
functions["set_iv"] = setIv@{ params ->
map["set_iv"] = setIv@{ params ->
val statId = params.getString(0)
val stat = Stats.getStat(statId)
val value = params.getIntOrNull(1)?.coerceIn(0, IVs.MAX_VALUE) ?: IVs.MAX_VALUE
@@ -332,7 +338,7 @@ object PokemonMoLangFunctions : AbstractMoLangFunctionHolder<Pokemon>() {
return@setIv DoubleValue.ZERO
}
}
functions["set_ev"] = setEv@{ params ->
map["set_ev"] = setEv@{ params ->
val statId = params.getString(0)
val stat = Stats.getStat(statId)
val value = (params.getIntOrNull(1) ?: 0).coerceIn(0, 255)
@@ -344,54 +350,49 @@ object PokemonMoLangFunctions : AbstractMoLangFunctionHolder<Pokemon>() {
return@setEv DoubleValue.ZERO
}
}
functions["set_ride_boost"] = setRideBoost@{ params ->
map["set_ride_boost"] = setRideBoost@{ params ->
val statName = params.getString(0).uppercase()
val stat =
RidingStat.entries.find { it.name.equals(statName, ignoreCase = true) }
?: return@setRideBoost DoubleValue.ZERO
val stat = RidingStat.entries.find { it.name.equals(statName, ignoreCase = true) }
?: return@setRideBoost DoubleValue.ZERO
val value = params.getDoubleOrNull(1)?.toFloat() ?: return@setRideBoost DoubleValue.ZERO
pokemon.setRideBoost(stat, value)
DoubleValue.ONE
return@setRideBoost DoubleValue.ONE
}
functions["add_ride_boost"] = addRideBoost@{ params ->
map["add_ride_boost"] = addRideBoost@{ params ->
val statName = params.getString(0).uppercase()
val stat =
RidingStat.entries.find { it.name.equals(statName, ignoreCase = true) }
?: return@addRideBoost DoubleValue.ZERO
val stat = RidingStat.entries.find { it.name.equals(statName, ignoreCase = true) }
?: return@addRideBoost DoubleValue.ZERO
val value = params.getDoubleOrNull(1)?.toFloat() ?: return@addRideBoost DoubleValue.ZERO
DoubleValue(if (pokemon.addRideBoost(stat, value)) 1.0 else 0.0)
return@addRideBoost DoubleValue(if (pokemon.addRideBoost(stat, value)) 1.0 else 0.0)
}
functions["initialize_moveset"] = initializeMoveset@{ params ->
map["initialize_moveset"] = put@{ params ->
val param = params.get<MoValue>(0) ?: StringValue(pokemon.form.defaultWildMovesetBuilder.toString())
if (param is DoubleValue) {
pokemon.initializeMoveset(param == DoubleValue.ONE)
} else {
val movesetBuilderId = ResourceLocation.parse(param.asString())
val movesetBuilder =
CobblemonMovesetBuilders.movesetBuilders[movesetBuilderId]
?: run {
Cobblemon.LOGGER.error(
"Tried initializing moveset from moveset builder ${param.asString()} but it does not exist.",
)
return@initializeMoveset DoubleValue.ZERO
}
val movesetBuilder = CobblemonMovesetBuilders.movesetBuilders[movesetBuilderId]
?: run {
Cobblemon.LOGGER.error("Tried initializing moveset from moveset builder ${param.asString()} but it does not exist.")
return@put DoubleValue.ZERO
}
pokemon.initializeMovesetFrom(movesetBuilder)
}
DoubleValue.ONE
}
functions["validate_moveset"] = { params ->
map["validate_moveset"] = { params ->
val includeLegacy = params.getBooleanOrNull(0) ?: true
pokemon.validateMoveset(includeLegacy)
DoubleValue.ONE
}
functions["teach_learnable_moves"] = { params ->
map["teach_learnable_moves"] = { params ->
val includeLegacy = params.getBooleanOrNull(0) ?: true
pokemon.teachLearnableMoves(includeLegacy)
DoubleValue.ONE
}
functions["teach_move"] = teachMove@{ params ->
map["teach_move"] = teachMove@{ params ->
val moveName = params.getString(0)
val moveTemplate = Moves.getByName(moveName) ?: return@teachMove DoubleValue.ZERO
val bypass = params.getBooleanOrNull(1) ?: false
@@ -401,9 +402,8 @@ object PokemonMoLangFunctions : AbstractMoLangFunctionHolder<Pokemon>() {
return@teachMove DoubleValue.ZERO
}
val alreadyKnows =
pokemon.moveSet.getMoves().any { it.template == moveTemplate } ||
pokemon.benchedMoves.any { it.moveTemplate == moveTemplate }
val alreadyKnows = pokemon.moveSet.getMoves().any { it.template == moveTemplate } ||
pokemon.benchedMoves.any { it.moveTemplate == moveTemplate }
if (alreadyKnows) {
return@teachMove DoubleValue.ZERO
}
@@ -416,43 +416,42 @@ object PokemonMoLangFunctions : AbstractMoLangFunctionHolder<Pokemon>() {
DoubleValue.ONE
}
functions["can_learn_move"] = canLearnMove@{ params ->
map["can_learn_move"] = canLearnMove@{ params ->
val moveName = params.getString(0)
val moveTemplate = Moves.getByName(moveName) ?: return@canLearnMove DoubleValue.ZERO
val includeLegacy = params.getBooleanOrNull(1) ?: true
val canLearn =
if (includeLegacy) {
LearnsetQuery.ANY.canLearn(moveTemplate, pokemon.form.moves)
} else {
LearnsetQuery.LEGAL.canLearn(moveTemplate, pokemon.form.moves)
}
DoubleValue(if (canLearn) 1.0 else 0.0)
val canLearn = if (includeLegacy) {
LearnsetQuery.ANY.canLearn(moveTemplate, pokemon.form.moves)
} else {
LearnsetQuery.LEGAL.canLearn(moveTemplate, pokemon.form.moves)
}
return@canLearnMove DoubleValue(if (canLearn) 1.0 else 0.0)
}
functions["unlearn_move"] = unlearnMove@{ params ->
map["unlearn_move"] = unlearnMove@{ params ->
val moveName = params.getString(0)
val moveTemplate = Moves.getByName(moveName)
if (moveTemplate != null) {
pokemon.unlearnMove(moveTemplate)
return@unlearnMove DoubleValue.ONE
} else {
return@unlearnMove DoubleValue.ZERO
}
DoubleValue.ZERO
}
functions["can_evolve"] = { DoubleValue(pokemon.evolutions.any()) }
functions["force_evolve"] = forceEvolve@{ params ->
map["can_evolve"] = { _ ->
DoubleValue(pokemon.evolutions.any())
}
map["force_evolve"] = forceEvolve@{ params ->
val idx = params.getInt(0)
val evolution =
if (idx >= 0) pokemon.evolutions.elementAtOrNull(idx) else return@forceEvolve DoubleValue.ZERO
val evolution = if (idx >= 0) pokemon.evolutions.elementAtOrNull(idx) else return@forceEvolve DoubleValue.ZERO
evolution?.forceEvolve(pokemon)
StringValue(evolution.toString())
}
functions["entity"] = { pokemon.entity?.asMoLangValue() ?: DoubleValue.ZERO }
functions["is_alpha"] = { DoubleValue(pokemon.isAlpha) }
functions["size_category"] = { StringValue(pokemon.getSizeCategory().name) }
map["entity"] = { pokemon.entity?.asMoLangValue() ?: DoubleValue.ZERO }
map["is_alpha"] = { DoubleValue(pokemon.isAlpha) }
map["size_category"] = { _ -> StringValue(pokemon.getSizeCategory().name) }
return functions
return map
}
}
@@ -20,50 +20,52 @@ import com.cobblemon.mod.common.util.getStringOrNull
object PokemonPropertiesMoLangFunctions : AbstractMoLangFunctionHolder<PokemonProperties>() {
override fun PokemonProperties.moLangFunctions(): MutableMap<String, (MoParams) -> Any> {
val props = this
val functions = hashMapOf<String, (MoParams) -> Any>()
val map = mutableMapOf<String, (MoParams) -> Any>()
functions["level"] = { DoubleValue(props.level?.toDouble() ?: 0) }
functions["set_level"] = { params ->
map["level"] = { DoubleValue(props.level?.toDouble() ?: 0) }
map["set_level"] = { params ->
props.level = params.getIntOrNull(0)
DoubleValue.ONE
}
functions["shiny"] = { DoubleValue(props.shiny) }
functions["set_shiny"] = { params ->
map["shiny"] = { DoubleValue(props.shiny) }
map["set_shiny"] = { params ->
props.shiny = params.getBooleanOrNull(0)
DoubleValue.ONE
}
functions["species"] = { props.species?.let { StringValue(it) } ?: DoubleValue.ZERO }
functions["set_species"] = { params ->
map["species"] = { props.species?.let { StringValue(it) } ?: DoubleValue.ZERO }
map["set_species"] = { params ->
props.species = params.getStringOrNull(0)
DoubleValue.ONE
}
functions["gender"] = { props.gender?.let { StringValue(it.name) } ?: DoubleValue.ZERO }
functions["set_gender"] = { params ->
map["gender"] = { props.gender?.let { StringValue(it.name) } ?: DoubleValue.ZERO }
map["set_gender"] = { params ->
props.gender = params.getStringOrNull(0)?.let { Gender.valueOf(it) }
DoubleValue.ONE
}
functions["form"] = { props.form?.let { StringValue(it) } ?: DoubleValue.ZERO }
functions["ivs"] = ivs@{
map["form"] = { props.form?.let { StringValue(it) } ?: DoubleValue.ZERO }
map["ivs"] = ivs@{
val ivs = props.ivs ?: return@ivs DoubleValue.ZERO
ivs.struct
return@ivs ivs.struct
}
functions["evs"] = evs@{
map["evs"] = evs@{
val evs = props.evs ?: return@evs DoubleValue.ZERO
evs.struct
return@evs evs.struct
}
functions["friendship"] = { DoubleValue(props.friendship?.toDouble() ?: DoubleValue.ZERO) }
functions["set_friendship"] = { params ->
map["friendship"] = { DoubleValue(props.friendship?.toDouble() ?: DoubleValue.ZERO) }
map["set_friendship"] = { params ->
props.friendship = params.getIntOrNull(0)
DoubleValue.ONE
}
functions["create"] = { (props.create()).struct }
functions["to_string"] = { params ->
map["create"] = {
val pokemon = props.create()
pokemon.struct
}
map["to_string"] = { params ->
val separator = params.getStringOrNull(0) ?: " "
StringValue(props.asString(separator = separator))
}
return functions
return map
}
}
@@ -22,84 +22,83 @@ import com.cobblemon.mod.common.util.asUUID
import com.cobblemon.mod.common.util.getPlayer
import com.cobblemon.mod.common.util.toProperties
object PokemonStoreMoLangFunctions : AbstractMoLangFunctionHolder<PokemonStore<*>>() {
object PokemonStoreMoLangFunctions: AbstractMoLangFunctionHolder<PokemonStore<*>>() {
override fun PokemonStore<*>.moLangFunctions(): MutableMap<String, (MoParams) -> Any> {
val store = this
val functions = hashMapOf<String, (MoParams) -> Any>()
functions["uuid"] = { StringValue(store.uuid.toString()) }
functions["add"] = { params ->
val map = mutableMapOf<String, (MoParams) -> Any>()
map["uuid"] = { StringValue(store.uuid.toString()) }
map["add"] = { params ->
val pokemon = params.get<ObjectValue<Pokemon>>(0)
DoubleValue(store.add(pokemon.obj))
}
functions["add_by_properties"] = { params ->
map["add_by_properties"] = { params ->
val props = params.getString(0).toProperties()
val player =
(store as? PlayerPartyStore)?.playerUUID?.getPlayer() // Only really know for sure when it's a player party store
val player = (store as? PlayerPartyStore)?.playerUUID?.getPlayer() // Only really know for sure when it's a player party store
val pokemon = props.create(player = player)
DoubleValue(store.add(pokemon))
}
functions["find_by_properties"] = { params ->
map["find_by_properties"] = { params ->
val props = params.getString(0).toProperties()
val pokemon = store.find { props.matches(it) }
pokemon?.asStruct() ?: DoubleValue.ZERO
}
functions["find_all_by_properties"] = { params ->
map["find_all_by_properties"] = { params ->
val props = params.getString(0).toProperties()
val pokemon = store.filter { props.matches(it) }
ArrayStruct(pokemon.mapIndexed { index, value -> "$index" to value.asStruct() }.toMap())
}
functions["find_by_id"] = { params ->
map["find_by_id"] = { params ->
val id = params.getString(0).asUUID
val pokemon = store.find { it.uuid == id }
pokemon?.asStruct() ?: DoubleValue.ZERO
}
functions["remove_by_id"] = removeById@{ params ->
map["remove_by_id"] = removeById@{ params ->
val id = params.getString(0).asUUID
val pokemon = store.find { it.uuid == id } ?: return@removeById DoubleValue.ZERO
DoubleValue(store.remove(pokemon))
return@removeById DoubleValue(store.remove(pokemon))
}
functions["average_level"] = averageLevel@{
map["average_level"] = averageLevel@{ _ ->
var numberOfPokemon = 0
var totalLevel = 0
for (pokemon in store) {
totalLevel += pokemon.level
numberOfPokemon++
}
if (numberOfPokemon == 0) return@averageLevel DoubleValue.ZERO
DoubleValue(totalLevel.toDouble() / numberOfPokemon)
if (numberOfPokemon == 0) {
return@averageLevel DoubleValue.ZERO
}
return@averageLevel DoubleValue(totalLevel.toDouble() / numberOfPokemon)
}
functions["count"] = { DoubleValue(store.count()) }
functions["count_by_properties"] = { params ->
map["count"] = { _ -> DoubleValue(store.count()) }
map["count_by_properties"] = { params ->
val props = params.getString(0).toProperties()
DoubleValue(store.count { props.matches(it) })
}
functions["highest_level"] = {
map["highest_level"] = {
val highest = store.maxOfOrNull { it.level } ?: 0
DoubleValue(highest)
}
functions["lowest_level"] = {
map["lowest_level"] = {
val lowest = store.minOfOrNull { it.level } ?: 0
DoubleValue(lowest)
}
functions["heal"] = {
map["heal"] = {
for (pokemon in store) {
pokemon.heal()
}
DoubleValue.ONE
}
functions["healing_remainder_percent"] = {
map["healing_remainder_percent"] = { _ ->
var totalPercent = 0.0f
for (pokemon in store) {
totalPercent += (1.0f - (pokemon.currentHealth.toFloat() / pokemon.maxHealth))
}
DoubleValue(totalPercent)
}
functions["has_usable_pokemon"] = { DoubleValue(store.any { !it.isFainted() }) }
functions["pokemon"] = { store.map { it.asStruct() }.asArrayValue() }
return functions
map["has_usable_pokemon"] = { _ -> DoubleValue(store.any { !it.isFainted() }) }
map["pokemon"] = {
store.map { it.asStruct() }.asArrayValue()
}
return map
}
}
@@ -25,63 +25,62 @@ import java.util.*
object ServerMoLangFunctions : AbstractMoLangFunctionHolder<MinecraftServer>() {
override fun MinecraftServer.moLangFunctions(): MutableMap<String, (MoParams) -> Any> {
val server = this
val functions = hashMapOf<String, (MoParams) -> Any>()
val map = mutableMapOf<String, (MoParams) -> Any>()
functions["get_world"] = getWorld@{ params ->
val world =
server.getLevel(
ResourceKey.create(
Registries.DIMENSION,
params.getString(0).asIdentifierDefaultingNamespace(namespace = "minecraft"),
),
) ?: return@getWorld DoubleValue.ZERO
map["get_world"] = getWorld@{ params ->
val world = server.getLevel(
ResourceKey.create(
Registries.DIMENSION,
params.getString(0).asIdentifierDefaultingNamespace(namespace = "minecraft")
)
) ?: return@getWorld DoubleValue.ZERO
world
return@getWorld world
.registryAccess()
.registryOrThrow(Registries.DIMENSION)
.wrapAsHolder(world)
.asWorldMoLangValue()
}
functions["broadcast"] = { params ->
map["broadcast"] = { params ->
val message = params.getString(0)
server.playerList.broadcastSystemMessage(message.text(), params.getBooleanOrNull(1) == true)
DoubleValue.ONE
}
functions["get_player_by_uuid"] = getPlayerByUuid@{ params ->
map["get_player_by_uuid"] = getPlayerByUuid@{ params ->
val uuid = UUID.fromString(params.getString(0))
val player = server.playerList.getPlayer(uuid) ?: return@getPlayerByUuid DoubleValue.ZERO
player.asMoLangValue()
return@getPlayerByUuid player.asMoLangValue()
}
functions["get_player_by_username"] = getPlayerByUsername@{ params ->
map["get_player_by_username"] = getPlayerByUsername@{ params ->
val username = params.getString(0)
val player = server.playerList.getPlayerByName(username) ?: return@getPlayerByUsername DoubleValue.ZERO
player.asMoLangValue()
return@getPlayerByUsername player.asMoLangValue()
}
functions["data"] = { params ->
map["data"] = { params ->
Cobblemon.molangData.load(
UUID(0L, 0L),
params.getStringOrNull(0),
params.getStringOrNull(0)
)
}
functions["save_data"] = { params ->
map["save_data"] = { params ->
Cobblemon.molangData.save(
UUID(0L, 0L),
params.getStringOrNull(index = 0),
params.getStringOrNull(index = 0)
)
DoubleValue.ONE
}
// Maybe later...
// functions.put("stop") { params ->
// map.put("stop") { params ->
// Cobblemon.LOGGER.warn("Server is being stopped from a MoLang script.")
// server.stopServer()
// }
return functions
return map
}
}
@@ -20,34 +20,27 @@ import net.minecraft.server.level.ServerPlayer
object SpawnablePositionMoLangFunctions : AbstractMoLangFunctionHolder<SpawnablePosition>() {
override fun SpawnablePosition.moLangFunctions(): MutableMap<String, (MoParams) -> Any> {
val spawningContext = this
val worldValue =
spawningContext
.world
.registryAccess()
.registryOrThrow(Registries.DIMENSION)
.wrapAsHolder(spawningContext.world)
.asWorldMoLangValue()
val biomeValue =
spawningContext
.biomeHolder
.asBiomeMoLangValue()
val functions = hashMapOf<String, (MoParams) -> Any>()
functions["biome"] = { biomeValue }
functions["world"] = { worldValue }
functions["light"] = { DoubleValue(spawningContext.light.toDouble()) }
functions["x"] = { DoubleValue(spawningContext.position.x.toDouble()) }
functions["y"] = { DoubleValue(spawningContext.position.y.toDouble()) }
functions["z"] = { DoubleValue(spawningContext.position.z.toDouble()) }
functions["moon_phase"] = { DoubleValue(spawningContext.moonPhase.toDouble()) }
functions["can_see_sky"] = { DoubleValue(spawningContext.canSeeSky) }
functions["sky_light"] = { DoubleValue(spawningContext.skyLight.toDouble()) }
functions["player"] = player@{
val causeEntity = spawningContext.cause.entity ?: return@player DoubleValue.ZERO
if (causeEntity is ServerPlayer) return@player causeEntity.asMoLangValue()
DoubleValue.ZERO
val map = mutableMapOf<String, (MoParams) -> Any>()
val worldValue = spawningContext.world.registryAccess().registryOrThrow(Registries.DIMENSION)
.wrapAsHolder(spawningContext.world).asWorldMoLangValue()
val biomeValue = spawningContext.biomeHolder.asBiomeMoLangValue()
map["biome"] = { _ -> biomeValue }
map["world"] = { _ -> worldValue }
map["light"] = { _ -> DoubleValue(spawningContext.light.toDouble()) }
map["x"] = { _ -> DoubleValue(spawningContext.position.x.toDouble()) }
map["y"] = { _ -> DoubleValue(spawningContext.position.y.toDouble()) }
map["z"] = { _ -> DoubleValue(spawningContext.position.z.toDouble()) }
map["moon_phase"] = { _ -> DoubleValue(spawningContext.moonPhase.toDouble()) }
map["can_see_sky"] = { _ -> DoubleValue(spawningContext.canSeeSky) }
map["sky_light"] = { _ -> DoubleValue(spawningContext.skyLight.toDouble()) }
map["player"] = put@{ _ ->
val causeEntity = spawningContext.cause.entity ?: return@put DoubleValue.ZERO
if (causeEntity is ServerPlayer) {
return@put causeEntity.asMoLangValue()
} else {
return@put DoubleValue.ZERO
}
}
return functions
return map
}
}
@@ -17,23 +17,23 @@ import com.cobblemon.mod.common.util.asArrayValue
object SpeciesMoLangFunctions : AbstractMoLangFunctionHolder<Species>() {
override fun Species.moLangFunctions(): MutableMap<String, (MoParams) -> Any> {
val species = this
val functions = hashMapOf<String, (MoParams) -> Any>()
val map = mutableMapOf<String, (MoParams) -> Any>(
"identifier" to { StringValue(species.resourceIdentifier.toString()) },
"name" to { StringValue(species.name) },
"primary_type" to { StringValue(species.primaryType.showdownId) },
"secondary_type" to { StringValue(species.secondaryType?.showdownId ?: "null") },
"experience_group" to { StringValue(species.experienceGroup.name) },
"height" to { DoubleValue(species.height) },
"weight" to { DoubleValue(species.weight) },
"base_scale" to { DoubleValue(species.baseScale) },
"hitbox_width" to { DoubleValue(species.hitbox.width) },
"hitbox_height" to { DoubleValue(species.hitbox.height) },
"hitbox_fixed" to { DoubleValue(species.hitbox.fixed) },
"catch_rate" to { DoubleValue(species.catchRate) },
"labels" to { species.labels.asArrayValue { StringValue(it) } },
"has_label" to { params -> DoubleValue(species.labels.contains(params.getString(0))) }
)
functions["identifier"] = { StringValue(species.resourceIdentifier.toString()) }
functions["name"] = { StringValue(species.name) }
functions["primary_type"] = { StringValue(species.primaryType.showdownId) }
functions["secondary_type"] = { StringValue(species.secondaryType?.showdownId ?: "null") }
functions["experience_group"] = { StringValue(species.experienceGroup.name) }
functions["height"] = { DoubleValue(species.height) }
functions["weight"] = { DoubleValue(species.weight) }
functions["base_scale"] = { DoubleValue(species.baseScale) }
functions["hitbox_width"] = { DoubleValue(species.hitbox.width) }
functions["hitbox_height"] = { DoubleValue(species.hitbox.height) }
functions["hitbox_fixed"] = { DoubleValue(species.hitbox.fixed) }
functions["catch_rate"] = { DoubleValue(species.catchRate) }
functions["labels"] = { species.labels.asArrayValue { StringValue(it) } }
functions["has_label"] = { params -> DoubleValue(species.labels.contains(params.getString(0))) }
return functions
return map
}
}
@@ -24,6 +24,7 @@ import com.cobblemon.mod.common.entity.npc.NPCEntity
import com.cobblemon.mod.common.net.messages.client.effect.SpawnSnowstormParticlePacket
import com.cobblemon.mod.common.net.messages.client.sound.UnvalidatedPlaySoundS2CPacket
import com.cobblemon.mod.common.util.*
import java.util.UUID
import net.minecraft.core.BlockPos
import net.minecraft.core.Holder
import net.minecraft.core.registries.Registries
@@ -45,89 +46,85 @@ import net.minecraft.world.level.storage.loot.parameters.LootContextParamSets
import net.minecraft.world.level.storage.loot.parameters.LootContextParams
import net.minecraft.world.phys.AABB
import net.minecraft.world.phys.Vec3
import java.util.UUID
object WorldMoLangFunctions : AbstractMoLangFunctionHolder<Holder<Level>>() {
override fun Holder<Level>.moLangFunctions(): MutableMap<String, (MoParams) -> Any> {
val world = this.value()
val functions = hashMapOf<String, (MoParams) -> Any>()
val map = mutableMapOf<String, (MoParams) -> Any>(
)
functions["is_time_of_day"] = isTimeOfDay@{ params ->
val timeOfDay =
TimeRange.timeRanges[params.getString(0).lowercase()]
?: return@isTimeOfDay DoubleValue.ZERO
map["is_time_of_day"] = put@{ params ->
val timeOfDay = TimeRange.timeRanges[params.getString(0).lowercase()]
?: return@put DoubleValue.ZERO
val time = world.dayTime % 24000
DoubleValue(timeOfDay.contains(time.toInt()))
return@put DoubleValue(timeOfDay.contains(time.toInt()))
}
functions["game_time"] = { DoubleValue(world.gameTime.toDouble()) }
functions["time_of_day"] = { DoubleValue((world.dayTime % 24000).toDouble()) }
functions["server"] = { server()?.asMoLangValue() ?: DoubleValue.ZERO }
functions["is_raining_at"] = { params ->
map["game_time"] = { _ -> DoubleValue(world.gameTime.toDouble()) }
map["time_of_day"] = put@{
val time = world.dayTime % 24000
return@put DoubleValue(time.toDouble())
}
map["server"] = { _ -> server()?.asMoLangValue() ?: DoubleValue.ZERO }
map["is_raining_at"] = put@{ params ->
val x = params.getInt(0)
val y = params.getInt(1)
val z = params.getInt(2)
DoubleValue(world.isRainingAt(BlockPos(x, y, z)))
return@put DoubleValue(world.isRainingAt(BlockPos(x, y, z)))
}
functions["is_snowing_at"] = { params ->
map["is_snowing_at"] = put@{ params ->
val x = params.getInt(0)
val y = params.getInt(1)
val z = params.getInt(2)
val blockPos = BlockPos(x, y, z)
when {
!world.isRaining -> {
DoubleValue.ZERO
}
!world.canSeeSky(blockPos) -> {
DoubleValue.ZERO
}
!world.isRaining -> return@put DoubleValue.ZERO
!world.canSeeSky(blockPos) -> return@put DoubleValue.ZERO
(world.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING, blockPos).y > blockPos.y) -> {
DoubleValue.ZERO
return@put DoubleValue.ZERO
}
else -> {
val biome = world.getBiome(blockPos).value() as Biome
DoubleValue(biome.getPrecipitationAt(blockPos) == Biome.Precipitation.SNOW)
return@put DoubleValue(biome.getPrecipitationAt(blockPos) == Biome.Precipitation.SNOW)
}
}
}
functions["is_chunk_loaded_at"] = { params ->
map["is_chunk_loaded_at"] = put@{ params ->
val x = params.getInt(0)
val y = params.getInt(1)
val z = params.getInt(2)
DoubleValue(world.isLoaded(BlockPos(x, y, z)))
return@put DoubleValue(world.isLoaded(BlockPos(x, y, z)))
}
functions["is_thundering"] = { DoubleValue(world.isThundering) }
functions["is_raining"] = { DoubleValue(world.isRaining) }
functions["set_block"] = setBlock@{ params ->
map["is_thundering"] = { _ -> DoubleValue(world.isThundering) }
map["is_raining"] = { _ -> DoubleValue(world.isRaining) }
map["set_block"] = put@{ params ->
val x = params.getInt(0)
val y = params.getInt(1)
val z = params.getInt(2)
val block =
world.blockRegistry.get(params.getString(3).asIdentifierDefaultingNamespace())
?: run {
Cobblemon.LOGGER.error("Unknown block: ${params.getString(3)}")
return@setBlock DoubleValue.ZERO
}
val block = world.blockRegistry.get(params.getString(3).asIdentifierDefaultingNamespace())
?: run {
Cobblemon.LOGGER.error("Unknown block: ${params.getString(3)}")
return@put DoubleValue.ZERO
}
world.setBlock(BlockPos(x, y, z), block.defaultBlockState(), Block.UPDATE_ALL)
}
functions["is_air"] = { params ->
map["is_air"] = put@{ params ->
val x = params.getDouble(0).toInt()
val y = params.getDouble(1).toInt()
val z = params.getDouble(2).toInt()
val blockState = world.getBlockState(BlockPos(x, y, z))
DoubleValue(blockState.isAir)
return@put DoubleValue(blockState.isAir)
}
functions["get_block"] = { params ->
map["get_block"] = put@{ params ->
val x = params.getInt(0)
val y = params.getInt(1)
val z = params.getInt(2)
val block = world.getBlockState(BlockPos(x, y, z)).block
world.blockRegistry.wrapAsHolder(block).asMoLangValue(Registries.BLOCK)
return@put world.blockRegistry.wrapAsHolder(block).asMoLangValue(Registries.BLOCK)
}
functions["spawn_explosion"] = { params ->
map["spawn_explosion"] = { params ->
val x = params.getDouble(0)
val y = params.getDouble(1)
val z = params.getDouble(2)
@@ -139,33 +136,32 @@ object WorldMoLangFunctions : AbstractMoLangFunctionHolder<Holder<Level>>() {
z,
range,
ExplosionInteraction.valueOf(
params.getStringOrNull(4)?.uppercase() ?: ExplosionInteraction.TNT.name,
),
params.getStringOrNull(4)?.uppercase() ?: ExplosionInteraction.TNT.name
)
)
}
functions["spawn_lightning"] = { params ->
map["spawn_lightning"] = put@{ params ->
val x = params.getDouble(0)
val y = params.getDouble(1)
val z = params.getDouble(2)
val lightning = LightningBolt(EntityType.LIGHTNING_BOLT, world)
lightning.setPos(x, y, z)
world.addFreshEntity(lightning)
DoubleValue.ONE
return@put DoubleValue.ONE
}
// q.entity.world.spawn_bedrock_particles(effect, x, y, z, [player]) - sends to everyone nearby or just to the player if they're set.
functions["spawn_bedrock_particles"] = { params ->
map["spawn_bedrock_particles"] = { params ->
val particle = params.getString(0).asResource()
val x = params.getDouble(1)
val y = params.getDouble(2)
val z = params.getDouble(3)
val player =
params.getOrNull<MoValue>(4)?.let {
when (it) {
is StringValue -> world.getPlayerByUUID(UUID.fromString(it.value))
is ObjectValue<*> -> it.obj
else -> null
}
} as? ServerPlayer
val player = params.getOrNull<MoValue>(4)?.let {
when (it) {
is StringValue -> world.getPlayerByUUID(UUID.fromString(it.value))
is ObjectValue<*> -> it.obj
else -> null
}
} as? ServerPlayer
val pos = Vec3(x, y, z)
val packet = SpawnSnowstormParticlePacket(particle, pos)
@@ -175,7 +171,7 @@ object WorldMoLangFunctions : AbstractMoLangFunctionHolder<Holder<Level>>() {
packet.sendToPlayersAround(x, y, z, 64.0, world.dimension())
}
}
functions["spawn_pokemon"] = spawnPokemon@{ params ->
map["spawn_pokemon"] = put@{ params ->
val x = params.getInt(0)
val y = params.getInt(1)
val z = params.getInt(2)
@@ -183,50 +179,49 @@ object WorldMoLangFunctions : AbstractMoLangFunctionHolder<Holder<Level>>() {
val pos = BlockPos(x, y, z)
if (!Level.isInSpawnableBounds(pos)) return@spawnPokemon DoubleValue.ZERO
if (!Level.isInSpawnableBounds(pos)) {
return@put DoubleValue.ZERO
}
val pokemon = props.createEntity(world)
pokemon.moveTo(pos, pokemon.yRot, pokemon.xRot)
if (world.addFreshEntity(pokemon)) return@spawnPokemon pokemon.struct
DoubleValue.ZERO
if (world.addFreshEntity(pokemon)) {
return@put pokemon.struct
} else {
return@put DoubleValue.ZERO
}
}
functions["spawn_npc"] = spawnNpc@{ params ->
map["spawn_npc"] = put@{ params ->
val x = params.getDouble(0)
val y = params.getDouble(1)
val z = params.getDouble(2)
val npcClass =
params
.getStringOrNull(3)
?.let { NPCClasses.getByIdentifier(it.asIdentifierDefaultingNamespace()) }
val npcClass = params.getStringOrNull(3)
?.let { NPCClasses.getByIdentifier(it.asIdentifierDefaultingNamespace()) }
val level = params.getInt(4)
if (npcClass == null) return@spawnNpc DoubleValue.ZERO
if (npcClass == null) return@put DoubleValue.ZERO
val npc = NPCEntity(world)
npc.moveTo(x, y, z, npc.yRot, npc.xRot)
npc.npc = npcClass
npc.initialize(level)
if (world.addFreshEntity(npc)) return@spawnNpc npc.asMoLangValue()
DoubleValue.ZERO
if (world.addFreshEntity(npc)) {
return@put npc.asMoLangValue()
}
return@put DoubleValue.ZERO
}
functions["play_sound_on_server"] = { params ->
map["play_sound_on_server"] = { params ->
val sound = params.getString(0).asResource()
val soundSource = params.getString(1).uppercase()
val x = params.getDouble(2)
val y = params.getDouble(3)
val z = params.getDouble(4)
val player =
params.getOrNull<MoValue>(5)?.let {
when (it) {
is StringValue -> world.getPlayerByUUID(UUID.fromString(it.value))
is ObjectValue<*> -> it.obj
else -> null
}
} as? ServerPlayer
val player = params.getOrNull<MoValue>(5)?.let {
when (it) {
is StringValue -> world.getPlayerByUUID(UUID.fromString(it.value))
is ObjectValue<*> -> it.obj
else -> null
}
} as? ServerPlayer
val volume = params.getDoubleOrNull(6)?.toFloat() ?: 1.0f
val pitch = params.getDoubleOrNull(7)?.toFloat() ?: 1.0f
@@ -238,30 +233,29 @@ object WorldMoLangFunctions : AbstractMoLangFunctionHolder<Holder<Level>>() {
packet.sendToPlayersAround(x, y, z, 16.0, world.dimension())
}
}
functions["get_entities_around"] = { params ->
map["get_entities_around"] = put@{ params ->
val x = params.getDouble(0)
val y = params.getDouble(1)
val z = params.getDouble(2)
val range = params.getDouble(3) * 2
val entities = world.getEntities(null, AABB.ofSize(Vec3(x, y, z), range, range, range))
entities
return@put entities
.filterIsInstance<LivingEntity>()
.map { it.asMostSpecificMoLangValue() }
.asArrayValue()
}
functions["is_healer_in_use"] = isHealerInUse@{ params ->
map["is_healer_in_use"] = put@{ params ->
val pos = params.get<ArrayStruct>(0).asBlockPos()
val healer =
world.getBlockEntity(pos, CobblemonBlockEntities.HEALING_MACHINE).orElse(null)
?: return@isHealerInUse DoubleValue.ONE
return@isHealerInUse DoubleValue(healer.isInUse)
val healer = world.getBlockEntity(pos, CobblemonBlockEntities.HEALING_MACHINE).orElse(null)
?: return@put DoubleValue.ONE
return@put DoubleValue(healer.isInUse)
}
functions["spawn_loot_table_items"] = spawnLootTableItems@{ params ->
map["spawn_loot_table_items"] = put@{ params ->
val serverLevel = world as? ServerLevel
if (serverLevel == null) {
Cobblemon.LOGGER.warn("spawn_loot_table_items: world is not a ServerLevel")
return@spawnLootTableItems DoubleValue.ZERO
return@put DoubleValue.ZERO
}
val lootTableId = params.getString(0).asIdentifierDefaultingNamespace()
val x = params.getDouble(1)
@@ -271,11 +265,9 @@ object WorldMoLangFunctions : AbstractMoLangFunctionHolder<Holder<Level>>() {
val lootTableKey = ResourceKey.create(Registries.LOOT_TABLE, lootTableId)
val lootTable = serverLevel.server.reloadableRegistries().getLootTable(lootTableKey)
val lootParams =
LootParams
.Builder(serverLevel)
.withParameter(LootContextParams.ORIGIN, Vec3(x, y, z))
.create(LootContextParamSets.COMMAND)
val lootParams = LootParams.Builder(serverLevel)
.withParameter(LootContextParams.ORIGIN, Vec3(x, y, z))
.create(LootContextParamSets.COMMAND)
val items = lootTable.getRandomItems(lootParams)
val spawnedItems = ArrayStruct(hashMapOf())
@@ -293,10 +285,9 @@ object WorldMoLangFunctions : AbstractMoLangFunctionHolder<Holder<Level>>() {
}
}
}
spawnedItems
return@put spawnedItems
}
return functions
return map
}
}
@@ -1,18 +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.molangui
enum class GuiMode {
OVERLAY,
SCREEN;
companion object {
fun fromString(value: String): GuiMode? = entries.firstOrNull { it.name.equals(value, ignoreCase = true) }
}
}
@@ -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.molangui
import com.cobblemon.mod.common.api.scripting.CobblemonScripts
import java.util.concurrent.ConcurrentHashMap
object MolangUiActionRegistry {
private val handlers = ConcurrentHashMap<ActionKey, MolangUiActionHandler>()
fun register(key: ActionKey, handler: MolangUiActionHandler) {
check(handlers.putIfAbsent(key, handler) == null) { "Molang UI action '$key' is already registered" }
}
fun unregister(key: ActionKey): Boolean = handlers.remove(key) != null
internal fun dispatch(
session: MolangUiSession,
action: ActionKey,
parameters: Map<String, ParameterValue>
) {
val definition = session.contract.actions[action]
?: throw IllegalArgumentException("undeclared action '${action.value}'")
val registered = handlers[action]
if (registered != null) {
registered.handle(MolangUiActionContext(session, session.player, parameters))
return
}
val scriptId = definition.handler
?: throw IllegalStateException("action '${action.value}' has no registered handler")
val script = CobblemonScripts.scripts[scriptId]
?: throw IllegalStateException("Molang UI action script '$scriptId' was not loaded")
val runtime = MolangUiServerRuntimeFactory.create(session)
parameters.forEach { (name, value) -> runtime.environment.setSimpleVariable(name, value.asMoLangValue()) }
script.resolve(runtime, emptyMap())
}
internal fun executeOnOpen(session: MolangUiSession) {
val scriptId = session.contract.onOpen ?: return
val script = CobblemonScripts.scripts[scriptId]
?: throw IllegalStateException("Molang UI on_open script '$scriptId' was not loaded")
script.resolve(MolangUiServerRuntimeFactory.create(session), emptyMap())
}
}
@@ -1,201 +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.molangui
import com.google.gson.JsonArray
import com.cobblemon.mod.common.api.data.DataRegistry
import com.cobblemon.mod.common.api.molangui.container.CollectionConfig
import com.cobblemon.mod.common.api.molangui.container.CollectionSource
import com.cobblemon.mod.common.api.molangui.container.CloseItemsPolicy
import com.cobblemon.mod.common.api.molangui.container.ItemFilterEntry
import com.cobblemon.mod.common.api.molangui.container.ShiftClickRule
import com.cobblemon.mod.common.api.molangui.container.SlotLayout
import com.cobblemon.mod.common.api.reactive.SimpleObservable
import com.cobblemon.mod.common.net.messages.client.molangui.MolangUiCompatibilityRequestPacket
import com.cobblemon.mod.common.util.cobblemonResource
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import net.minecraft.resources.ResourceLocation
import net.minecraft.core.registries.BuiltInRegistries
import net.minecraft.server.level.ServerPlayer
import net.minecraft.server.packs.PackType
import net.minecraft.server.packs.resources.ResourceManager
import com.cobblemon.mod.common.Cobblemon
import java.util.concurrent.atomic.AtomicLong
object MolangUiContractRegistry : DataRegistry {
override val id = cobblemonResource("molang_ui_contracts")
override val type = PackType.SERVER_DATA
override val observable = SimpleObservable<MolangUiContractRegistry>()
private val contracts = linkedMapOf<ResourceLocation, MolangUiContract>()
private val parseFailures = AtomicLong()
private const val DIRECTORY = "molang_ui"
override fun reload(manager: ResourceManager) {
val next = linkedMapOf<ResourceLocation, MolangUiContract>()
manager.listResources(DIRECTORY) { it.path.endsWith(".json") }.forEach { (location, resource) ->
val id = ResourceLocation.fromNamespaceAndPath(
location.namespace,
location.path.removePrefix("$DIRECTORY/").removeSuffix(".json")
)
try {
val json = resource.openAsReader().use { JsonParser.parseReader(it).asJsonObject }
next[id] = parse(id, json)
} catch (error: Exception) {
parseFailures.incrementAndGet()
Cobblemon.LOGGER.error("Rejected Molang UI contract '$location': ${error.message}")
}
}
require(next.size <= MolangUiPacketLimits.MAX_CONTRACTS) {
"Molang UI contract count exceeds ${MolangUiPacketLimits.MAX_CONTRACTS}"
}
contracts.clear()
contracts.putAll(next)
Cobblemon.LOGGER.info("Loaded ${contracts.size} validated Molang UI contracts")
observable.emit(this)
}
override fun sync(player: ServerPlayer) {
MolangUiCompatibilityRequestPacket(versions()).sendToPlayer(player)
}
internal fun parse(
id: ResourceLocation,
json: JsonObject,
itemExists: (ResourceLocation) -> Boolean = BuiltInRegistries.ITEM::containsKey
): MolangUiContract {
require(!json.has("blocks_input")) { "blocks_input is unsupported; input handling is determined by mode" }
val formatVersion = json.get("format_version").requireUiInt("format_version")
require(formatVersion == MOLANG_UI_FORMAT_VERSION) { "unsupported format_version $formatVersion" }
val contractVersion = json.get("contract_version").requireUiString("contract_version")
MolangUiPacketLimits.requireIdentifier(contractVersion, "contract_version")
val mode = GuiMode.fromString(json.get("mode").requireUiString("mode"))
?: throw IllegalArgumentException("unknown mode '${json.get("mode").asString}'")
val parameters = parseParameters(json.getAsJsonObject("parameters"))
val actions = parseActions(json.getAsJsonObject("actions"))
val collections = parseCollections(json.getAsJsonObject("collections"), itemExists)
val shiftRules = parseShiftRules(json.getAsJsonArray("shift_click_rules"), collections.keys)
val closePolicy = json.get("on_close_items")?.requireUiString("on_close_items")?.let { raw ->
CloseItemsPolicy.fromId(raw) ?: throw IllegalArgumentException("unknown on_close_items policy '$raw'")
} ?: CloseItemsPolicy.RETURN
return MolangUiContract(
id, formatVersion, contractVersion, mode, parameters, actions,
json.get("on_open")?.requireUiString("on_open")?.let { ResourceLocation.tryParse(it) ?: error("invalid on_open script id") },
collections, shiftRules, closePolicy,
json.get("exclusive")?.requireUiBoolean("exclusive") ?: false
)
}
private fun parseParameters(json: JsonObject?): Map<String, ParameterDefinition> {
require((json?.size() ?: 0) <= MolangUiPacketLimits.MAX_ACTION_FIELDS) { "too many parameters" }
return json?.entrySet()?.associate { (name, element) ->
MolangUiParameters.requireName(name)
val obj = element.asJsonObject
val type = ParameterType.fromId(obj.get("type").requireUiString("parameters.$name.type"))
?: throw IllegalArgumentException("unknown type for parameter '$name'")
val default = obj.get("default")?.let { parseDefault(type, it, "parameters.$name.default") }
name to ParameterDefinition(type, obj.get("required")?.requireUiBoolean("parameters.$name.required") ?: (default == null), default)
}?.toMap() ?: emptyMap()
}
private fun parseDefault(type: ParameterType, element: JsonElement, path: String): ParameterDefault {
if (element.isJsonObject && element.asJsonObject.has("molang")) {
require(element.asJsonObject.size() == 1) { "expression '$path' contains unknown fields" }
return ParameterDefault.Molang(element.asJsonObject.get("molang").requireUiString("$path.molang"))
}
val value = when (type) {
ParameterType.STRING -> ParameterValue.StringValue(element.requireUiString(path))
ParameterType.NUMBER -> ParameterValue.NumberValue(element.requireUiNumber(path))
ParameterType.BOOLEAN -> ParameterValue.BooleanValue(element.requireUiBoolean(path))
ParameterType.VEC3 -> {
require(element.isJsonArray && element.asJsonArray.size() == 3) { "'$path' must be a three-number array" }
ParameterValue.Vec3Value(
element.asJsonArray[0].requireUiNumber("$path[0]"),
element.asJsonArray[1].requireUiNumber("$path[1]"),
element.asJsonArray[2].requireUiNumber("$path[2]")
)
}
}
MolangUiPacketLimits.requireValue(value.asWireValue(), path)
return ParameterDefault.Literal(value)
}
private fun parseActions(json: JsonObject?): Map<ActionKey, ActionDefinition> {
val result = json?.entrySet()?.associate { (name, element) ->
val key = ActionKey(name)
val obj = element.asJsonObject
val handler = obj.get("handler")?.requireUiString("actions.$name.handler")?.let { ResourceLocation.tryParse(it) ?: error("invalid handler for action '$name'") }
val params = obj.getAsJsonObject("parameters")?.entrySet()?.associate { (paramName, typeElement) ->
MolangUiPacketLimits.requireIdentifier(paramName, "action parameter name")
paramName to (ParameterType.fromId(typeElement.requireUiString("actions.$name.parameters.$paramName"))
?: throw IllegalArgumentException("unknown type for action parameter '$name.$paramName'"))
}?.toMap() ?: emptyMap()
require(params.size <= MolangUiPacketLimits.MAX_ACTION_FIELDS) { "action '$name' has too many parameters" }
key to ActionDefinition(handler, params)
}?.toMap() ?: emptyMap()
require(result.size <= MolangUiPacketLimits.MAX_ACTION_FIELDS) { "contract has too many actions" }
return result
}
private fun parseCollections(
json: JsonObject?,
itemExists: (ResourceLocation) -> Boolean
): Map<String, CollectionConfig> {
require((json?.size() ?: 0) <= MolangUiPacketLimits.MAX_CONTRACTS) { "too many container collections" }
return json?.entrySet()?.associate { (name, element) ->
MolangUiPacketLimits.requireIdentifier(name, "collection name")
val obj = element.asJsonObject
val source = CollectionSource.fromId(obj.get("source")?.requireUiString("collections.$name.source") ?: "session")
?: throw IllegalArgumentException("unknown source for collection '$name'")
val filters = obj.getAsJsonArray("allowed_items")?.map { raw ->
val filter = ItemFilterEntry.parse(raw.requireUiString("collections.$name.allowed_items"))
?: throw IllegalArgumentException("invalid item filter '${raw.asString}' in '$name'")
if (filter is ItemFilterEntry.Item) require(itemExists(filter.id)) {
"unknown item '${filter.id}' in collection '$name'"
}
filter
}?.toList() ?: emptyList()
val layoutObj = obj.getAsJsonObject("layout") ?: throw IllegalArgumentException("collection '$name' is missing layout")
val offset = layoutObj.pair("offset", 0, 0)
val grid = layoutObj.pair("grid_dimensions", 1, 1)
val slotSize = layoutObj.pair("slot_size", 18, 18)
val spacing = layoutObj.pair("slot_spacing", 0, 0)
val layout = SlotLayout(offset.first, offset.second, grid.first, grid.second, slotSize.first, slotSize.second, spacing.first, spacing.second)
name to CollectionConfig(
obj.get("size").requireUiInt("collections.$name.size"), filters,
obj.get("take_only")?.requireUiBoolean("collections.$name.take_only") ?: false, source,
obj.getAsJsonArray("slots")?.map { it.requireUiInt("collections.$name.slots") }?.toList(), layout
)
}?.toMap() ?: emptyMap()
}
private fun parseShiftRules(array: JsonArray?, collections: Set<String>): List<ShiftClickRule> =
array?.map { element ->
val obj = element.asJsonObject
val from = obj.stringList("from")
val to = obj.stringList("to")
val valid = collections + ShiftClickRule.PLAYER_SIDE_NAMES
require(from.isNotEmpty() && to.isNotEmpty() && (from + to).all { it in valid }) { "shift-click rule references an unknown collection" }
ShiftClickRule(from, to)
}?.toList() ?: emptyList()
fun getContract(id: ResourceLocation): MolangUiContract? = contracts[id]
fun getAllContracts(): Map<ResourceLocation, MolangUiContract> = contracts.toMap()
fun versions(): Map<ResourceLocation, String> = contracts.mapValues { it.value.contractVersion }
fun parseFailureCount(): Long = parseFailures.get()
}
private fun JsonObject.pair(name: String, first: Int, second: Int): Pair<Int, Int> {
val value = getAsJsonArray(name) ?: return first to second
require(value.size() == 2) { "'$name' must have two entries" }
return value[0].requireUiInt(name) to value[1].requireUiInt(name)
}
private fun JsonObject.stringList(name: String): List<String> = getAsJsonArray(name)?.map { it.requireUiString(name) }?.toList()
?: throw IllegalArgumentException("missing array '$name'")
@@ -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.api.molangui
import com.cobblemon.mod.common.api.molangui.widget.WidgetDefinition
import net.minecraft.resources.ResourceLocation
data class MolangUiDefinition(
val guiId: ResourceLocation,
val formatVersion: Int,
val contractVersion: String,
val mode: GuiMode,
val parameters: Map<String, ParameterType>,
val actions: Set<ActionKey>,
val exclusive: Boolean,
val root: WidgetDefinition,
val variables: Map<String, Double>
)
@@ -1,26 +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.molangui
import java.util.concurrent.atomic.AtomicLong
object MolangUiDiagnostics {
private val activeSessions = AtomicLong()
private val rejectedActions = AtomicLong()
private val compatibilityFailures = AtomicLong()
internal fun sessionOpened() { activeSessions.incrementAndGet() }
internal fun sessionClosed() { activeSessions.updateAndGet { value -> (value - 1).coerceAtLeast(0) } }
internal fun actionRejected() { rejectedActions.incrementAndGet() }
internal fun compatibilityFailures(count: Long) { compatibilityFailures.addAndGet(count) }
fun snapshot() = Snapshot(activeSessions.get(), rejectedActions.get(), compatibilityFailures.get(), MolangUiContractRegistry.parseFailureCount())
data class Snapshot(val activeSessions: Long, val rejectedActions: Long, val compatibilityFailures: Long, val parseFailures: Long)
}
@@ -1,35 +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.molangui
import com.google.gson.JsonElement
internal fun JsonElement?.requireUiString(path: String): String {
require(this != null && isJsonPrimitive && asJsonPrimitive.isString) { "'$path' must be a string" }
return asString
}
internal fun JsonElement?.requireUiBoolean(path: String): Boolean {
require(this != null && isJsonPrimitive && asJsonPrimitive.isBoolean) { "'$path' must be a boolean" }
return asBoolean
}
internal fun JsonElement?.requireUiNumber(path: String): Double {
require(this != null && isJsonPrimitive && asJsonPrimitive.isNumber) { "'$path' must be a number" }
return asDouble.also { require(it.isFinite()) { "'$path' must be finite" } }
}
internal fun JsonElement?.requireUiInt(path: String): Int {
require(this != null && isJsonPrimitive && asJsonPrimitive.isNumber) { "'$path' must be an integer" }
return try {
asBigDecimal.intValueExact()
} catch (error: ArithmeticException) {
throw IllegalArgumentException("'$path' must be an integer in the 32-bit range", error)
}
}
@@ -1,151 +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.molangui
import com.cobblemon.mod.common.api.molangui.container.SlotLayout
import com.cobblemon.mod.common.api.molangui.container.CollectionConfig
import com.cobblemon.mod.common.api.molangui.container.CloseItemsPolicy
import com.cobblemon.mod.common.api.molangui.container.ShiftClickRule
import net.minecraft.core.BlockPos
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import java.util.UUID
const val MOLANG_UI_FORMAT_VERSION = 1
enum class ParameterType(val id: String) {
STRING("string"),
NUMBER("number"),
BOOLEAN("boolean"),
VEC3("vec3");
companion object {
fun fromId(id: String): ParameterType? = entries.firstOrNull { it.id == id }
}
}
sealed interface ParameterValue {
val type: ParameterType
fun asWireValue(): String
data class StringValue(val value: String) : ParameterValue {
override val type = ParameterType.STRING
override fun asWireValue() = value
}
data class NumberValue(val value: Double) : ParameterValue {
init { require(value.isFinite()) { "number parameters must be finite" } }
override val type = ParameterType.NUMBER
override fun asWireValue() = value.toString()
}
data class BooleanValue(val value: Boolean) : ParameterValue {
override val type = ParameterType.BOOLEAN
override fun asWireValue() = value.toString()
}
data class Vec3Value(val x: Double, val y: Double, val z: Double) : ParameterValue {
init { require(x.isFinite() && y.isFinite() && z.isFinite()) { "vec3 parameters must be finite" } }
override val type = ParameterType.VEC3
override fun asWireValue() = "$x,$y,$z"
}
companion object {
fun parse(type: ParameterType, value: String): ParameterValue? = when (type) {
ParameterType.STRING -> StringValue(value)
ParameterType.NUMBER -> value.toDoubleOrNull()?.takeIf(Double::isFinite)?.let(::NumberValue)
ParameterType.BOOLEAN -> value.toBooleanStrictOrNull()?.let(::BooleanValue)
ParameterType.VEC3 -> value.split(',').takeIf { it.size == 3 }
?.map { it.trim().toDoubleOrNull() }
?.takeIf { it.all { part -> part != null && part.isFinite() } }
?.let { Vec3Value(it[0]!!, it[1]!!, it[2]!!) }
}
}
}
data class ParameterDefinition(
val type: ParameterType,
val required: Boolean = true,
val default: ParameterDefault? = null
)
sealed interface ParameterDefault {
data class Literal(val value: ParameterValue) : ParameterDefault
data class Molang(val expression: String) : ParameterDefault
}
@JvmInline
value class ActionKey(val value: String) {
init { require(value.isNotBlank() && value.length <= MolangUiPacketLimits.MAX_IDENTIFIER_LENGTH) }
}
data class ActionDefinition(
val handler: ResourceLocation? = null,
val parameters: Map<String, ParameterType> = emptyMap()
)
data class MolangUiContract(
val id: ResourceLocation,
val formatVersion: Int,
val contractVersion: String,
val mode: GuiMode,
val parameters: Map<String, ParameterDefinition>,
val actions: Map<ActionKey, ActionDefinition>,
val onOpen: ResourceLocation?,
val collections: Map<String, CollectionConfig>,
val shiftClickRules: List<ShiftClickRule>,
val closeItemsPolicy: CloseItemsPolicy,
val exclusive: Boolean
) {
init {
require(mode == GuiMode.SCREEN || collections.isEmpty()) { "overlay views cannot have container collections" }
SlotLayout.validateCollections(collections.mapValues { it.value.layout })
}
}
data class MolangUiOpenRequest(
val uiId: ResourceLocation,
val parameters: Map<String, ParameterValue> = emptyMap(),
val blockPos: BlockPos? = null
)
sealed interface MolangUiOpenResult {
data class Opened(val session: MolangUiSessionHandle) : MolangUiOpenResult
data class Rejected(val reason: RejectionReason, val detail: String) : MolangUiOpenResult
}
enum class RejectionReason {
UNKNOWN_UI,
INCOMPATIBLE_CLIENT,
INVALID_PARAMETERS,
SESSION_LIMIT,
INVALID_CONTAINER,
INTERNAL_ERROR
}
enum class SessionState { OPENING, ACTIVE, CLOSING, CLOSED }
interface MolangUiSessionHandle : AutoCloseable {
val sessionId: UUID
val uiId: ResourceLocation
val state: SessionState
fun updateParameters(parameters: Map<String, ParameterValue>): Boolean
fun updateVariables(variables: Map<String, Double>): Boolean
override fun close()
}
data class MolangUiActionContext(
val session: MolangUiSessionHandle,
val player: ServerPlayer,
val parameters: Map<String, ParameterValue>
)
fun interface MolangUiActionHandler {
fun handle(context: MolangUiActionContext)
}
@@ -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.molangui
object MolangUiPacketLimits {
const val MAX_CONTRACTS = 256
const val MAX_ACTION_FIELDS = 32
const val MAX_IDENTIFIER_LENGTH = 128
const val MAX_VALUE_LENGTH = 4 * 1024
const val MAX_PACKET_BYTES = 256 * 1024
fun requireIdentifier(value: String, label: String): String {
require(value.isNotBlank() && value.length <= MAX_IDENTIFIER_LENGTH) {
"$label must contain 1..$MAX_IDENTIFIER_LENGTH characters"
}
return value
}
fun requireValue(value: String, label: String): String {
require(value.length <= MAX_VALUE_LENGTH) { "$label exceeds $MAX_VALUE_LENGTH characters" }
return value
}
}
@@ -1,68 +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.molangui
import com.bedrockk.molang.runtime.value.DoubleValue
import com.bedrockk.molang.runtime.value.MoValue
import com.bedrockk.molang.runtime.value.StringValue
internal object MolangUiParameters {
val GENERAL_FUNCTIONS = setOf("format", "to_string", "to_number", "to_int", "is_number", "is_int", "is_blank")
private val reserved = GENERAL_FUNCTIONS + setOf(
"gui_id", "gui_time", "delta", "screen_width", "screen_height", "mouse_x", "mouse_y", "item", "translate", "event"
)
private val namePattern = Regex("[a-z_][a-z0-9_]*")
fun requireName(name: String) {
MolangUiPacketLimits.requireIdentifier(name, "parameter name")
require(namePattern.matches(name) && name !in reserved) { "invalid or reserved parameter name '$name'" }
}
fun decode(types: Map<String, ParameterType>, values: Map<String, String>): Map<String, ParameterValue> {
require(values.size <= MolangUiPacketLimits.MAX_ACTION_FIELDS) { "too many parameters" }
return values.mapValues { (name, raw) ->
requireName(name)
val type = types[name] ?: throw IllegalArgumentException("undeclared parameter '$name'")
MolangUiPacketLimits.requireValue(raw, name)
ParameterValue.parse(type, raw) ?: throw IllegalArgumentException("invalid ${type.id} parameter '$name'")
}
}
fun validate(values: Map<String, ParameterValue>) {
require(values.size <= MolangUiPacketLimits.MAX_ACTION_FIELDS) { "too many parameters" }
values.forEach { (name, value) ->
requireName(name)
MolangUiPacketLimits.requireValue(value.asWireValue(), name)
}
}
fun requireVariableName(name: String): String {
MolangUiPacketLimits.requireIdentifier(name, "variable name")
return name.removePrefix("v.").also {
require(namePattern.matches(it)) { "invalid variable '$name'" }
}
}
fun validateVariables(values: Map<String, Double>) {
val names = mutableSetOf<String>()
values.forEach { (name, value) ->
val canonical = requireVariableName(name)
require(names.add(canonical) && value.isFinite()) {
"invalid or duplicate variable '$name'"
}
}
}
}
internal fun ParameterValue.asMoLangValue(): MoValue = when (this) {
is ParameterValue.StringValue -> StringValue(value)
is ParameterValue.NumberValue -> DoubleValue(value)
is ParameterValue.BooleanValue -> DoubleValue(value)
is ParameterValue.Vec3Value -> StringValue(asWireValue())
}
@@ -1,95 +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.molangui
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.molangui.template.TemplateLibraryRegistry
import com.cobblemon.mod.common.api.molangui.widget.WidgetDefinition
import com.cobblemon.mod.common.api.molangui.widget.WidgetTree
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.packs.resources.ResourceManager
import java.util.concurrent.atomic.AtomicLong
/** Client asset registry. Server code must use [MolangUiContractRegistry]. */
object MolangUiRegistry {
private const val DIRECTORY = "molang_ui"
private val definitions = linkedMapOf<ResourceLocation, MolangUiDefinition>()
private val parseFailures = AtomicLong()
fun reload(manager: ResourceManager) {
TemplateLibraryRegistry.reload(manager)
val next = linkedMapOf<ResourceLocation, MolangUiDefinition>()
manager.listResources(DIRECTORY) { it.path.endsWith(".json") && !it.path.contains("/templates/") }
.forEach { (identifier, resource) ->
val guiId = ResourceLocation.fromNamespaceAndPath(
identifier.namespace,
identifier.path.removePrefix("$DIRECTORY/").removeSuffix(".json")
)
try {
val json = resource.openAsReader().use { JsonParser.parseReader(it).asJsonObject }
next[guiId] = parse(guiId, json)
} catch (error: Exception) {
parseFailures.incrementAndGet()
Cobblemon.LOGGER.error("Rejected Molang UI view '$identifier': ${error.message}")
}
}
require(next.size <= MolangUiPacketLimits.MAX_CONTRACTS) { "too many Molang UI views" }
definitions.clear()
definitions.putAll(next)
Cobblemon.LOGGER.info("Loaded ${definitions.size} validated Molang UI views")
}
internal fun parse(guiId: ResourceLocation, json: JsonObject): MolangUiDefinition {
require(!json.has("blocks_input")) { "blocks_input is unsupported; input handling is determined by mode" }
val formatVersion = json.get("format_version").requireUiInt("format_version")
require(formatVersion == MOLANG_UI_FORMAT_VERSION) { "unsupported format_version $formatVersion" }
val contractVersion = json.get("contract_version").requireUiString("contract_version")
MolangUiPacketLimits.requireIdentifier(contractVersion, "contract_version")
val mode = GuiMode.fromString(json.get("mode").requireUiString("mode"))
?: throw IllegalArgumentException("unknown mode '${json.get("mode").asString}'")
val parameterJson = json.getAsJsonObject("parameters")
require((parameterJson?.size() ?: 0) <= MolangUiPacketLimits.MAX_ACTION_FIELDS) { "too many parameters" }
val parameters = parameterJson?.entrySet()?.associate { (name, value) ->
MolangUiParameters.requireName(name)
val type = ParameterType.fromId(value.requireUiString("parameters.$name"))
?: throw IllegalArgumentException("unknown parameter type for '$name'")
name to type
}?.toMap() ?: emptyMap()
val actions = json.getAsJsonArray("actions")?.map { ActionKey(it.requireUiString("actions")) }?.toSet() ?: emptySet()
require(actions.size <= MolangUiPacketLimits.MAX_ACTION_FIELDS) { "too many declared actions" }
val variables = json.getAsJsonObject("variables")?.entrySet()?.associate { (name, value) ->
MolangUiPacketLimits.requireIdentifier(name, "variable name")
name to value.requireUiNumber("variables.$name")
}?.toMap() ?: emptyMap()
MolangUiParameters.validateVariables(variables)
require(!json.has("imports")) { "template imports are unsupported; use qualified template references" }
val root = WidgetTree.parse(json)
val referencedActions = linkedSetOf<ActionKey>().also { collectActions(root, it) }
val undeclared = referencedActions - actions
require(undeclared.isEmpty()) { "widgets reference undeclared actions: ${undeclared.joinToString { it.value }}" }
return MolangUiDefinition(
guiId, formatVersion, contractVersion, mode, parameters, actions,
json.get("exclusive")?.requireUiBoolean("exclusive") ?: false,
root, variables
)
}
private fun collectActions(node: WidgetDefinition, output: MutableSet<ActionKey>) {
node.events.values.forEach { output += it.action }
node.children.forEach { collectActions(it, output) }
node.itemTemplate?.let { collectActions(it, output) }
}
fun getDefinition(guiId: ResourceLocation): MolangUiDefinition? = definitions[guiId]
fun getAllDefinitions(): Map<ResourceLocation, MolangUiDefinition> = definitions.toMap()
fun getVersionMap(): Map<ResourceLocation, String> = definitions.mapValues { it.value.contractVersion }
fun parseFailureCount(): Long = parseFailures.get()
}
@@ -1,93 +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.molangui
import com.bedrockk.molang.runtime.MoLangRuntime
import com.bedrockk.molang.runtime.value.DoubleValue
import com.bedrockk.molang.runtime.value.MoValue
import com.bedrockk.molang.runtime.value.StringValue
import com.cobblemon.mod.common.api.molang.function.GeneralMoLangFunctions
import net.minecraft.core.registries.BuiltInRegistries
import net.minecraft.resources.ResourceLocation
import net.minecraft.world.item.Item
import net.minecraft.world.item.ItemStack
/** Builds a server runtime with only UI-scoped capabilities. It never exposes q.player. */
internal object MolangUiServerRuntimeFactory {
fun create(session: MolangUiSession): MoLangRuntime = MoLangRuntime().also { runtime ->
GeneralMoLangFunctions.functions.filterKeys { it in MolangUiParameters.GENERAL_FUNCTIONS }.forEach { (name, function) ->
runtime.environment.query.addFunction(name) { function(it) }
}
runtime.environment.query.addFunction("parameter") { params ->
session.getParameter(params.getString(0))?.asMoLangValue() ?: DoubleValue.ZERO
}
runtime.environment.query.addFunction("update_parameter") { params ->
val name = params.getString(0)
val raw = params.get<MoValue>(1).asString()
val type = session.contract.parameters[name]?.type
DoubleValue(type != null && ParameterValue.parse(type, raw)?.let { session.updateParameters(mapOf(name to it)) } == true)
}
runtime.environment.query.addFunction("update_variable") { params ->
val value = params.getDouble(1)
DoubleValue(value.isFinite() && session.updateVariables(mapOf(params.getString(0) to value)))
}
runtime.environment.query.addFunction("close_ui") {
session.close()
DoubleValue.ONE
}
runtime.environment.query.addFunction("count_item") { params ->
val item = validatedItem(params.getString(0)) ?: return@addFunction DoubleValue.ZERO
DoubleValue(session.player.inventory.items.filter { it.`is`(item) }.sumOf(ItemStack::getCount).toDouble())
}
runtime.environment.query.addFunction("remove_items") { params ->
val item = validatedItem(params.getString(0)) ?: return@addFunction DoubleValue.ZERO
val count = params.getDouble(1).asNonnegativeInt() ?: return@addFunction DoubleValue.ZERO
DoubleValue(count > 0 && removeAtomically(session, item, count))
}
runtime.environment.query.addFunction("slot") { params ->
val index = params.getDouble(1).asNonnegativeInt() ?: return@addFunction StringValue("")
val stack = session.containerMenu?.getStack(params.getString(0), index)
if (stack == null || stack.isEmpty) StringValue("") else StringValue(BuiltInRegistries.ITEM.getKey(stack.item).toString())
}
runtime.environment.query.addFunction("replace_slot") { params ->
val itemId = params.getString(5)
validatedItem(itemId) ?: return@addFunction DoubleValue.ZERO
val inputIndex = params.getDouble(1).asNonnegativeInt() ?: return@addFunction DoubleValue.ZERO
val consumeCount = params.getDouble(2).asNonnegativeInt() ?: return@addFunction DoubleValue.ZERO
val outputIndex = params.getDouble(4).asNonnegativeInt() ?: return@addFunction DoubleValue.ZERO
val outputCount = params.getDouble(6).asNonnegativeInt() ?: return@addFunction DoubleValue.ZERO
val result = session.containerMenu?.replaceSlotAtomically(
params.getString(0), inputIndex, consumeCount,
params.getString(3), outputIndex, itemId, outputCount
) ?: false
DoubleValue(result)
}
}
private fun validatedItem(raw: String) = ResourceLocation.tryParse(raw)
?.takeIf(BuiltInRegistries.ITEM::containsKey)
?.let(BuiltInRegistries.ITEM::get)
private fun Double.asNonnegativeInt(): Int? =
takeIf { it.isFinite() && it in 0.0..Int.MAX_VALUE.toDouble() && it % 1.0 == 0.0 }?.toInt()
private fun removeAtomically(session: MolangUiSession, item: Item, count: Int): Boolean {
val stacks = session.player.inventory.items.filter { it.`is`(item) }
if (stacks.sumOf(ItemStack::getCount) < count) return false
var remaining = count
stacks.forEach { stack ->
if (remaining == 0) return@forEach
val removed = minOf(stack.count, remaining)
stack.shrink(removed)
remaining -= removed
}
session.player.inventory.setChanged()
return true
}
}
@@ -1,317 +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.molangui
import com.bedrockk.molang.runtime.MoLangRuntime
import net.minecraft.core.BlockPos
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.molangui.container.CollectionSource
import com.cobblemon.mod.common.api.molangui.container.MolangContainerMenu
import com.cobblemon.mod.common.api.molangui.container.ServerCollectionSetup
import com.cobblemon.mod.common.net.messages.client.molangui.MolangUiClosePacket
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.util.asExpressionLike
import net.minecraft.network.chat.Component
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.Container
import net.minecraft.world.MenuProvider
import net.minecraft.world.SimpleContainer
import net.minecraft.world.entity.player.Inventory
import net.minecraft.world.entity.player.Player
import net.minecraft.world.inventory.AbstractContainerMenu
import java.util.UUID
internal class MolangUiSession(
override val sessionId: UUID,
override val uiId: ResourceLocation,
val player: ServerPlayer,
val contract: MolangUiContract,
initialParameters: Map<String, ParameterValue>,
val blockPos: BlockPos?
) : MolangUiSessionHandle {
override var state: SessionState = SessionState.OPENING
private set
private val parameters = initialParameters.toMutableMap()
internal var containerMenu: MolangContainerMenu? = null
private var clientWasNotified = false
override fun updateParameters(parameters: Map<String, ParameterValue>): Boolean {
assertServerThread()
if (state != SessionState.ACTIVE || !validateParameterUpdate(parameters)) return false
MolangUiUpdatePacket(sessionId, parameters.mapValues { it.value.asWireValue() }).sendToPlayer(player)
this.parameters.putAll(parameters)
return true
}
override fun updateVariables(variables: Map<String, Double>): Boolean {
assertServerThread()
if (state != SessionState.ACTIVE || variables.size > MolangUiPacketLimits.MAX_ACTION_FIELDS) return false
try {
MolangUiParameters.validateVariables(variables)
} catch (error: IllegalArgumentException) {
return false
}
MolangUiUpdatePacket(sessionId, emptyMap(), variables).sendToPlayer(player)
return true
}
override fun close() = closeInternal(notifyClient = true, closeMenu = true)
internal fun closeFromClient() = closeInternal(notifyClient = false, closeMenu = true)
internal fun closeFromMenu() = closeInternal(notifyClient = true, closeMenu = false)
internal fun activate() {
assertServerThread()
check(state == SessionState.OPENING)
state = SessionState.ACTIVE
}
internal fun notifyOpened(hasContainer: Boolean) {
check(state == SessionState.OPENING)
clientWasNotified = true
MolangUiOpenPacket(
sessionId, uiId, contract.contractVersion, contract.mode,
parameters.mapValues { it.value.asWireValue() }, contract.exclusive,
hasContainer, contract.collections.mapValues { it.value.layout }
).sendToPlayer(player)
}
internal fun getParameter(name: String): ParameterValue? = parameters[name]
private fun validateParameterUpdate(values: Map<String, ParameterValue>): Boolean =
values.size <= MolangUiPacketLimits.MAX_ACTION_FIELDS && values.all { (name, value) ->
contract.parameters[name]?.type == value.type && value.asWireValue().length <= MolangUiPacketLimits.MAX_VALUE_LENGTH
}
private fun closeInternal(notifyClient: Boolean, closeMenu: Boolean) {
assertServerThread()
if (state == SessionState.CLOSING || state == SessionState.CLOSED) return
state = SessionState.CLOSING
val menu = containerMenu
containerMenu = null
try {
cleanup("notify client") {
if (notifyClient && clientWasNotified) MolangUiClosePacket(sessionId).sendToPlayer(player)
}
cleanup("close container") {
if (closeMenu && menu != null && player.containerMenu === menu) player.closeContainer()
}
cleanup("release items") { menu?.releaseOwnedItemsOnce(player) }
} finally {
state = SessionState.CLOSED
MolangUiService.remove(sessionId)
}
}
private inline fun cleanup(operation: String, action: () -> Unit) {
try {
action()
} catch (error: Exception) {
Cobblemon.LOGGER.error("Failed to $operation while closing Molang UI session $sessionId", error)
}
}
private fun assertServerThread() {
check(player.server.isSameThread) { "Molang UI sessions are confined to the server thread" }
}
}
object MolangUiService {
private const val MAX_OVERLAYS_PER_PLAYER = 8
private val sessions = linkedMapOf<UUID, MolangUiSession>()
private val disabled = mutableMapOf<UUID, Set<ResourceLocation>>()
private val rateLimiters = mutableMapOf<UUID, MolangUiRateLimiter>()
fun open(player: ServerPlayer, request: MolangUiOpenRequest): MolangUiOpenResult {
check(player.server.isSameThread) { "MolangUiService.open must run on the server thread" }
val contract = MolangUiContractRegistry.getContract(request.uiId)
?: return MolangUiOpenResult.Rejected(RejectionReason.UNKNOWN_UI, "Unknown Molang UI '${request.uiId}'")
// Intentionally does not wait for the compatibility round-trip; the client re-validates on open.
if (disabled[player.uuid]?.contains(request.uiId) == true) {
return MolangUiOpenResult.Rejected(RejectionReason.INCOMPATIBLE_CLIENT, "Client view is missing or incompatible")
}
val parameters = resolveParameters(contract, request.parameters)
?: return MolangUiOpenResult.Rejected(RejectionReason.INVALID_PARAMETERS, "Parameters do not match the contract")
val playerSessions = sessions.values.filter { it.player.uuid == player.uuid && it.state != SessionState.CLOSED }
if (contract.mode == GuiMode.OVERLAY && playerSessions.count { it.contract.mode == GuiMode.OVERLAY } >= MAX_OVERLAYS_PER_PLAYER) {
return MolangUiOpenResult.Rejected(RejectionReason.SESSION_LIMIT, "At most $MAX_OVERLAYS_PER_PLAYER overlays may be active")
}
val collectionSetup = try {
createCollections(player, contract, request.blockPos)
?: return MolangUiOpenResult.Rejected(RejectionReason.INVALID_CONTAINER, "The bound block container is unavailable or invalid")
} catch (error: Exception) {
Cobblemon.LOGGER.error("Failed to prepare Molang UI '${request.uiId}'", error)
return MolangUiOpenResult.Rejected(RejectionReason.INTERNAL_ERROR, error.message ?: "Unable to prepare UI")
}
val session = MolangUiSession(UUID.randomUUID(), request.uiId, player, contract, parameters, request.blockPos)
sessions[session.sessionId] = session
MolangUiDiagnostics.sessionOpened()
return try {
if (contract.mode == GuiMode.SCREEN) {
playerSessions.filter { it.contract.mode == GuiMode.SCREEN }.forEach(MolangUiSession::close)
}
check(session.state == SessionState.OPENING) { "Molang UI closed while replacing its previous screen" }
MolangUiActionRegistry.executeOnOpen(session)
check(session.state == SessionState.OPENING) { "Molang UI closed during on_open" }
val hasContainer = contract.collections.isNotEmpty()
session.notifyOpened(hasContainer)
check(session.state == SessionState.OPENING) { "Molang UI closed while notifying the client" }
if (hasContainer) openContainer(session, collectionSetup)
session.activate()
MolangUiOpenResult.Opened(session)
} catch (error: Exception) {
Cobblemon.LOGGER.error("Failed to open Molang UI '${request.uiId}'", error)
session.close()
MolangUiOpenResult.Rejected(RejectionReason.INTERNAL_ERROR, error.message ?: "Unable to open UI")
}
}
fun handleAction(sessionId: UUID, player: ServerPlayer, actionRaw: String, raw: Map<String, String>) {
check(player.server.isSameThread)
val session = sessions[sessionId]
if (session == null || session.player.uuid != player.uuid || session.state != SessionState.ACTIVE) {
MolangUiDiagnostics.actionRejected()
return
}
if (!rateLimiters.getOrPut(sessionId) { MolangUiRateLimiter() }.tryAcquire()) {
MolangUiDiagnostics.actionRejected()
return
}
val action = runCatching { ActionKey(actionRaw) }.getOrNull()
val definition = action?.let(session.contract.actions::get)
if (action == null || definition == null || raw.keys != definition.parameters.keys || raw.size > MolangUiPacketLimits.MAX_ACTION_FIELDS) {
MolangUiDiagnostics.actionRejected()
return
}
val typed = linkedMapOf<String, ParameterValue>()
for ((name, type) in definition.parameters) {
val value = raw[name]
if (value == null || value.length > MolangUiPacketLimits.MAX_VALUE_LENGTH) {
MolangUiDiagnostics.actionRejected(); return
}
typed[name] = ParameterValue.parse(type, value) ?: run { MolangUiDiagnostics.actionRejected(); return }
}
try {
MolangUiActionRegistry.dispatch(session, action, typed)
} catch (error: Exception) {
MolangUiDiagnostics.actionRejected()
Cobblemon.LOGGER.error("Molang UI action '${action.value}' failed in session $sessionId", error)
}
}
fun handleClosed(sessionId: UUID, player: ServerPlayer) {
val session = sessions[sessionId] ?: return
if (session.player.uuid == player.uuid) session.closeFromClient() else MolangUiDiagnostics.actionRejected()
}
fun applyCompatibility(player: ServerPlayer, clientVersions: Map<ResourceLocation, String>) {
val incompatible = MolangUiContractRegistry.versions().filter { (id, version) -> clientVersions[id] != version }.keys
if (incompatible.isEmpty()) disabled.remove(player.uuid) else disabled[player.uuid] = incompatible
MolangUiDiagnostics.compatibilityFailures(incompatible.size.toLong())
sessions.values.filter { it.player.uuid == player.uuid && it.uiId in incompatible }.toList().forEach(MolangUiSession::close)
}
fun requestCompatibility(player: ServerPlayer) = MolangUiContractRegistry.sync(player)
fun closeAllForPlayer(playerId: UUID) {
sessions.values.filter { it.player.uuid == playerId }.toList().forEach(MolangUiSession::close)
disabled.remove(playerId)
}
fun closeAll() {
sessions.values.toList().forEach(MolangUiSession::close)
disabled.clear()
}
fun getSession(sessionId: UUID): MolangUiSessionHandle? = sessions[sessionId]
fun getSessionForPlayer(player: ServerPlayer, id: ResourceLocation): MolangUiSessionHandle? =
sessions.values.firstOrNull { it.player.uuid == player.uuid && it.uiId == id && it.state == SessionState.ACTIVE }
fun activeSessionCount(): Int = sessions.size
internal fun remove(sessionId: UUID) {
if (sessions.remove(sessionId) != null) MolangUiDiagnostics.sessionClosed()
rateLimiters.remove(sessionId)
}
private fun resolveParameters(contract: MolangUiContract, supplied: Map<String, ParameterValue>): Map<String, ParameterValue>? {
if (supplied.keys.any { it !in contract.parameters }) return null
val values = linkedMapOf<String, ParameterValue>()
for ((name, definition) in contract.parameters) {
val suppliedValue = supplied[name]
val value = suppliedValue ?: when (val default = definition.default) {
is ParameterDefault.Literal -> default.value
is ParameterDefault.Molang -> runCatching {
val result = default.expression.asExpressionLike().resolveString(MoLangRuntime())
ParameterValue.parse(definition.type, result)
}.getOrNull()
null -> null
}
if (value == null) {
if (definition.required) return null else continue
}
if (value.type != definition.type || value.asWireValue().length > MolangUiPacketLimits.MAX_VALUE_LENGTH) return null
values[name] = value
}
return values
}
private fun createCollections(
player: ServerPlayer,
contract: MolangUiContract,
blockPos: BlockPos?
): List<ServerCollectionSetup>? {
val setups = mutableListOf<ServerCollectionSetup>()
for ((name, config) in contract.collections) {
val container: Container = when (config.source) {
CollectionSource.SESSION -> SimpleContainer(config.size)
CollectionSource.BLOCK_ENTITY -> {
val pos = blockPos ?: return null
if (player.distanceToSqr(pos.x + .5, pos.y + .5, pos.z + .5) > 64.0 || !player.level().hasChunk(pos.x shr 4, pos.z shr 4)) return null
val blockEntity = player.level().getBlockEntity(pos)
if (blockEntity !is Container) return null
if ((config.slots ?: (0 until config.size).toList()).any { it >= blockEntity.containerSize }) return null
blockEntity
}
}
setups += ServerCollectionSetup(name, config, container)
}
return setups
}
private fun openContainer(session: MolangUiSession, setup: List<ServerCollectionSetup>) {
val owned = setup.filter { it.config.source == CollectionSource.SESSION }.mapTo(mutableSetOf()) { it.name }
val opened = session.player.openMenu(object : MenuProvider {
override fun getDisplayName() = Component.empty()
override fun createMenu(containerId: Int, inventory: Inventory, player: Player): AbstractContainerMenu =
MolangContainerMenu(containerId, inventory).also { menu ->
check(session.state == SessionState.OPENING) { "Molang UI closed before creating its container" }
session.containerMenu = menu
menu.initializeServer(
setup, session.contract.shiftClickRules, owned, session.contract.closeItemsPolicy,
owner = session.player, session = session, blockPos = session.blockPos
)
}
})
check(opened.isPresent) { "player rejected the Molang UI container menu" }
}
}
internal class MolangUiRateLimiter(private var lastNanos: Long = System.nanoTime()) {
private var tokens = 40.0
fun tryAcquire(now: Long = System.nanoTime()): Boolean {
val elapsed = (now - lastNanos).coerceAtLeast(0) / 1_000_000_000.0
tokens = minOf(40.0, tokens + elapsed * 20.0)
lastNanos = now
if (tokens < 1.0) return false
tokens -= 1.0
return true
}
}
@@ -1,94 +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.molangui.container
import net.minecraft.core.registries.BuiltInRegistries
import net.minecraft.core.registries.Registries
import net.minecraft.resources.ResourceLocation
import net.minecraft.tags.TagKey
import net.minecraft.world.item.ItemStack
data class CollectionConfig(
val size: Int,
val allowedItems: List<ItemFilterEntry> = emptyList(),
val takeOnly: Boolean = false,
val source: CollectionSource = CollectionSource.SESSION,
val slots: List<Int>? = null,
val layout: SlotLayout
) {
init {
require(size in 1..MAX_COLLECTION_SIZE) { "collection size must be in 1..$MAX_COLLECTION_SIZE" }
require(layout.slotCount == size) { "collection size must match its grid dimensions" }
require(slots == null || slots.size == size) { "block collection slot mapping must match collection size" }
require(slots == null || slots.distinct().size == slots.size) { "block collection slot mapping contains duplicates" }
require(slots == null || slots.all { it >= 0 }) { "block collection slot indices must be nonnegative" }
require(source == CollectionSource.BLOCK_ENTITY || slots == null) { "slot mapping is only valid for block_entity sources" }
}
fun isItemAllowed(stack: ItemStack): Boolean {
return !takeOnly && matchesFilter(stack)
}
fun matchesFilter(stack: ItemStack): Boolean {
if (allowedItems.isEmpty()) return true
return allowedItems.any { it.matches(stack) }
}
companion object { const val MAX_COLLECTION_SIZE = 256 }
}
enum class CollectionSource(val id: String) {
SESSION("session"), BLOCK_ENTITY("block_entity");
companion object { fun fromId(id: String) = entries.firstOrNull { it.id == id } }
}
data class ShiftClickRule(
val from: List<String>,
val to: List<String>
) {
companion object {
const val PLAYER_INVENTORY = "player_inventory"
const val PLAYER_HOTBAR = "player_hotbar"
val PLAYER_SIDE_NAMES = listOf(PLAYER_INVENTORY, PLAYER_HOTBAR)
}
}
sealed interface ItemFilterEntry {
fun matches(stack: ItemStack): Boolean
data class Item(val id: ResourceLocation) : ItemFilterEntry {
override fun matches(stack: ItemStack): Boolean {
if (!BuiltInRegistries.ITEM.containsKey(id)) return false
return stack.`is`(BuiltInRegistries.ITEM.get(id))
}
}
data class Tag(val id: ResourceLocation) : ItemFilterEntry {
override fun matches(stack: ItemStack): Boolean = stack.`is`(TagKey.create(Registries.ITEM, id))
}
companion object {
fun parse(value: String): ItemFilterEntry? {
val trimmed = value.trim()
val isTag = trimmed.startsWith('#')
val id = ResourceLocation.tryParse(if (isTag) trimmed.substring(1) else trimmed) ?: return null
return if (isTag) Tag(id) else Item(id)
}
}
}
enum class CloseItemsPolicy(val id: String) {
DROP("drop"),
RETURN("return"),
DESTROY("destroy");
companion object {
fun fromId(id: String): CloseItemsPolicy? = entries.firstOrNull { it.id == id }
}
}
@@ -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.molangui.container
import net.minecraft.world.Container
data class ServerCollectionSetup(
val name: String,
val config: CollectionConfig,
val container: Container
)
@@ -1,300 +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.molangui.container
import com.cobblemon.mod.common.CobblemonMenuType
import com.cobblemon.mod.common.api.molangui.MolangUiSession
import com.cobblemon.mod.common.api.molangui.SessionState
import net.minecraft.core.BlockPos
import net.minecraft.core.registries.BuiltInRegistries
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.Container
import net.minecraft.world.SimpleContainer
import net.minecraft.world.entity.player.Inventory
import net.minecraft.world.entity.player.Player
import net.minecraft.world.inventory.AbstractContainerMenu
import net.minecraft.world.inventory.Slot
import net.minecraft.world.item.ItemStack
private data class CollectionSlotRange(
val name: String,
val startIndex: Int,
val endIndex: Int
)
class MolangContainerMenu(
containerId: Int,
private val playerInventory: Inventory
) : AbstractContainerMenu(CobblemonMenuType.MOLANG_UI, containerId) {
private val collections: MutableMap<String, Container> = mutableMapOf()
private val collectionRanges: MutableList<CollectionSlotRange> = mutableListOf()
private var playerInventoryStart: Int = 0
private var playerHotbarStart: Int = 0
private var playerHotbarEnd: Int = 0
private var shiftClickRules: List<ShiftClickRule> = emptyList()
private val collectionConfigs: MutableMap<String, CollectionConfig> = mutableMapOf()
private val sessionOwnedCollections: MutableSet<String> = mutableSetOf()
private var closeItemsPolicy: CloseItemsPolicy = CloseItemsPolicy.RETURN
private var owner: ServerPlayer? = null
private var owningSession: MolangUiSession? = null
private var blockPos: BlockPos? = null
private var originalBlockContainer: Container? = null
private var ownedItemsReleased = false
internal fun initializeServer(
collectionSetup: List<ServerCollectionSetup>,
shiftClickRules: List<ShiftClickRule> = emptyList(),
sessionOwned: Set<String> = emptySet(),
closeItemsPolicy: CloseItemsPolicy = CloseItemsPolicy.RETURN,
owner: ServerPlayer,
session: MolangUiSession,
blockPos: BlockPos? = null
) {
this.shiftClickRules = shiftClickRules
this.sessionOwnedCollections.addAll(sessionOwned)
this.closeItemsPolicy = closeItemsPolicy
this.owner = owner
this.owningSession = session
this.blockPos = blockPos
for (setup in collectionSetup) {
val (name, config, container) = setup
collections[name] = container
if (config.source == CollectionSource.BLOCK_ENTITY) originalBlockContainer = container
collectionConfigs[name] = config
val startIndex = slots.size
addCollectionSlots(name, config, container)
val endIndex = slots.size
collectionRanges.add(CollectionSlotRange(name, startIndex, endIndex))
}
addPlayerSlots()
}
fun initializeClient(
collectionLayouts: Map<String, SlotLayout>
) {
SlotLayout.validateCollections(collectionLayouts)
for ((name, layout) in collectionLayouts) {
val config = CollectionConfig(size = layout.slotCount, layout = layout)
val container = SimpleContainer(layout.slotCount)
collections[name] = container
collectionConfigs[name] = config
val startIndex = slots.size
addCollectionSlots(name, config, container)
val endIndex = slots.size
collectionRanges.add(CollectionSlotRange(name, startIndex, endIndex))
}
addPlayerSlots()
}
private fun addCollectionSlots(
name: String,
config: CollectionConfig,
container: Container
) {
val layout = config.layout
var containerIndex = 0
for (row in 0 until layout.rows) {
for (col in 0 until layout.columns) {
val x = layout.offsetX + col * (layout.slotWidth + layout.spacingX)
val y = layout.offsetY + row * (layout.slotHeight + layout.spacingY)
val mappedIndex = config.slots?.getOrNull(containerIndex) ?: containerIndex
require(mappedIndex in 0 until container.containerSize) { "collection '$name' maps outside its container" }
addSlot(MolangSlot(
container = container,
index = mappedIndex,
x = x,
y = y,
collectionConfig = config
))
containerIndex++
}
}
}
private fun addPlayerSlots() {
playerInventoryStart = slots.size
for (row in 0 until 3) {
for (col in 0 until 9) {
val inventoryIndex = 9 + row * 9 + col
val x = 8 + col * 18
val y = 84 + row * 18
addSlot(Slot(playerInventory, inventoryIndex, x, y))
}
}
playerHotbarStart = slots.size
for (col in 0 until 9) {
addSlot(Slot(playerInventory, col, 8 + col * 18, 142))
}
playerHotbarEnd = slots.size
}
override fun quickMoveStack(player: Player, index: Int): ItemStack {
val slot = slots.getOrNull(index) ?: return ItemStack.EMPTY
if (!slot.hasItem() || !slot.mayPickup(player)) return ItemStack.EMPTY
val slotItem = slot.item
val original = slotItem.copy()
val clickedCollection = collectionRanges.firstOrNull { index >= it.startIndex && index < it.endIndex }
val isPlayerInventory = index >= playerInventoryStart && index < playerInventoryEnd()
val isHotbar = index >= playerHotbarStart && index < playerHotbarEnd
val sourceName = clickedCollection?.name ?: when {
isPlayerInventory -> ShiftClickRule.PLAYER_INVENTORY
isHotbar -> ShiftClickRule.PLAYER_HOTBAR
else -> return ItemStack.EMPTY
}
val destinations = findShiftClickRule(sourceName)?.to ?: if (clickedCollection != null) {
ShiftClickRule.PLAYER_SIDE_NAMES
} else {
collectionRanges.map { it.name }
}
if (!tryMoveToCollections(slotItem, slot, destinations)) return ItemStack.EMPTY
if (slotItem.isEmpty) {
slot.setByPlayer(ItemStack.EMPTY)
} else {
slot.setChanged()
}
if (slotItem.count == original.count) return ItemStack.EMPTY
slot.onTake(player, slotItem)
return original
}
override fun stillValid(player: Player): Boolean {
val expectedOwner = owner ?: return true // client menu
if (player.uuid != expectedOwner.uuid || owningSession?.state != SessionState.ACTIVE) return false
val pos = blockPos ?: return true
if (player.distanceToSqr(pos.x + .5, pos.y + .5, pos.z + .5) > 64.0) return false
return player.level().getBlockEntity(pos) === originalBlockContainer
}
override fun removed(player: Player) {
try {
super.removed(player)
} finally {
try {
releaseOwnedItemsOnce(player)
} finally {
owningSession?.closeFromMenu()
}
}
}
fun releaseOwnedItemsOnce(player: Player) {
if (ownedItemsReleased) return
ownedItemsReleased = true
for (name in sessionOwnedCollections) {
val container = collections[name] ?: continue
when (closeItemsPolicy) {
CloseItemsPolicy.DROP -> clearContainer(player, container)
CloseItemsPolicy.RETURN -> returnItemsToPlayer(player, container)
CloseItemsPolicy.DESTROY -> container.clearContent()
}
}
}
fun getStack(collection: String, index: Int): ItemStack? {
return collectionSlot(collection, index)?.item
}
fun replaceSlotAtomically(
inputCollection: String,
inputIndex: Int,
consumeCount: Int,
outputCollection: String,
outputIndex: Int,
outputItemId: String,
outputCount: Int
): Boolean {
if (consumeCount <= 0 || outputCount <= 0) return false
val input = collectionSlot(inputCollection, inputIndex) ?: return false
val output = collectionSlot(outputCollection, outputIndex) ?: return false
val id = ResourceLocation.tryParse(outputItemId)?.takeIf(BuiltInRegistries.ITEM::containsKey) ?: return false
val produced = ItemStack(BuiltInRegistries.ITEM.get(id), outputCount)
val outputConfig = collectionConfigs[outputCollection] ?: return false
if (!outputConfig.matchesFilter(produced)) return false
val source = input.item
if (source.count < consumeCount) return false
val remaining = source.copyWithCount(source.count - consumeCount)
val sameSlot = input.container === output.container && input.containerSlot == output.containerSlot
val existing = if (sameSlot) remaining else output.item
if (!existing.isEmpty && !ItemStack.isSameItemSameComponents(existing, produced)) return false
val limit = output.getMaxStackSize(produced)
if (existing.count > limit || outputCount > limit - existing.count) return false
val result = if (existing.isEmpty) produced else existing.copyWithCount(existing.count + outputCount)
if (!sameSlot) input.set(remaining)
output.set(result)
return true
}
private fun playerInventoryEnd() = playerHotbarStart
private fun findShiftClickRule(fromCollection: String): ShiftClickRule? {
return shiftClickRules.firstOrNull { rule ->
rule.from.contains(fromCollection)
}
}
private fun collectionSlot(name: String, index: Int): Slot? {
val range = collectionRanges.firstOrNull { it.name == name } ?: return null
if (index !in 0 until range.endIndex - range.startIndex) return null
return slots[range.startIndex + index]
}
private fun tryMoveToCollections(stack: ItemStack, source: Slot, toNames: List<String>): Boolean {
val originalCount = stack.count
for (name in toNames) {
val indices = when (name) {
ShiftClickRule.PLAYER_INVENTORY -> playerInventoryStart until playerHotbarStart
ShiftClickRule.PLAYER_HOTBAR -> playerHotbarStart until playerHotbarEnd
else -> collectionRanges.firstOrNull { it.name == name }?.let { it.startIndex until it.endIndex } ?: continue
}
for (merge in listOf(true, false)) {
for (index in indices) {
if (stack.isEmpty) return true
val destination = slots[index]
if (destination.container === source.container && destination.containerSlot == source.containerSlot) continue
if (destination.hasItem() != merge || destination.item.count >= destination.getMaxStackSize(stack)) continue
destination.safeInsert(stack)
}
}
}
return stack.count < originalCount
}
private fun returnItemsToPlayer(player: Player, container: Container) {
val inventory = player.inventory
for (i in 0 until container.containerSize) {
val stack = container.getItem(i)
if (!stack.isEmpty) {
if (!inventory.add(stack)) {
player.drop(stack, false)
}
container.setItem(i, ItemStack.EMPTY)
}
}
}
}
@@ -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.molangui.container
import net.minecraft.world.Container
import net.minecraft.world.inventory.Slot
import net.minecraft.world.item.ItemStack
class MolangSlot(
container: Container,
index: Int,
x: Int,
y: Int,
private val collectionConfig: CollectionConfig
) : Slot(container, index, x, y) {
override fun mayPlace(stack: ItemStack): Boolean {
return collectionConfig.isItemAllowed(stack) && container.canPlaceItem(containerSlot, stack)
}
override fun getMaxStackSize(stack: ItemStack): Int = container.getMaxStackSize(stack)
}
@@ -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.molangui.container
import com.cobblemon.mod.common.api.molangui.MolangUiPacketLimits
import net.minecraft.world.entity.player.Inventory
data class SlotLayout(
val offsetX: Int,
val offsetY: Int,
val columns: Int,
val rows: Int,
val slotWidth: Int = 18,
val slotHeight: Int = 18,
val spacingX: Int = 0,
val spacingY: Int = 0
) {
init {
require(columns in 1..MAX_GRID_DIMENSION && rows in 1..MAX_GRID_DIMENSION) { "slot grid dimensions must be in 1..$MAX_GRID_DIMENSION" }
validateAxis(offsetX, columns, slotWidth, spacingX)
validateAxis(offsetY, rows, slotHeight, spacingY)
}
val slotCount: Int get() = columns * rows
private fun validateAxis(offset: Int, count: Int, size: Int, spacing: Int) {
require(size > 0 && spacing >= 0) { "slot size must be positive and spacing nonnegative" }
val stride = size.toLong() + spacing
val lastSlotOffset = (count - 1) * stride
require(stride <= Int.MAX_VALUE && lastSlotOffset <= Int.MAX_VALUE && offset + lastSlotOffset + size <= Int.MAX_VALUE) {
"slot grid coordinates exceed the integer range"
}
}
companion object {
const val MAX_GRID_DIMENSION = 16
fun validateCollections(layouts: Map<String, SlotLayout>) {
require(layouts.size <= MolangUiPacketLimits.MAX_CONTRACTS) { "too many container collections" }
layouts.keys.forEach { name ->
MolangUiPacketLimits.requireIdentifier(name, "collection name")
require(name !in ShiftClickRule.PLAYER_SIDE_NAMES) { "collection name '$name' is reserved for player slots" }
}
// Native container packets encode slot indices as signed shorts.
require(layouts.values.sumOf { it.slotCount.toLong() } + Inventory.INVENTORY_SIZE <= Short.MAX_VALUE + 1L) {
"container slot indices exceed the native packet range"
}
}
}
}
@@ -1,89 +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.molangui.event
import com.bedrockk.molang.runtime.MoLangRuntime
import com.bedrockk.molang.runtime.struct.QueryStruct
import com.bedrockk.molang.runtime.value.DoubleValue
import com.bedrockk.molang.runtime.value.StringValue
import com.cobblemon.mod.common.api.molangui.MolangUiPacketLimits
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.molangui.widget.WidgetDefinition
import com.cobblemon.mod.common.api.molangui.widget.WidgetInstance
import com.cobblemon.mod.common.api.molangui.widget.WidgetType
import com.cobblemon.mod.common.net.messages.server.molangui.MolangUiActionPacket
import com.cobblemon.mod.common.util.asExpressionLike
import java.util.UUID
object ClientEventProcessor {
fun processEvent(
event: WidgetEvent,
widget: WidgetDefinition,
runtime: MoLangRuntime,
sessionId: UUID
) {
val context = QueryStruct(hashMapOf())
event.data.forEach { (key, value) -> context.addFunction(key) { DoubleValue(value) } }
context.addFunction("type") { StringValue(event.type.id) }
context.addFunction("widget_id") { StringValue(event.widgetId) }
val functions = runtime.environment.query.functions
val previous = functions.remove("event")
runtime.environment.query.addFunction("event") { context }
try {
widget.clientEvents[event.type.clientId]?.asExpressionLike()?.resolve(runtime)
widget.events[event.type.id]?.let { serverHandler ->
// Action arguments may depend on variables changed by the client handler.
val parameters = serverHandler.parameters.mapValues { (_, property) -> property.resolveString(runtime) }
require(parameters.size <= MolangUiPacketLimits.MAX_ACTION_FIELDS) { "too many action parameters" }
parameters.forEach { (name, value) ->
MolangUiPacketLimits.requireIdentifier(name, "action parameter")
MolangUiPacketLimits.requireValue(value, name)
}
MolangUiActionPacket(sessionId, serverHandler.action.value, parameters).sendToServer()
}
} catch (error: Exception) {
Cobblemon.LOGGER.warn("Failed UI event {} on widget {}: {}", event.type.id, event.widgetId, error.message)
} finally {
functions.remove("event")
if (previous != null) functions["event"] = previous
}
}
fun hitTest(root: WidgetInstance, x: Float, y: Float): WidgetInstance? {
if (!root.visible || !root.enabled) return null
val isScrollPanel = root.definition.type == WidgetType.SCROLL_PANEL
if (isScrollPanel && !root.containsPoint(x, y)) return null
for (child in root.children.asReversed()) {
if (!child.visible) continue
val testY = if (isScrollPanel) y - root.scrollOffset else y
val hit = hitTest(child, x, testY)
if (hit != null) return hit
}
return if (root.containsPoint(x, y)) root else null
}
}
enum class WidgetEventType(val id: String) {
CLICK("on_click"),
CHANGE("on_change"),
HOVER_ENTER("on_hover_enter"),
HOVER_EXIT("on_hover_exit"),
SCROLL("on_scroll"),
KEY("on_key");
val clientId: String get() = "${id}_client"
}
data class WidgetEvent(
val type: WidgetEventType,
val widgetId: String,
val data: Map<String, Double> = emptyMap()
)
@@ -1,151 +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.molangui.layout
import com.cobblemon.mod.common.api.molangui.widget.WidgetInstance
import com.bedrockk.molang.runtime.MoLangRuntime
object LayoutEngine {
fun layout(root: WidgetInstance, screenWidth: Float, screenHeight: Float, runtime: MoLangRuntime): Unit = root.withItemContext(runtime) {
require(screenWidth.isFinite() && screenHeight.isFinite() && screenWidth >= 0f && screenHeight >= 0f) {
"invalid screen dimensions"
}
if (!root.visible) return@withItemContext
root.computedWidth = root.widthExpr.resolve(screenWidth, runtime)
root.computedHeight = root.heightExpr.resolve(screenHeight, runtime)
val anchor = root.definition.getAnchor()
val offsetX = root.offsetXExpr.resolve(screenWidth, runtime)
val offsetY = root.offsetYExpr.resolve(screenHeight, runtime)
root.absoluteX = (screenWidth - root.computedWidth) * anchor.xFactor + offsetX
root.absoluteY = (screenHeight - root.computedHeight) * anchor.yFactor + offsetY
validateBounds(root)
layoutChildren(root, runtime)
}
private fun layoutChildren(parent: WidgetInstance, runtime: MoLangRuntime): Unit = parent.withItemContext(runtime) {
val layout = parent.definition.getStringProperty("layout")
val spacing = parent.definition.getProperty("spacing")?.resolveFloat(runtime) ?: 0f
require(spacing.isFinite()) { "non-finite spacing at '${parent.definition.id}'" }
val padding = parent.paddingProperties.map { it.resolveFloat(runtime) }
require(padding.size == 4 && padding.all { it.isFinite() && it >= 0f }) {
"invalid padding at '${parent.definition.id}'"
}
val innerWidth = (parent.computedWidth - padding[3] - padding[1]).coerceAtLeast(0f)
val innerHeight = (parent.computedHeight - padding[0] - padding[2]).coerceAtLeast(0f)
val innerX = parent.absoluteX + padding[3]
val innerY = parent.absoluteY + padding[0]
for (child in parent.children) {
if (!child.visible) continue
child.computedWidth = child.withItemContext(runtime) { child.widthExpr.resolve(innerWidth, runtime) }
child.computedHeight = child.withItemContext(runtime) { child.heightExpr.resolve(innerHeight, runtime) }
}
when (layout) {
"stack_vertical" -> layoutStackVertical(parent, innerX, innerY, innerWidth, innerHeight, spacing, runtime)
"stack_horizontal" -> layoutStackHorizontal(parent, innerX, innerY, innerWidth, innerHeight, spacing, runtime)
"grid" -> layoutGrid(parent, innerX, innerY, innerWidth, spacing, runtime)
else -> layoutAnchored(parent, innerX, innerY, innerWidth, innerHeight, runtime)
}
for (child in parent.children) {
if (!child.visible) continue
validateBounds(child)
layoutChildren(child, runtime)
}
}
private fun layoutAnchored(parent: WidgetInstance, innerX: Float, innerY: Float, innerWidth: Float, innerHeight: Float, runtime: MoLangRuntime) {
for (child in parent.children) {
if (!child.visible) continue
val anchor = child.definition.getAnchor()
val offsetX = child.withItemContext(runtime) { child.offsetXExpr.resolve(innerWidth, runtime) }
val offsetY = child.withItemContext(runtime) { child.offsetYExpr.resolve(innerHeight, runtime) }
child.absoluteX = innerX + (innerWidth - child.computedWidth) * anchor.xFactor + offsetX
child.absoluteY = innerY + (innerHeight - child.computedHeight) * anchor.yFactor + offsetY
}
}
private fun layoutStackVertical(parent: WidgetInstance, innerX: Float, innerY: Float, innerWidth: Float, innerHeight: Float, spacing: Float, runtime: MoLangRuntime) {
val visibleChildren = parent.children.filter { it.visible }
resolveFillSizes(visibleChildren, innerHeight, spacing, isVertical = true)
var currentY = innerY
for (child in visibleChildren) {
child.absoluteX = innerX + child.withItemContext(runtime) { child.offsetXExpr.resolve(innerWidth, runtime) }
child.absoluteY = currentY + child.withItemContext(runtime) { child.offsetYExpr.resolve(innerHeight, runtime) }
currentY += child.computedHeight + spacing
}
}
private fun layoutStackHorizontal(parent: WidgetInstance, innerX: Float, innerY: Float, innerWidth: Float, innerHeight: Float, spacing: Float, runtime: MoLangRuntime) {
val visibleChildren = parent.children.filter { it.visible }
resolveFillSizes(visibleChildren, innerWidth, spacing, isVertical = false)
var currentX = innerX
for (child in visibleChildren) {
child.absoluteX = currentX + child.withItemContext(runtime) { child.offsetXExpr.resolve(innerWidth, runtime) }
child.absoluteY = innerY + child.withItemContext(runtime) { child.offsetYExpr.resolve(innerHeight, runtime) }
currentX += child.computedWidth + spacing
}
}
private fun layoutGrid(parent: WidgetInstance, innerX: Float, innerY: Float, innerWidth: Float, spacing: Float, runtime: MoLangRuntime) {
val columns = (parent.definition.getProperty("columns")?.resolveInt(runtime) ?: 1).coerceIn(1, 256)
val visibleChildren = parent.children.filter { it.visible }
val cellWidth = ((innerWidth - spacing * (columns - 1)) / columns).coerceAtLeast(0f)
var col = 0
var currentY = innerY
var rowHeight = 0f
for (child in visibleChildren) {
if (child.widthExpr is SizeExpression.Fill) child.computedWidth = cellWidth
child.absoluteX = innerX + col * (cellWidth + spacing)
child.absoluteY = currentY
rowHeight = maxOf(rowHeight, child.computedHeight)
col++
if (col >= columns) {
col = 0
currentY += rowHeight + spacing
rowHeight = 0f
}
}
}
private fun resolveFillSizes(children: List<WidgetInstance>, totalSpace: Float, spacing: Float, isVertical: Boolean) {
val totalSpacing = spacing * (children.size - 1).coerceAtLeast(0)
var usedSpace = totalSpacing
var fillCount = 0
for (child in children) {
val expr = if (isVertical) child.heightExpr else child.widthExpr
if (expr is SizeExpression.Fill) {
fillCount++
} else {
usedSpace += if (isVertical) child.computedHeight else child.computedWidth
}
}
if (fillCount > 0) {
val fillSize = ((totalSpace - usedSpace) / fillCount).coerceAtLeast(0f)
for (child in children) {
val expr = if (isVertical) child.heightExpr else child.widthExpr
if (expr is SizeExpression.Fill) {
if (isVertical) child.computedHeight = fillSize else child.computedWidth = fillSize
}
}
}
}
private fun validateBounds(widget: WidgetInstance) {
fun validAxis(position: Float, size: Float): Boolean =
position.isFinite() && size.isFinite() && size >= 0f && size.toDouble() <= Int.MAX_VALUE &&
position.toDouble() >= Int.MIN_VALUE && position.toDouble() + size.toDouble() <= Int.MAX_VALUE
require(validAxis(widget.absoluteX, widget.computedWidth) && validAxis(widget.absoluteY, widget.computedHeight)) {
"widget '${widget.definition.id}' has invalid or overflowing bounds"
}
}
}
@@ -1,75 +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.molangui.layout
import com.cobblemon.mod.common.api.molang.ExpressionLike
import com.cobblemon.mod.common.util.asExpressionLike
import com.bedrockk.molang.runtime.MoLangRuntime
import com.cobblemon.mod.common.api.molangui.widget.WidgetExpression
sealed class SizeExpression {
abstract fun resolve(parentSize: Float, runtime: MoLangRuntime): Float
data class Pixels(val value: Float) : SizeExpression() {
init { require(value.isFinite()) { "pixel size must be finite" } }
override fun resolve(parentSize: Float, runtime: MoLangRuntime) = value
}
data class Percent(val fraction: Float) : SizeExpression() {
init { require(fraction.isFinite()) { "percentage must be finite" } }
override fun resolve(parentSize: Float, runtime: MoLangRuntime) = parentSize * fraction
}
data class PercentWithOffset(val fraction: Float, val offset: Float) : SizeExpression() {
init { require(fraction.isFinite() && offset.isFinite()) { "percentage and offset must be finite" } }
override fun resolve(parentSize: Float, runtime: MoLangRuntime) = parentSize * fraction + offset
}
data object Fill : SizeExpression() {
override fun resolve(parentSize: Float, runtime: MoLangRuntime) = parentSize
}
data class MolangSize(val expression: ExpressionLike) : SizeExpression() {
override fun resolve(parentSize: Float, runtime: MoLangRuntime) = expression.resolveFloat(runtime)
}
companion object {
private val PERCENT_PATTERN = Regex("""^(\d+(?:\.\d+)?)%$""")
private val PERCENT_OFFSET_PATTERN = Regex("""^(\d+(?:\.\d+)?)%\s*([+-])\s*(\d+(?:\.\d+)?)$""")
fun parse(value: Any): SizeExpression {
return when (value) {
is Number -> Pixels(value.toFloat())
is String -> parseString(value)
is WidgetExpression -> MolangSize(value.source.asExpressionLike())
else -> throw IllegalArgumentException("size must be a number, percentage, fill, or explicit Molang expression")
}
}
private fun parseString(value: String): SizeExpression {
val trimmed = value.trim()
return when {
trimmed == "fill" -> Fill
trimmed.toFloatOrNull() != null -> Pixels(trimmed.toFloat())
PERCENT_PATTERN.matches(trimmed) -> {
val pct = PERCENT_PATTERN.matchEntire(trimmed)!!.groupValues[1].toFloat()
Percent(pct / 100f)
}
PERCENT_OFFSET_PATTERN.matches(trimmed) -> {
val match = PERCENT_OFFSET_PATTERN.matchEntire(trimmed)!!
val pct = match.groupValues[1].toFloat() / 100f
val sign = if (match.groupValues[2] == "+") 1f else -1f
val offset = match.groupValues[3].toFloat() * sign
PercentWithOffset(pct, offset)
}
else -> throw IllegalArgumentException("invalid size '$value'; Molang requires an explicit expression object")
}
}
}
}
@@ -1,114 +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.molangui.template
import com.google.gson.Gson
import com.google.gson.JsonObject
import com.google.gson.JsonElement
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.packs.resources.ResourceManager
import com.cobblemon.mod.common.Cobblemon
object TemplateLibraryRegistry {
private val gson = Gson()
private val libraries: MutableMap<ResourceLocation, Map<String, TemplateDefinition>> = mutableMapOf()
private const val DIRECTORY = "molang_ui/templates"
fun reload(manager: ResourceManager) {
val next = mutableMapOf<ResourceLocation, Map<String, TemplateDefinition>>()
val resources = manager.listResources(DIRECTORY) { it.path.endsWith(".json") }
for ((location, resource) in resources) {
try {
val json = resource.openAsReader().use { gson.fromJson(it, JsonObject::class.java) }
val namespace = location.namespace
val name = location.path.removePrefix("$DIRECTORY/").removeSuffix(".json")
val libraryId = ResourceLocation.fromNamespaceAndPath(namespace, name)
val templates = parseTemplates(json, namespace)
next[libraryId] = templates
} catch (e: Exception) {
Cobblemon.LOGGER.error("Rejected Molang UI template library '$location': ${e.message}")
}
}
libraries.clear()
libraries.putAll(next)
}
fun getTemplate(qualifiedName: String): TemplateDefinition? {
val dotIndex = qualifiedName.lastIndexOf('.')
if (dotIndex == -1) return null
val libraryName = qualifiedName.substring(0, dotIndex)
val templateName = qualifiedName.substring(dotIndex + 1)
val library = if (':' in libraryName) {
ResourceLocation.tryParse(libraryName)?.let(libraries::get)
} else {
libraries.entries.singleOrNull { it.key.path == libraryName }?.value
}
return library?.get(templateName)
}
private fun parseTemplates(json: JsonObject, namespace: String): Map<String, TemplateDefinition> {
val result = mutableMapOf<String, TemplateDefinition>()
val templatesObj = json.getAsJsonObject("templates") ?: return result
for ((name, element) in templatesObj.entrySet()) {
val templateJson = element.asJsonObject
val inputs = mutableMapOf<String, TemplateInput>()
templateJson.getAsJsonObject("inputs")?.let { inputsObj ->
for ((inputName, inputEl) in inputsObj.entrySet()) {
val inputObj = inputEl.asJsonObject
val type = TemplateInputType.fromId(inputObj.get("type")?.asString ?: "string")
?: throw IllegalArgumentException("unknown template input type for '$inputName'")
val default = inputObj.get("default")
require(default == null || type.accepts(default)) { "invalid template default at '$name.$inputName'" }
inputs[inputName] = TemplateInput(type, default)
}
}
val extends = templateJson.get("extends")?.asString
result[name] = TemplateDefinition(
name = name,
namespace = namespace,
inputs = inputs,
extends = extends,
body = templateJson
)
}
return result
}
}
data class TemplateDefinition(
val name: String,
val namespace: String,
val inputs: Map<String, TemplateInput> = emptyMap(),
val extends: String? = null,
val body: JsonObject
)
data class TemplateInput(
val type: TemplateInputType = TemplateInputType.STRING,
val default: JsonElement? = null
)
enum class TemplateInputType(val id: String) {
STRING("string"), NUMBER("number"), BOOLEAN("boolean"), EXPRESSION("expression"), OBJECT("object"), ARRAY("array");
fun accepts(value: JsonElement): Boolean = when (this) {
STRING -> value.isJsonPrimitive && value.asJsonPrimitive.isString
NUMBER -> value.isJsonPrimitive && value.asJsonPrimitive.isNumber && value.asDouble.isFinite()
BOOLEAN -> value.isJsonPrimitive && value.asJsonPrimitive.isBoolean
EXPRESSION -> value.isJsonObject && value.asJsonObject.size() == 1 && value.asJsonObject.get("molang")?.let {
it.isJsonPrimitive && it.asJsonPrimitive.isString
} == true
OBJECT -> value.isJsonObject
ARRAY -> value.isJsonArray
}
companion object { fun fromId(id: String) = entries.firstOrNull { it.id == id } }
}
@@ -1,135 +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.molangui.template
import com.cobblemon.mod.common.api.molangui.widget.WidgetTree
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive
object TemplateResolver {
fun resolve(root: JsonObject, localTemplates: Map<String, TemplateDefinition>): JsonObject =
Resolution(localTemplates).resolveNode(root, emptyList(), 1)
private class Resolution(private val localTemplates: Map<String, TemplateDefinition>) {
private var nodes = 0
private var values = 0
fun resolveNode(node: JsonObject, chain: List<String>, depth: Int): JsonObject {
require(depth <= WidgetTree.MAX_DEPTH) { "template expansion exceeds maximum widget depth ${WidgetTree.MAX_DEPTH}" }
require(++nodes <= WidgetTree.MAX_NODES) { "template expansion exceeds maximum widget count ${WidgetTree.MAX_NODES}" }
val result = JsonObject()
node.entrySet().filter { it.key != "controls" && it.key != "item_template" }.forEach { (key, value) ->
result.add(key, copy(value))
}
node.getAsJsonArray("controls")?.let { controls ->
val resolvedControls = JsonArray()
controls.forEach { element ->
val wrapper = element.takeIf(JsonElement::isJsonObject)?.asJsonObject
?: throw IllegalArgumentException("control entries must be objects")
require(wrapper.size() == 1) { "control entries must contain exactly one widget" }
val (key, value) = wrapper.entrySet().single()
val output = JsonObject()
require(value.isJsonObject) { "widget '$key' must be an object" }
output.add(key.substringBefore('@'), resolveWidgetEntry(key, value.asJsonObject, chain, depth + 1))
resolvedControls.add(output)
}
result.add("controls", resolvedControls)
}
node.getAsJsonObject("item_template")?.let {
result.add("item_template", resolveNode(it, chain, depth + 1))
}
return result
}
private fun resolveWidgetEntry(
key: String,
overrides: JsonObject,
chain: List<String>,
depth: Int
): JsonObject {
if ('@' !in key) return resolveNode(overrides, chain, depth)
val templateRef = key.substringAfter('@')
require(templateRef !in chain) { "template expansion cycle: ${(chain + templateRef).joinToString(" -> ")}" }
require(depth <= WidgetTree.MAX_DEPTH) { "template expansion exceeds maximum widget depth ${WidgetTree.MAX_DEPTH}" }
val template = findTemplate(templateRef)
?: throw IllegalArgumentException("missing template '$templateRef'")
val inherited = resolveTemplateChain(templateRef, template, emptyList())
val inputs = inherited.inputs.mapValues { (name, input) ->
val supplied = overrides.get(name)
val value = supplied ?: input.default
?: throw IllegalArgumentException("missing required template input '$name' for '$templateRef'")
require(input.type.accepts(value)) {
"template input '$name' for '$templateRef' must be ${input.type.id}"
}
value
}
val substituted = copy(inherited.body, inputs).asJsonObject
val nonInputs = JsonObject().also { obj ->
overrides.entrySet().filter { it.key !in inherited.inputs }.forEach { (name, value) -> obj.add(name, value) }
}
return resolveNode(mergeJson(substituted, nonInputs), chain + templateRef, depth)
}
private fun resolveTemplateChain(
ref: String,
template: TemplateDefinition,
chain: List<String>
): TemplateDefinition {
require(ref !in chain) { "template inheritance cycle: ${(chain + ref).joinToString(" -> ")}" }
require(chain.size < WidgetTree.MAX_DEPTH) { "template inheritance exceeds maximum depth ${WidgetTree.MAX_DEPTH}" }
val nextChain = chain + ref
val body = JsonObject().also { obj ->
template.body.entrySet().filter { it.key != "inputs" && it.key != "extends" }
.forEach { (name, value) -> obj.add(name, value) }
}
val parentRef = template.extends ?: return template.copy(body = body)
val parent = findTemplate(parentRef)
?: throw IllegalArgumentException("missing parent template '$parentRef' referenced by '$ref'")
val inherited = resolveTemplateChain(parentRef, parent, nextChain)
return template.copy(inputs = inherited.inputs + template.inputs, body = mergeJson(inherited.body, body))
}
private fun findTemplate(ref: String) =
localTemplates[ref] ?: TemplateLibraryRegistry.getTemplate(ref)
private fun copy(element: JsonElement, inputs: Map<String, JsonElement>? = null, depth: Int = 0): JsonElement {
require(depth <= 128) { "template JSON exceeds maximum depth 128" }
require(++values <= 1_048_576) { "template expansion exceeds maximum JSON work" }
if (inputs != null && element.isJsonPrimitive && element.asJsonPrimitive.isString) {
val text = element.asString
if (text.startsWith('$') && text.length > 1 && text.indexOf('$', 1) == -1) {
val supplied = inputs[text.substring(1)]
?: throw IllegalArgumentException("unknown template input reference '$text'")
return copy(supplied, depth = depth + 1)
}
return JsonPrimitive(text)
}
if (element.isJsonArray) return JsonArray().also { out -> element.asJsonArray.forEach { out.add(copy(it, inputs, depth + 1)) } }
if (element.isJsonObject) return JsonObject().also { out ->
element.asJsonObject.entrySet().forEach { (key, value) ->
out.add(key, copy(value, inputs, depth + 1))
}
}
return element.deepCopy()
}
private fun mergeJson(base: JsonObject, overlay: JsonObject): JsonObject {
val result = copy(base).asJsonObject
overlay.entrySet().forEach { (key, value) ->
val existing = result.get(key)
result.add(key, if (value.isJsonObject && existing?.isJsonObject == true) {
mergeJson(existing.asJsonObject, value.asJsonObject)
} else copy(value))
}
return result
}
}
}
@@ -1,68 +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.molangui.widget
import com.cobblemon.mod.common.api.molangui.ActionKey
data class WidgetDefinition(
val id: String,
val type: WidgetType,
val properties: Map<String, WidgetProperty> = emptyMap(),
val children: List<WidgetDefinition> = emptyList(),
val events: Map<String, ServerWidgetEvent> = emptyMap(),
val clientEvents: Map<String, String> = emptyMap(),
val hoverOverrides: Map<String, WidgetProperty> = emptyMap(),
val itemTemplate: WidgetDefinition? = null
) {
fun getProperty(name: String): WidgetProperty? = properties[name]
fun getStringProperty(name: String): String? = (properties[name]?.raw as? String)
fun getAnchor(): Anchor = Anchor.fromId(getStringProperty("anchor") ?: "top_left")
}
data class ServerWidgetEvent(
val action: ActionKey,
val parameters: Map<String, WidgetProperty> = emptyMap()
)
enum class WidgetType(val id: String) {
PANEL("panel"),
LABEL("label"),
IMAGE("image"),
FILL("fill"),
BUTTON("button"),
LIST("list"),
PROGRESS_BAR("progress_bar"),
SCROLL_PANEL("scroll_panel"),
POKEMON_RENDER("pokemon_render"),
NUMBER_INPUT("number_input"),
ITEM_SLOT("item_slot"),
SLOT_GRID("slot_grid");
companion object {
private val BY_ID = entries.associateBy { it.id }
fun fromId(id: String): WidgetType? = BY_ID[id]
}
}
enum class Anchor(val id: String, val xFactor: Float, val yFactor: Float) {
TOP_LEFT("top_left", 0f, 0f),
TOP_CENTER("top_center", 0.5f, 0f),
TOP_RIGHT("top_right", 1f, 0f),
CENTER_LEFT("center_left", 0f, 0.5f),
CENTER("center", 0.5f, 0.5f),
CENTER_RIGHT("center_right", 1f, 0.5f),
BOTTOM_LEFT("bottom_left", 0f, 1f),
BOTTOM_CENTER("bottom_center", 0.5f, 1f),
BOTTOM_RIGHT("bottom_right", 1f, 1f);
companion object {
private val BY_ID = entries.associateBy { it.id }
fun fromId(id: String): Anchor = BY_ID[id] ?: TOP_LEFT
}
}
@@ -1,92 +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.molangui.widget
import com.bedrockk.molang.runtime.MoLangRuntime
import com.bedrockk.molang.runtime.struct.QueryStruct
import com.bedrockk.molang.runtime.value.DoubleValue
import com.bedrockk.molang.runtime.value.MoValue
import com.bedrockk.molang.runtime.value.StringValue
import com.cobblemon.mod.common.api.molangui.MolangUiPacketLimits
import com.cobblemon.mod.common.api.molangui.layout.SizeExpression
import com.google.gson.JsonElement
import com.google.gson.JsonParser
internal object WidgetExpansion {
private data class Node(val definition: WidgetDefinition, val context: QueryStruct?, val children: List<Node>)
fun refresh(definition: WidgetDefinition, runtime: MoLangRuntime, previous: WidgetInstance? = null): WidgetInstance {
var count = 0
fun plan(def: WidgetDefinition, context: QueryStruct?, depth: Int): Node = withItemContext(runtime, context) {
require(depth <= WidgetTree.MAX_DEPTH && ++count <= WidgetTree.MAX_NODES) {
"expanded widget tree exceeds ${WidgetTree.MAX_NODES} widgets or depth ${WidgetTree.MAX_DEPTH}"
}
val children = if (def.type == WidgetType.LIST) {
val template = requireNotNull(def.itemTemplate) { "list '${def.id}' requires item_template" }
items(def, runtime).mapIndexed { index, values ->
val item = QueryStruct(hashMapOf())
values.forEach { (key, value) -> item.addFunction(key) { value } }
item.addFunction("index") { DoubleValue(index.toDouble()) }
plan(template.copy(id = "${template.id}[$index]"), item, depth + 1)
}
} else def.children.map { plan(it, context, depth + 1) }
Node(def, context, children)
}
val planned = plan(definition, null, 1)
fun apply(node: Node, old: WidgetInstance?, parent: WidgetInstance?): WidgetInstance {
val widget = old?.takeIf { it.definition == node.definition } ?: WidgetInstance(node.definition, parent).apply {
val size = node.definition.getProperty("size")?.raw as? List<*>
if (size?.size == 2) {
widthExpr = SizeExpression.parse(size[0] ?: 0)
heightExpr = SizeExpression.parse(size[1] ?: 0)
}
val offset = node.definition.getProperty("offset")?.raw as? List<*>
if (offset?.size == 2) {
offsetXExpr = SizeExpression.parse(offset[0] ?: 0)
offsetYExpr = SizeExpression.parse(offset[1] ?: 0)
}
}
widget.itemContext = node.context
val children = node.children.mapIndexed { index, child -> apply(child, widget.children.getOrNull(index), widget) }
widget.children.clear()
widget.children.addAll(children)
return widget
}
return apply(planned, previous, null)
}
private fun items(definition: WidgetDefinition, runtime: MoLangRuntime): List<Map<String, MoValue>> =
when (definition.getStringProperty("source_type") ?: "data") {
"count" -> {
val count = definition.getProperty("source")?.resolveDouble(runtime) ?: 0.0
require(count.isFinite() && count in 0.0..WidgetTree.MAX_NODES.toDouble() && count % 1.0 == 0.0) {
"list '${definition.id}' requires a whole count between 0 and ${WidgetTree.MAX_NODES}"
}
List(count.toInt()) { emptyMap() }
}
"data" -> {
val source = definition.getProperty("source")?.resolveString(runtime) ?: "[]"
require(source.length <= MolangUiPacketLimits.MAX_VALUE_LENGTH) { "list '${definition.id}' source is too large" }
val array = JsonParser.parseString(source).asJsonArray
require(array.size() <= WidgetTree.MAX_NODES) { "list '${definition.id}' has too many items" }
array.map { element -> element.asJsonObject.entrySet().associate { (key, item) -> key to value(item) } }
}
else -> throw IllegalArgumentException("unsupported list source_type at '${definition.id}'")
}
private fun value(element: JsonElement): MoValue {
if (!element.isJsonPrimitive) return StringValue(element.toString())
val primitive = element.asJsonPrimitive
return when {
primitive.isBoolean -> DoubleValue(primitive.asBoolean)
primitive.isNumber -> DoubleValue(primitive.asDouble.also { require(it.isFinite()) { "non-finite list item value" } })
else -> StringValue(primitive.asString)
}
}
}
@@ -1,67 +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.molangui.widget
import com.cobblemon.mod.common.api.molangui.layout.SizeExpression
import com.bedrockk.molang.runtime.MoLangRuntime
import com.bedrockk.molang.runtime.struct.QueryStruct
class WidgetInstance(
val definition: WidgetDefinition,
val parent: WidgetInstance? = null
) {
var absoluteX: Float = 0f
var absoluteY: Float = 0f
var computedWidth: Float = 0f
var computedHeight: Float = 0f
var widthExpr: SizeExpression = SizeExpression.Pixels(0f)
var heightExpr: SizeExpression = SizeExpression.Pixels(0f)
var offsetXExpr: SizeExpression = SizeExpression.Pixels(0f)
var offsetYExpr: SizeExpression = SizeExpression.Pixels(0f)
internal val paddingProperties: List<WidgetProperty> =
(definition.getProperty("padding")?.raw as? List<*>)?.map {
when (it) {
is Number -> WidgetProperty.literal(it)
is WidgetExpression -> WidgetProperty.molang(it.source)
else -> throw IllegalArgumentException("invalid padding at '${definition.id}'")
}
} ?: List(4) { WidgetProperty.literal(0) }
val children: MutableList<WidgetInstance> = mutableListOf()
var visible: Boolean = true
var enabled: Boolean = true
var hovered: Boolean = false
var scrollOffset: Float = 0f
var rendererState: Any? = null
var itemContext: QueryStruct? = null
inline fun <T> withItemContext(runtime: MoLangRuntime, action: () -> T): T =
withItemContext(runtime, itemContext, action)
fun containsPoint(x: Float, y: Float): Boolean {
return x >= absoluteX && x <= absoluteX + computedWidth &&
y >= absoluteY && y <= absoluteY + computedHeight
}
}
inline fun <T> withItemContext(runtime: MoLangRuntime, context: QueryStruct?, action: () -> T): T {
val functions = runtime.environment.query.functions
val previous = functions.remove("item")
if (context != null) runtime.environment.query.addFunction("item") { context }
try {
return action()
} finally {
functions.remove("item")
if (previous != null) functions["item"] = previous
}
}
@@ -1,63 +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.molangui.widget
import com.cobblemon.mod.common.api.molang.ExpressionLike
import com.cobblemon.mod.common.util.asExpressionLike
import com.bedrockk.molang.runtime.MoLangRuntime
class WidgetProperty private constructor(val raw: Any, expressionSource: String?) {
val expression: ExpressionLike? = expressionSource?.asExpressionLike()
fun resolveDouble(runtime: MoLangRuntime): Double {
return expression?.resolveDouble(runtime) ?: when (raw) {
is Number -> raw.toDouble()
is String -> parseNumericString(raw)
is Boolean -> if (raw) 1.0 else 0.0
else -> 0.0
}
}
fun resolveString(runtime: MoLangRuntime): String {
return expression?.resolveString(runtime) ?: raw.toString()
}
fun resolveBoolean(runtime: MoLangRuntime): Boolean {
return expression?.resolveBoolean(runtime) ?: when (raw) {
is Boolean -> raw
is Number -> raw.toDouble() != 0.0
is String -> raw.toBooleanStrictOrNull() ?: (raw.isNotEmpty())
else -> false
}
}
fun resolveInt(runtime: MoLangRuntime): Int {
if (expression == null && raw is String && raw.startsWith("0x", ignoreCase = true)) {
return raw.substring(2).toLongOrNull(16)?.toInt() ?: 0
}
return resolveDouble(runtime).toInt()
}
fun resolveFloat(runtime: MoLangRuntime): Float = resolveDouble(runtime).toFloat()
companion object {
fun literal(value: Any): WidgetProperty = WidgetProperty(value, null)
fun molang(source: String): WidgetProperty = WidgetProperty(source, source)
private fun parseNumericString(value: String): Double {
value.toDoubleOrNull()?.let { return it }
if (value.startsWith("0x", ignoreCase = true)) {
value.substring(2).toLongOrNull(16)?.let { return it.toDouble() }
}
return 0.0
}
}
}
data class WidgetExpression(val source: String)
@@ -1,195 +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.molangui.widget
import com.cobblemon.mod.common.api.molangui.layout.SizeExpression
import com.cobblemon.mod.common.api.molangui.ActionKey
import com.cobblemon.mod.common.api.molangui.requireUiString
import com.cobblemon.mod.common.api.molangui.template.TemplateDefinition
import com.cobblemon.mod.common.api.molangui.template.TemplateInput
import com.cobblemon.mod.common.api.molangui.template.TemplateInputType
import com.cobblemon.mod.common.api.molangui.template.TemplateResolver
import com.google.gson.JsonElement
import com.google.gson.JsonObject
object WidgetTree {
const val MAX_DEPTH = 32
const val MAX_NODES = 2048
private val UNSUPPORTED_PROPERTIES = setOf(
"transition", "enter", "exit", "static", "on_submit", "on_submit_client", "on_slot_change", "on_slot_change_client"
)
private val EVENT_NAMES = setOf(
"on_click", "on_change", "on_hover_enter", "on_hover_exit", "on_scroll", "on_key"
)
fun parse(guiJson: JsonObject): WidgetDefinition {
val localTemplates = parseLocalTemplates(guiJson)
val rootJson = guiJson.getAsJsonObject("root")
?: throw IllegalArgumentException("UI definition is missing 'root'")
val resolved = TemplateResolver.resolve(rootJson, localTemplates)
val ids = mutableSetOf<String>()
val count = intArrayOf(0)
return parseWidget("root", resolved, ids, 1, count)
}
fun parseWidget(id: String, json: JsonObject): WidgetDefinition =
parseWidget(id, json, mutableSetOf(), 1, intArrayOf(0))
private fun parseWidget(
id: String,
json: JsonObject,
ids: MutableSet<String>,
depth: Int,
count: IntArray
): WidgetDefinition {
require(id.isNotBlank() && id.length <= 128) { "invalid widget id '$id'" }
require(ids.add(id)) { "duplicate widget id '$id'" }
require(depth <= MAX_DEPTH) { "widget tree exceeds maximum depth $MAX_DEPTH at '$id'" }
count[0]++
require(count[0] <= MAX_NODES) { "widget tree exceeds maximum node count $MAX_NODES" }
val typeName = json.get("type")?.asString ?: "panel"
val type = WidgetType.fromId(typeName)
?: throw IllegalArgumentException("unsupported widget renderer '$typeName' at '$id'")
val properties = linkedMapOf<String, WidgetProperty>()
val events = linkedMapOf<String, ServerWidgetEvent>()
val clientEvents = linkedMapOf<String, String>()
var hoverOverrides = emptyMap<String, WidgetProperty>()
val reserved = setOf("type", "controls", "item_template", "hover", "inputs", "extends")
json.entrySet().forEach { (key, value) ->
require(key !in UNSUPPORTED_PROPERTIES) { "unsupported widget property '$id.$key'" }
when {
key.endsWith("_client") && key.removeSuffix("_client") in EVENT_NAMES ->
clientEvents[key] = readExpression(value, "$id.$key")
key in EVENT_NAMES -> {
events[key] = parseServerEvent(value, "$id.$key")
}
key == "hover" && value.isJsonObject -> hoverOverrides = parsePropertyMap(value.asJsonObject, "$id.hover")
key !in reserved -> properties[key] = parseProperty(value, "$id.$key")
}
}
val children = mutableListOf<WidgetDefinition>()
json.getAsJsonArray("controls")?.forEach { element ->
require(element.isJsonObject && element.asJsonObject.size() == 1) { "controls under '$id' must be single-entry objects" }
val (childId, childValue) = element.asJsonObject.entrySet().single()
require(childValue.isJsonObject) { "widget '$childId' must be an object" }
children += parseWidget(childId, childValue.asJsonObject, ids, depth + 1, count)
}
val itemTemplate = json.getAsJsonObject("item_template")?.let { template ->
parseWidget("$id#item_template", template, ids, depth + 1, count)
}
validateGeometry(id, type, properties)
return WidgetDefinition(
id, type, properties.toMap(), children.toList(), events.toMap(), clientEvents.toMap(),
hoverOverrides.toMap(), itemTemplate
)
}
private fun parseLocalTemplates(guiJson: JsonObject): Map<String, TemplateDefinition> {
val result = linkedMapOf<String, TemplateDefinition>()
guiJson.getAsJsonObject("templates")?.entrySet()?.forEach { (name, element) ->
val body = element.asJsonObject
val inputs = linkedMapOf<String, TemplateInput>()
body.getAsJsonObject("inputs")?.entrySet()?.forEach { (inputName, inputElement) ->
val input = inputElement.asJsonObject
val type = TemplateInputType.fromId(input.get("type")?.asString ?: "string")
?: throw IllegalArgumentException("unknown template input type at '$name.$inputName'")
val default = input.get("default")
require(default == null || type.accepts(default)) { "invalid template default at '$name.$inputName'" }
inputs[inputName] = TemplateInput(type, default)
}
result[name] = TemplateDefinition(name, "", inputs.toMap(), body.get("extends")?.asString, body)
}
return result.toMap()
}
private fun parsePropertyMap(json: JsonObject, path: String) = json.entrySet().associate { (key, value) ->
key to parseProperty(value, "$path.$key")
}
private fun parseProperty(value: JsonElement, path: String): WidgetProperty {
if (value.isJsonObject && value.asJsonObject.has("molang")) {
require(value.asJsonObject.size() == 1) {
"expression '$path' must contain only a string 'molang' field"
}
return WidgetProperty.molang(value.asJsonObject.get("molang").requireUiString("$path.molang"))
}
return WidgetProperty.literal(jsonToLiteral(value, path))
}
private fun jsonToLiteral(element: JsonElement, path: String): Any = when {
element.isJsonPrimitive -> element.asJsonPrimitive.let { when {
it.isNumber -> it.asDouble.also { number -> require(number.isFinite()) { "non-finite number at '$path'" } }
it.isBoolean -> it.asBoolean
else -> it.asString
} }
element.isJsonArray -> element.asJsonArray.mapIndexed { index, item ->
if (item.isJsonObject && item.asJsonObject.has("molang")) {
require(item.asJsonObject.size() == 1) { "expression '$path[$index]' contains unknown fields" }
WidgetExpression(item.asJsonObject.get("molang").requireUiString("$path[$index].molang"))
} else jsonToLiteral(item, "$path[$index]")
}.toList()
element.isJsonObject -> element.asJsonObject.entrySet().associate { (key, item) -> key to jsonToLiteral(item, "$path.$key") }.toMap()
else -> throw IllegalArgumentException("null is not a supported widget value at '$path'")
}
private fun readExpression(element: JsonElement, path: String): String {
require(element.isJsonObject && element.asJsonObject.size() == 1) { "client event '$path' must be an explicit molang object" }
return element.asJsonObject.get("molang").requireUiString("$path.molang").also {
require(!it.contains("send_action")) { "client event '$path' may not dispatch server actions" }
}
}
private fun parseServerEvent(element: JsonElement, path: String): ServerWidgetEvent {
if (element.isJsonPrimitive && element.asJsonPrimitive.isString) {
val action = element.asString
require(action.startsWith("action@") && action.length > 7) { "server event '$path' must declare exactly one action" }
return ServerWidgetEvent(ActionKey(action.substringAfter("action@")))
}
require(element.isJsonObject) { "server event '$path' must be an object" }
val obj = element.asJsonObject
val action = ActionKey(obj.get("action")?.asString
?: throw IllegalArgumentException("server event '$path' is missing action"))
val parameters = obj.getAsJsonObject("parameters")?.entrySet()?.associate { (name, value) ->
name to parseProperty(value, "$path.parameters.$name")
}?.toMap() ?: emptyMap()
require(obj.keySet().all { it == "action" || it == "parameters" }) { "server event '$path' contains unknown fields" }
return ServerWidgetEvent(action, parameters)
}
private fun validateGeometry(id: String, type: WidgetType, properties: Map<String, WidgetProperty>) {
listOf("size", "offset", "grid_dimensions", "slot_size", "slot_spacing").forEach { key ->
val raw = properties[key]?.raw ?: return@forEach
require(raw is List<*> && raw.size == 2) { "'$id.$key' must contain exactly two values" }
raw.filterIsInstance<Double>().forEach { require(it.isFinite()) { "non-finite geometry at '$id.$key'" } }
}
listOf("size", "offset").forEach { key ->
(properties[key]?.raw as? List<*>)?.forEach { SizeExpression.parse(requireNotNull(it)) }
}
properties["padding"]?.raw?.let { padding ->
require(padding is List<*> && padding.size == 4 && padding.all { it is Number || it is WidgetExpression }) {
"'$id.padding' must contain four numbers or explicit Molang expressions"
}
padding.filterIsInstance<Number>().forEach {
require(it.toFloat().isFinite() && it.toFloat() >= 0f) { "invalid padding at '$id'" }
}
}
if (type == WidgetType.SLOT_GRID) {
val dimensions = properties["grid_dimensions"]?.raw as? List<*>
?: throw IllegalArgumentException("slot grid '$id' is missing grid_dimensions")
require(dimensions.all { it is Double && it in 1.0..16.0 && it == it.toInt().toDouble() }) {
"slot grid '$id' must have integer dimensions in 1..16"
}
val collectionName = properties["collection_name"]?.raw
require(collectionName is String && collectionName.isNotBlank()) { "slot grid '$id' is missing collection_name" }
}
}
}
@@ -269,7 +269,7 @@ object PokeBalls : JsonDataRegistry<PokeBall> {
createDefault("dive_ball", CatchRateModifiers.SUBMERGED_IN_WATER, waterDragValue = 0.99F)
createDefault("nest_ball", CatchRateModifiers.NEST)
createDefault("repeat_ball", CatchRateModifiers.REPEAT)
createDefault("timer_ball", CatchRateModifiers.turnBased { turn -> (1F + turn * (1229F / 4096F)).coerceAtMost(4F) })
createDefault("timer_ball", CatchRateModifiers.turnBased { turn -> (1F * turn * (1229F / 4096F)).coerceAtMost(4F) })
createDefault("luxury_ball", effects = listOf(FriendshipEarningBoostEffect(2F)))
createDefault("premier_ball")
createDefault("dusk_ball", CatchRateModifiers.LIGHT_LEVEL)
@@ -278,7 +278,7 @@ object PokeBalls : JsonDataRegistry<PokeBall> {
createDefault("cherish_ball")
createDefault("park_ball", CatchRateModifiers.PARK)
createDefault("dream_ball", CatchRateModifiers.statusBoosting(4F, Statuses.SLEEP))
createDefault("beast_ball", DynamicMultiplierModifier({ _, pokemon -> if (pokemon.hasLabels(CobblemonPokemonLabels.ULTRA_BEAST)) 5F else 0.1F }) { _, _ -> true })
createDefault("beast_ball", LabelModifier(5F, true, CobblemonPokemonLabels.ULTRA_BEAST)/*, LabelModifier(0.1F, false, CobblemonPokemonLabels.ULTRA_BEAST))*/)
createDefault("ancient_poke_ball", ancient = true)
createDefault("ancient_citrine_ball", ancient = true)
createDefault("ancient_verdant_ball", ancient = true)
@@ -297,15 +297,13 @@ object PokeBalls : JsonDataRegistry<PokeBall> {
createDefault("ancient_origin_ball", GuaranteedModifier(), ancient = true)
// Luxury ball effect, low priority as it must be triggered before soothe bell as of gen 4
CobblemonEvents.FRIENDSHIP_UPDATED.subscribe(priority = Priority.LOW) { event ->
if (!event.earned) {
return@subscribe
}
var increment = (event.newFriendship - event.pokemon.friendship).toFloat()
if (increment <= 0) {
if (increment <= 0) //these affects are only meant to affect positive gains
return@subscribe
if (increment <= 1F) {
event.pokemon.caughtBall.effects.filterIsInstance<FriendshipEarningBoostEffect>()
.forEach { increment *= it.multiplier }
}
event.pokemon.caughtBall.effects.filterIsInstance<FriendshipEarningBoostEffect>()
.forEach { increment *= it.multiplier }
event.newFriendship = event.pokemon.friendship + increment.roundToInt()
}
}
@@ -340,6 +338,7 @@ object PokeBalls : JsonDataRegistry<PokeBall> {
ancient: Boolean = false
): PokeBall {
val identifier = cobblemonResource(name)
//val finalModifiers = if (appendUltraBeastPenalty) modifiers + listOf(LabelModifier(0.1F, true, CobblemonPokemonLabels.ULTRA_BEAST)) else modifiers
val pokeball = PokeBall(identifier, modifier, effects, waterDragValue, model2d, model3d, throwPower, ancient)
this.defaults[identifier] = pokeball
return pokeball
@@ -54,20 +54,22 @@ object CatchRateModifiers {
/**
* Used by [PokeBalls.LOVE_BALL].
* Boosts the catch rate 2.5× if any of the player's active Pokémon is of the opposite gender,
* or 8× if that Pokémon is also of the same species.
* Boosts the catch rate if the Pokémon is of the same species and opposite gender
*/
val LOVE = BattleModifier { _, playerPokemon, pokemon ->
if (pokemon.gender == Gender.GENDERLESS) {
return@BattleModifier 1F
if (
pokemon.gender != Gender.GENDERLESS
&& playerPokemon.mapNotNull { it.battlePokemon?.originalPokemon }
.any {
it.gender != Gender.GENDERLESS &&
it.species.name.equals(pokemon.species.name, true) &&
it.gender != pokemon.gender
}
) {
if (playerPokemon.any { it.battlePokemon?.originalPokemon?.species?.resourceIdentifier == pokemon.species.resourceIdentifier }) 8F else 2.5F
}
val oppositeGender = playerPokemon
.mapNotNull { it.battlePokemon?.originalPokemon }
.filter { it.gender != Gender.GENDERLESS && it.gender != pokemon.gender }
when {
oppositeGender.any { it.species.resourceIdentifier == pokemon.species.resourceIdentifier } -> 8F
oppositeGender.isNotEmpty() -> 2.5F
else -> 1F
else {
1F
}
}
@@ -79,7 +81,7 @@ object CatchRateModifiers {
* 1× otherwise.
*/
val MOON_PHASES: CatchRateModifier = WorldStateModifier { _, entity ->
if (entity.level().dayTime % 24000 < 12000)
if (entity.level().gameTime in 12000..24000)
return@WorldStateModifier 1F
when (entity.level().moonPhase) {
2, 6 -> 1.5F
@@ -42,7 +42,7 @@ class PokedexManager(
}
// Java interop isn't great lol
@Deprecated("Use obtain(Pokemon) instead")
@Deprecated("Use encounter(PokedexEntityData) instead")
fun catch(pokemon: Pokemon) {
obtain(pokemon)
}
@@ -109,10 +109,8 @@ open class Learnset {
evolutionMoves
}
// Syncs level up, TM, and egg moves.
// We only sync level up moves atm
fun shouldSynchronize(other: Learnset) = other.levelUpMoves != this.levelUpMoves
|| other.tmMoves != this.tmMoves
|| other.eggMoves != this.eggMoves
fun decode(buffer: RegistryFriendlyByteBuf) {
this.levelUpMoves.clear()
@@ -124,8 +122,6 @@ open class Learnset {
}
levelUpMoves[level] = moves
}
decodeMoveList(buffer, this.tmMoves)
decodeMoveList(buffer, this.eggMoves)
}
fun encode(buffer: RegistryFriendlyByteBuf) {
@@ -137,21 +133,5 @@ open class Learnset {
buffer.writeInt(move.num)
}
}
encodeMoveList(buffer, this.tmMoves)
encodeMoveList(buffer, this.eggMoves)
}
private fun decodeMoveList(buffer: RegistryFriendlyByteBuf, list: MutableList<MoveTemplate>) {
list.clear()
repeat(times = buffer.readSizedInt(IntSize.U_SHORT)) {
Moves.getByNumericalId(buffer.readInt())?.let(list::add)
}
}
private fun encodeMoveList(buffer: RegistryFriendlyByteBuf, list: List<MoveTemplate>) {
buffer.writeSizedInt(IntSize.U_SHORT, list.size)
for (move in list) {
buffer.writeInt(move.num)
}
}
}
@@ -54,8 +54,8 @@ enum class Stats(override val identifier: ResourceLocation, override val display
"hp" -> HP
"atk", "attack" -> ATTACK // Hyper Cutter states the full stat name "Attack"
"def", "defense", "defence" -> DEFENCE // Big Pecks states the full stat name "Defense"
"spa", "special_attack" -> SPECIAL_ATTACK
"spd", "special_defence", "special_defense" -> SPECIAL_DEFENCE
"spa" -> SPECIAL_ATTACK
"spd" -> SPECIAL_DEFENCE
"spe", "speed" -> SPEED
"evasion" -> EVASION
else -> ACCURACY
@@ -40,10 +40,6 @@ import kotlin.math.*
class BirdBehaviour : RidingBehaviour<BirdSettings, BirdState> {
companion object {
val KEY = cobblemonResource("air/bird")
private const val FLY_POSE_SPEED = 0.1
private const val GLIDE_ENTRY_SPEED = 0.19
private const val GLIDE_EXIT_SPEED = 0.13
}
override val key: ResourceLocation = KEY
@@ -53,7 +49,7 @@ class BirdBehaviour : RidingBehaviour<BirdSettings, BirdState> {
}
val poseProvider = PoseProvider<BirdSettings, BirdState>(PoseType.HOVER)
.with(PoseOption(PoseType.FLY) { _, state, _ -> state.rideVelocity.get().z > FLY_POSE_SPEED })
.with(PoseOption(PoseType.FLY) { _, state, _ -> state.rideVelocity.get().z > 0.2 })
val globalBird: BirdSettings
get() = CobblemonRideSettings.bird
@@ -196,23 +192,21 @@ class BirdBehaviour : RidingBehaviour<BirdSettings, BirdState> {
}
// speed up and slow down based on input
if (driver.zza != 0.0f && state.stamina.get() > 0.0) {
activeInput = true
if (newVelocity.length() < glideTopSpeed) {
//make sure it can't exceed top speed
val forwardInput = when {
state.stamina.get() == 0.0f -> 0.0
driver.zza > 0 && newVelocity.z > topSpeed -> 0.0
driver.zza < 0 && newVelocity.z < (-topSpeed / 3.0) -> 0.0
else -> driver.zza.sign
}
newVelocity = Vec3(
newVelocity.x,
newVelocity.y,
(newVelocity.z + (accel * forwardInput.toDouble())))
if (driver.zza != 0.0f && state.stamina.get() > 0.0 && newVelocity.length() < glideTopSpeed) {
//make sure it can't exceed top speed
val forwardInput = when {
state.stamina.get() == 0.0f -> 0.0
driver.zza > 0 && newVelocity.z > topSpeed -> 0.0
driver.zza < 0 && newVelocity.z < (-topSpeed / 3.0) -> 0.0
else -> driver.zza.sign
}
newVelocity = Vec3(
newVelocity.x,
newVelocity.y,
(newVelocity.z + (accel * forwardInput.toDouble())))
activeInput = true
}
val controller = (vehicle as? OrientationControllable)?.orientationController
@@ -270,9 +264,11 @@ class BirdBehaviour : RidingBehaviour<BirdSettings, BirdState> {
//Check if the ride should be gliding
if (driver.isLocalPlayer) {
val powered = activeInput && state.stamina.get() > 0.0
val requiredSpeed = if (state.gliding.get()) GLIDE_EXIT_SPEED else GLIDE_ENTRY_SPEED
state.gliding.set(!powered && newVelocity.z >= requiredSpeed)
if ((activeInput && state.stamina.get() > 0.0) || state.rideVelocity.get().z < 0.2) {
state.gliding.set(false)
} else {
state.gliding.set(true)
}
}
//air resistance
@@ -193,7 +193,7 @@ class HorseBehaviour : RidingBehaviour<HorseSettings, HorseState> {
val newStam = if (state.sprinting.get()) {
max(0.0f,stam - stamDrainRate)
} else {
min(1.0f,stam + stamDrainRate * 8) // Need to change this to probably be in seconds eventually? idk something that isn't this magic number.
min(1.0f,stam + stamDrainRate * 4)
}
state.stamina.set(newStam.coerceIn(0F, 1F))
@@ -147,7 +147,7 @@ class DolphinBehaviour : RidingBehaviour<DolphinSettings, DolphinState> {
val stamDrainRate = (1.0f / boostTime).toFloat()
val newStam = if (state.boosting.get()) max(0.0f,stam - stamDrainRate)
else (stam + stamDrainRate * 2).coerceIn(0F, 1F)
else (stam + stamDrainRate).coerceIn(0F, 1F)
state.stamina.set(newStam)
}
@@ -12,7 +12,6 @@ import com.cobblemon.mod.common.api.conditional.RegistryLikeCondition
import com.cobblemon.mod.common.api.spawning.MoonPhaseRange
import com.cobblemon.mod.common.api.spawning.TimeRange
import com.cobblemon.mod.common.api.spawning.position.SpawnablePosition
import com.cobblemon.mod.common.block.entity.PokeSnackBlockEntity
import com.cobblemon.mod.common.util.Merger
import com.cobblemon.mod.common.util.math.orMax
import com.cobblemon.mod.common.util.math.orMin
@@ -60,7 +59,6 @@ abstract class SpawningCondition<T : SpawnablePosition> {
var structures: MutableList<Either<ResourceLocation, TagKey<Structure>>>? = null
var isSlimeChunk: Boolean? = null
var markers: MutableList<String>? = null
var isPokeSnack: Boolean? = null
@Transient
var appendages = mutableListOf<AppendageCondition>()
@@ -77,9 +75,7 @@ abstract class SpawningCondition<T : SpawnablePosition> {
}
protected open fun fits(spawnablePosition: T): Boolean {
if (isPokeSnack != null && (spawnablePosition.spawner is PokeSnackBlockEntity.Spawner != isPokeSnack!!)) {
return false
} else if (spawnablePosition.position.x < minX.orMin() || spawnablePosition.position.x > maxX.orMax()) {
if (spawnablePosition.position.x < minX.orMin() || spawnablePosition.position.x > maxX.orMax()) {
return false
} else if (spawnablePosition.position.y < minY.orMin() || spawnablePosition.position.y > maxY.orMax()) {
return false
@@ -32,7 +32,6 @@ import com.cobblemon.mod.common.api.spawning.selection.SpawnSelectionData
class EmptySpawnDetail : SpawnDetail() {
companion object {
val TYPE = "empty"
val EMPTY = EmptySpawnDetail()
}
override val type: String = TYPE
@@ -11,19 +11,14 @@ package com.cobblemon.mod.common.api.spawning.detail
import com.cobblemon.mod.common.Cobblemon.LOGGER
import com.cobblemon.mod.common.api.drop.DropTable
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.api.spawning.position.AreaSpawnablePosition
import com.cobblemon.mod.common.api.spawning.position.SpawnablePosition
import com.cobblemon.mod.common.api.spawning.selection.SpawnSelectionData
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
import com.cobblemon.mod.common.util.weightedSelection
import com.google.gson.annotations.SerializedName
import net.minecraft.core.registries.Registries
import net.minecraft.resources.ResourceLocation
import net.minecraft.util.Mth.clamp
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.Items
import kotlin.math.ceil
/**
* A spawn detail that spawns a herd of Pokémon. This relies on the same [PokemonSpawnAction]s as
@@ -41,6 +36,7 @@ import kotlin.math.ceil
class PokemonHerdSpawnDetail : SpawnDetail() {
companion object {
val TYPE = "pokemon-herd"
const val HERD_LEADER = "leader"
}
override val type: String = TYPE
@@ -54,14 +50,14 @@ class PokemonHerdSpawnDetail : SpawnDetail() {
get() = "${id}__LEVEL"
class Herdable {
var pokemon = PokemonProperties()
var pokemon: PokemonProperties = PokemonProperties()
val levelRange: IntRange? = null
var levelRangeOffset: IntRange? = null
val herdLevelRange: IntRange? = null
var dropTable: DropTable? = null
var isLeader = false
var isFollower = true
var weight = 1F
var isLeader: Boolean = false
var isFollower: Boolean = true
var weight: Float = 1F
var maxTimes = 10
var heldItem: ResourceLocation? = null
}
@@ -73,23 +69,22 @@ class PokemonHerdSpawnDetail : SpawnDetail() {
selectionData: SpawnSelectionData
) {
selectionData.removeSpawnDetails { it != this }
selectionData.guaranteedBucket = bucket
selectionData.removeSpawnablePositions { _, pos -> pos.distanceTo(spawnablePosition) < minDistanceBetweenSpawns }
val herdSpawnCount = selectionData.spawnActions.count { it.detail == this } + 1
val lacksPossibleLeader = lacksPossibleLeader(selectionData)
if (herdSpawnCount >= maxHerdSize || getValidHerdMembersForRole(selectionData, useLeaderRole = lacksPossibleLeader).isEmpty()) {
if (herdSpawnCount >= maxHerdSize || getValidHerdMembers(selectionData).isEmpty()) {
// We can't spawn any more in the herd so remove this spawn from the selection pool.
selectionData.removeSpawnDetails { it == this }
}
}
fun lacksPossibleLeader(selectionData: SpawnSelectionData): Boolean {
val level = getOrSetHerdLevel(selectionData)
val herdable = getValidHerdMembers(selectionData)
val herdCounts = getHerdMemberCounts(selectionData)
val leaderIsPossible = herdable.any { it.isLeader }
val leaderIsPossible = herdable.any { it.herdLevelRange?.contains(level) != false && it.isLeader }
val leaderIsSelected = herdCounts.any { it.key?.isLeader == true && it.value > 0 }
return leaderIsPossible && !leaderIsSelected
}
@@ -98,42 +93,25 @@ class PokemonHerdSpawnDetail : SpawnDetail() {
return (selectionData.context[herdLevelKey] as? Int) ?: run {
val level = levelRange.random()
selectionData.context[herdLevelKey] = level
level
return level
}
}
private fun getValidHerdMembersForRole(selectionData: SpawnSelectionData, useLeaderRole: Boolean): List<Herdable> {
return getValidHerdMembers(selectionData).filter { (useLeaderRole && it.isLeader) || (!useLeaderRole && it.isFollower) }
}
override fun createSpawnAction(
spawnablePosition: SpawnablePosition,
bucket: String,
selectionData: SpawnSelectionData
): SpawnAction<*> {
val herdable = getValidHerdMembersForRole(
selectionData = selectionData,
useLeaderRole = lacksPossibleLeader(selectionData)
).weightedSelection { it.weight } ?: herdablePokemon[0]
var level = selectionData.context[herdLevelKey] as? Int ?: 1
// Attempt to do more detailed anti-wall-spawn logic. Herd members could spawn in walls because
// we've just been assuming they're 1 block wide (or at least, all in the herd have the same size).
// Some pokemon are not that though.
if (spawnablePosition is AreaSpawnablePosition) {
val species = herdable.pokemon.species?.let { PokemonSpecies.getByIdentifier(it.asIdentifierDefaultingNamespace()) }
if (species != null) {
val height = ceil(species.hitbox.height * species.baseScale).toInt()
val width = ceil(species.hitbox.width * species.baseScale).toInt()
if (!spawnablePosition.hasSpace(width, height)) {
return EmptySpawnAction(spawnablePosition, bucket, EmptySpawnDetail.EMPTY)
}
}
}
level = herdable.levelRange?.let { clamp(level, it.first, it.last) } ?: level
): PokemonSpawnAction {
val validHerdMembers = getValidHerdMembers(selectionData)
var searchedForLeader = false
val herdable = if (lacksPossibleLeader(selectionData)) {
searchedForLeader = true
validHerdMembers.filter { it.isLeader }
} else {
validHerdMembers.filter { it.isFollower }
}.weightedSelection { it.weight } ?: herdablePokemon[0]
val level = selectionData.context[herdLevelKey] as? Int ?: 1
val levelRange = herdable.levelRangeOffset?.let { offset ->
val min = level + offset.first
val max = level + offset.last
@@ -158,7 +136,11 @@ class PokemonHerdSpawnDetail : SpawnDetail() {
drops = herdable.dropTable,
heldItem = heldItem,
levelRange = levelRange
)
).also {
if (herdable.isLeader && searchedForLeader) {
it.labels += HERD_LEADER
}
}
}
fun getHerdMemberCounts(selectionData: SpawnSelectionData): Map<Herdable?, Int> {
@@ -71,7 +71,7 @@ class FishingSpawnCause(
}
fun alterSize(pokemonEntity: PokemonEntity, effect: SpawnBait.Effect) {
pokemonEntity.pokemon.scaleModifier += effect.value.toFloat() / 1000.0f
pokemonEntity.pokemon.scaleModifier *= effect.value.toFloat() / 1000.0f
}
fun saveDropsReroll(pokemonEntity: PokemonEntity, effect: SpawnBait.Effect) {
@@ -53,39 +53,34 @@ open class AreaSpawnablePosition(
*/
open fun isSafeSpace(world: ServerLevel, pos: BlockPos, state: BlockState): Boolean = !state.isSolid
fun hasSpace(width: Int, height: Int): Boolean {
val sizeX = width.takeIf { it > 0 } ?: 1
val sizeY = height.takeIf { it > 0 } ?: 1
val minX = floor(position.x + 0.5 - (sizeX - 1) / 2F).toInt() - 1
val maxX = ceil(position.x + 0.5 + (sizeX + 1) / 2F).toInt() + 1
val maxY = ceil(position.y + (sizeY + 1) / 2F).toInt() + 1
val minZ = floor(position.z + 0.5 - (sizeX - 1) / 2F).toInt() - 1
val maxZ = ceil(position.z + 0.5 + (sizeX + 1) / 2F).toInt() + 1
val mutable = BlockPos.MutableBlockPos()
for (x in minX until maxX) {
for (y in (position.y + 1)..maxY) {
for (z in minZ until maxZ) {
val state = zone.getBlockState(mutable.set(x, y, z))
if (!isSafeSpace(world, mutable, state)) {
return false
}
}
}
}
return true
}
override fun postFilter(detail: SpawnDetail): Boolean {
if (!super.postFilter(detail)) {
return false
}
if (detail.width > 1 || detail.height > 1) {
return hasSpace(detail.width, detail.height)
val sizeX = detail.width.takeIf { it > 0 } ?: 1
val sizeY = detail.height.takeIf { it > 0 } ?: 1
val minX = floor(position.x + 0.5 - (sizeX - 1) / 2F).toInt() - 1
val maxX = ceil(position.x + 0.5 + (sizeX + 1) / 2F).toInt() + 1
val maxY = ceil(position.y + (sizeY + 1) / 2F).toInt() + 1
val minZ = floor(position.z + 0.5 - (sizeX - 1) / 2F).toInt() - 1
val maxZ = ceil(position.z + 0.5 + (sizeX + 1) / 2F).toInt() + 1
val mutable = BlockPos.MutableBlockPos()
for (x in minX until maxX) {
for (y in (position.y + 1)..maxY) {
for (z in minZ until maxZ) {
val state = zone.getBlockState(mutable.set(x, y, z))
if (!isSafeSpace(world, mutable, state)) {
return false
}
}
}
}
}
return true
@@ -28,7 +28,7 @@ import java.io.InputStreamReader
* @since July 8th, 2022
*/
class BestSpawnerConfig {
val version = 2
val version = 0
/** Whether an external config will be replaced by an internal one once [version] is higher on the internal. */
val replaceWithNewVersion = true
@SerializedName("spawnablePositionTypeWeights", alternate = ["contextWeights"])
@@ -40,11 +40,11 @@ class BestSpawnerConfig {
)
val worldBuckets = mutableMapOf(
"common" to 94.05F,
"common" to 94F,
"uncommon" to 5F,
"rare" to 0.5F,
"ultra-rare" to 0.2F,
"boss" to 0.25F,
"boss" to 0.3F,
)
val fishingBuckets = mutableMapOf(
@@ -89,7 +89,6 @@ open class FlatSelector : SpawningSelector<FlatSelector.SpawnablePositionSelecti
val spawnablePositions: List<SpawnablePosition>,
val selectingSpawnInformation: MutableMap<String, SpawnablePositionBucketSelectionData>,
): SpawnSelectionData {
override var guaranteedBucket: String? = null
override val spawnActions = mutableListOf<SpawnAction<*>>()
override val context = mutableMapOf<String, Any>()
@@ -101,7 +101,6 @@ open class FlatSpawnablePositionWeightedSelector : SpawningSelector<FlatSpawnabl
val spawner: Spawner,
val spawnablePositions: List<SpawnablePosition>
) : SpawnSelectionData {
override var guaranteedBucket: String? = null
override val spawnActions = mutableListOf<SpawnAction<*>>()
override val context = mutableMapOf<String, Any>()
@@ -21,7 +21,6 @@ import com.cobblemon.mod.common.api.spawning.position.SpawnablePosition
* @since April 21st, 2025
*/
interface SpawnSelectionData {
var guaranteedBucket: String?
val context: MutableMap<String, Any>
val spawnActions: MutableList<SpawnAction<*>>
fun removeSpawnDetails(shouldRemove: (SpawnDetail) -> Boolean)
@@ -29,7 +28,6 @@ interface SpawnSelectionData {
companion object {
val EMPTY = object : SpawnSelectionData {
override var guaranteedBucket: String? = null
override val context: MutableMap<String, Any> = mutableMapOf()
override val spawnActions: MutableList<SpawnAction<*>> = mutableListOf()
override fun removeSpawnDetails(shouldRemove: (SpawnDetail) -> Boolean) {}
@@ -47,7 +47,7 @@ interface SpawningSelector<T : SpawnSelectionData> {
val spawnActions = selectionData.spawnActions
while (spawnActions.size < maxSpawns) {
val bucket = selectionData.guaranteedBucket ?: bucketSelector()
val bucket = bucketSelector()
val spawnAction = selectSpawnAction(
spawner = spawner,
bucket = bucket,
@@ -17,7 +17,6 @@ import com.cobblemon.mod.common.api.spawning.influence.BucketNormalizingInfluenc
import com.cobblemon.mod.common.api.spawning.influence.SpawnBaitInfluence
import com.cobblemon.mod.common.api.spawning.influence.SpawningInfluence
import com.cobblemon.mod.common.api.spawning.rules.SpawnRule
import com.cobblemon.mod.common.block.entity.PokeSnackBlockEntity
import com.cobblemon.mod.common.util.mutableLazy
import net.minecraft.core.BlockPos
import net.minecraft.server.level.ServerLevel
@@ -67,10 +66,10 @@ object PokeSnackSpawnerFactory {
},
)
fun create(ctx: Context): PokeSnackBlockEntity.Spawner {
fun create(ctx: Context): FixedAreaSpawner {
val influences = influenceBuilders.mapNotNull { it(ctx) }
return PokeSnackBlockEntity.Spawner(
return FixedAreaSpawner(
name = ctx.name,
spawnPool = spawnPool,
buckets = Cobblemon.bestSpawner.config.pokeSnackBuckets,
@@ -46,7 +46,6 @@ open class BottomlessStore(override val uuid: UUID) : PokemonStore<BottomlessPos
pokemon.forEachIndexed { index, pokemon ->
pokemon.storeCoordinates.set(StoreCoordinates(this, BottomlessPosition(index)))
}
rebuildPokemonByUuid()
}
override fun saveToNBT(nbt: CompoundTag, registryAccess: RegistryAccess): CompoundTag {
@@ -17,10 +17,10 @@ import com.cobblemon.mod.common.api.storage.factory.PokemonStoreFactory
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.util.DataKeys
import com.google.gson.JsonObject
import java.util.UUID
import net.minecraft.core.RegistryAccess
import net.minecraft.nbt.CompoundTag
import net.minecraft.server.level.ServerPlayer
import java.util.UUID
/**
* The base class for all stores of [Pokemon]. A store specifies the kind of coordinate it needs to be given
@@ -53,7 +53,7 @@ abstract class PokemonStore<T : StorePosition> : Iterable<Pokemon> {
* Runs initialization logic for this store, knowing that it has just been constructed in a [PokemonStoreFactory].
*
* The minimum of what this function should do is iterate over all the Pokémon in this store and set their store
* coordinates. Built-in stores then rebuild the UUID index via [rebuildPokemonByUuid].
* coordinates.
*
* If this does not get called, or it does not do its job properly, serious de-sync issues may follow.
*/
@@ -77,6 +77,7 @@ abstract class PokemonStore<T : StorePosition> : Iterable<Pokemon> {
remove(pokemon)
val position = getFirstAvailablePosition() ?: return false // Couldn't fit, shrug emoji
set(position, pokemon)
Cobblemon.playerDataManager.getTMData(uuid)?.syncTMsFromPokemon(pokemon)
return true
}
@@ -98,7 +99,6 @@ abstract class PokemonStore<T : StorePosition> : Iterable<Pokemon> {
setAtPosition(position, pokemon)
pokemon.storeCoordinates.set(StoreCoordinates(this, position))
pokemonByUuid[pokemon.uuid] = pokemon
}
/** Swaps the Pokémon at the specified positions. If one of the spaces is empty, it will simply move the not-null one to that space. */
@@ -147,26 +147,10 @@ abstract class PokemonStore<T : StorePosition> : Iterable<Pokemon> {
pokemon.recall()
pokemon.storeCoordinates.set(null)
setAtPosition(currentPosition.position, null)
pokemonByUuid.remove(pokemon.uuid)
return true
}
/** In-memory UUID index. Not serialized; rebuilt by [rebuildPokemonByUuid]. */
private val pokemonByUuid = hashMapOf<UUID, Pokemon>()
/** Gets the [Pokemon] with this UUID, or null if it is not in this store. */
operator fun get(uuid: UUID): Pokemon? = pokemonByUuid[uuid]
/**
* Rebuilds the UUID index from the store's current contents.
*
* Call after load or any structural mutation that does not go through [set] / [remove], such as
* filling slots directly from NBT/JSON or shrinking a PC.
*/
protected fun rebuildPokemonByUuid() {
pokemonByUuid.clear()
forEach { pokemonByUuid[it.uuid] = it }
}
operator fun get(uuid: UUID) = find { it.uuid.equals(uuid) }
open fun handleInvalidSpeciesNBT(nbt: CompoundTag) {
Cobblemon.LOGGER.error("Failed to read unknown species: ${nbt.getString(DataKeys.POKEMON_SPECIES_IDENTIFIER)}")
@@ -138,7 +138,6 @@ open class PartyStore(override val uuid: UUID) : PokemonStore<PartyPosition>() {
val pokemon = get(slot) ?: continue
pokemon.storeCoordinates.set(StoreCoordinates(this, PartyPosition(slot)))
}
rebuildPokemonByUuid()
}
override fun onPokemonChanged(pokemon: Pokemon) {
@@ -280,7 +279,7 @@ open class PartyStore(override val uuid: UUID) : PokemonStore<PartyPosition>() {
}
}
return result.sortedBy { it.health <= 0 }
return result
}
fun clearParty() {
forEach {
@@ -105,7 +105,6 @@ open class PCStore(
override fun initialize() {
boxes.forEach { it.initialize() }
backupStore.initialize()
rebuildPokemonByUuid()
}
fun relocateEvictedBoxPokemon(pokemon: Pokemon) {
@@ -136,7 +135,6 @@ open class PCStore(
tryRestoreBackedUpPokemon()
}
rebuildPokemonByUuid()
pcChangeObservable.emit(Unit)
}
@@ -144,7 +142,6 @@ open class PCStore(
this.lockedSize = lockNewSize
boxes.removeAll(boxList)
boxList.flatMap { it.asIterable() }.forEach(overflowHandler)
rebuildPokemonByUuid()
pcChangeObservable.emit(Unit)
}
Loaded 100 of 3701 files, more files were not shown because too many files have changed in this diff. Show more