Compare commits

..
1794 changed files with 22578 additions and 99509 deletions
-3
View File
@@ -157,9 +157,6 @@ build:
paths:
- neoforge/build/libs
- fabric/build/libs
exclude:
- neoforge/build/libs/*-dev-shadow.jar
- fabric/build/libs/*-dev-shadow.jar
publish:
stage: deploy
+9 -135
View File
@@ -3,169 +3,43 @@
### 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, Soft Sand, Electirizer, Magmarizer, 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, Terrain Extender, Medicinal Brew, Moomoo Milk, HP Up, Protein, Iron, Carbos, Calcium, Zinc, PP Up, PP Max, Upgrade, Dubious Disc, and Loaded Dice.
- 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:
- 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.
- Mint Leaves can now be placed on the ground as Leaf Litter, or in Flower Pots
- Added Oricorio Flowers
- Added Terrain Plants
- Berries can now be placed in Flower Pots
- The evolution tree can now be viewed on the Pokédex, showing a Pokémon's full family line and what's needed to evolve into each stage.
- Added custom sounds to a bunch of items that can be placed as blocks: liquid bottles, pill bottles, powders and dice.
### New rideable Pokémon
### Pokémon Added
#### Generation 4
- Snover
- Abomasnow
- Burmy
- Wormadam
- Mothim
#### Generation 5
- Tympole
- Palpitoad
- Seismitoad
#### Generation 6
- Helioptile
- Heliolisk
#### Generation 7
- Grubbin
- Charjabug
- Vikavolt
- Minior
#### Generation 8
- Eternatus
#### Generation 9
- Greavard
- Houndstone
#### Regional Forms
### Model updates for the following Pokémon
- Sudowoodo
- Spheal
- Cacnea
- Cacturne
- Binacle
- Barbaracle
- Glimmet
- Glimmora
- Deerling
- Sawsbuck (All forms)
- Absol
- Poliwag
- Politoed
- Ekans
- Oddish
- Gloom
- Vileplume
- Bellossom
- Shroomish
### Animation updates for the following Pokémon
- Treecko (New Shoulder Mount!)
- Bonsly (New Shoulder Mount!)
- Sudowoodo
- Cacnea
- Cacturne
- Ekans
- Arbok
- Oddish
- Gloom
- Vileplume
- Bellossom
#### Added sounds to the following moves
### Changes
- Restructured Botany Pots integration folder structure
- Decrease default pastured mon wander radius to 32.
- Factor in the weight of pokemon 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)
- Trading has been overhauled to allow offering multiple Pokémon, up to the entire party, in a single trade
- Selected Pokémon are now highlighted in both parties with new party slot assets
- Hovering an offered Pokémon now shows its summary, and clicking the active Pokémon no longer exits the summary view
- The trade animation now moves the offered Pokémon towards the Poké Ball
- Parties are compacted after a trade so no empty slots are left between Pokémon
- Terrain Seeds can now be planted
- Terrain Seeds are now consumed during PvP and NPC battles
- Glow Item Frames can be crafted using Bright Powder as an alternative to Glow Ink Sacs
- Bright Powder can be used on Signs and Hanging Signs to make text glow, as an alternative to Glow Ink Sacs
- Updated various recipes to display in Recipe Groups within the Recipe Book
- Updated most recipes to display more logically/consistently within the Recipe Book's categories
- Updated the following recipes
- Never-Melt Ice, Focus Sash, Blunder Policy, Weakness Policy, Blank TM
- Make ride rotations less jittery in multiplayer.
- Changes Disc Shelf TMs to use textures instead of tinting and overlays to line up with item display
- Blank TM can now be inserted into the Disc Shelf
- Added TM creative tab
- Recall animation now renders ball in hand with the player arm raised.
- Made regional and Vivillon split-evo locations now mutually exclusive
- Item evolutions now unlock **all** locked evolutions whose requirements are met instead of stopping after the first.
- Exceptions being "forced" evolutions which will continue to stop after the first one
### Fixes
- Correctly grants "They're Minerals!" advancement when planting a type gem
- Shoulder mounted Pokémon now follow player body animations, improving their positioning with custom player animations and default actions.
- Shoulder mounted Pokémon now sit slightly higher with chest armor equipped for better alignment, with a configurable blacklist.
- Type Gem Blocks now have advancements to give you the recipe in the recipe book (the data for the advancements were inside the recipes somehow??)
- Some other recipes without advancements have also been corrected
- Corrected the TM Recipes for Charge Beam and Payday
- Type Gem blocks now properly generate clusters off of the block.
- Starter Screen Categories will no longer stop rendering if they are bigger than the viewport, and you are not viewing top or bottom edges
- Random Starter option now properly handled in height calculation, so now it won't run off into other categories or be un-accessible due to scroll limits if it causes a new row
- Mochi and Feathers no longer drop glass bottles on use
- Fixed a rare set of concurrency bugs related to reloading packs on NeoForge; fixes a ModernFix issue
- Removed unnecessary conditions for Mimikyu's standing pose. Hopefully fixes the rare instance of it using its shoulder animations during battle in multiplayer scenarios.
- Fixed Rockruff line not blinking
- Fixed Flygon sometimes using its running animation instead of walking
- Fixed Bramblin always sleeping outside of battle
- Fixed Dragonair using its battle idle as a normal idle
- Fixed Lycanroc's head movement
- Added head movement to Archaludon
- Fixed Duraludon line not having any sleep animations
- Fixed Diglett line not having their faint animations
- Fixed Kingambit moving its legs while walking
- Fixed Alolan Diglett's cry not playing
- Fixed shiny alpha Snorlax using the non-shiny texture
- Various Structure fixes
- Fixed shiny Pokémon under certain circumstances forgetting taught TMs
- Fixed certain Pokémon sounds sometimes being doubled when played at specific times in their animations
- Fixed ball capture beam origin coming from behind the ball.
- Fixed Safari Ball catch rate outside of battle
### Developer
- 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
- Trade network packets (`TradeUpdatedPacket`, `UpdateTradeOfferPacket`, and `TradeCompletedPacket`) now receive an array of Pokémon instead of a single Pokémon to execute trades
- `TradeEvent.Pre` and `TradeEvent.Post` now expose every Pokémon each participant receives (`tradeParticipant1Pokemon` and `tradeParticipant2Pokemon` are now lists), and their Molang context includes `trade_participant_1_pokemon_list` and `trade_participant_2_pokemon_list`
- Evolution requirements can now set `pokedexIcon` and `pokedexTooltip` directly in their JSON to control how they're shown on the Pokédex's evolution tree, for example:
```json
{
"variant": "attack_defence_ratio",
"pokedexIcon": "minecraft:diamond_sword",
"pokedexTooltip": "cobblemon.ui.pokedex.requirement.attack_defence_ratio.tooltip"
}
```
- Fixed wrong order of operations in the code flow prior to the `FOSSIL_REVIVED` event causing pokemon to appear without entity link
- Fixed TestPartySlot command not correctly comparing the provided spec with the target Pokémon
### Molang & Datapacks
- A new `negate` variant for evolution requirements has been added, which will negate (invert) the provided inner condition.
- Added Molang functions for individual Pokédex ids, wherein a subdex is passed in as a string to `dex_caught_count` etc.
### Particles
#### New Move Animations
### Localization
- Updated translations for:
- Updated translations for:
+9
View File
@@ -0,0 +1,9 @@
- Some kind of particle effect for them? they're meant to have glowing eyes, so maybe some red particles on the eye locator
--- global posers? stacked posers?
--- red texture seems to be what some people wanted.
/- shoulder mounts part of it
- omni boost
- roar when player comes within reach, adjust levels
- Riding settings to control what the min scale is for a pokemon to be ridable.
+2 -5
View File
@@ -1,6 +1,3 @@
import utilities.gitBranch
import utilities.gitShortCommit
plugins {
id("cobblemon.root-conventions")
}
@@ -9,6 +6,6 @@ version = "${project.property("mod_version")}+${project.property("mc_version")}"
val isSnapshot = project.property("snapshot")?.equals("true") == true
if (isSnapshot) {
val fixedBranchName = gitBranch().substringAfter("/")
version = "$version-${fixedBranchName}-${gitShortCommit()}"
val fixedBranchName = versioning.info.branch.substringAfter("/")
version = "$version-${fixedBranchName}-${versioning.info.build}"
}
+8 -15
View File
@@ -1,8 +1,9 @@
import utilities.isSnapshot
import utilities.version
import utilities.gitBranch
import utilities.gitCommit
import java.time.OffsetDateTime
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
/*
*
@@ -65,20 +66,14 @@ 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")
}
}
// Git info lives in its own output dir (not processResources) so a new commit only rewrites this one file.
val writeGitInfo by tasks.registering(WriteProperties::class) {
destinationFile = layout.buildDirectory.file("generated/gitInfo/cobblemon-git.properties")
property("commit", gitCommit())
property("branch", gitBranch())
}
sourceSets.main { output.dir(mapOf("builtBy" to writeGitInfo), layout.buildDirectory.dir("generated/gitInfo")) }
tasks.processResources {
exclude("data/**/README.md")
}
@@ -105,13 +100,11 @@ sourceSets {
property("modid", "cobblemon")
property("version", project.version())
property("isSnapshot", if(rootProject.isSnapshot()) "true" else "false")
property("gitCommit", versioning.info.commit)
property("branch", versioning.info.branch)
System.getProperty("buildNumber")?.let { property("buildNumber", it) }
property("timestamp", OffsetDateTime.now(ZoneOffset.UTC).format(DateTimeFormatter.ofPattern("MM/dd/yyyy hh:mm:ss")) + " UTC")
}
}
}
}
// The platform jars are built from :common:jar, so the remapped common jars are only needed by `publish`
tasks.assemble {
setDependsOn(dependsOn.filterNot { (it as? TaskProvider<*>)?.name in setOf("remapJar", "remapSourcesJar") })
}
@@ -10,13 +10,11 @@ package com.cobblemon.mod.common.mixin.client;
import com.cobblemon.mod.common.OrientationControllable;
import com.cobblemon.mod.common.client.render.player.MountedPlayerRenderer;
import com.cobblemon.mod.common.client.render.player.RecallArmAnimationTracker;
import com.cobblemon.mod.common.duck.RidePassenger;
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity;
import net.minecraft.client.model.HumanoidModel;
import net.minecraft.client.model.geom.ModelPart;
import net.minecraft.client.player.AbstractClientPlayer;
import net.minecraft.util.Mth;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import org.spongepowered.asm.mixin.Final;
@@ -98,14 +96,4 @@ public class HumanoidModelMixin {
MountedPlayerRenderer.INSTANCE.animate((HumanoidModel<AbstractClientPlayer>)(Object)this, pokemonEntity, player, netHeadYaw, headPitch, ageInTicks, limbSwing, limbSwingAmount);
}
}
@Inject(method = "setupAnim*", at = @At("TAIL"))
public void cobblemon$poseRecallArm(LivingEntity entity, float limbSwing, float limbSwingAmount, float ageInTicks, float netHeadYaw, float headPitch, CallbackInfo ci) {
if (!(entity instanceof AbstractClientPlayer) || entity.getVehicle() instanceof PokemonEntity) return;
float raise = RecallArmAnimationTracker.INSTANCE.raiseFactor(entity.getUUID());
if (raise <= 0f) return;
this.rightArm.xRot = Mth.lerp(raise, this.rightArm.xRot, (float) (-Math.PI / 2.0));
this.rightArm.yRot = Mth.lerp(raise, this.rightArm.yRot, 0f);
this.rightArm.zRot = Mth.lerp(raise, this.rightArm.zRot, 0f);
}
}
@@ -1,32 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.mixin.client;
import com.cobblemon.mod.common.client.render.player.RecallArmAnimationTracker;
import com.mojang.blaze3d.vertex.PoseStack;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.entity.layers.ItemInHandLayer;
import net.minecraft.world.entity.HumanoidArm;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.item.ItemDisplayContext;
import net.minecraft.world.item.ItemStack;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(ItemInHandLayer.class)
public class ItemInHandLayerMixin {
@Inject(method = "renderArmWithItem", at = @At("HEAD"), cancellable = true)
private void cobblemon$hideRecallHandItem(LivingEntity entity, ItemStack itemStack, ItemDisplayContext itemDisplayContext, HumanoidArm humanoidArm, PoseStack poseStack, MultiBufferSource multiBufferSource, int light, CallbackInfo ci) {
if (humanoidArm == HumanoidArm.RIGHT && RecallArmAnimationTracker.INSTANCE.isRecalling(entity.getUUID())) {
ci.cancel();
}
}
}
@@ -9,7 +9,6 @@
package com.cobblemon.mod.common.mixin.client;
import com.cobblemon.mod.common.OrientationControllable;
import com.cobblemon.mod.common.client.render.player.FirstPersonRecallBallRenderer;
import com.cobblemon.mod.common.item.PokedexItem;
import com.mojang.blaze3d.vertex.PoseStack;
import net.minecraft.client.Minecraft;
@@ -32,9 +31,6 @@ public class ItemInHandRendererMixin {
if (abstractClientPlayer.isUsingItem() && abstractClientPlayer.getUseItem().getItem() instanceof PokedexItem) {
ci.cancel();
}
else if (interactionHand == InteractionHand.MAIN_HAND && FirstPersonRecallBallRenderer.INSTANCE.render(poseStack, multiBufferSource, j, f)) {
ci.cancel();
}
else if (abstractClientPlayer instanceof OrientationControllable controllable && controllable.getOrientationController().isActive()) {
}
@@ -0,0 +1,30 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.mixin.client;
import com.cobblemon.mod.common.client.render.RenderAlphaScope;
import net.minecraft.client.renderer.RenderStateShard;
import net.minecraft.client.renderer.RenderType;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(RenderStateShard.class)
public abstract class RenderStateShardMixin {
@Inject(method = "setupRenderState", at = @At("RETURN"))
private void cobblemon$applyScopedAlpha(CallbackInfo ci) {
if ((Object) this instanceof RenderType) RenderAlphaScope.afterMaterialSetup();
}
@Inject(method = "clearRenderState", at = @At("RETURN"))
private void cobblemon$restoreScopedAlpha(CallbackInfo ci) {
if ((Object) this instanceof RenderType) RenderAlphaScope.afterMaterialClear();
}
}
@@ -8,12 +8,8 @@ object CobblemonBuildDetails {
const val VERSION = "{{ version }}"
const val SNAPSHOT = {{ isSnapshot }}
// Read at runtime rather than templated in, so a new commit doesn't invalidate compiled code.
private val git = java.util.Properties().apply {
CobblemonBuildDetails::class.java.getResourceAsStream("/cobblemon-git.properties")?.use(::load)
}
@JvmField val GIT_COMMIT: String = git.getProperty("commit", "Not Specified")
@JvmField val BRANCH: String = git.getProperty("branch", "Not Specified")
const val GIT_COMMIT = "{{ gitCommit | default("Not Specified") }}"
const val BRANCH = "{{ branch | default("Not Specified") }}"
fun smallCommitHash(): String {
return this.GIT_COMMIT.substring(0, 7)
@@ -22,6 +22,8 @@ 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
@@ -69,6 +71,7 @@ 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
@@ -259,6 +262,7 @@ 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)
@@ -321,6 +325,11 @@ object Cobblemon {
// Start up the data provider.
CobblemonDataProvider.registerDefaults()
MolangUiActionRegistry.register(
VendingMachinePageHandler.ACTION,
VendingMachinePageHandler
)
SHINY_ASPECT.register()
GENDER_ASPECT.register()
COSMETIC_SLOT_ASPECT.register()
@@ -450,6 +459,7 @@ object Cobblemon {
}
PlatformEvents.SERVER_STOPPED.subscribe {
MolangUiService.closeAll()
storage.unregisterAll(it.server.registryAccess())
playerDataManager.saveAllStores()
playerDataManager.saveExecutor.shutdown()
@@ -109,21 +109,4 @@ object CobblemonBlockEntities : PlatformRegistry<Registry<BlockEntityType<*>>, R
"poke_snack",
BlockEntityType.Builder.of(::PokeSnackBlockEntity, CobblemonBlocks.POKE_SNACK, CobblemonBlocks.POKE_CAKE).build(null)
)
@JvmField
val TERRAIN_PLANT: BlockEntityType<TerrainPlantBlockEntity> = this.create(
"terrain_plant",
BlockEntityType.Builder.of(::TerrainPlantBlockEntity,
CobblemonBlocks.VOLTACONIA,
CobblemonBlocks.LUCIBELL,
CobblemonBlocks.VERDROOT
).build(null)
)
@JvmField
val MISTLEAF_FLOWER: BlockEntityType<MistleafFlowerBlockEntity> = this.create(
"mistleaf",
BlockEntityType.Builder.of(::MistleafFlowerBlockEntity,
CobblemonBlocks.MISTLEAF
).build(null)
)
}
@@ -16,7 +16,8 @@ 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.*
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
@@ -27,7 +28,6 @@ import com.cobblemon.mod.common.block.sign.CobblemonSignBlock
import com.cobblemon.mod.common.block.sign.CobblemonWallHangingSignBlock
import com.cobblemon.mod.common.block.sign.CobblemonWallSignBlock
import com.cobblemon.mod.common.block.tmmachine.TMMachineBlock
import com.cobblemon.mod.common.block.general.MintLitterBlock
import com.cobblemon.mod.common.mixin.invoker.BlocksInvoker
import com.cobblemon.mod.common.mixin.invoker.DoorBlockInvoker
import com.cobblemon.mod.common.mixin.invoker.FireBlockInvoker
@@ -653,21 +653,15 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<
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 ELECTIRIZER = create("electirizer", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.METAL).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.IZER))
@JvmField
val MAGMARIZER = create("magmarizer", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.METAL).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.IZER))
@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(CobblemonSounds.ITEM_BLOCK_POWDER_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.POUCH))
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(CobblemonSounds.ITEM_BLOCK_POWDER_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.POUCH))
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(CobblemonSounds.ITEM_BLOCK_POWDER_SOUNDS).mapColor(MapColor.NONE).noOcclusion().lightLevel{3}, DecorativeItemBlock.DecorativeItemBlockType.POUCH))
@JvmField
val SOFT_SAND = create("soft_sand", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_POWDER_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.POUCH))
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))
@@ -697,7 +691,7 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<
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(CobblemonSounds.ITEM_BLOCK_DICE_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.DICE))
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
@@ -708,46 +702,40 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<
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(CobblemonSounds.ITEM_BLOCK_BOTTLE_LIQUID_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.THROAT_SPRAY))
@JvmField
val TERRAIN_EXTENDER = create("terrain_extender", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.EXTENDER))
@JvmField
val MOOMOO_MILK = create("moomoo_milk", StackableItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_BOTTLE_LIQUID_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.GLASS_BOTTLE))
@JvmField
val MEDICINAL_BREW = create("medicinal_brew", StackableItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_BOTTLE_LIQUID_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.GLASS_BOTTLE))
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(CobblemonSounds.ITEM_BLOCK_BOTTLE_LIQUID_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.STATUS_RESTORE))
val FULL_HEAL = create("full_heal", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.STATUS_RESTORE))
@JvmField
val ANTIDOTE = create("antidote", StackableItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_BOTTLE_LIQUID_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.STATUS_RESTORE))
val ANTIDOTE = create("antidote", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.STATUS_RESTORE))
@JvmField
val AWAKENING = create("awakening", StackableItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_BOTTLE_LIQUID_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.STATUS_RESTORE))
val AWAKENING = create("awakening", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.STATUS_RESTORE))
@JvmField
val BURN_HEAL = create("burn_heal", StackableItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_BOTTLE_LIQUID_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.STATUS_RESTORE))
val BURN_HEAL = create("burn_heal", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.STATUS_RESTORE))
@JvmField
val ICE_HEAL = create("ice_heal", StackableItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_BOTTLE_LIQUID_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.STATUS_RESTORE))
val ICE_HEAL = create("ice_heal", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.STATUS_RESTORE))
@JvmField
val PARALYZE_HEAL = create("paralyze_heal", StackableItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_BOTTLE_LIQUID_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.STATUS_RESTORE))
val PARALYZE_HEAL = create("paralyze_heal", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.STATUS_RESTORE))
@JvmField
val ETHER = create("ether", StackableItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_BOTTLE_LIQUID_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.ETHER))
val ETHER = create("ether", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.ETHER))
@JvmField
val MAX_ETHER = create("max_ether", StackableItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_BOTTLE_LIQUID_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.ETHER))
val MAX_ETHER = create("max_ether", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.ETHER))
@JvmField
val ELIXIR = create("elixir", StackableItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_BOTTLE_LIQUID_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.ETHER))
val ELIXIR = create("elixir", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.ETHER))
@JvmField
val MAX_ELIXIR = create("max_elixir", StackableItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_BOTTLE_LIQUID_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.ETHER))
val MAX_ELIXIR = create("max_elixir", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.ETHER))
@JvmField
val POTION = create("potion", StackableItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_BOTTLE_LIQUID_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.POTION))
val POTION = create("potion", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.POTION))
@JvmField
val SUPER_POTION = create("super_potion", StackableItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_BOTTLE_LIQUID_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.POTION))
val SUPER_POTION = create("super_potion", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.POTION))
@JvmField
val HYPER_POTION = create("hyper_potion", StackableItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_BOTTLE_LIQUID_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.POTION))
val HYPER_POTION = create("hyper_potion", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.POTION))
@JvmField
val MAX_POTION = create("max_potion", StackableItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_BOTTLE_LIQUID_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.MAX_POTION))
val MAX_POTION = create("max_potion", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.MAX_POTION))
@JvmField
val FULL_RESTORE = create("full_restore", StackableItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_BOTTLE_LIQUID_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.MAX_POTION))
val FULL_RESTORE = create("full_restore", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.MAX_POTION))
@JvmField
val DIRE_HIT = create("dire_hit", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.BATTLE_ITEM))
@@ -767,21 +755,21 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<
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(CobblemonSounds.ITEM_BLOCK_BOTTLE_PILLS_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.VITAMIN))
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(CobblemonSounds.ITEM_BLOCK_BOTTLE_PILLS_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.VITAMIN))
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(CobblemonSounds.ITEM_BLOCK_BOTTLE_PILLS_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.VITAMIN))
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(CobblemonSounds.ITEM_BLOCK_BOTTLE_PILLS_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.VITAMIN))
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(CobblemonSounds.ITEM_BLOCK_BOTTLE_PILLS_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.VITAMIN))
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(CobblemonSounds.ITEM_BLOCK_BOTTLE_PILLS_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.VITAMIN))
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(CobblemonSounds.ITEM_BLOCK_BOTTLE_PILLS_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.VITAMIN))
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(CobblemonSounds.ITEM_BLOCK_BOTTLE_PILLS_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.VITAMIN))
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 }))
@@ -798,7 +786,6 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<
@JvmField
val PC = create("pc", PCBlock(BlockBehaviour.Properties.of().mapColor(MapColor.METAL).sound(SoundType.METAL).pushReaction(PushReaction.BLOCK).strength(2F).noOcclusion().lightLevel { if ((it.getValue(PCBlock.ON) as Boolean) && (it.getValue(PCBlock.PART) == PCBlock.PCPart.TOP)) 13 else 0 }))
@JvmField
val TM_MACHINE = create(
"tm_machine",
TMMachineBlock(
@@ -821,13 +808,16 @@ 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)))
@JvmField
val PASTURE = create("pasture", PastureBlock(BlockBehaviour.Properties.of().mapColor(MapColor.COLOR_BROWN).sound(SoundType.WOOD).strength(2F).noOcclusion().pushReaction(PushReaction.BLOCK).lightLevel { if ((it.getValue(PastureBlock.ON) as Boolean) && (it.getValue(PastureBlock.PART) == PastureBlock.PasturePart.TOP)) 13 else 0 }))
@JvmField
val RED_MINT = create("red_mint", MintBlock(MintType.RED, BlockBehaviour.Properties.of().mapColor(MapColor.COLOR_RED).noCollission().randomTicks().instabreak().sound(CobblemonSounds.MINT_SOUNDS).pushReaction(PushReaction.DESTROY)))
@JvmField
val BLUE_MINT = create("blue_mint", MintBlock(MintType.BLUE, BlockBehaviour.Properties.of().mapColor(MapColor.COLOR_BLUE).noCollission().randomTicks().instabreak().sound(CobblemonSounds.MINT_SOUNDS).pushReaction(PushReaction.DESTROY)))
@@ -869,64 +859,6 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<
@JvmField
val POTTED_SACCHARINE_SAPLING = this.create("potted_saccharine_sapling", BlocksInvoker.createFlowerPotBlock(SACCHARINE_SAPLING))
@JvmField
val RED_MINT_LEAF_LITTER = this.mintLitter("red_mint_leaf_litter", MapColor.COLOR_RED)
@JvmField
val POTTED_RED_MINT = this.pottedPlant("potted_red_mint", RED_MINT_LEAF_LITTER)
@JvmField
val BLUE_MINT_LEAF_LITTER = this.mintLitter("blue_mint_leaf_litter", MapColor.COLOR_BLUE)
@JvmField
val POTTED_BLUE_MINT = this.pottedPlant("potted_blue_mint", BLUE_MINT_LEAF_LITTER)
@JvmField
val CYAN_MINT_LEAF_LITTER = this.mintLitter("cyan_mint_leaf_litter", MapColor.COLOR_CYAN)
@JvmField
val POTTED_CYAN_MINT = this.pottedPlant("potted_cyan_mint", CYAN_MINT_LEAF_LITTER)
@JvmField
val PINK_MINT_LEAF_LITTER = this.mintLitter("pink_mint_leaf_litter", MapColor.COLOR_PINK)
@JvmField
val POTTED_PINK_MINT = this.pottedPlant("potted_pink_mint", PINK_MINT_LEAF_LITTER)
@JvmField
val GREEN_MINT_LEAF_LITTER = this.mintLitter("green_mint_leaf_litter", MapColor.COLOR_GREEN)
@JvmField
val POTTED_GREEN_MINT = this.pottedPlant("potted_green_mint", GREEN_MINT_LEAF_LITTER)
@JvmField
val WHITE_MINT_LEAF_LITTER = this.mintLitter("white_mint_leaf_litter", MapColor.SNOW)
@JvmField
val POTTED_WHITE_MINT = this.pottedPlant("potted_white_mint", WHITE_MINT_LEAF_LITTER)
@JvmField
val LOBAILA = this.create("lobaila", LobailaFlowerBlock(BlockBehaviour.Properties.of().mapColor(MapColor.PLANT).noCollission().instabreak().sound(SoundType.GRASS).offsetType(BlockBehaviour.OffsetType.XZ).ignitedByLava().pushReaction(PushReaction.DESTROY)))
@JvmField
val POMOHANA = this.create("pomohana", PomohanaFlowerBlock(BlockBehaviour.Properties.of().mapColor(MapColor.PLANT).noCollission().instabreak().sound(SoundType.GRASS).ignitedByLava().pushReaction(PushReaction.DESTROY)))
@JvmField
val PAUVILLEA = this.create("pauvillea",PauvilleaFlowerBlock(BlockBehaviour.Properties.of().mapColor(MapColor.PLANT).noCollission().sound(SoundType.GRASS).instabreak().ignitedByLava().pushReaction(PushReaction.DESTROY)))
@JvmField
val WISTENSU = this.create("wistensu", WistensuFlowerHeadBlock(BlockBehaviour.Properties.of().mapColor(MapColor.PLANT).noCollission().sound(SoundType.GRASS).instabreak().ignitedByLava().pushReaction(PushReaction.DESTROY).lightLevel{11}))
@JvmField
val WISTENSU_VINE = this.create("wistensu_vine", WistensuFlowerVineBlock(BlockBehaviour.Properties.of().mapColor(MapColor.PLANT).noCollission().sound(SoundType.GRASS).instabreak().ignitedByLava().pushReaction(PushReaction.DESTROY)))
@JvmField
val ELECTRIC_SEED = this.terrainSeed("electric_seed", TerrainSeedBlock.TerrainSeedType.ELECTRIC)
@JvmField
val PSYCHIC_SEED = this.terrainSeed("psychic_seed", TerrainSeedBlock.TerrainSeedType.PSYCHIC)
@JvmField
val MISTY_SEED = this.terrainSeed("misty_seed", TerrainSeedBlock.TerrainSeedType.MISTY)
@JvmField
val GRASSY_SEED = this.terrainSeed("grassy_seed", TerrainSeedBlock.TerrainSeedType.GRASSY)
@JvmField
val VOLTACONIA = this.terrainPlant("voltaconia") { state -> if (state.getValue(TerrainPlantBlock.GLOWING)) 8 else 0 }
@JvmField
val VERDROOT = this.terrainPlant("verdroot") { state -> if (state.getValue(TerrainPlantBlock.GLOWING)) 5 else 0 }
@JvmField
val LUCIBELL = this.terrainPlant("lucibell") { state -> if (state.getValue(TerrainPlantBlock.GLOWING)) 8 else 0 }
@JvmField
val MISTLEAF = this.create(
"mistleaf",
MistleafFlowerBlock {
if ((it.getValue(MistleafFlowerBlock.GLOWING) >= 1)) it.getValue(MistleafFlowerBlock.GLOWING) + 2 else 0
}
)
@JvmField
val HEARTY_GRAIN_BALE = this.create("hearty_grain_bale", BaleBlock(BlockBehaviour.Properties.of().mapColor(MapColor.PLANT).instrument(NoteBlockInstrument.BANJO).strength(0.5F).sound(CobblemonSounds.HEARTY_GRAIN_BALE_SOUNDS)))
@JvmField
@@ -1103,147 +1035,6 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<
val YACHE_BERRY = this.berryBlock("yache")
//val BINDING_SOIL = this.create("binding_soil", BindingSoilBlock(BlockBehaviour.Properties.of()))
@JvmField
val POTTED_AGUAV_BERRY = this.create("potted_aguav_berry", BlocksInvoker.createFlowerPotBlock(AGUAV_BERRY))
@JvmField
val POTTED_APICOT_BERRY = this.create("potted_apicot_berry", BlocksInvoker.createFlowerPotBlock(APICOT_BERRY))
@JvmField
val POTTED_ASPEAR_BERRY = this.create("potted_aspear_berry", BlocksInvoker.createFlowerPotBlock(ASPEAR_BERRY))
@JvmField
val POTTED_BABIRI_BERRY = this.create("potted_babiri_berry", BlocksInvoker.createFlowerPotBlock(BABIRI_BERRY))
@JvmField
val POTTED_BELUE_BERRY = this.create("potted_belue_berry", BlocksInvoker.createFlowerPotBlock(BELUE_BERRY))
@JvmField
val POTTED_BLUK_BERRY = this.create("potted_bluk_berry", BlocksInvoker.createFlowerPotBlock(BLUK_BERRY))
@JvmField
val POTTED_CHARTI_BERRY = this.create("potted_charti_berry", BlocksInvoker.createFlowerPotBlock(CHARTI_BERRY))
@JvmField
val POTTED_CHERI_BERRY = this.create("potted_cheri_berry", BlocksInvoker.createFlowerPotBlock(CHERI_BERRY))
@JvmField
val POTTED_CHESTO_BERRY = this.create("potted_chesto_berry", BlocksInvoker.createFlowerPotBlock(CHESTO_BERRY))
@JvmField
val POTTED_CHILAN_BERRY = this.create("potted_chilan_berry", BlocksInvoker.createFlowerPotBlock(CHILAN_BERRY))
@JvmField
val POTTED_CHOPLE_BERRY = this.create("potted_chople_berry", BlocksInvoker.createFlowerPotBlock(CHOPLE_BERRY))
@JvmField
val POTTED_COBA_BERRY = this.create("potted_coba_berry", BlocksInvoker.createFlowerPotBlock(COBA_BERRY))
@JvmField
val POTTED_COLBUR_BERRY = this.create("potted_colbur_berry", BlocksInvoker.createFlowerPotBlock(COLBUR_BERRY))
@JvmField
val POTTED_CORNN_BERRY = this.create("potted_cornn_berry", BlocksInvoker.createFlowerPotBlock(CORNN_BERRY))
@JvmField
val POTTED_CUSTAP_BERRY = this.create("potted_custap_berry", BlocksInvoker.createFlowerPotBlock(CUSTAP_BERRY))
@JvmField
val POTTED_DURIN_BERRY = this.create("potted_durin_berry", BlocksInvoker.createFlowerPotBlock(DURIN_BERRY))
@JvmField
val POTTED_EGGANT_BERRY = this.create("potted_eggant_berry", BlocksInvoker.createFlowerPotBlock(EGGANT_BERRY))
@JvmField
val POTTED_ENIGMA_BERRY = this.create("potted_enigma_berry", BlocksInvoker.createFlowerPotBlock(ENIGMA_BERRY))
@JvmField
val POTTED_FIGY_BERRY = this.create("potted_figy_berry", BlocksInvoker.createFlowerPotBlock(FIGY_BERRY))
@JvmField
val POTTED_GANLON_BERRY = this.create("potted_ganlon_berry", BlocksInvoker.createFlowerPotBlock(GANLON_BERRY))
@JvmField
val POTTED_GREPA_BERRY = this.create("potted_grepa_berry", BlocksInvoker.createFlowerPotBlock(GREPA_BERRY))
@JvmField
val POTTED_HABAN_BERRY = this.create("potted_haban_berry", BlocksInvoker.createFlowerPotBlock(HABAN_BERRY))
@JvmField
val POTTED_HONDEW_BERRY = this.create("potted_hondew_berry", BlocksInvoker.createFlowerPotBlock(HONDEW_BERRY))
@JvmField
val POTTED_HOPO_BERRY = this.create("potted_hopo_berry", BlocksInvoker.createFlowerPotBlock(HOPO_BERRY))
@JvmField
val POTTED_IAPAPA_BERRY = this.create("potted_iapapa_berry", BlocksInvoker.createFlowerPotBlock(IAPAPA_BERRY))
@JvmField
val POTTED_JABOCA_BERRY = this.create("potted_jaboca_berry", BlocksInvoker.createFlowerPotBlock(JABOCA_BERRY))
@JvmField
val POTTED_KASIB_BERRY = this.create("potted_kasib_berry", BlocksInvoker.createFlowerPotBlock(KASIB_BERRY))
@JvmField
val POTTED_KEBIA_BERRY = this.create("potted_kebia_berry", BlocksInvoker.createFlowerPotBlock(KEBIA_BERRY))
@JvmField
val POTTED_KEE_BERRY = this.create("potted_kee_berry", BlocksInvoker.createFlowerPotBlock(KEE_BERRY))
@JvmField
val POTTED_KELPSY_BERRY = this.create("potted_kelpsy_berry", BlocksInvoker.createFlowerPotBlock(KELPSY_BERRY))
@JvmField
val POTTED_LANSAT_BERRY = this.create("potted_lansat_berry", BlocksInvoker.createFlowerPotBlock(LANSAT_BERRY))
@JvmField
val POTTED_LEPPA_BERRY = this.create("potted_leppa_berry", BlocksInvoker.createFlowerPotBlock(LEPPA_BERRY))
@JvmField
val POTTED_LIECHI_BERRY = this.create("potted_liechi_berry", BlocksInvoker.createFlowerPotBlock(LIECHI_BERRY))
@JvmField
val POTTED_LUM_BERRY = this.create("potted_lum_berry", BlocksInvoker.createFlowerPotBlock(LUM_BERRY))
@JvmField
val POTTED_MAGO_BERRY = this.create("potted_mago_berry", BlocksInvoker.createFlowerPotBlock(MAGO_BERRY))
@JvmField
val POTTED_MAGOST_BERRY = this.create("potted_magost_berry", BlocksInvoker.createFlowerPotBlock(MAGOST_BERRY))
@JvmField
val POTTED_MARANGA_BERRY = this.create("potted_maranga_berry", BlocksInvoker.createFlowerPotBlock(MARANGA_BERRY))
@JvmField
val POTTED_MICLE_BERRY = this.create("potted_micle_berry", BlocksInvoker.createFlowerPotBlock(MICLE_BERRY))
@JvmField
val POTTED_NANAB_BERRY = this.create("potted_nanab_berry", BlocksInvoker.createFlowerPotBlock(NANAB_BERRY))
@JvmField
val POTTED_NOMEL_BERRY = this.create("potted_nomel_berry", BlocksInvoker.createFlowerPotBlock(NOMEL_BERRY))
@JvmField
val POTTED_OCCA_BERRY = this.create("potted_occa_berry", BlocksInvoker.createFlowerPotBlock(OCCA_BERRY))
@JvmField
val POTTED_ORAN_BERRY = this.create("potted_oran_berry", BlocksInvoker.createFlowerPotBlock(ORAN_BERRY))
@JvmField
val POTTED_PAMTRE_BERRY = this.create("potted_pamtre_berry", BlocksInvoker.createFlowerPotBlock(PAMTRE_BERRY))
@JvmField
val POTTED_PASSHO_BERRY = this.create("potted_passho_berry", BlocksInvoker.createFlowerPotBlock(PASSHO_BERRY))
@JvmField
val POTTED_PAYAPA_BERRY = this.create("potted_payapa_berry", BlocksInvoker.createFlowerPotBlock(PAYAPA_BERRY))
@JvmField
val POTTED_PECHA_BERRY = this.create("potted_pecha_berry", BlocksInvoker.createFlowerPotBlock(PECHA_BERRY))
@JvmField
val POTTED_PERSIM_BERRY = this.create("potted_persim_berry", BlocksInvoker.createFlowerPotBlock(PERSIM_BERRY))
@JvmField
val POTTED_PETAYA_BERRY = this.create("potted_petaya_berry", BlocksInvoker.createFlowerPotBlock(PETAYA_BERRY))
@JvmField
val POTTED_PINAP_BERRY = this.create("potted_pinap_berry", BlocksInvoker.createFlowerPotBlock(PINAP_BERRY))
@JvmField
val POTTED_POMEG_BERRY = this.create("potted_pomeg_berry", BlocksInvoker.createFlowerPotBlock(POMEG_BERRY))
@JvmField
val POTTED_QUALOT_BERRY = this.create("potted_qualot_berry", BlocksInvoker.createFlowerPotBlock(QUALOT_BERRY))
@JvmField
val POTTED_RABUTA_BERRY = this.create("potted_rabuta_berry", BlocksInvoker.createFlowerPotBlock(RABUTA_BERRY))
@JvmField
val POTTED_RAWST_BERRY = this.create("potted_rawst_berry", BlocksInvoker.createFlowerPotBlock(RAWST_BERRY))
@JvmField
val POTTED_RAZZ_BERRY = this.create("potted_razz_berry", BlocksInvoker.createFlowerPotBlock(RAZZ_BERRY))
@JvmField
val POTTED_RINDO_BERRY = this.create("potted_rindo_berry", BlocksInvoker.createFlowerPotBlock(RINDO_BERRY))
@JvmField
val POTTED_ROSELI_BERRY = this.create("potted_roseli_berry", BlocksInvoker.createFlowerPotBlock(ROSELI_BERRY))
@JvmField
val POTTED_ROWAP_BERRY = this.create("potted_rowap_berry", BlocksInvoker.createFlowerPotBlock(ROWAP_BERRY))
@JvmField
val POTTED_SALAC_BERRY = this.create("potted_salac_berry", BlocksInvoker.createFlowerPotBlock(SALAC_BERRY))
@JvmField
val POTTED_SHUCA_BERRY = this.create("potted_shuca_berry", BlocksInvoker.createFlowerPotBlock(SHUCA_BERRY))
@JvmField
val POTTED_SITRUS_BERRY = this.create("potted_sitrus_berry", BlocksInvoker.createFlowerPotBlock(SITRUS_BERRY))
@JvmField
val POTTED_SPELON_BERRY = this.create("potted_spelon_berry", BlocksInvoker.createFlowerPotBlock(SPELON_BERRY))
@JvmField
val POTTED_STARF_BERRY = this.create("potted_starf_berry", BlocksInvoker.createFlowerPotBlock(STARF_BERRY))
@JvmField
val POTTED_TAMATO_BERRY = this.create("potted_tamato_berry", BlocksInvoker.createFlowerPotBlock(TAMATO_BERRY))
@JvmField
val POTTED_TANGA_BERRY = this.create("potted_tanga_berry", BlocksInvoker.createFlowerPotBlock(TANGA_BERRY))
@JvmField
val POTTED_TOUGA_BERRY = this.create("potted_touga_berry", BlocksInvoker.createFlowerPotBlock(TOUGA_BERRY))
@JvmField
val POTTED_WACAN_BERRY = this.create("potted_wacan_berry", BlocksInvoker.createFlowerPotBlock(WACAN_BERRY))
@JvmField
val POTTED_WATMEL_BERRY = this.create("potted_watmel_berry", BlocksInvoker.createFlowerPotBlock(WATMEL_BERRY))
@JvmField
val POTTED_WEPEAR_BERRY = this.create("potted_wepear_berry", BlocksInvoker.createFlowerPotBlock(WEPEAR_BERRY))
@JvmField
val POTTED_WIKI_BERRY = this.create("potted_wiki_berry", BlocksInvoker.createFlowerPotBlock(WIKI_BERRY))
@JvmField
val POTTED_YACHE_BERRY = this.create("potted_yache_berry", BlocksInvoker.createFlowerPotBlock(YACHE_BERRY))
init {
/**
* Makes all blocks in array flammable by adding them to FireBlock's flammableRegistry.
@@ -1345,48 +1136,4 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<
}
fun typeGemClusters() = typeGemClusters.toMap()
private fun pottedPlant(name: String, flower: Block) = this.create(name, BlocksInvoker.createFlowerPotBlock(flower))
private fun mintLitter(name: String, color: MapColor) = this.create(
name,
MintLitterBlock(
BlockBehaviour.Properties.of()
.sound(CobblemonSounds.MINT_SOUNDS)
.mapColor(color)
.noOcclusion()
.noCollission()
.pushReaction(PushReaction.DESTROY)
)
)
private fun terrainSeed(name: String, type: TerrainSeedBlock.TerrainSeedType) = this.create(
name,
TerrainSeedBlock(
type,
BlockBehaviour.Properties.of()
.pushReaction(PushReaction.DESTROY)
.ignitedByLava()
.mapColor(MapColor.PLANT)
.offsetType(BlockBehaviour.OffsetType.XZ)
.noCollission()
.randomTicks()
.instabreak()
)
)
private fun terrainPlant(name: String, lightLevel: (BlockState) -> Int) = this.create(
name,
TerrainPlantBlock(
BlockBehaviour.Properties.of()
.mapColor(MapColor.PLANT)
.noCollission()
.instabreak()
.sound(SoundType.GRASS)
.offsetType(BlockBehaviour.OffsetType.XZ)
.ignitedByLava()
.pushReaction(PushReaction.DESTROY)
.lightLevel(lightLevel)
)
)
}
@@ -77,5 +77,6 @@ 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)
}
}
@@ -12,7 +12,6 @@ import com.cobblemon.mod.common.api.net.NetworkPacket
import com.mojang.brigadier.arguments.ArgumentType
import kotlin.reflect.KClass
import net.minecraft.commands.synchronization.ArgumentTypeInfo
import net.minecraft.core.HolderLookup
import net.minecraft.resources.ResourceKey
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.MinecraftServer
@@ -169,16 +168,12 @@ interface CobblemonImplementation {
* TODO
*
* @param identifier
* @param reloaderFactory
* @param reloader
* @param type
* @param dependencies
*/
fun registerResourceReloader(
identifier: ResourceLocation,
type: PackType,
dependencies: Collection<ResourceLocation>,
reloaderFactory: (HolderLookup.Provider) -> PreparableReloadListener
)
fun registerResourceReloader(identifier: ResourceLocation, reloader: PreparableReloadListener, type: PackType, dependencies: Collection<ResourceLocation>)
/**
* TODO
*
@@ -465,6 +465,8 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
val PASTURE = blockItem("pasture", CobblemonBlocks.PASTURE)
@JvmField
val DISPLAY_CASE = blockItem("display_case", CobblemonBlocks.DISPLAY_CASE)
@JvmField
val VENDING_MACHINE = blockItem("vending_machine", CobblemonBlocks.VENDING_MACHINE)
// @JvmField
// val INCENSE_SWEET = blockItem("incense_sweet", CobblemonBlocks.INCENSE_SWEET) // TODO after 1.7
@@ -477,8 +479,8 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField val DUBIOUS_DISC = itemNameBlockItem("dubious_disc", CobblemonBlocks.DUBIOUS_DISC)
@JvmField val DEEP_SEA_SCALE = noSettingsItem("deep_sea_scale")
@JvmField val DEEP_SEA_TOOTH = noSettingsItem("deep_sea_tooth")
@JvmField val ELECTIRIZER = itemNameBlockItem("electirizer", CobblemonBlocks.ELECTIRIZER)
@JvmField val MAGMARIZER = itemNameBlockItem("magmarizer", CobblemonBlocks.MAGMARIZER)
@JvmField val ELECTIRIZER = noSettingsItem("electirizer")
@JvmField val MAGMARIZER = noSettingsItem("magmarizer")
@JvmField val OVAL_STONE = noSettingsItem("oval_stone")
@JvmField val PROTECTOR = noSettingsItem("protector")
@JvmField val REAPER_CLOTH = noSettingsItem("reaper_cloth")
@@ -711,7 +713,7 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val PEP_UP_FLOWER = compostableBlockItem("pep_up_flower", CobblemonBlocks.PEP_UP_FLOWER)
@JvmField
val MEDICINAL_BREW = itemNameBlockItem("medicinal_brew", CobblemonBlocks.MEDICINAL_BREW)
val MEDICINAL_BREW = noSettingsItem("medicinal_brew")
@JvmField
val REMEDY = compostableItem("remedy", RemedyItem(RemedyItem.NORMAL), 0.65f)
@JvmField
@@ -719,7 +721,7 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val SUPERB_REMEDY = compostableItem("superb_remedy", RemedyItem(RemedyItem.SUPERB), 1f)
@JvmField
val MOOMOO_MILK = create("moomoo_milk", MoomooMilk(Item.Properties().craftRemainder(Items.GLASS_BOTTLE).stacksTo((16)), CobblemonBlocks.MOOMOO_MILK))
val MOOMOO_MILK = create("moomoo_milk", MoomooMilk(Item.Properties().craftRemainder(Items.GLASS_BOTTLE).stacksTo((16))))
@JvmField
val POTION = create("potion", PotionItem(PotionType.POTION, CobblemonBlocks.POTION))
@@ -748,27 +750,27 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val RED_MINT_SEEDS = mintSeed("red", MintType.RED.getCropBlock())
@JvmField
val RED_MINT_LEAF = compostableItemNameBlockItem("red_mint_leaf", CobblemonBlocks.RED_MINT_LEAF_LITTER, 0.50f)
val RED_MINT_LEAF = mintLeaf("red", MintLeafItem(MintType.RED))
@JvmField
val BLUE_MINT_SEEDS = mintSeed("blue", MintType.BLUE.getCropBlock())
@JvmField
val BLUE_MINT_LEAF = compostableItemNameBlockItem("blue_mint_leaf", CobblemonBlocks.BLUE_MINT_LEAF_LITTER, 0.50f)
val BLUE_MINT_LEAF = mintLeaf("blue", MintLeafItem(MintType.BLUE))
@JvmField
val CYAN_MINT_SEEDS = mintSeed("cyan", MintType.CYAN.getCropBlock())
@JvmField
val CYAN_MINT_LEAF = compostableItemNameBlockItem("cyan_mint_leaf", CobblemonBlocks.CYAN_MINT_LEAF_LITTER, 0.50f)
val CYAN_MINT_LEAF = mintLeaf("cyan", MintLeafItem(MintType.CYAN))
@JvmField
val PINK_MINT_SEEDS = mintSeed("pink", MintType.PINK.getCropBlock())
@JvmField
val PINK_MINT_LEAF = compostableItemNameBlockItem("pink_mint_leaf", CobblemonBlocks.PINK_MINT_LEAF_LITTER, 0.50f)
val PINK_MINT_LEAF = mintLeaf("pink", MintLeafItem(MintType.PINK))
@JvmField
val GREEN_MINT_SEEDS = mintSeed("green", MintType.GREEN.getCropBlock())
@JvmField
val GREEN_MINT_LEAF = compostableItemNameBlockItem("green_mint_leaf", CobblemonBlocks.GREEN_MINT_LEAF_LITTER, 0.50f)
val GREEN_MINT_LEAF = mintLeaf("green", MintLeafItem(MintType.GREEN))
@JvmField
val WHITE_MINT_SEEDS = mintSeed("white", MintType.WHITE.getCropBlock())
@JvmField
val WHITE_MINT_LEAF = compostableItemNameBlockItem("white_mint_leaf", CobblemonBlocks.WHITE_MINT_LEAF_LITTER, 0.50f)
val WHITE_MINT_LEAF = mintLeaf("white", MintLeafItem(MintType.WHITE))
val mints = mutableMapOf<String, MintItem>()
@@ -969,7 +971,7 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val EJECT_PACK = heldItem("eject_pack")
@JvmField
val ELECTRIC_SEED = itemNameBlockItem("electric_seed", CobblemonBlocks.ELECTRIC_SEED)
val ELECTRIC_SEED = heldItem("electric_seed")
@JvmField
val EVERSTONE = heldItem("everstone")
@JvmField
@@ -985,7 +987,7 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val FOCUS_SASH = heldItem("focus_sash")
@JvmField
val GRASSY_SEED = itemNameBlockItem("grassy_seed", CobblemonBlocks.GRASSY_SEED)
val GRASSY_SEED = heldItem("grassy_seed")
@JvmField
val GRIP_CLAW = heldItem("grip_claw")
@JvmField
@@ -1033,7 +1035,7 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val POWER_WEIGHT = heldItem("power_weight")
@JvmField
val PSYCHIC_SEED = itemNameBlockItem("psychic_seed", CobblemonBlocks.PSYCHIC_SEED)
val PSYCHIC_SEED = heldItem("psychic_seed")
@JvmField
val PROTECTIVE_PADS = heldItem("protective_pads")
@JvmField
@@ -1057,7 +1059,7 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val SILVER_POWDER = heldItem("silver_powder")
@JvmField
val SOFT_SAND = itemNameBlockItem("soft_sand", CobblemonBlocks.SOFT_SAND)
val SOFT_SAND = heldItem("soft_sand")
@JvmField
val SMOKE_BALL = itemNameBlockItem("smoke_ball", CobblemonBlocks.SMOKE_BALL)
@JvmField
@@ -1065,7 +1067,7 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val STICKY_BARB = heldItem("sticky_barb")
@JvmField
val TERRAIN_EXTENDER = itemNameBlockItem("terrain_extender", CobblemonBlocks.TERRAIN_EXTENDER)
val TERRAIN_EXTENDER = heldItem("terrain_extender")
@JvmField
val THROAT_SPRAY = itemNameBlockItem("throat_spray", CobblemonBlocks.THROAT_SPRAY)
@JvmField
@@ -1085,13 +1087,13 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val MIRROR_HERB = compostableHeldItem("mirror_herb", null, 0.85F)
@JvmField
val MISTY_SEED = itemNameBlockItem("misty_seed", CobblemonBlocks.MISTY_SEED)
val MISTY_SEED = heldItem("misty_seed")
@JvmField
val POWER_HERB = compostableHeldItem("power_herb", null, 0.85F)
@JvmField
val WHITE_HERB = compostableHeldItem("white_herb", null, 0.85F)
@JvmField
val BRIGHT_POWDER = glowItem("bright_powder", CobblemonBlocks.BRIGHT_POWDER)
val BRIGHT_POWDER = itemNameBlockItem("bright_powder", CobblemonBlocks.BRIGHT_POWDER)
@JvmField
val METAL_POWDER = itemNameBlockItem("metal_powder", CobblemonBlocks.METAL_POWDER)
@JvmField
@@ -1103,7 +1105,7 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val SMOOTH_ROCK = itemNameBlockItem("smooth_rock", CobblemonBlocks.SMOOTH_ROCK)
@JvmField
val ICY_ROCK = heldItem("icy_rock")
val ICY_ROCK = itemNameBlockItem("icy_rock", CobblemonBlocks.ICY_ROCK)
// Mulch
@JvmField
@@ -1558,25 +1560,6 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
//@JvmField
//val BINDING_SOIL = blockItem("binding_soil", CobblemonBlocks.BINDING_SOIL)
// Plants, Flora, and Flowers
@JvmField
val LOBAILA = compostableBlockItem("lobaila", CobblemonBlocks.LOBAILA, 0.65f)
@JvmField
val POMOHANA = compostableBlockItem("pomohana", CobblemonBlocks.POMOHANA, 0.3f)
@JvmField
val PAUVILLEA = compostableBlockItem("pauvillea", CobblemonBlocks.PAUVILLEA, 0.65f)
@JvmField
val WISTENSU = compostableBlockItem("wistensu", CobblemonBlocks.WISTENSU, 0.5f)
@JvmField
val VOLTACONIA = compostableBlockItem("voltaconia", CobblemonBlocks.VOLTACONIA, 0.85f)
@JvmField
val VERDROOT = compostableBlockItem("verdroot", CobblemonBlocks.VERDROOT, 0.85f)
@JvmField
val MISTLEAF = compostableBlockItem("mistleaf", CobblemonBlocks.MISTLEAF, 0.85f)
@JvmField
val LUCIBELL = compostableBlockItem("lucibell", CobblemonBlocks.LUCIBELL, 0.85f)
private fun blockItem(name: String, block: Block, rarity: Rarity = Rarity.COMMON): BlockItem = this.create(name, BlockItem(block, Item.Properties().rarity(rarity)))
private fun itemNameBlockItem(name: String, block: Block, rarity: Rarity = Rarity.COMMON): ItemNameBlockItem = this.create(name, ItemNameBlockItem(block, Item.Properties().rarity(rarity)))
@@ -1779,6 +1762,4 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
private fun foodItem(nutrition: Int, saturationModifier: Float): Item {
return CobblemonItem(Item.Properties().food(FoodProperties.Builder().nutrition(nutrition).saturationModifier(saturationModifier).build()))
}
private fun glowItem(name: String, block: Block): GlowItem = this.create(name, GlowItem(block, Item.Properties()))
}
@@ -10,6 +10,7 @@ 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
@@ -22,9 +23,10 @@ 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,6 +32,10 @@ import com.cobblemon.mod.common.client.net.effect.SpawnSnowstormParticleHandler
import com.cobblemon.mod.common.client.net.gui.ExpGainedDataPacketHandler
import com.cobblemon.mod.common.client.net.gui.InteractPokemonUIPacketHandler
import com.cobblemon.mod.common.client.net.gui.PokedexUIPacketHandler
import com.cobblemon.mod.common.client.net.molangui.MolangUiCloseHandler
import com.cobblemon.mod.common.client.net.molangui.MolangUiCompatibilityRequestHandler
import com.cobblemon.mod.common.client.net.molangui.MolangUiOpenHandler
import com.cobblemon.mod.common.client.net.molangui.MolangUiUpdateHandler
import com.cobblemon.mod.common.client.net.gui.SetActiveTMPacketHandler
import com.cobblemon.mod.common.client.net.gui.SetTMMachineContainerDataPacketHandler
import com.cobblemon.mod.common.client.net.gui.SummaryUIPacketHandler
@@ -83,6 +87,10 @@ 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
@@ -180,6 +188,9 @@ 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
@@ -239,6 +250,9 @@ 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
@@ -425,7 +439,6 @@ object CobblemonNetwork {
list.add(PacketRegisterInfo(MovesRegistrySyncPacket.ID, MovesRegistrySyncPacket::decode, DataRegistrySyncPacketHandler()))
list.add(PacketRegisterInfo(BerryRegistrySyncPacket.ID, BerryRegistrySyncPacket::decode, DataRegistrySyncPacketHandler()))
list.add(PacketRegisterInfo(SpeciesRegistrySyncPacket.ID, SpeciesRegistrySyncPacket::decode, DataRegistrySyncPacketHandler()))
list.add(PacketRegisterInfo(SpeciesEvolutionSyncPacket.ID, SpeciesEvolutionSyncPacket::decode, DataRegistrySyncPacketHandler()))
list.add(PacketRegisterInfo(PropertiesCompletionRegistrySyncPacket.ID, PropertiesCompletionRegistrySyncPacket::decode, DataRegistrySyncPacketHandler()))
list.add(PacketRegisterInfo(StandardSpeciesFeatureSyncPacket.ID, StandardSpeciesFeatureSyncPacket::decode, DataRegistrySyncPacketHandler()))
list.add(PacketRegisterInfo(GlobalSpeciesFeatureSyncPacket.ID, GlobalSpeciesFeatureSyncPacket::decode, DataRegistrySyncPacketHandler()))
@@ -446,6 +459,11 @@ object CobblemonNetwork {
list.add(PacketRegisterInfo(RideSettingsSyncPacket.ID, RideSettingsSyncPacket::decode, RideSettingsSyncHandler))
list.add(PacketRegisterInfo(TechnicalMachineRegistrySyncPacket.ID, TechnicalMachineRegistrySyncPacket::decode, DataRegistrySyncPacketHandler()))
list.add(PacketRegisterInfo(MolangUiOpenPacket.ID, MolangUiOpenPacket::decode, MolangUiOpenHandler))
list.add(PacketRegisterInfo(MolangUiClosePacket.ID, MolangUiClosePacket::decode, MolangUiCloseHandler))
list.add(PacketRegisterInfo(MolangUiUpdatePacket.ID, MolangUiUpdatePacket::decode, MolangUiUpdateHandler))
list.add(PacketRegisterInfo(MolangUiCompatibilityRequestPacket.ID, MolangUiCompatibilityRequestPacket::decode, MolangUiCompatibilityRequestHandler))
// Effects
list.add(PacketRegisterInfo(SpawnSnowstormParticlePacket.ID, SpawnSnowstormParticlePacket::decode, SpawnSnowstormParticleHandler))
list.add(PacketRegisterInfo(SpawnSnowstormEntityParticlePacket.ID, SpawnSnowstormEntityParticlePacket::decode, SpawnSnowstormEntityParticleHandler))
@@ -644,6 +662,10 @@ 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))
@@ -657,4 +679,4 @@ object CobblemonNetwork {
fun sendPacketToPlayer(player: ServerPlayer, packet: NetworkPacket<*>) {
Cobblemon.implementation.networkManager.sendPacketToPlayer(player, packet)
}
}
}
@@ -148,9 +148,6 @@ object CobblemonSounds : PlatformRegistry<Registry<SoundEvent>, ResourceKey<Regi
@JvmField
val MONITOR_GLITCHING = this.create("block.monitor.glitching")
@JvmField
val BRIGHT_POWDER_USE = this.create("block.bright_powder.use")
@JvmField
val FOSSIL_MACHINE_ACTIVATE = this.create("block.fossil_machine.activate")
@JvmField
@@ -660,65 +657,5 @@ object CobblemonSounds : PlatformRegistry<Registry<SoundEvent>, ResourceKey<Regi
SoundEvents.WOOL_FALL
)
@JvmField
val ITEM_BLOCK_POWDER_PLACE = this.create("block.item_block.powder.place")
@JvmField
val ITEM_BLOCK_POWDER_BREAK = this.create("block.item_block.powder.break")
@JvmField
val ITEM_BLOCK_POWDER_STEP = this.create("block.item_block.powder.step")
@JvmField
val ITEM_BLOCK_POWDER_SOUNDS = SoundType(1f, 1.1f,
ITEM_BLOCK_POWDER_BREAK,
ITEM_BLOCK_POWDER_STEP,
ITEM_BLOCK_POWDER_PLACE,
ITEM_BLOCK_POWDER_STEP,
ITEM_BLOCK_POWDER_STEP
)
@JvmField
val ITEM_BLOCK_BOTTLE_LIQUID_PLACE = this.create("block.item_block.bottle_liquid.place")
@JvmField
val ITEM_BLOCK_BOTTLE_LIQUID_BREAK = this.create("block.item_block.bottle_liquid.break")
@JvmField
val ITEM_BLOCK_BOTTLE_LIQUID_STEP = this.create("block.item_block.bottle_liquid.step")
@JvmField
val ITEM_BLOCK_BOTTLE_LIQUID_SOUNDS = SoundType(1f, 1.1f,
ITEM_BLOCK_BOTTLE_LIQUID_BREAK,
ITEM_BLOCK_BOTTLE_LIQUID_STEP,
ITEM_BLOCK_BOTTLE_LIQUID_PLACE,
ITEM_BLOCK_BOTTLE_LIQUID_PLACE,
ITEM_BLOCK_BOTTLE_LIQUID_PLACE
)
@JvmField
val ITEM_BLOCK_BOTTLE_PILLS_PLACE = this.create("block.item_block.bottle_pills.place")
@JvmField
val ITEM_BLOCK_BOTTLE_PILLS_BREAK = this.create("block.item_block.bottle_pills.break")
@JvmField
val ITEM_BLOCK_BOTTLE_PILLS_STEP = this.create("block.item_block.bottle_pills.step")
@JvmField
val ITEM_BLOCK_BOTTLE_PILLS_SOUNDS = SoundType(1f, 1.1f,
ITEM_BLOCK_BOTTLE_PILLS_BREAK,
ITEM_BLOCK_BOTTLE_PILLS_STEP,
ITEM_BLOCK_BOTTLE_PILLS_PLACE,
ITEM_BLOCK_BOTTLE_PILLS_PLACE,
ITEM_BLOCK_BOTTLE_PILLS_PLACE
)
@JvmField
val ITEM_BLOCK_DICE_PLACE = this.create("block.item_block.dice.place")
@JvmField
val ITEM_BLOCK_DICE_BREAK = this.create("block.item_block.dice.break")
@JvmField
val ITEM_BLOCK_DICE_STEP = this.create("block.item_block.dice.step")
@JvmField
val ITEM_BLOCK_DICE_SOUNDS = SoundType(1f, 1.1f,
ITEM_BLOCK_DICE_BREAK,
ITEM_BLOCK_DICE_STEP,
ITEM_BLOCK_DICE_PLACE,
ITEM_BLOCK_DICE_STEP,
ITEM_BLOCK_DICE_STEP
)
private fun create(name: String): SoundEvent = this.create(name, SoundEvent.createVariableRangeEvent(cobblemonResource(name)))
}
@@ -11,9 +11,6 @@ package com.cobblemon.mod.common.api.conditional
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import com.google.gson.JsonPrimitive
import com.google.gson.JsonSerializationContext
import com.google.gson.JsonSerializer
import java.lang.reflect.Type
/**
@@ -24,19 +21,11 @@ import java.lang.reflect.Type
* @author Hiroku
* @since July 17th, 2022
*/
interface RegistryLikeAdapter<B> : JsonDeserializer<RegistryLikeCondition<B>>, JsonSerializer<RegistryLikeCondition<B>> {
interface RegistryLikeAdapter<B> : JsonDeserializer<RegistryLikeCondition<B>> {
val registryLikeConditions: MutableList<(JsonElement) -> RegistryLikeCondition<B>?>
override fun deserialize(json: JsonElement, type: Type, ctx: JsonDeserializationContext): RegistryLikeCondition<B> {
return registryLikeConditions.firstNotNullOfOrNull { it(json) }
?: throw IllegalArgumentException("Unable to deserialize $json")
}
override fun serialize(src: RegistryLikeCondition<B>, typeOfSrc: Type, ctx: JsonSerializationContext): JsonElement {
return when (src) {
is RegistryLikeTagCondition<B> -> JsonPrimitive(RegistryLikeTagCondition.PREFIX + src.tag.location())
is RegistryLikeIdentifierCondition<B> -> JsonPrimitive(src.identifier.toString())
else -> throw IllegalArgumentException("Unable to serialize $src")
}
}
}
@@ -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.data
import com.cobblemon.mod.common.util.endsWith
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import net.minecraft.core.HolderLookup
import java.io.BufferedReader
import java.io.File
import java.util.concurrent.ExecutionException
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.packs.PackType
import net.minecraft.server.packs.resources.ResourceManager
/**
* A [DataRegistry] that consumes JSON files.
* Every deserialized instance is attached to an [Identifier].
* For example a file under data/mymod/[resourcePath]/entry.json would be backed by the identifier modid:entry.
*
* @param T The type of the data consumed by this registry.
*
* @author Licious
* @since August 5th, 2022
*/
interface ServerJsonDataRegistry<T> : DataRegistry {
/**
* The [TypeToken] of type [T].
*/
val typeToken: TypeToken<T>
/**
* The folder location for the data this registry will consume.
*/
val resourcePath: String
override val type: PackType
get() = PackType.SERVER_DATA
override fun reload(manager: ResourceManager) {
//NO-OP, handled by reload(manager, registry)
}
fun reload(manager: ResourceManager, registry: HolderLookup.Provider) {
val gson = setupParser(registry)
val data = hashMapOf<ResourceLocation, T>()
manager.listResources(resourcePath) { path -> path.endsWith(JSON_EXTENSION) }.forEach { (identifier, resource) ->
if (identifier.namespace == "pixelmon") {
return@forEach
}
resource.open().use { stream ->
stream.bufferedReader().use { reader ->
val resolvedIdentifier = ResourceLocation.fromNamespaceAndPath(identifier.namespace, File(identifier.path).nameWithoutExtension)
data[resolvedIdentifier] = parse(reader, resolvedIdentifier, gson)
}
}
}
reload(data, registry)
}
fun parse(reader: BufferedReader, identifier: ResourceLocation, gson: Gson): T {
return try {
gson.fromJson(reader, typeToken.type)
} catch (exception: Exception) {
throw ExecutionException("Error loading JSON for data: $identifier", exception)
}
}
/**
* Allows the creation of [Gson] instances which have adapters that are registry-aware, e.g. [com.cobblemon.mod.common.pokemon.evolution.adapters.LegacyItemConditionWrapperAdapter]
*/
fun setupParser(registry: HolderLookup.Provider): Gson
/**
* Reloads this registry from the deserialized data.
*
* @param data A map of the data associating an instance to the respective identifier from the [ResourceManager].
*/
fun reload(data: Map<ResourceLocation, T>, registry: HolderLookup.Provider)
companion object {
const val JSON_EXTENSION = ".json"
}
}
@@ -9,14 +9,12 @@
package com.cobblemon.mod.common.api.events.pokemon
import com.bedrockk.molang.runtime.value.DoubleValue
import com.bedrockk.molang.runtime.value.MoValue
import com.cobblemon.mod.common.api.events.Cancelable
import com.cobblemon.mod.common.api.molang.MoLangFunctions.asMoLangValue
import com.cobblemon.mod.common.api.molang.MoLangFunctions.moLangFunctionMap
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.trade.TradeParticipant
import com.cobblemon.mod.common.util.asArrayValue
import com.cobblemon.mod.common.util.getPlayer
/**
@@ -33,9 +31,9 @@ interface TradeEvent {
val tradeParticipant1: TradeParticipant
/**
* All the [Pokemon] the first participant receives in the trade.
* The [Pokemon] being traded by the first participant.
*/
val tradeParticipant1Pokemon: List<Pokemon>
val tradeParticipant1Pokemon: Pokemon
/**
* The second [TradeParticipant].
@@ -43,19 +41,9 @@ interface TradeEvent {
val tradeParticipant2: TradeParticipant
/**
* All the [Pokemon] the second participant receives in the trade.
* The [Pokemon] being traded by the second participant.
*/
val tradeParticipant2Pokemon: List<Pokemon>
fun createContext(): MutableMap<String, MoValue> = mutableMapOf(
"trade_participant_1" to (tradeParticipant1.uuid.getPlayer()?.asMoLangValue() ?: DoubleValue.ZERO),
"trade_participant_2" to (tradeParticipant2.uuid.getPlayer()?.asMoLangValue() ?: DoubleValue.ZERO),
// Kept for backwards compatibility, this is the first Pokémon of each side.
"trade_participant_1_pokemon" to tradeParticipant1Pokemon.first().struct,
"trade_participant_2_pokemon" to tradeParticipant2Pokemon.first().struct,
"trade_participant_1_pokemon_list" to tradeParticipant1Pokemon.asArrayValue { it.struct },
"trade_participant_2_pokemon_list" to tradeParticipant2Pokemon.asArrayValue { it.struct }
)
val tradeParticipant2Pokemon: Pokemon
/**
* Event fired when a trade is about to happen. Cancelling this event prevents the trade from occurring.
@@ -65,11 +53,16 @@ interface TradeEvent {
*/
data class Pre(
override val tradeParticipant1: TradeParticipant,
override val tradeParticipant1Pokemon: List<Pokemon>,
override val tradeParticipant1Pokemon: Pokemon,
override val tradeParticipant2: TradeParticipant,
override val tradeParticipant2Pokemon: List<Pokemon>
override val tradeParticipant2Pokemon: Pokemon
) : TradeEvent, Cancelable() {
val context = createContext()
val context = mutableMapOf(
"trade_participant_1" to (tradeParticipant1.uuid.getPlayer()?.asMoLangValue() ?: DoubleValue.ZERO),
"trade_participant_2" to (tradeParticipant2.uuid.getPlayer()?.asMoLangValue() ?: DoubleValue.ZERO),
"trade_participant_1_pokemon" to tradeParticipant1Pokemon.struct,
"trade_participant_2_pokemon" to tradeParticipant2Pokemon.struct
)
val functions = moLangFunctionMap(
cancelFunc
@@ -84,10 +77,15 @@ interface TradeEvent {
*/
data class Post(
override val tradeParticipant1: TradeParticipant,
override val tradeParticipant1Pokemon: List<Pokemon>,
override val tradeParticipant1Pokemon: Pokemon,
override val tradeParticipant2: TradeParticipant,
override val tradeParticipant2Pokemon: List<Pokemon>
override val tradeParticipant2Pokemon: Pokemon
) : TradeEvent {
val context = createContext()
val context = mutableMapOf(
"trade_participant_1" to (tradeParticipant1.uuid.getPlayer()?.asMoLangValue() ?: DoubleValue.ZERO),
"trade_participant_2" to (tradeParticipant2.uuid.getPlayer()?.asMoLangValue() ?: DoubleValue.ZERO),
"trade_participant_1_pokemon" to tradeParticipant1Pokemon.struct,
"trade_participant_2_pokemon" to tradeParticipant2Pokemon.struct
)
}
}
@@ -40,7 +40,7 @@ object Fossils: JsonDataRegistry<Fossil> {
.registerTypeAdapter(ResourceLocation::class.java, IdentifierAdapter)
.registerTypeAdapter(PokemonProperties::class.java, pokemonPropertiesShortAdapter)
.registerTypeAdapter(TypeToken.getParameterized(RegistryLikeCondition::class.java, Item::class.java).type, ItemLikeConditionAdapter)
.registerTypeAdapter(ItemPredicate::class.java, LegacyItemConditionWrapperAdapter())
.registerTypeAdapter(ItemPredicate::class.java, LegacyItemConditionWrapperAdapter)
.create()
override val typeToken: TypeToken<Fossil> = TypeToken.get(Fossil::class.java)
@@ -10,6 +10,7 @@ package com.cobblemon.mod.common.api.gui
import com.cobblemon.mod.common.client.gui.CobblemonRenderable
import net.minecraft.client.gui.components.AbstractWidget
import net.minecraft.client.gui.components.Renderable
import net.minecraft.client.gui.components.events.GuiEventListener
import net.minecraft.client.gui.narration.NarrationElementOutput
import net.minecraft.network.chat.Component
@@ -54,8 +55,6 @@ abstract class ParentWidget(
}
override fun mouseClicked(pMouseX: Double, pMouseY: Double, pButton: Int): Boolean {
if (!active) return false
return children.any {
it.mouseClicked(pMouseX, pMouseY, pButton)
}
@@ -1,116 +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.gui
import kotlin.math.max
import kotlin.math.min
interface TreeNode<T> {
val children: List<TreeNode<T>>
}
class TreeDrawingNode<T>(
val value: T,
val children: List<TreeDrawingNode<T>>,
) {
var x = 0
var y = 0
var offset = 0
}
fun <T> createDrawingTree(treeNode: TreeNode<T>): TreeDrawingNode<T> {
val root = setup(treeNode, 0, mutableListOf<Int>(), mutableListOf<Int>())
addMods(root, 0)
return root
}
@Suppress("UNCHECKED_CAST")
private fun <T> setup(
treeNode: TreeNode<T>,
depth: Int,
nexts: MutableList<Int>,
offset: MutableList<Int>,
): TreeDrawingNode<T> {
val children = treeNode.children.map { child ->
setup(child, depth + 1, nexts, offset)
}
val treeDrawingNode = TreeDrawingNode<T>(treeNode as T, children)
treeDrawingNode.y = depth
while (nexts.size <= depth) {
nexts.add(0)
}
while (offset.size <= depth) {
offset.add(0)
}
var place =
if (children.isEmpty()) {
nexts[depth]
} else if (children.size == 1) {
// children.first().x - 1
children.first().x
} else {
// (children[0].x + children[1].x) / 2
(children.first().x + children.last().x) / 2
}
offset[depth] = max(offset[depth], nexts[depth] - place)
if (children.isEmpty()) {
treeDrawingNode.x = place
} else {
treeDrawingNode.x = place + offset[depth]
}
nexts[depth] += 2
treeDrawingNode.offset = offset[depth]
return treeDrawingNode
}
private fun <T> addMods(
treeNode: TreeDrawingNode<T>,
modSum: Int,
) {
treeNode.x += modSum
val newModSum = modSum + treeNode.offset
treeNode.children.forEach { child ->
addMods(child, newModSum)
}
}
data class TreeDrawingBounds(val minX: Int, val maxX: Int, val minY: Int, val maxY: Int) {
val width = maxX - minX
val height = maxY - minY
val xCenter = (maxX + minX) / 2.0
val yCenter = (maxY + minY) / 2.0
}
fun <T> calculateBounds(treeDrawingNode: TreeDrawingNode<T>): TreeDrawingBounds {
var minX = treeDrawingNode.x
var maxX = treeDrawingNode.x
var minY = treeDrawingNode.y
var maxY = treeDrawingNode.y
for (child in treeDrawingNode.children) {
val childBounds = calculateBounds(child)
minX = min(minX, childBounds.minX)
maxX = max(maxX, childBounds.maxX)
minY = min(minY, childBounds.minY)
maxY = max(maxY, childBounds.maxY)
}
return TreeDrawingBounds(minX, maxX, minY, maxY)
}
@@ -10,7 +10,7 @@ package com.cobblemon.mod.common.api.interaction
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.conditional.RegistryLikeCondition
import com.cobblemon.mod.common.api.data.ServerJsonDataRegistry
import com.cobblemon.mod.common.api.data.JsonDataRegistry
import com.cobblemon.mod.common.api.molang.ExpressionLike
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.api.pokemon.requirement.Requirement
@@ -26,7 +26,6 @@ import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.reflect.TypeToken
import net.minecraft.advancements.critereon.ItemPredicate
import net.minecraft.core.HolderLookup
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import net.minecraft.server.packs.PackType
@@ -37,31 +36,28 @@ import net.minecraft.world.level.block.Block
import net.minecraft.world.level.levelgen.structure.Structure
import net.minecraft.world.level.material.Fluid
object PokemonInteractions : ServerJsonDataRegistry<PokemonInteractionSet> {
object PokemonInteractions : JsonDataRegistry<PokemonInteractionSet> {
override val id = cobblemonResource("pokemon_interactions")
override val type = PackType.SERVER_DATA
override fun setupParser(registry: HolderLookup.Provider): Gson {
return GsonBuilder()
.registerTypeAdapter(ResourceLocation::class.java, IdentifierAdapter)
.registerTypeAdapter(PokemonProperties::class.java, pokemonPropertiesShortAdapter)
.registerTypeAdapter(ItemPredicate::class.java, LegacyItemConditionWrapperAdapter(registry))
.registerTypeAdapter(Requirement::class.java, CobblemonRequirementAdapter)
.registerTypeAdapter(InteractionEffect::class.java, InteractionEffectAdapter)
.registerTypeAdapter(ExpressionLike::class.java, ExpressionLikeAdapter)
.registerTypeAdapter(TypeToken.getParameterized(RegistryLikeCondition::class.java, Biome::class.java).type, BiomeLikeConditionAdapter)
.registerTypeAdapter(TypeToken.getParameterized(RegistryLikeCondition::class.java, Block::class.java).type, BlockLikeConditionAdapter)
.registerTypeAdapter(TypeToken.getParameterized(RegistryLikeCondition::class.java, Item::class.java).type, ItemLikeConditionAdapter)
.registerTypeAdapter(TypeToken.getParameterized(RegistryLikeCondition::class.java, Structure::class.java).type, StructureLikeConditionAdapter)
.registerTypeAdapter(TypeToken.getParameterized(RegistryLikeCondition::class.java, Fluid::class.java).type, FluidLikeConditionAdapter)
.registerTypeAdapter(IntRange::class.java, IntRangeAdapter)
.registerTypeAdapterFactory(LowerCaseEnumTypeAdapterFactory())
.setPrettyPrinting()
.create()
}
override val observable = SimpleObservable<PokemonInteractions>()
override val typeToken: TypeToken<PokemonInteractionSet> = TypeToken.get(PokemonInteractionSet::class.java)
override val resourcePath = "pokemon_interactions"
override val gson: Gson = GsonBuilder()
.registerTypeAdapter(ResourceLocation::class.java, IdentifierAdapter)
.registerTypeAdapter(PokemonProperties::class.java, pokemonPropertiesShortAdapter)
.registerTypeAdapter(ItemPredicate::class.java, LegacyItemConditionWrapperAdapter)
.registerTypeAdapter(Requirement::class.java, CobblemonRequirementAdapter)
.registerTypeAdapter(InteractionEffect::class.java, InteractionEffectAdapter)
.registerTypeAdapter(ExpressionLike::class.java, ExpressionLikeAdapter)
.registerTypeAdapter(TypeToken.getParameterized(RegistryLikeCondition::class.java, Biome::class.java).type, BiomeLikeConditionAdapter)
.registerTypeAdapter(TypeToken.getParameterized(RegistryLikeCondition::class.java, Block::class.java).type, BlockLikeConditionAdapter)
.registerTypeAdapter(TypeToken.getParameterized(RegistryLikeCondition::class.java, Item::class.java).type, ItemLikeConditionAdapter)
.registerTypeAdapter(TypeToken.getParameterized(RegistryLikeCondition::class.java, Structure::class.java).type, StructureLikeConditionAdapter)
.registerTypeAdapter(TypeToken.getParameterized(RegistryLikeCondition::class.java, Fluid::class.java).type, FluidLikeConditionAdapter)
.registerTypeAdapter(IntRange::class.java, IntRangeAdapter)
.registerTypeAdapterFactory(LowerCaseEnumTypeAdapterFactory())
.setPrettyPrinting()
.create()
val interactions = mutableListOf<PokemonInteractionSet>()
@@ -69,7 +65,7 @@ object PokemonInteractions : ServerJsonDataRegistry<PokemonInteractionSet> {
PokemonInteractionsSyncPacket(interactions).sendToPlayer(player)
}
override fun reload(data: Map<ResourceLocation, PokemonInteractionSet>, registry: HolderLookup.Provider) {
override fun reload(data: Map<ResourceLocation, PokemonInteractionSet>) {
interactions.clear()
val split = data.entries
.partition {
@@ -91,4 +87,4 @@ object PokemonInteractions : ServerJsonDataRegistry<PokemonInteractionSet> {
.flatMap { it.interactions }
.firstOrNull(interactionCheck)
}
}
}
@@ -45,6 +45,11 @@ 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 message = params.get<MoValue>(0).asString()
LOGGER.info(message)
@@ -55,11 +60,6 @@ object GeneralMoLangFunctions {
struct.map.remove(variable)
DoubleValue.ONE
}
functions["delete_variables"] = { params ->
val struct = params.get<VariableStruct>(0)
struct.map.clear()
DoubleValue.ONE
}
functions["get_variable"] = { params ->
val struct = params.get<VariableStruct>(0)
val variable = params.getString(1)
@@ -16,7 +16,6 @@ import com.cobblemon.mod.common.api.molang.ObjectValue
import com.cobblemon.mod.common.api.pokedex.AbstractPokedexManager
import com.cobblemon.mod.common.api.pokedex.CaughtCount
import com.cobblemon.mod.common.api.pokedex.CaughtPercent
import com.cobblemon.mod.common.api.pokedex.Dexes
import com.cobblemon.mod.common.api.pokedex.PokedexEntryProgress
import com.cobblemon.mod.common.api.pokedex.PokedexManager
import com.cobblemon.mod.common.api.pokedex.SeenCount
@@ -75,27 +74,6 @@ object PokedexMoLangFunctions : AbstractMoLangFunctionHolder<AbstractPokedexMana
functions["caught_percent"] = { DoubleValue(pokedex.getGlobalCalculatedValue(CaughtPercent)) }
functions["seen_percent"] = { DoubleValue(pokedex.getGlobalCalculatedValue(SeenPercent)) }
functions["dex_caught_count"] = { params ->
val dexId = params.getString(0).asIdentifierDefaultingNamespace()
if (Dexes.dexEntryMap.containsKey(dexId)) DoubleValue(pokedex.getDexCalculatedValue(dexId, CaughtCount))
else DoubleValue.ZERO
}
functions["dex_seen_count"] = { params ->
val dexId = params.getString(0).asIdentifierDefaultingNamespace()
if (Dexes.dexEntryMap.containsKey(dexId)) DoubleValue(pokedex.getDexCalculatedValue(dexId, SeenCount))
else DoubleValue.ZERO
}
functions["dex_caught_percent"] = { params ->
val dexId = params.getString(0).asIdentifierDefaultingNamespace()
if (Dexes.dexEntryMap.containsKey(dexId)) DoubleValue(pokedex.getDexCalculatedValue(dexId, CaughtPercent).toDouble())
else DoubleValue.ZERO
}
functions["dex_seen_percent"] = { params ->
val dexId = params.getString(0).asIdentifierDefaultingNamespace()
if (Dexes.dexEntryMap.containsKey(dexId)) DoubleValue(pokedex.getDexCalculatedValue(dexId, SeenPercent).toDouble())
else DoubleValue.ZERO
}
when (pokedex) {
is PokedexManager -> {
functions["player_id"] = { StringValue(pokedex.uuid.toString()) }
@@ -0,0 +1,18 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the 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) }
}
}
@@ -0,0 +1,50 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.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())
}
}
@@ -0,0 +1,201 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the 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'")
@@ -0,0 +1,24 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.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>
)
@@ -0,0 +1,26 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the 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)
}
@@ -0,0 +1,35 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the 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)
}
}
@@ -0,0 +1,151 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the 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)
}
@@ -0,0 +1,29 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.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
}
}
@@ -0,0 +1,68 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the 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())
}
@@ -0,0 +1,95 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the 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()
}
@@ -0,0 +1,93 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the 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
}
}
@@ -0,0 +1,317 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the 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
}
}
@@ -0,0 +1,94 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the 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 }
}
}
@@ -0,0 +1,17 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.molangui.container
import net.minecraft.world.Container
data class ServerCollectionSetup(
val name: String,
val config: CollectionConfig,
val container: Container
)
@@ -0,0 +1,300 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the 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)
}
}
}
}
@@ -0,0 +1,28 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.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)
}
@@ -0,0 +1,56 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.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"
}
}
}
}
@@ -0,0 +1,89 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the 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()
)
@@ -0,0 +1,151 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the 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"
}
}
}
@@ -0,0 +1,75 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the 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")
}
}
}
}
@@ -0,0 +1,114 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the 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 } }
}
@@ -0,0 +1,135 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the 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
}
}
}
@@ -0,0 +1,68 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the 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
}
}
@@ -0,0 +1,92 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the 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)
}
}
}
@@ -0,0 +1,67 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the 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
}
}
@@ -0,0 +1,63 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the 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)
@@ -0,0 +1,195 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the 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" }
}
}
}
@@ -8,10 +8,8 @@
package com.cobblemon.mod.common.api.orientation
import com.cobblemon.mod.common.util.math.QuatSpring
import com.cobblemon.mod.common.util.math.geometry.toDegrees
import com.cobblemon.mod.common.util.math.geometry.toRadians
import com.cobblemon.mod.common.util.math.simpleSpringDamperQuat
import net.minecraft.client.Minecraft
import net.minecraft.util.Mth
import net.minecraft.world.entity.LivingEntity
@@ -28,8 +26,6 @@ open class OrientationController(val entity: LivingEntity) {
val FORWARDS: Vector3f = Vector3f(0.0f, 0.0f, -1.0f)
val UP: Vector3f = Vector3f(0.0f, 1.0f, 0.0f)
val LEFT: Vector3f = Vector3f(-1.0f, 0.0f, 0.0f)
private const val TICK_DT = 1f / 20f // Minecraft server tick rate
private const val RENDER_SMOOTH_HALFLIFE = 0.1f // seconds to close ~half the gap
}
open var active: Boolean = false
@@ -37,8 +33,6 @@ open class OrientationController(val entity: LivingEntity) {
var orientation: Matrix3f? = null
private set
private val renderSpring = QuatSpring()
private var renderOrientationO: Matrix3f? = null
private var renderOrientation: Matrix3f? = null
@@ -60,8 +54,6 @@ open class OrientationController(val entity: LivingEntity) {
orientation = null
renderOrientationO = null
renderOrientation = null
renderSpring.quat.identity()
renderSpring.angVel.set(0f, 0f, 0f)
active = false
}
@@ -85,26 +77,18 @@ open class OrientationController(val entity: LivingEntity) {
return oldQuat
}
/** Every tick move the current render orientation towards the last known true orientation **/
fun tick() {
if (!active || orientation == null) return
renderOrientationO = renderOrientation
val current = orientation!!
val targetQuat = Quaternionf().setFromUnnormalized(current)
if (renderOrientation == null) {
// First tick: snap the spring to the target instead of easing in from identity
renderSpring.quat.set(targetQuat)
renderSpring.angVel.set(0f, 0f, 0f)
}
// Use a "quat spring" to smoothly move the render orientation towards the goal orientation.
// It takes into account the orientations angular velocities and should be much more resilient
// to sharp changes in goal orientation or late/missed packets.
simpleSpringDamperQuat(renderSpring, targetQuat, 0.15f, TICK_DT)
val current = orientation
val renderMatrix = this.renderOrientation ?: current
val renderQuat = Quaternionf().setFromUnnormalized(renderMatrix)
val targetQuat = Quaternionf().setFromUnnormalized(current)
val dampingFactor = 0.66f // Smooth interpolation to reduce jitter of orientations recieved from the server.
renderQuat.slerp(targetQuat, dampingFactor)
val newRenderOrientation = Matrix3f()
renderSpring.quat.get(newRenderOrientation)
renderQuat.get(newRenderOrientation)
renderOrientation = newRenderOrientation
}
@@ -108,7 +108,7 @@ object CatchRateModifiers {
* Used by [PokeBalls.SAFARI_BALL].
* Checks if the target is not battling, if true boost by *1.5
*/
val SAFARI = MultiplierModifier(1.5f) { _, poke -> !poke.entity?.isBattling!! }
val SAFARI = WorldStateModifier { _, entity -> if (!entity.isBattling) 1.5F else 1F }
/**
* Used by [PokeBalls.PARK_BALL].
@@ -13,10 +13,6 @@ import com.cobblemon.mod.common.api.molang.MoLangFunctions.addStandardFunctions
import com.cobblemon.mod.common.api.molang.ObjectValue
import com.cobblemon.mod.common.api.pokedex.entry.PokedexEntry
import com.cobblemon.mod.common.api.pokedex.entry.PokedexForm
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.api.pokemon.feature.ChoiceSpeciesFeatureProvider
import com.cobblemon.mod.common.api.pokemon.feature.SpeciesFeatures
import com.cobblemon.mod.common.api.pokemon.feature.WeightedChoiceSpeciesFeatureProvider
import com.cobblemon.mod.common.pokedex.scanner.PokedexEntityData
import com.cobblemon.mod.common.pokemon.Gender
import net.minecraft.resources.ResourceLocation
@@ -76,57 +72,6 @@ abstract class AbstractPokedexManager {
} ?: PokedexEntryProgress.UNREGISTERED
}
fun getHighestKnowledgeFor(entry: PokedexEntry, form: PokedexForm?): PokedexEntryProgress {
if (form == null) return getHighestKnowledgeFor(entry)
val speciesRecord = getSpeciesRecord(entry.speciesId) ?: return PokedexEntryProgress.UNREGISTERED
val hasAllAspects = entry.conditionAspects.all(speciesRecord::hasAspect)
if (!hasAllAspects) {
return PokedexEntryProgress.UNREGISTERED
}
return speciesRecord.getFormRecord(form.displayForm)?.knowledge ?: PokedexEntryProgress.UNREGISTERED
}
fun getHighestKnowledgeFor(entry: PokedexEntry, form: PokedexForm?, aspects: Iterable<String>): PokedexEntryProgress {
val formKnowledge = getHighestKnowledgeFor(entry, form)
if (formKnowledge == PokedexEntryProgress.UNREGISTERED) return PokedexEntryProgress.UNREGISTERED
val seenAspects = getSeenAspects(entry)
// Aspects that select one of the species' own forms are already covered by the form knowledge check above,
// so exempt them (and every other value of the same feature, since defaults like "phony" never appear on a form directly).
val species = PokemonSpecies.getByIdentifier(entry.speciesId)
val formAspects = species?.forms?.flatMapTo(hashSetOf()) { it.aspects } ?: emptySet()
val formSelectingFeatureAspects = species?.let(SpeciesFeatures::getFeaturesFor).orEmpty()
.mapNotNull { provider ->
val allAspects: Collection<String> = when (provider) {
is ChoiceSpeciesFeatureProvider -> provider.getAllAspects()
is WeightedChoiceSpeciesFeatureProvider -> provider.getAllAspects().keys
else -> return@mapNotNull null
}
allAspects.takeIf { it.any { aspect -> aspect in formAspects } }
}
.flatten()
.toHashSet()
val allSeenAspects = aspects
.filter { aspect ->
if (Dexes.equivalentFormsLookup[aspect] != null) return@filter false
if (aspect in formAspects || aspect in formSelectingFeatureAspects) return@filter false
if (aspect.uppercase() in setOf(Gender.MALE.name, Gender.FEMALE.name, Gender.GENDERLESS.name)) return@filter false
if ("shiny".equals(aspect, ignoreCase = true)) return@filter false
return@filter true
}
.all { aspect -> seenAspects.contains(aspect) }
if (!allSeenAspects) return PokedexEntryProgress.UNREGISTERED
return formKnowledge
}
fun getEncounteredForms(entry: PokedexEntry) : List<PokedexForm> {
return getFormsWithKnowledge(entry, PokedexEntryProgress.SEEN)
}
@@ -10,9 +10,6 @@ package com.cobblemon.mod.common.api.pokedex
import com.cobblemon.mod.common.api.data.JsonDataRegistry
import com.cobblemon.mod.common.api.pokedex.def.PokedexDef
import com.cobblemon.mod.common.api.pokedex.entry.PokedexEntry
import com.cobblemon.mod.common.api.pokedex.entry.PokedexForm
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.api.reactive.SimpleObservable
import com.cobblemon.mod.common.net.messages.client.data.PokedexDexSyncPacket
import com.cobblemon.mod.common.util.adapters.CodecBackedAdapter
@@ -43,17 +40,6 @@ object Dexes : JsonDataRegistry<PokedexDef> {
//Maps a dex id to its PokedexDef
val dexEntryMap = linkedMapOf<ResourceLocation, PokedexDef>()
val equivalentForms = mapOf(
"galar" to listOf("region-bias-galar", "bias-galar", "galar-bias", "galarian", "galar"),
"alola" to listOf("region-bias-alola", "bias-alola", "alola-bias", "alolan", "alola"),
"hisui" to listOf("region-bias-hisui", "bias-hisui", "hisui-bias", "hisuian", "hisui"),
"paldea" to listOf("region-bias-paldea", "bias-paldea", "paldea-bias", "paldean", "paldea"),
)
val equivalentFormsLookup = equivalentForms.flatMap { (key, values) ->
values.map { it to key }
}.toMap()
override fun reload(data: Map<ResourceLocation, PokedexDef>) {
dexEntryMap.clear()
data.entries.sortedBy { it.value.sortOrder }.forEach { (id, def) -> dexEntryMap[id] = def }
@@ -63,41 +49,4 @@ object Dexes : JsonDataRegistry<PokedexDef> {
override fun sync(player: ServerPlayer) {
PokedexDexSyncPacket(dexEntryMap.values).sendToPlayer(player)
}
fun findDexEntryBySpeciesId(regionId: ResourceLocation, speciesId: ResourceLocation): PokedexEntry? {
return dexEntryMap[regionId]?.getEntries()?.firstOrNull { it -> it.speciesId == speciesId }
}
fun findDexEntryBySpeciesId(speciesId: ResourceLocation): PokedexEntry? {
return findDexEntryBySpeciesId(cobblemonResource("national"), speciesId)
}
fun findRootDexEntryBySpeciesId(speciesId: ResourceLocation, formName: String): Pair<PokedexEntry, PokedexForm> {
var rootSpecies = PokemonSpecies.getByIdentifier(speciesId)!!
val initialFormEquivalents = getFormEquivalents(formName)
var rootForm = rootSpecies.forms.firstOrNull { initialFormEquivalents.contains(it.name.lowercase()) }
?: rootSpecies.standardForm
var preEvolution = rootForm.preEvolution
while (preEvolution != null) {
rootSpecies = preEvolution.species
rootForm = preEvolution.resolveForm(rootForm.aspects.toSet())
preEvolution = rootForm.preEvolution
}
val entry = findDexEntryBySpeciesId(rootSpecies.resourceIdentifier)
?: throw RuntimeException("Root dex entry not found")
val formEquivalents = getFormEquivalents(rootForm.name)
val form =
entry.forms.firstOrNull { formEquivalents.contains(it.displayForm.lowercase()) } ?:
entry.forms.first { it.displayForm.equals("Normal", ignoreCase = true) }
return entry to form
}
fun getFormEquivalents(formName: String): List<String> {
val key = equivalentFormsLookup[formName.lowercase()]
return equivalentForms[key] ?: listOf(formName.lowercase())
}
}
@@ -121,7 +121,6 @@ class SpeciesDexRecord {
fun addAspects(addedAspects: Set<String>) {
aspects.addAll(addedAspects)
pokedexManager.onSpeciesRecordUpdated(this)
}
/** Returns true if the given Pokémon contains new information. Internal because it's only to be called from [FormDexRecord.wouldBeDifferent]. */
@@ -44,9 +44,6 @@ import com.cobblemon.mod.common.api.riding.sound.RideSoundSettingsList
import com.cobblemon.mod.common.api.spawning.TimeRange
import com.cobblemon.mod.common.api.types.ElementalType
import com.cobblemon.mod.common.api.types.adapters.ElementalTypeAdapter
import com.cobblemon.mod.common.net.messages.client.data.FormEvolutionOverride
import com.cobblemon.mod.common.net.messages.client.data.SpeciesEvolutionEntry
import com.cobblemon.mod.common.net.messages.client.data.SpeciesEvolutionSyncPacket
import com.cobblemon.mod.common.net.messages.client.data.SpeciesRegistrySyncPacket
import com.cobblemon.mod.common.pokemon.FormData
import com.cobblemon.mod.common.pokemon.Species
@@ -131,7 +128,7 @@ object PokemonSpecies : JsonDataRegistry<Species> {
.registerTypeAdapter(TypeToken.getParameterized(RegistryLikeCondition::class.java, Fluid::class.java).type, FluidLikeConditionAdapter)
.registerTypeAdapter(EggGroup::class.java, EggGroupAdapter)
.registerTypeAdapter(MobEffect::class.java, RegistryElementAdapter<MobEffect>(BuiltInRegistries::MOB_EFFECT))
.registerTypeAdapter(ItemPredicate::class.java, LegacyItemConditionWrapperAdapter())
.registerTypeAdapter(ItemPredicate::class.java, LegacyItemConditionWrapperAdapter)
.registerTypeAdapter(RidingBehaviourSettings::class.java, RidingBehaviourSettingsAdapter)
.registerTypeAdapter(RideSoundSettingsList::class.java, RideSoundSettingsListAdapter)
.registerTypeAdapter(ObtainableItemCondition::class.java, ObtainableItemConditionAdapter)
@@ -246,18 +243,6 @@ object PokemonSpecies : JsonDataRegistry<Species> {
override fun sync(player: ServerPlayer) {
SpeciesRegistrySyncPacket(species.toList()).sendToPlayer(player)
// Sent after the species themselves so every species is already registered by the time this is decoded.
val evolutionEntries = species.map { singleSpecies ->
val formOverrides = singleSpecies.forms.mapNotNull { form ->
if (form.ownEvolutions == null && form.ownPreEvolution == null) {
null
} else {
FormEvolutionOverride(form.name, form.ownEvolutions, form.ownPreEvolution)
}
}
SpeciesEvolutionEntry(singleSpecies.resourceIdentifier, singleSpecies.evolutions, singleSpecies.preEvolution, formOverrides)
}
SpeciesEvolutionSyncPacket(evolutionEntries).sendToPlayer(player)
}
/**
@@ -25,8 +25,6 @@ interface PreEvolution {
val form: FormData
fun resolveForm(extraAspects: Set<String> = emptySet()): FormData = form
companion object {
fun of(species: Species, form: FormData = species.standardForm): PreEvolution = StandardPreEvolution(species, form)
@@ -11,8 +11,6 @@ package com.cobblemon.mod.common.api.pokemon.requirement
import com.cobblemon.mod.common.api.pokemon.evolution.ContextEvolution
import com.cobblemon.mod.common.api.pokemon.evolution.Evolution
import com.cobblemon.mod.common.pokemon.Pokemon
import net.minecraft.resources.ResourceLocation
import java.util.WeakHashMap
/**
* Represents a universal requirement suitable for various checks on pokemon..
@@ -34,11 +32,4 @@ fun interface Requirement {
*/
fun check(pokemon: Pokemon): Boolean
val pokedexIcon: ResourceLocation? get() = pokedexIconOverrides[this]
val pokedexTooltip: String? get() = pokedexTooltipOverrides[this]
companion object {
val pokedexIconOverrides = WeakHashMap<Requirement, ResourceLocation>()
val pokedexTooltipOverrides = WeakHashMap<Requirement, String>()
}
}
@@ -1,62 +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.riding.util
import net.minecraft.core.BlockPos
import net.minecraft.world.level.BlockGetter
import net.minecraft.world.phys.AABB
import kotlin.math.floor
/**
* Walks every block position inside [searchBox] and turns each solid block into one
* full-cube [FieldPrimitive].
*
* @author Jackson
* @since July 27th, 2026
*/
fun collectPrimitivesFromBlocks(
level: BlockGetter,
searchBox: AABB,
shape: PrimitiveShape,
includeFluids: Boolean = false
): List<FieldPrimitive> {
val primitives = mutableListOf<FieldPrimitive>()
val minX = floor(searchBox.minX).toInt()
val maxX = floor(searchBox.maxX).toInt()
val minY = floor(searchBox.minY).toInt()
val maxY = floor(searchBox.maxY).toInt()
val minZ = floor(searchBox.minZ).toInt()
val maxZ = floor(searchBox.maxZ).toInt()
val pos = BlockPos.MutableBlockPos()
for (x in minX..maxX) {
for (y in minY..maxY) {
for (z in minZ..maxZ) {
pos.set(x, y, z)
val state = level.getBlockState(pos)
val isSolid = !state.getCollisionShape(level, pos).isEmpty
val isFluid = includeFluids && !state.fluidState.isEmpty
if (!isSolid && !isFluid) continue
primitives.add(
FieldPrimitive(
shape = shape,
centerX = x + 0.5f, centerY = y + 0.5f, centerZ = z + 0.5f,
halfWidth = 0.5f, halfHeight = 0.5f, halfDepth = 0.5f
)
)
}
}
}
return primitives
}
@@ -1,155 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.riding.util
import net.minecraft.world.phys.Vec3
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
import kotlin.math.sqrt
/**
* Helpful Functions when working with signed distance fields/functions.
* https://iquilezles.org/articles/distfunctions/
*
* @author Jackson
* @since July 27th, 2026
*/
/** Which distance formula a primitive uses. */
enum class PrimitiveShape {
BOX,
ROUNDED_BOX,
ELLIPSOID
}
/** Tunable constants for the field. */
data class DistanceFieldTuning(
val smoothingRange: Float = 0.5f,
val roundingRadius: Float = 0.08f
)
/** A single shape in the field - a center position, half-extents, and which formula to use. */
data class FieldPrimitive(
val shape: PrimitiveShape,
val centerX: Float, val centerY: Float, val centerZ: Float,
val halfWidth: Float, val halfHeight: Float, val halfDepth: Float
)
/** Fast approximate 1/sqrt(x) - the Quake III trick, cheaper than a real sqrt + divide. */
fun fastInverseSqrt(value: Float): Float {
var x = value
val halfX = 0.5f * x
var bits = java.lang.Float.floatToIntBits(x)
bits = 0x5f3759df - (bits shr 1)
x = java.lang.Float.intBitsToFloat(bits)
x *= (1.5f - halfX * x * x)
return x
}
/** Exact distance from a point to an axis-aligned box. Inputs are relative to the box's own center. */
fun boxDistance(relX: Float, relY: Float, relZ: Float, halfWidth: Float, halfHeight: Float, halfDepth: Float): Float {
val qx = abs(relX) - halfWidth
val qy = abs(relY) - halfHeight
val qz = abs(relZ) - halfDepth
val ox = max(qx, 0f)
val oy = max(qy, 0f)
val oz = max(qz, 0f)
val outside = sqrt(ox * ox + oy * oy + oz * oz)
val inside = min(max(qx, max(qy, qz)), 0f)
return outside + inside
}
/** Same as boxDistance but with rounded corners/edges https://iquilezles.org/articles/distfunctions/ */
fun roundedBoxDistance(relX: Float, relY: Float, relZ: Float, halfWidth: Float, halfHeight: Float, halfDepth: Float, roundingRadius: Float): Float =
boxDistance(relX, relY, relZ, halfWidth - roundingRadius, halfHeight - roundingRadius, halfDepth - roundingRadius) - roundingRadius
/** Approximate (lower-bound) distance to an ellipsoid https://iquilezles.org/articles/ellipsoids/. */
fun ellipsoidDistance(relX: Float, relY: Float, relZ: Float, halfWidth: Float, halfHeight: Float, halfDepth: Float): Float {
val px = relX / halfWidth
val py = relY / halfHeight
val pz = relZ / halfDepth
val inverseMagnitude = fastInverseSqrt(px * px + py * py + pz * pz)
val trueDistance = sqrt(relX * relX + relY * relY + relZ * relZ)
return trueDistance * (1.0f - inverseMagnitude)
}
/** Distance from a point to one primitive, dispatched by its shape. */
fun distanceToPrimitive(primitive: FieldPrimitive, sampleX: Float, sampleY: Float, sampleZ: Float, tuning: DistanceFieldTuning): Float {
val relX = sampleX - primitive.centerX
val relY = sampleY - primitive.centerY
val relZ = sampleZ - primitive.centerZ
return when (primitive.shape) {
PrimitiveShape.BOX -> boxDistance(relX, relY, relZ, primitive.halfWidth, primitive.halfHeight, primitive.halfDepth)
PrimitiveShape.ROUNDED_BOX -> roundedBoxDistance(relX, relY, relZ, primitive.halfWidth, primitive.halfHeight, primitive.halfDepth, tuning.roundingRadius)
PrimitiveShape.ELLIPSOID -> ellipsoidDistance(relX, relY, relZ, primitive.halfWidth, primitive.halfHeight, primitive.halfDepth)
}
}
///** Normalized quadratic smooth-min https://iquilezles.org/articles/smin/ */
//fun smoothMinimum(a: Float, b: Float, smoothingRange: Float): Float {
// if (smoothingRange <= 0.001f) return min(a, b)
// val k = smoothingRange * 4.0f
// val h = (0.5f + 0.5f * (b - a) / k).coerceIn(0f, 1f)
// return b + (a - b) * h - k * h * (1f - h)
//}
//
///** Folds every primitive's distance together into one field value at a point. */
//fun sampleField(primitives: List<FieldPrimitive>, sampleX: Float, sampleY: Float, sampleZ: Float, tuning: DistanceFieldTuning): Float {
// var result = Float.POSITIVE_INFINITY
// for (primitive in primitives) {
// val distance = distanceToPrimitive(primitive, sampleX, sampleY, sampleZ, tuning)
// result = if (result == Float.POSITIVE_INFINITY) distance else smoothMinimum(result, distance, tuning.smoothingRange)
// }
// return result
//}
/** Normalized quadratic smooth-min https://iquilezles.org/articles/smin/ */
fun smoothMinimum(a: Float, b: Float, smoothingRange: Float): Float {
if (smoothingRange <= 0.001f) return min(a, b)
val k = smoothingRange * 4.0f
val h = (0.5f + 0.5f * (b - a) / k).coerceIn(0f, 1f)
return b + (a - b) * h - k * h * (1f - h)
}
/** Folds nearby primitive distances together into one field value at a point. */
fun sampleField(primitives: List<FieldPrimitive>, sampleX: Float, sampleY: Float, sampleZ: Float, tuning: DistanceFieldTuning): Float {
if (primitives.isEmpty()) return Float.POSITIVE_INFINITY
val distances = FloatArray(primitives.size) { distanceToPrimitive(primitives[it], sampleX, sampleY, sampleZ, tuning) }
val minDistance = distances.min()
if (tuning.smoothingRange <= 0.001f) return minDistance
// Anything more than a few blend-widths past the closest primitive is cut - its
// contribution would be negligible in a correct blend anyway, but left in, it still
// drags the chained result down the more of them there are.
val cutoff = tuning.smoothingRange * 3f
var result = Float.POSITIVE_INFINITY
for (distance in distances) {
if (distance - minDistance > cutoff) continue
result = if (result == Float.POSITIVE_INFINITY) distance else smoothMinimum(result, distance, tuning.smoothingRange)
}
return result
}
/** Finite-difference gradient of the field at a point - points away from the surface, magnitude ~1 for a clean field. */
fun estimateGradient(primitives: List<FieldPrimitive>, point: Vec3, tuning: DistanceFieldTuning, epsilon: Float = 0.05f): Vec3 {
val px = point.x.toFloat()
val py = point.y.toFloat()
val pz = point.z.toFloat()
val dx = sampleField(primitives, px + epsilon, py, pz, tuning) - sampleField(primitives, px - epsilon, py, pz, tuning)
val dy = sampleField(primitives, px, py + epsilon, pz, tuning) - sampleField(primitives, px, py - epsilon, pz, tuning)
val dz = sampleField(primitives, px, py, pz + epsilon, tuning) - sampleField(primitives, px, py, pz - epsilon, tuning)
val gradient = Vec3(dx.toDouble(), dy.toDouble(), dz.toDouble())
return if (gradient.lengthSqr() > 1e-8) gradient.normalize() else Vec3(0.0, 1.0, 0.0)
}
@@ -1,105 +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.riding.util
import net.minecraft.core.particles.DustParticleOptions
import net.minecraft.server.level.ServerLevel
import net.minecraft.world.entity.Entity
import net.minecraft.world.phys.AABB
import net.minecraft.world.phys.Vec3
import org.joml.Vector3f
/** One shell to draw - a target field value, a color, a particle size. */
data class DistanceFieldLayer(
val isoValue: Float,
val color: Vector3f,
val particleSize: Float = 1.0f
)
/**
* Marches a grid of vertical columns through the field around [center] and spawns a
* particle everywhere it crosses one of [layers]' target values. Server-side only.
*/
fun visualizeDistanceField(
referenceEntity: Entity,
center: Vec3,
layers: List<DistanceFieldLayer>,
shape: PrimitiveShape = PrimitiveShape.BOX,
horizontalRadius: Double = 4.0,
verticalRange: Double = 4.0,
sampleSpacing: Double = 0.5,
marchStep: Double = 0.1,
tuning: DistanceFieldTuning = DistanceFieldTuning()
) {
val level = referenceEntity.level()
if (level !is ServerLevel) return
val searchBox = AABB(
center.x - horizontalRadius, center.y - verticalRange, center.z - horizontalRadius,
center.x + horizontalRadius, center.y, center.z + horizontalRadius
)
val primitives = collectPrimitivesFromBlocks(level, searchBox, shape, includeFluids = true)
if (primitives.isEmpty()) return
var x = -horizontalRadius
while (x <= horizontalRadius) {
var z = -horizontalRadius
while (z <= horizontalRadius) {
marchColumn(level, primitives, center, x, z, verticalRange, marchStep, layers, tuning)
z += sampleSpacing
}
x += sampleSpacing
}
}
private fun marchColumn(
level: ServerLevel,
primitives: List<FieldPrimitive>,
center: Vec3,
offsetX: Double,
offsetZ: Double,
verticalRange: Double,
marchStep: Double,
layers: List<DistanceFieldLayer>,
tuning: DistanceFieldTuning
) {
val previousValue = FloatArray(layers.size) { Float.NaN }
var y = verticalRange
while (y >= -verticalRange) {
val worldX = (center.x + offsetX).toFloat()
val worldY = (center.y + y).toFloat()
val worldZ = (center.z + offsetZ).toFloat()
val fieldValue = sampleField(primitives, worldX, worldY, worldZ, tuning)
for (layerIndex in layers.indices) {
val layer = layers[layerIndex]
val previous = previousValue[layerIndex]
if (!previous.isNaN() && crossedTarget(previous, fieldValue, layer.isoValue)) {
val t = (layer.isoValue - previous) / (fieldValue - previous)
val crossingY = (y + marchStep) - marchStep * t
spawnLayerParticle(level, center.add(offsetX, crossingY, offsetZ), layer)
}
previousValue[layerIndex] = fieldValue
}
y -= marchStep
}
}
private fun crossedTarget(previous: Float, current: Float, target: Float): Boolean =
(previous - target) * (current - target) < 0f
private fun spawnLayerParticle(level: ServerLevel, pos: Vec3, layer: DistanceFieldLayer) {
val particle = DustParticleOptions(layer.color, layer.particleSize)
level.sendParticles(particle, pos.x, pos.y, pos.z, 1, 0.0, 0.0, 0.0, 0.0)
}
@@ -10,8 +10,6 @@ package com.cobblemon.mod.common.api.spawning
import com.cobblemon.mod.common.api.spawning.TimeRange.Companion.timeRanges
import com.cobblemon.mod.common.api.spawning.condition.SpawningCondition
import com.cobblemon.mod.common.util.lang
import net.minecraft.network.chat.MutableComponent
import com.cobblemon.mod.common.util.adapters.IntRangesAdapter
/**
@@ -51,25 +49,4 @@ class TimeRange : IntRanges {
constructor() : super()
constructor(vararg ranges: IntRange) : super(*ranges)
fun displayName(): MutableComponent? {
val key = timeRanges.entries.firstOrNull { entry -> this == entry.value }?.key ?: return null
return lang("ui.time_range.$key")
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is TimeRange) return false
if (other.ranges.size != this.ranges.size) return false
other.ranges.forEachIndexed { index, otherRange ->
if (this.ranges[index] != otherRange) return false
}
return true
}
override fun hashCode(): Int {
return ranges.hashCode()
}
}
@@ -8,8 +8,6 @@
package com.cobblemon.mod.common.api.spawning.condition
import com.cobblemon.mod.common.util.lang
import net.minecraft.network.chat.MutableComponent
import net.minecraft.world.level.Level
/**
@@ -44,7 +42,4 @@ enum class MoonPhase {
fun ofWorld(world: Level): MoonPhase = VALUES[world.moonPhase]
}
fun displayName(): MutableComponent {
return lang("ui.moon_phase.${this.name.lowercase()}")
}
}
@@ -34,7 +34,6 @@ class ElementalType(
val resourceLocation: ResourceLocation = ResourceLocation.fromNamespaceAndPath(Cobblemon.MODID, "ui/types.png"),
val showdownId: String = ShowdownIdentifiable.REGEX.replace(name.lowercase(), ""),
val typeGem: ResourceLocation = ResourceLocation.fromNamespaceAndPath(Cobblemon.MODID, showdownId + "_gem"),
val order: Int = 1000
) : ShowdownIdentifiable {
override fun showdownId(): String {
@@ -25,8 +25,7 @@ object ElementalTypes {
hue = 0xE8E8DA,
textureXMultiplier = 0,
primaryColor = 0x8FA689,
secondaryColor = 0xFFFBE2,
order = 0
secondaryColor = 0xFFFBE2
)
@JvmField
@@ -36,8 +35,7 @@ object ElementalTypes {
hue = 0xFF6E21,
textureXMultiplier = 1,
primaryColor = 0xF92D2D,
secondaryColor = 0xF99B1D,
order = 1
secondaryColor = 0xF99B1D
)
@JvmField
@@ -47,8 +45,7 @@ object ElementalTypes {
hue = 0x3FA5FF,
textureXMultiplier = 2,
primaryColor = 0x377AF6,
secondaryColor = 0x50DDF9,
order = 2
secondaryColor = 0x50DDF9
)
@JvmField
@@ -58,8 +55,7 @@ object ElementalTypes {
hue = 0x62D14F,
textureXMultiplier = 3,
primaryColor = 0x0EAE95,
secondaryColor = 0xC6F554,
order = 3
secondaryColor = 0xC6F554
)
@JvmField
@@ -69,8 +65,7 @@ object ElementalTypes {
hue = 0xFFD314,
textureXMultiplier = 4,
primaryColor = 0xF7AB19,
secondaryColor = 0xFFF257,
order = 4
secondaryColor = 0xFFF257
)
@JvmField
@@ -80,8 +75,7 @@ object ElementalTypes {
hue = 0x54F2F2,
textureXMultiplier = 5,
primaryColor = 0x41BAED,
secondaryColor = 0x91F4F9,
order = 5
secondaryColor = 0x91F4F9
)
@JvmField
@@ -91,8 +85,7 @@ object ElementalTypes {
hue = 0xEF565D,
textureXMultiplier = 6,
primaryColor = 0xC8356B,
secondaryColor = 0xF36A3F,
order = 6
secondaryColor = 0xF36A3F
)
@JvmField
@@ -102,8 +95,7 @@ object ElementalTypes {
hue = 0xD651FF,
textureXMultiplier = 7,
primaryColor = 0x7B41DD,
secondaryColor = 0xE568F4,
order = 7
secondaryColor = 0xE568F4
)
@JvmField
@@ -113,8 +105,7 @@ object ElementalTypes {
hue = 0xF4A453,
textureXMultiplier = 8,
primaryColor = 0xE08835,
secondaryColor = 0xFFD057,
order = 8
secondaryColor = 0xFFD057
)
@JvmField
@@ -124,8 +115,7 @@ object ElementalTypes {
hue = 0xB8B2FF,
textureXMultiplier = 9,
primaryColor = 0x6083F6,
secondaryColor = 0xD5CCFA,
order = 9
secondaryColor = 0xD5CCFA
)
@JvmField
@@ -135,8 +125,7 @@ object ElementalTypes {
hue = 0xFF5E9E,
textureXMultiplier = 10,
primaryColor = 0xE553CE,
secondaryColor = 0xF69DD5,
order = 10
secondaryColor = 0xF69DD5
)
@JvmField
@@ -146,8 +135,7 @@ object ElementalTypes {
hue = 0xD3D319,
textureXMultiplier = 11,
primaryColor = 0x9FBE1B,
secondaryColor = 0xE9FB5B,
order = 11
secondaryColor = 0xE9FB5B
)
@JvmField
@@ -157,8 +145,7 @@ object ElementalTypes {
hue = 0xB7A16E,
textureXMultiplier = 12,
primaryColor = 0x9F814A,
secondaryColor = 0xEECB7A,
order = 12
secondaryColor = 0xEECB7A
)
@JvmField
@@ -168,8 +155,7 @@ object ElementalTypes {
hue = 0x9C80F7,
textureXMultiplier = 13,
primaryColor = 0x4F57DD,
secondaryColor = 0xC182F1,
order = 13
secondaryColor = 0xC182F1
)
@JvmField
@@ -179,8 +165,7 @@ object ElementalTypes {
hue = 0x7580FF,
textureXMultiplier = 14,
primaryColor = 0x4E68E2,
secondaryColor = 0x47C7F7,
order = 14
secondaryColor = 0x47C7F7
)
@JvmField
@@ -190,8 +175,7 @@ object ElementalTypes {
hue = 0x587DA0,
textureXMultiplier = 15,
primaryColor = 0x465B99,
secondaryColor = 0x689EBF,
order = 15
secondaryColor = 0x689EBF
)
@JvmField
@@ -201,8 +185,7 @@ object ElementalTypes {
hue = 0xABD1F4,
textureXMultiplier = 16,
primaryColor = 0x628CB9,
secondaryColor = 0xD6F7F3,
order = 16
secondaryColor = 0xD6F7F3
)
@JvmField
@@ -212,21 +195,18 @@ object ElementalTypes {
hue = 0xFF7FE5,
textureXMultiplier = 17,
primaryColor = 0xE2678C,
secondaryColor = 0xF5A7B7,
order = 17
secondaryColor = 0xF5A7B7
)
@JvmStatic
@JvmOverloads
fun register(name: String, displayName: MutableComponent, hue: Int, textureXMultiplier: Int, primaryColor: Int, secondaryColor: Int, order: Int = 1000): ElementalType {
fun register(name: String, displayName: MutableComponent, hue: Int, textureXMultiplier: Int, primaryColor: Int, secondaryColor: Int): ElementalType {
return ElementalType(
name = name,
displayName = displayName,
hue = hue,
textureXMultiplier = textureXMultiplier,
primaryColor = primaryColor,
secondaryColor = secondaryColor,
order = order
secondaryColor = secondaryColor
).also {
allTypes.add(it)
}
@@ -0,0 +1,97 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.vending
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.data.JsonDataRegistry
import com.cobblemon.mod.common.api.molangui.ActionKey
import com.cobblemon.mod.common.api.molangui.MolangUiActionContext
import com.cobblemon.mod.common.api.molangui.MolangUiActionHandler
import com.cobblemon.mod.common.api.molangui.ParameterValue
import com.cobblemon.mod.common.api.reactive.SimpleObservable
import com.cobblemon.mod.common.util.adapters.IdentifierAdapter
import com.cobblemon.mod.common.util.cobblemonResource
import com.google.gson.GsonBuilder
import com.google.gson.reflect.TypeToken
import net.minecraft.core.registries.BuiltInRegistries
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import net.minecraft.server.packs.PackType
data class CatalogEntry(val item: ResourceLocation, val price: Int)
data class VendingMachineCatalog(val entries: List<CatalogEntry> = emptyList()) {
val pageCount: Int get() = if (entries.isEmpty()) 1 else (entries.size + PAGE_SIZE - 1) / PAGE_SIZE
fun clampPage(page: Int): Int = page.coerceIn(0, pageCount - 1)
fun pageParameters(page: Int): Map<String, ParameterValue> {
val safePage = clampPage(page)
val parameters = linkedMapOf<String, ParameterValue>(
"page" to ParameterValue.NumberValue(safePage.toDouble()),
"page_count" to ParameterValue.NumberValue(pageCount.toDouble()),
"page_text" to ParameterValue.StringValue("${safePage + 1}/$pageCount")
)
for (row in 0 until PAGE_SIZE) {
val entry = entries.getOrNull(safePage * PAGE_SIZE + row)
parameters["row${row}_item"] = ParameterValue.StringValue(entry?.item?.toString() ?: "")
parameters["row${row}_price"] = ParameterValue.StringValue(entry?.price?.toString() ?: "")
}
return parameters
}
companion object { const val PAGE_SIZE = 3 }
}
object VendingMachinePageHandler : MolangUiActionHandler {
val ACTION = ActionKey("vending_page")
override fun handle(context: MolangUiActionContext) {
val requested = (context.parameters["page"] as? ParameterValue.NumberValue)?.value ?: return
val catalog = VendingMachineCatalogs.catalog(VendingMachineCatalogs.DEFAULT)
context.session.updateParameters(catalog.pageParameters(requested.toInt()))
}
}
object VendingMachineCatalogs : JsonDataRegistry<VendingMachineCatalog> {
override val id = cobblemonResource("vending_machine")
override val type = PackType.SERVER_DATA
override val observable = SimpleObservable<VendingMachineCatalogs>()
override val typeToken: TypeToken<VendingMachineCatalog> = TypeToken.get(VendingMachineCatalog::class.java)
override val resourcePath = "vending_machine"
override val gson = GsonBuilder()
.registerTypeAdapter(ResourceLocation::class.java, IdentifierAdapter)
.create()
val DEFAULT = cobblemonResource("default")
private val catalogs = mutableMapOf<ResourceLocation, VendingMachineCatalog>()
fun catalog(id: ResourceLocation): VendingMachineCatalog = catalogs[id] ?: VendingMachineCatalog()
override fun reload(data: Map<ResourceLocation, VendingMachineCatalog>) {
catalogs.clear()
data.forEach { (id, catalog) -> catalogs[id] = validate(id, catalog) }
Cobblemon.LOGGER.info("Loaded ${catalogs.size} vending machine catalogs")
observable.emit(this)
}
override fun sync(player: ServerPlayer) = Unit
private fun validate(id: ResourceLocation, catalog: VendingMachineCatalog): VendingMachineCatalog {
val kept = catalog.entries.filter { entry ->
val known = BuiltInRegistries.ITEM.containsKey(entry.item)
if (!known) Cobblemon.LOGGER.error("Vending catalog '$id' drops unknown item '${entry.item}'")
else if (entry.price <= 0) Cobblemon.LOGGER.error("Vending catalog '$id' drops '${entry.item}' with price ${entry.price}")
known && entry.price > 0
}
return VendingMachineCatalog(kept)
}
}
@@ -48,8 +48,7 @@ class DecorativeItemBlock(settings: Properties, val type: DecorativeItemBlockTyp
NEVERMELT,
SCOPE,
WIDE,
ZOOM,
IZER;
ZOOM;
override fun getSerializedName(): String = this.name.lowercase()
@@ -77,7 +76,6 @@ class DecorativeItemBlock(settings: Properties, val type: DecorativeItemBlockTyp
val SCOPE_HITBOX = Shapes.box(0.625, 0.0, 0.4375, 0.9375, 0.125, 0.75)
val WIDE_HITBOX = Shapes.box(0.625, 0.0, 0.125, 0.9375, 0.0625, 0.625)
val ZOOM_HITBOX = Shapes.box(0.125, 0.0, 0.1875, 0.375, 0.1875, 0.5)
val IZER_HITBOX = Shapes.box(0.25, 0.0, 0.25, 0.75, 0.375, 0.75)
fun getShape(type: DecorativeItemBlockType): VoxelShape {
return when (type) {
@@ -89,7 +87,6 @@ class DecorativeItemBlock(settings: Properties, val type: DecorativeItemBlockTyp
DecorativeItemBlockType.SCOPE -> SCOPE_HITBOX
DecorativeItemBlockType.WIDE -> WIDE_HITBOX
DecorativeItemBlockType.ZOOM -> ZOOM_HITBOX
DecorativeItemBlockType.IZER -> IZER_HITBOX
DecorativeItemBlockType.FLATTENED_10x4 -> SMOOSHED4_HITBOX
DecorativeItemBlockType.FLATTENED_8x4 -> FLAT4_HITBOX
DecorativeItemBlockType.FLATTENED_8x6 -> FLAT6_HITBOX
@@ -103,15 +103,15 @@ class MintBlock(private val mintType: MintType, settings: Properties) : CropBloc
GREEN -> CobblemonItems.GREEN_MINT_SEEDS
WHITE -> CobblemonItems.WHITE_MINT_SEEDS
}
// These don't seem to get called to anywhere besides the direct item, and since I'm trying to do funny stuff there, just commenting this out
// fun getLeaf(): MintLeafItem = when (this) {
// RED -> CobblemonItems.RED_MINT_LEAF
// BLUE -> CobblemonItems.BLUE_MINT_LEAF
// CYAN -> CobblemonItems.CYAN_MINT_LEAF
// PINK -> CobblemonItems.PINK_MINT_LEAF
// GREEN -> CobblemonItems.GREEN_MINT_LEAF
// WHITE -> CobblemonItems.WHITE_MINT_LEAF
// }
fun getLeaf(): MintLeafItem = when (this) {
RED -> CobblemonItems.RED_MINT_LEAF
BLUE -> CobblemonItems.BLUE_MINT_LEAF
CYAN -> CobblemonItems.CYAN_MINT_LEAF
PINK -> CobblemonItems.PINK_MINT_LEAF
GREEN -> CobblemonItems.GREEN_MINT_LEAF
WHITE -> CobblemonItems.WHITE_MINT_LEAF
}
fun getCropBlock(): MintBlock = when (this) {
RED -> CobblemonBlocks.RED_MINT
@@ -45,9 +45,7 @@ class StackableItemBlock(settings: Properties, val type: StackableItemBlockType)
IRON_BALL,
THROAT_SPRAY,
UPGRADE,
DICE,
GLASS_BOTTLE,
EXTENDER;
DICE;
override fun getSerializedName(): String = this.name.lowercase()
@@ -135,18 +133,6 @@ class StackableItemBlock(settings: Properties, val type: StackableItemBlockType)
Shapes.box(0.125, 0.0, 0.625, 0.5625, 0.1875, 0.875),
Shapes.box(0.125, 0.0, 0.4375, 0.5625, 0.1875, 0.875),
Shapes.box(0.125, 0.0, 0.4375, 0.5625, 0.375, 0.875)
),
StackableItemBlockType.GLASS_BOTTLE to listOf(
Shapes.box(0.375, 0.0, 0.375, 0.625, 0.375, 0.625),
Shapes.box(0.1875, 0.0, 0.1875, 0.8125, 0.375, 0.6875),
Shapes.box(0.125, 0.0, 0.125, 0.8125, 0.375, 0.875),
Shapes.box(0.125, 0.0, 0.125, 0.875, 0.375, 0.875)
),
StackableItemBlockType.EXTENDER to listOf(
Shapes.box(0.25, 0.0, 0.375, 0.625, 0.5625, 0.625),
Shapes.box(0.125, 0.0, 0.25, 0.875, 0.5625, 0.75),
Shapes.box(0.0625, 0.0, 0.0625, 0.875, 0.5625, 0.875),
Shapes.box(0.0625, 0.0, 0.0625, 0.9375, 0.5625, 0.9375)
)
)
@@ -0,0 +1,61 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.block
import com.cobblemon.mod.common.api.molangui.MolangUiOpenRequest
import com.cobblemon.mod.common.api.molangui.MolangUiService
import com.cobblemon.mod.common.api.vending.VendingMachineCatalogs
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.core.BlockPos
import net.minecraft.core.Direction
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.InteractionResult
import net.minecraft.world.entity.player.Player
import net.minecraft.world.item.context.BlockPlaceContext
import net.minecraft.world.level.Level
import net.minecraft.world.level.block.Block
import net.minecraft.world.level.block.Rotation
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.level.block.state.StateDefinition
import net.minecraft.world.level.block.state.properties.BlockStateProperties.HORIZONTAL_FACING
import net.minecraft.world.phys.BlockHitResult
@Suppress("OVERRIDE_DEPRECATION")
class VendingMachineBlock(settings: Properties) : Block(settings) {
init {
registerDefaultState(stateDefinition.any().setValue(HORIZONTAL_FACING, Direction.NORTH))
}
override fun getStateForPlacement(context: BlockPlaceContext): BlockState =
defaultBlockState().setValue(HORIZONTAL_FACING, context.horizontalDirection.opposite)
override fun createBlockStateDefinition(builder: StateDefinition.Builder<Block, BlockState>) {
builder.add(HORIZONTAL_FACING)
}
override fun rotate(state: BlockState, rotation: Rotation): BlockState =
state.setValue(HORIZONTAL_FACING, rotation.rotate(state.getValue(HORIZONTAL_FACING)))
override fun useWithoutItem(
state: BlockState,
world: Level,
pos: BlockPos,
player: Player,
hitResult: BlockHitResult
): InteractionResult {
if (player !is ServerPlayer) return InteractionResult.SUCCESS
val catalog = VendingMachineCatalogs.catalog(VendingMachineCatalogs.DEFAULT)
MolangUiService.open(player, MolangUiOpenRequest(UI_ID, catalog.pageParameters(0), pos))
return InteractionResult.SUCCESS
}
companion object {
private val UI_ID = cobblemonResource("vending_machine")
}
}
@@ -1,59 +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.block.decor
import net.minecraft.core.BlockPos
import net.minecraft.world.item.context.BlockPlaceContext
import net.minecraft.world.level.BlockGetter
import net.minecraft.world.level.block.Block
import net.minecraft.world.level.block.SimpleWaterloggedBlock
import net.minecraft.world.level.block.TallFlowerBlock
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.level.block.state.StateDefinition
import net.minecraft.world.level.block.state.properties.BlockStateProperties.WATERLOGGED
import net.minecraft.world.level.block.state.properties.DoubleBlockHalf
import net.minecraft.world.level.material.FluidState
import net.minecraft.world.level.material.Fluids
import net.minecraft.world.phys.shapes.CollisionContext
import net.minecraft.world.phys.shapes.Shapes
import net.minecraft.world.phys.shapes.VoxelShape
class LobailaFlowerBlock(properties: Properties?) : TallFlowerBlock(properties), SimpleWaterloggedBlock {
init {
registerDefaultState(stateDefinition.any()
.setValue(HALF, DoubleBlockHalf.LOWER)
.setValue(WATERLOGGED, false))
}
override fun getFluidState(blockState: BlockState): FluidState? {
return if (blockState.getValue(WATERLOGGED)) Fluids.WATER.getSource(false) else super.getFluidState(blockState);
}
override fun createBlockStateDefinition(builder: StateDefinition.Builder<Block, BlockState>) {
builder.add(HALF, WATERLOGGED)
}
override fun getStateForPlacement(context: BlockPlaceContext): BlockState? {
val fluidState = context.level.getFluidState(context.clickedPos)
return this.defaultBlockState()
.setValue(
WATERLOGGED,
fluidState.type === Fluids.WATER
)
}
override fun getShape(state: BlockState, world: BlockGetter, pos: BlockPos, context: CollisionContext): VoxelShape = AABB
companion object {
private val AABB = Shapes.box(0.125, 0.0, 0.125, 0.875, 1.0, 0.875)
}
}
@@ -1,87 +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.block.decor
import com.cobblemon.mod.common.CobblemonBlockEntities
import com.cobblemon.mod.common.block.entity.MistleafFlowerBlockEntity
import net.minecraft.core.BlockPos
import net.minecraft.world.level.BlockGetter
import net.minecraft.world.level.Level
import net.minecraft.world.level.LevelReader
import net.minecraft.world.level.block.BaseEntityBlock
import net.minecraft.world.level.block.Block
import net.minecraft.world.level.block.EntityBlock
import net.minecraft.world.level.block.SoundType
import net.minecraft.world.level.block.TallFlowerBlock
import net.minecraft.world.level.block.entity.BlockEntity
import net.minecraft.world.level.block.entity.BlockEntityTicker
import net.minecraft.world.level.block.entity.BlockEntityType
import net.minecraft.world.level.block.state.BlockBehaviour
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.level.block.state.StateDefinition
import net.minecraft.world.level.block.state.properties.DoubleBlockHalf
import net.minecraft.world.level.block.state.properties.IntegerProperty
import net.minecraft.world.level.material.MapColor
import net.minecraft.world.level.material.PushReaction
import net.minecraft.world.phys.shapes.CollisionContext
import net.minecraft.world.phys.shapes.Shapes
import net.minecraft.world.phys.shapes.VoxelShape
class MistleafFlowerBlock(lightLevel: (BlockState) -> Int) :
EntityBlock,
TallFlowerBlock(
Properties.of()
.mapColor(MapColor.PLANT)
.noCollission()
.instabreak()
.sound(SoundType.GRASS)
.offsetType(OffsetType.XZ)
.ignitedByLava()
.pushReaction(PushReaction.DESTROY)
.lightLevel(lightLevel)
) {
init {
registerDefaultState(stateDefinition.any()
.setValue(HALF, DoubleBlockHalf.LOWER)
.setValue(GLOWING, 0))
}
override fun createBlockStateDefinition(builder: StateDefinition.Builder<Block, BlockState>) {
builder.add(HALF, GLOWING)
}
override fun isValidBonemealTarget(level: LevelReader, pos: BlockPos, state: BlockState): Boolean {
return false
}
override fun getShape(state: BlockState, world: BlockGetter, pos: BlockPos, context: CollisionContext): VoxelShape = AABB
override fun newBlockEntity(pos: BlockPos, state: BlockState): BlockEntity? {
return MistleafFlowerBlockEntity(pos, state)
}
override fun <T : BlockEntity?> getTicker(
level: Level,
state: BlockState,
blockEntityType: BlockEntityType<T?>
): BlockEntityTicker<T?>? {
return if (level.isClientSide) null else BaseEntityBlock.createTickerHelper(blockEntityType as BlockEntityType<*>,
CobblemonBlockEntities.MISTLEAF_FLOWER, MistleafFlowerBlockEntity::serverTick) as BlockEntityTicker<T?>?
}
companion object {
private val AABB = Shapes.box(0.125, 0.0, 0.125, 0.875, 1.0, 0.875)
val GLOWING = IntegerProperty.create("glowing", 0, 8)
}
override fun hasAnalogOutputSignal(state: BlockState): Boolean { return true }
override fun getAnalogOutputSignal(state: BlockState, level: Level, pos: BlockPos): Int {
return state.getValue(GLOWING) as Int
}
}
@@ -1,104 +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.block.decor
import com.mojang.serialization.MapCodec
import com.mojang.serialization.codecs.RecordCodecBuilder
import net.minecraft.core.BlockPos
import net.minecraft.core.Direction
import net.minecraft.server.level.ServerLevel
import net.minecraft.util.RandomSource
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.context.BlockPlaceContext
import net.minecraft.world.level.BlockGetter
import net.minecraft.world.level.Level
import net.minecraft.world.level.LevelAccessor
import net.minecraft.world.level.LevelReader
import net.minecraft.world.level.block.Block
import net.minecraft.world.level.block.Blocks
import net.minecraft.world.level.block.BonemealableBlock
import net.minecraft.world.level.block.DirectionalBlock
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.level.block.state.StateDefinition
import net.minecraft.world.phys.shapes.CollisionContext
import net.minecraft.world.phys.shapes.Shapes
import net.minecraft.world.phys.shapes.VoxelShape
class PauvilleaFlowerBlock(properties: Properties?) : DirectionalBlock(properties), BonemealableBlock {
init {
registerDefaultState(stateDefinition.any()
.setValue(FACING, Direction.DOWN))
}
override fun createBlockStateDefinition(builder: StateDefinition.Builder<Block, BlockState>) {
builder.add(FACING)
}
override fun getStateForPlacement(ctx: BlockPlaceContext): BlockState? {
var blockState = defaultBlockState()
val worldView = ctx.level
val blockPos = ctx.clickedPos
blockState = blockState.setValue(FACING, ctx.clickedFace) as BlockState
if (blockState.canSurvive(worldView, blockPos)) {
return blockState
}
return null
}
override fun updateShape(state: BlockState, direction: Direction, neighborState: BlockState, level: LevelAccessor, pos: BlockPos, neighborPos: BlockPos): BlockState {
val facing = state.getValue(FACING)
if (direction == facing.opposite && !canSurvive(state, level, pos)) {
return Blocks.AIR.defaultBlockState()
}
return super.updateShape(state, direction, neighborState, level, pos, neighborPos)
}
override fun canSurvive(state: BlockState, level: LevelReader, pos: BlockPos): Boolean {
val direction = state.getValue(FACING) as Direction
val blockState = level.getBlockState(pos.relative(direction.opposite))
return blockState.isCollisionShapeFullBlock(level, pos) or blockState.canOcclude()
}
override fun codec(): MapCodec<PauvilleaFlowerBlock> = CODEC
override fun getShape(state: BlockState, blockGetter: BlockGetter, pos: BlockPos, collisionContext: CollisionContext): VoxelShape {
return when (state.getValue(FACING)) {
Direction.DOWN -> CEILING
Direction.NORTH -> WALL_NORTH
Direction.EAST -> WALL_EAST
Direction.SOUTH -> WALL_SOUTH
Direction.WEST -> WALL_WEST
Direction.UP -> FLOOR
}
}
companion object {
val CODEC: MapCodec<PauvilleaFlowerBlock> = RecordCodecBuilder.mapCodec {
it.group(propertiesCodec()).apply(it, ::PauvilleaFlowerBlock)
}
val FLOOR = Shapes.box(0.075, 0.0, 0.075, 0.925, 0.4375, 0.925)
val WALL_NORTH = Shapes.box(0.0, 0.0, 0.8125, 1.0, 1.0, 1.0)
val WALL_EAST = Shapes.box(0.0, 0.0, 0.0, 0.1875, 1.0, 1.0)
val WALL_SOUTH = Shapes.box(0.0, 0.0, 0.0, 1.0, 1.0, 0.1875)
val WALL_WEST = Shapes.box(0.8125, 0.0, 0.0, 1.0, 1.0, 1.0)
val CEILING = Shapes.box(0.075, 0.0, 0.075, 0.925, 1.0, 0.925)
}
override fun isValidBonemealTarget(level: LevelReader?, pos: BlockPos?, state: BlockState?): Boolean {
return true
}
override fun isBonemealSuccess(level: Level?, random: RandomSource?, pos: BlockPos?, state: BlockState?): Boolean {
return true
}
override fun performBonemeal(level: ServerLevel, random: RandomSource?, pos: BlockPos, state: BlockState?) {
popResource(level, pos, ItemStack(this))
}
}
@@ -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.block.decor
import net.minecraft.core.BlockPos
import net.minecraft.world.level.BlockGetter
import net.minecraft.world.level.block.PinkPetalsBlock
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.phys.shapes.CollisionContext
import net.minecraft.world.phys.shapes.Shapes
import net.minecraft.world.phys.shapes.VoxelShape
class PomohanaFlowerBlock(properties: Properties?) : PinkPetalsBlock(properties) {
override fun getShape(state: BlockState, world: BlockGetter, pos: BlockPos, context: CollisionContext): VoxelShape = AABB
companion object {
private val AABB = Shapes.box(0.125, 0.0, 0.125, 0.875, 1.0, 0.875)
}
}
@@ -1,105 +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.block.decor
import com.cobblemon.mod.common.CobblemonBlockEntities
import com.cobblemon.mod.common.CobblemonBlocks
import com.cobblemon.mod.common.block.entity.TerrainPlantBlockEntity
import net.minecraft.core.BlockPos
import net.minecraft.server.level.ServerLevel
import net.minecraft.util.RandomSource
import net.minecraft.world.entity.Entity
import net.minecraft.world.level.BlockGetter
import net.minecraft.world.level.Level
import net.minecraft.world.level.LevelReader
import net.minecraft.world.level.block.BaseEntityBlock
import net.minecraft.world.level.block.Block
import net.minecraft.world.level.block.EntityBlock
import net.minecraft.world.level.block.TallFlowerBlock
import net.minecraft.world.level.block.entity.BlockEntity
import net.minecraft.world.level.block.entity.BlockEntityTicker
import net.minecraft.world.level.block.entity.BlockEntityType
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.level.block.state.StateDefinition
import net.minecraft.world.level.block.state.properties.BooleanProperty
import net.minecraft.world.level.block.state.properties.DoubleBlockHalf
import net.minecraft.world.phys.shapes.CollisionContext
import net.minecraft.world.phys.shapes.Shapes
import net.minecraft.world.phys.shapes.VoxelShape
class TerrainPlantBlock(properties: Properties) : TallFlowerBlock(properties), EntityBlock {
init {
registerDefaultState(stateDefinition.any()
.setValue(HALF, DoubleBlockHalf.LOWER)
.setValue(GLOWING, false))
}
override fun createBlockStateDefinition(builder: StateDefinition.Builder<Block, BlockState>) {
builder.add(HALF, GLOWING)
}
override fun isValidBonemealTarget(level: LevelReader, pos: BlockPos, state: BlockState): Boolean {
return false
}
override fun tick(state: BlockState, level: ServerLevel, pos: BlockPos, random: RandomSource) {
if (state.`is`(CobblemonBlocks.LUCIBELL)) { this.checkInside(level, pos, state) }
}
private fun checkInside(level: Level, pos: BlockPos, state: BlockState) {
// Only lower will handle this
val upperState = level.getBlockState(pos.above()).takeIf { it.`is`(CobblemonBlocks.LUCIBELL) && it.getValue(HALF) == DoubleBlockHalf.UPPER }
val entityInside = level.getEntities(null, state.getShape(level, pos).bounds().move(pos)).isNotEmpty()
|| upperState?.let { level.getEntities(null, it.getShape(level, pos.above()).bounds().move(pos.above())).isNotEmpty() } ?: false
val glowingState = state.getValue(GLOWING)
if (glowingState != entityInside) {
level.setBlock(pos, state.setValue(GLOWING, entityInside), UPDATE_CLIENTS)
upperState?.let { level.setBlock(pos.above(), it.setValue(GLOWING, entityInside), UPDATE_CLIENTS) }
}
if (entityInside) level.scheduleTick(pos, this, 30) // tick occasionally to check if player inside
}
override fun entityInside(state: BlockState, level: Level, pos: BlockPos, entity: Entity) {
// Lucibell lower block will be our 'driver'
if (!level.isClientSide && !state.getValue(GLOWING) && state.`is`(CobblemonBlocks.LUCIBELL)) {
val halfState = state.getValue(HALF)
if (halfState == DoubleBlockHalf.UPPER) {
val lowerState = level.getBlockState(pos.below())
.takeIf { it.`is`(CobblemonBlocks.LUCIBELL) && it.getValue(HALF) == DoubleBlockHalf.LOWER }
?: return
(lowerState.block as TerrainPlantBlock).checkInside(level, pos.below(), lowerState)
} else {
checkInside(level, pos, state)
}
}
}
override fun getShape(state: BlockState, world: BlockGetter, pos: BlockPos, context: CollisionContext): VoxelShape = AABB
override fun newBlockEntity(pos: BlockPos, state: BlockState): BlockEntity {
return TerrainPlantBlockEntity(pos, state)
}
override fun <T : BlockEntity> getTicker(
level: Level,
state: BlockState,
blockEntityType: BlockEntityType<T?>
): BlockEntityTicker<T?>? {
return if (level.isClientSide) null else BaseEntityBlock.createTickerHelper(blockEntityType as BlockEntityType<*>,
CobblemonBlockEntities.TERRAIN_PLANT, TerrainPlantBlockEntity::serverTick) as BlockEntityTicker<T?>?
}
companion object {
private val AABB = Shapes.box(0.125, 0.0, 0.125, 0.875, 1.0, 0.875)
val GLOWING = BooleanProperty.create("glowing")
}
}
@@ -1,273 +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.block.decor
import com.cobblemon.mod.common.CobblemonBlocks
import com.cobblemon.mod.common.CobblemonItems
import com.cobblemon.mod.common.CobblemonSounds
import com.mojang.serialization.MapCodec
import com.mojang.serialization.codecs.RecordCodecBuilder
import net.minecraft.core.BlockPos
import net.minecraft.core.Direction
import net.minecraft.server.level.ServerLevel
import net.minecraft.util.RandomSource
import net.minecraft.util.StringRepresentable
import net.minecraft.world.entity.player.Player
import net.minecraft.world.item.Item
import net.minecraft.world.item.ItemStack
import net.minecraft.world.level.*
import net.minecraft.world.level.block.*
import net.minecraft.world.level.block.entity.BlockEntity
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.level.block.state.StateDefinition
import net.minecraft.world.level.block.state.properties.BlockStateProperties
import net.minecraft.world.level.block.state.properties.DoubleBlockHalf
import net.minecraft.world.level.block.state.properties.EnumProperty
import net.minecraft.world.level.block.state.properties.IntegerProperty
import net.minecraft.world.level.material.Fluids
import net.minecraft.world.phys.shapes.CollisionContext
import net.minecraft.world.phys.shapes.VoxelShape
@Suppress("OVERRIDE_DEPRECATION")
class TerrainSeedBlock(private val terrainSeedType: TerrainSeedType, settings: Properties) : CropBlock(settings) {
companion object {
val CODEC: MapCodec<TerrainSeedBlock> = RecordCodecBuilder.mapCodec { it.group(
TerrainSeedType.CODEC.fieldOf("terrainSeedType").forGetter(TerrainSeedBlock::terrainSeedType),
propertiesCodec()
).apply(it, ::TerrainSeedBlock) }
const val MATURE_AGE = 4
val AGE: IntegerProperty = IntegerProperty.create("age", 0, MATURE_AGE)
val HALF: EnumProperty<DoubleBlockHalf> = BlockStateProperties.DOUBLE_BLOCK_HALF
val AGE_TO_SHAPE = arrayOf(
box(2.0, 0.0, 2.0, 14.0, 6.0, 14.0), // Stage 0
box(2.0, 0.0, 2.0, 14.0, 16.0, 14.0), // Stage 1
box(2.0, 0.0, 2.0, 14.0, 16.0, 14.0), // Stage 2
box(2.0, 0.0, 2.0, 14.0, 16.0, 14.0), // Stage 3
box(2.0, 0.0, 2.0, 14.0, 16.0, 14.0), // Stage 4
)
val AGE_TO_SHAPE_TOP = arrayOf(
box(2.0, 0.0, 2.0, 14.0, 1.0, 14.0), // Stage 0
box(2.0, 0.0, 2.0, 14.0, 1.0, 14.0), // Stage 1
box(2.0, 0.0, 2.0, 14.0, 8.0, 14.0), // Stage 2
box(2.0, 0.0, 2.0, 14.0, 16.0, 14.0), // Stage 3
box(2.0, 0.0, 2.0, 14.0, 16.0, 14.0), // Stage 4
)
}
override fun codec(): MapCodec<out CropBlock> {
return CODEC
}
init {
this.registerDefaultState(
this.stateDefinition.any()
.setValue(AGE, 0)
.setValue(HALF, DoubleBlockHalf.LOWER)
)
}
override fun getAgeProperty(): IntegerProperty = AGE
override fun getMaxAge(): Int = MATURE_AGE
override fun createBlockStateDefinition(builder: StateDefinition.Builder<Block, BlockState>) {
builder.add(AGE, HALF)
}
override fun getBaseSeedId(): ItemLike = this.terrainSeedType.getSeed()
override fun getShape(state: BlockState, world: BlockGetter, pos: BlockPos, context: CollisionContext): VoxelShape =
if (state.getValue(HALF) == DoubleBlockHalf.UPPER) AGE_TO_SHAPE_TOP[getAge(state)] else AGE_TO_SHAPE[this.getAge(state)]
override fun randomTick(state: BlockState, world: ServerLevel, pos: BlockPos, random: RandomSource) {
if (state.getValue(HALF) == DoubleBlockHalf.UPPER || !canGrow(world, pos, state)) return
if (hasSufficientLight(world, pos) && random.nextInt(32) == 0) {
grow(world, pos, state, 1)
}
}
private fun canGrow(world: LevelReader, pos: BlockPos, state: BlockState): Boolean {
return !this.isMaxAge(state) && canSurvive(state, world, pos) && !(!canGrowInto(world, pos.above()) && state.getValue(AGE) >= 4)
}
private fun canGrowInto(world: LevelReader, pos: BlockPos): Boolean {
val blockState = world.getBlockState(pos)
return blockState.isAir || blockState.`is`(this.terrainSeedType.getCropBlock())
}
override fun updateShape(state: BlockState, direction: Direction, neighborState: BlockState, world: LevelAccessor, pos: BlockPos, neighborPos: BlockPos): BlockState {
return if (state.getValue(AGE) >= 2) {
super.updateShape(state, direction, neighborState, world, pos, neighborPos)
} else {
if (state.canSurvive(world, pos)) state else Blocks.AIR.defaultBlockState()
}
}
override fun isValidBonemealTarget(level: LevelReader, pos: BlockPos, state: BlockState): Boolean {
val posAndState = getLowerHalf(level, pos, state)
return if (posAndState == null) false else this.canGrow(
level,
posAndState.first,
posAndState.second
)
}
override fun performBonemeal(level: ServerLevel, random: RandomSource, pos: BlockPos, state: BlockState) {
val posAndState = getLowerHalf(level, pos, state)
if (posAndState != null) {
grow(level, posAndState.first, posAndState.second, getBonemealAgeIncrease(level))
}
}
fun grow(world: Level, pos: BlockPos, state: BlockState, increment: Int) {
if (!canGrow(world, pos, state)) return
val newAge = (this.getAge(state) + increment).coerceAtMost(this.maxAge)
world.setBlock(pos, state.setValue(this.ageProperty, newAge), UPDATE_CLIENTS)
val abovePos = pos.above()
val stateAbove = world.getBlockState(abovePos)
// Set top half depending on age
if (newAge >= (2)) {
if (stateAbove.isAir) {
world.setBlock(
abovePos,
this.defaultBlockState().setValue(AGE, newAge).setValue(HALF, DoubleBlockHalf.UPPER),
UPDATE_CLIENTS
)
}
else if (stateAbove.hasProperty(HALF) && stateAbove.getValue(HALF) == DoubleBlockHalf.UPPER) {
world.setBlock(
abovePos,
stateAbove.setValue(AGE, newAge),
UPDATE_CLIENTS
)
}
}
// Swaps blocks to the actual Flower when fully grown
if (newAge == this.maxAge) {
val nextStateLower = this.terrainSeedType.getFlowerBlock().defaultBlockState().setValue(HALF, DoubleBlockHalf.LOWER)
val nextStateUpper = this.terrainSeedType.getFlowerBlock().defaultBlockState().setValue(HALF, DoubleBlockHalf.UPPER)
world.setBlock(abovePos, nextStateUpper, UPDATE_CLIENTS)
world.setBlock(pos, nextStateLower, UPDATE_CLIENTS)
}
}
override fun getBonemealAgeIncrease(level: Level): Int {
return 1
}
override fun canSurvive(state: BlockState, world: LevelReader, pos: BlockPos): Boolean {
return when (state.getValue(HALF)) {
DoubleBlockHalf.LOWER -> {
val floorBlock = pos.below()
val floor = world.getBlockState(floorBlock)
floor.`is`(Blocks.FARMLAND)
}
DoubleBlockHalf.UPPER -> {
getLowerHalf(world, pos, state) != null
// Upper part survives only if the lower part is valid
}
}
}
private fun getLowerHalf(level: LevelReader, pos: BlockPos, state: BlockState): Pair<BlockPos, BlockState>? {
if (isLower(state)) {
return Pair(pos, state)
} else {
val blockPos = pos.below()
val blockState = level.getBlockState(blockPos)
return if (isLower(blockState)) Pair(
blockPos,
blockState
) else null
}
}
private fun isLower(state: BlockState): Boolean =
state.`is`(this.terrainSeedType.getCropBlock()) && state.getValue(HALF) == DoubleBlockHalf.LOWER
override fun getSoundType(state: BlockState): SoundType {
return CobblemonSounds.HEARTY_GRAINS_SOUNDS
}
@Suppress("unused")
enum class TerrainSeedType : StringRepresentable {
ELECTRIC,
PSYCHIC,
MISTY,
GRASSY;
fun getSeed(): Item = when (this) {
ELECTRIC -> CobblemonItems.ELECTRIC_SEED
PSYCHIC -> CobblemonItems.PSYCHIC_SEED
MISTY -> CobblemonItems.MISTY_SEED
GRASSY -> CobblemonItems.GRASSY_SEED
}
fun getCropBlock(): TerrainSeedBlock = when (this) {
ELECTRIC -> CobblemonBlocks.ELECTRIC_SEED
PSYCHIC -> CobblemonBlocks.PSYCHIC_SEED
MISTY -> CobblemonBlocks.MISTY_SEED
GRASSY -> CobblemonBlocks.GRASSY_SEED
}
fun getFlowerBlock(): Block = when (this) {
ELECTRIC -> CobblemonBlocks.VOLTACONIA
PSYCHIC -> CobblemonBlocks.LUCIBELL
MISTY -> CobblemonBlocks.MISTLEAF
GRASSY -> CobblemonBlocks.VERDROOT
}
override fun getSerializedName(): String = this.name.lowercase()
companion object {
val CODEC = StringRepresentable.fromEnum(::values)
}
}
override fun playerWillDestroy(level: Level, pos: BlockPos, state: BlockState, player: Player): BlockState {
if (!level.isClientSide) {
if (player.isCreative()) {
preventDropFromBottomPart(level, pos, state, player)
} else {
dropResources(state, level, pos, null as BlockEntity?, player, player.getMainHandItem())
}
}
return super.playerWillDestroy(level, pos, state, player)
}
override fun playerDestroy(
level: Level,
player: Player,
pos: BlockPos,
state: BlockState,
blockEntity: BlockEntity?,
tool: ItemStack
) {
super.playerDestroy(level, player, pos, Blocks.AIR.defaultBlockState(), blockEntity, tool)
}
fun preventDropFromBottomPart(level: Level, pos: BlockPos, state: BlockState, player: Player?) {
val doubleblockhalf = state.getValue<DoubleBlockHalf?>(DoublePlantBlock.HALF) as DoubleBlockHalf
if (doubleblockhalf == DoubleBlockHalf.UPPER) {
val blockpos = pos.below()
val blockstate = level.getBlockState(blockpos)
if (blockstate.`is`(state.getBlock()) && blockstate.getValue<DoubleBlockHalf?>(DoublePlantBlock.HALF) == DoubleBlockHalf.LOWER) {
val blockstate1 = if (blockstate.getFluidState()
.`is`(Fluids.WATER)
) Blocks.WATER.defaultBlockState() else Blocks.AIR.defaultBlockState()
level.setBlock(blockpos, blockstate1, 35)
level.levelEvent(player, 2001, blockpos, getId(blockstate))
}
}
}
}
@@ -1,80 +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.block.decor
import com.cobblemon.mod.common.CobblemonBlocks
import net.minecraft.core.BlockPos
import net.minecraft.core.Direction
import net.minecraft.world.item.context.BlockPlaceContext
import net.minecraft.world.level.LevelAccessor
import net.minecraft.world.level.LevelReader
import net.minecraft.world.level.block.Block
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.level.block.state.StateDefinition
import net.minecraft.world.level.block.state.properties.BlockStateProperties
import net.minecraft.world.level.block.state.properties.BooleanProperty
interface WistensuFlowerBlock {
fun additionalBlockStateDefinition(builder: StateDefinition.Builder<Block, BlockState>) {
builder.add(BlockStateProperties.FACING)
builder.add(ROOT)
}
fun adjustStateForPlacement(context: BlockPlaceContext, defaultState: BlockState): BlockState {
val upperBlock = context.level.getBlockState(context.clickedPos.relative(Direction.UP))
if (upperBlock.`is`(CobblemonBlocks.WISTENSU) || upperBlock.`is`(CobblemonBlocks.WISTENSU_VINE)) {
return defaultState
.setValue(BlockStateProperties.FACING, Direction.DOWN)
.setValue(ROOT, false)
} else {
var placeDirection = context.clickedFace
if (placeDirection == Direction.UP) placeDirection = Direction.DOWN
return defaultState
.setValue(BlockStateProperties.FACING, placeDirection)
.setValue(ROOT, true)
}
}
fun copyStateValues(target: BlockState, source: BlockState): BlockState {
return target
.setValue(ROOT, source.getValue(ROOT))
.setValue(BlockStateProperties.FACING, source.getValue(BlockStateProperties.FACING))
}
fun wisternCanSurvive(state: BlockState, level: LevelReader, pos: BlockPos): Boolean {
if (state.getValue(ROOT)) {
val facing = state.getValue(BlockStateProperties.FACING)
val targetPos = pos.relative(facing.opposite)
val directionState = level.getBlockState(targetPos)
return directionState.isFaceSturdy(level, targetPos, facing)
} else {
val topState = level.getBlockState(pos.relative(Direction.UP))
return topState.`is`(CobblemonBlocks.WISTENSU) || topState.`is`(CobblemonBlocks.WISTENSU_VINE)
}
}
fun wisternUpdateShape(state: BlockState, direction: Direction, neighborState: BlockState, level: LevelAccessor, pos: BlockPos, neighborPos: BlockPos, block: Block): BlockState? {
if (!wisternCanSurvive(state, level, pos)) {
level.scheduleTick(pos, block, 1)
}
if (direction == Direction.UP &&
(neighborState.`is`(CobblemonBlocks.WISTENSU) || neighborState.`is`(CobblemonBlocks.WISTENSU_VINE))) {
return state
.setValue(ROOT, false)
.setValue(BlockStateProperties.FACING, Direction.DOWN)
}
return null
}
companion object {
val ROOT = BooleanProperty.create("root")
}
}
@@ -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.block.decor
import com.cobblemon.mod.common.CobblemonBlocks
import com.mojang.serialization.MapCodec
import com.mojang.serialization.codecs.RecordCodecBuilder
import net.minecraft.core.BlockPos
import net.minecraft.core.Direction
import net.minecraft.server.level.ServerLevel
import net.minecraft.util.RandomSource
import net.minecraft.world.item.context.BlockPlaceContext
import net.minecraft.world.level.LevelAccessor
import net.minecraft.world.level.LevelReader
import net.minecraft.world.level.block.Block
import net.minecraft.world.level.block.GrowingPlantHeadBlock
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.level.block.state.StateDefinition
import net.minecraft.world.level.block.state.properties.BlockStateProperties
import net.minecraft.world.phys.shapes.VoxelShape
import kotlin.math.min
//TODO: Root/connection shit
class WistensuFlowerHeadBlock(properties: Properties?) : GrowingPlantHeadBlock(properties, Direction.DOWN, SHAPE, false, 0.1), WistensuFlowerBlock {
companion object {
val CODEC: MapCodec<WistensuFlowerHeadBlock> = RecordCodecBuilder.mapCodec {
it.group(propertiesCodec()).apply(it, ::WistensuFlowerHeadBlock)
}
val SHAPE: VoxelShape = box(1.0, 2.0, 1.0, 15.0, 16.0, 15.0)
}
override fun codec(): MapCodec<WistensuFlowerHeadBlock> = CODEC
override fun getBlocksToGrowWhenBonemealed(random: RandomSource): Int {
var d = 1.0
var i = 0
while (random.nextDouble() < d) {
d *= 0.826
++i
}
return i
}
override fun canGrowInto(state: BlockState): Boolean {
return state.isAir
}
override fun getBodyBlock(): Block {
return CobblemonBlocks.WISTENSU_VINE
}
override fun createBlockStateDefinition(builder: StateDefinition.Builder<Block, BlockState>) {
super.createBlockStateDefinition(builder)
additionalBlockStateDefinition(builder)
}
override fun updateShape(
state: BlockState,
direction: Direction,
neighborState: BlockState,
level: LevelAccessor,
pos: BlockPos,
neighborPos: BlockPos
): BlockState {
val updateState = wisternUpdateShape(state, direction, neighborState, level, pos, neighborPos, this)
if (updateState != null) return updateState
if (direction == Direction.DOWN &&
(neighborState.`is`(CobblemonBlocks.WISTENSU) || neighborState.`is`(CobblemonBlocks.WISTENSU_VINE))) {
return updateBodyAfterConvertedFromHead(state, bodyBlock.defaultBlockState())
}
return state
}
override fun getStateForPlacement(context: BlockPlaceContext): BlockState {
val defaultState = super.getStateForPlacement(context)!!
return adjustStateForPlacement(context, defaultState)
}
override fun getGrowIntoState(state: BlockState, random: RandomSource): BlockState {
return super.getGrowIntoState(state, random)
.setValue(WistensuFlowerBlock.ROOT, false)
.setValue(BlockStateProperties.FACING, Direction.DOWN)
}
override fun updateBodyAfterConvertedFromHead(head: BlockState, body: BlockState): BlockState {
return copyStateValues(body, head)
}
override fun performBonemeal(level: ServerLevel, random: RandomSource, pos: BlockPos, state: BlockState) {
var blockPos = pos.relative(growthDirection)
var i = min(state.getValue(AGE), MAX_AGE)
val j = getBlocksToGrowWhenBonemealed(random)
var currentState = state
for (k in 0..<j) {
if (!canGrowInto(level.getBlockState(blockPos))) break
level.setBlockAndUpdate(blockPos, getGrowIntoState(currentState, random))
currentState = level.getBlockState(blockPos)
blockPos = blockPos.relative(growthDirection)
i = min(i + 1, MAX_AGE)
}
}
override fun canSurvive(state: BlockState, level: LevelReader, pos: BlockPos): Boolean {
return wisternCanSurvive(state, level, pos)
}
}
@@ -1,77 +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.block.decor
import com.cobblemon.mod.common.CobblemonBlocks
import com.mojang.serialization.MapCodec
import com.mojang.serialization.codecs.RecordCodecBuilder
import net.minecraft.core.BlockPos
import net.minecraft.core.Direction
import net.minecraft.world.item.context.BlockPlaceContext
import net.minecraft.world.level.LevelAccessor
import net.minecraft.world.level.LevelReader
import net.minecraft.world.level.block.Block
import net.minecraft.world.level.block.GrowingPlantBodyBlock
import net.minecraft.world.level.block.GrowingPlantHeadBlock
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.level.block.state.StateDefinition
import net.minecraft.world.level.block.state.properties.BlockStateProperties
import net.minecraft.world.phys.shapes.VoxelShape
//TODO: Root/connection shit
class WistensuFlowerVineBlock(properties: Properties?) : GrowingPlantBodyBlock(properties, Direction.DOWN, SHAPE, false), WistensuFlowerBlock
{
companion object {
val CODEC: MapCodec<WistensuFlowerVineBlock> = RecordCodecBuilder.mapCodec {
it.group(propertiesCodec()).apply(it, ::WistensuFlowerVineBlock)
}
val SHAPE: VoxelShape = box(1.0, 0.0, 1.0, 15.0, 16.0, 15.0)
}
override fun codec(): MapCodec<WistensuFlowerVineBlock> = CODEC
override fun getHeadBlock(): GrowingPlantHeadBlock {
return CobblemonBlocks.WISTENSU
}
override fun createBlockStateDefinition(builder: StateDefinition.Builder<Block, BlockState>) {
super.createBlockStateDefinition(builder)
additionalBlockStateDefinition(builder)
}
override fun updateShape(
state: BlockState,
direction: Direction,
neighborState: BlockState,
level: LevelAccessor,
pos: BlockPos,
neighborPos: BlockPos
): BlockState {
val updateState = wisternUpdateShape(state, direction, neighborState, level, pos, neighborPos, this)
if (updateState != null) return updateState
if (direction == Direction.DOWN &&
!neighborState.`is`(CobblemonBlocks.WISTENSU) && !neighborState.`is`(CobblemonBlocks.WISTENSU_VINE)) {
return updateHeadAfterConvertedFromBody(state, headBlock.getStateForPlacement(level))
}
return state
}
override fun getStateForPlacement(context: BlockPlaceContext): BlockState {
val defaultState = super.getStateForPlacement(context)!!
return adjustStateForPlacement(context, defaultState)
}
override fun updateHeadAfterConvertedFromBody(head: BlockState, body: BlockState): BlockState {
return copyStateValues(body, head)
}
override fun canSurvive(state: BlockState, level: LevelReader, pos: BlockPos): Boolean {
return wisternCanSurvive(state, level, pos)
}
}
@@ -128,7 +128,6 @@ class DiscShelfBlockEntity(pos: BlockPos, state: BlockState) : BlockEntity(Cobbl
fun isValidItem(stack: ItemStack): Boolean {
return stack.item is TechnicalMachineItem
|| stack.item == CobblemonItems.BLANK_TM
|| stack.item == CobblemonItems.UPGRADE
|| stack.item == CobblemonItems.DUBIOUS_DISC
|| stack.has(DataComponents.JUKEBOX_PLAYABLE)
@@ -1,46 +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.block.entity
import com.cobblemon.mod.common.CobblemonBlockEntities
import com.cobblemon.mod.common.CobblemonBlocks
import com.cobblemon.mod.common.block.decor.MistleafFlowerBlock
import net.minecraft.core.BlockPos
import net.minecraft.world.level.Level
import net.minecraft.world.level.block.Block
import net.minecraft.world.level.block.entity.BlockEntity
import net.minecraft.world.level.block.state.BlockState
class MistleafFlowerBlockEntity(pos: BlockPos, state: BlockState): BlockEntity(CobblemonBlockEntities.MISTLEAF_FLOWER, pos, state) {
companion object {
fun serverTick(level: Level, pos: BlockPos, state: BlockState, mistleafFlowerBlockEntity: MistleafFlowerBlockEntity) {
if (level.isClientSide) return
if (state.block !is MistleafFlowerBlock) return
val glowing = state.getValue(MistleafFlowerBlock.GLOWING)
val wantedState = when (state.`is`(CobblemonBlocks.MISTLEAF)) {
//The light level and comparator output increases on odds from New to Full, and decreases on the evens from Full to New
((level.dayTime().toInt().mod(24000) in 12542..23459) && level.moonPhase == 4) -> 1 //New Moon
((level.dayTime().toInt().mod(24000) in 12542..23459) && level.moonPhase == 5) -> 3 //Waxing Crescent
((level.dayTime().toInt().mod(24000) in 12542..23459) && level.moonPhase == 6) -> 5 //First Quarter
((level.dayTime().toInt().mod(24000) in 12542..23459) && level.moonPhase == 7) -> 7 //Waxing Gibbous
((level.dayTime().toInt().mod(24000) in 12542..23459) && level.moonPhase == 0) -> 8 //Full Moon
((level.dayTime().toInt().mod(24000) in 12542..23459) && level.moonPhase == 1) -> 6 //Waning Gibbous
((level.dayTime().toInt().mod(24000) in 12542..23459) && level.moonPhase == 2) -> 4 // Third Quarter
((level.dayTime().toInt().mod(24000) in 12542..23459) && level.moonPhase == 3) -> 2 //Waning Crescent
else -> 0 //Daytime, and likely dimensions with no concept of day/night cycle or moon phase
}
if (glowing != wantedState) {
level.setBlock(pos, state.setValue(MistleafFlowerBlock.GLOWING, wantedState), Block.UPDATE_ALL)
}
}
}
}
@@ -1,37 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.block.entity
import com.cobblemon.mod.common.CobblemonBlockEntities
import com.cobblemon.mod.common.CobblemonBlocks
import com.cobblemon.mod.common.block.decor.TerrainPlantBlock
import net.minecraft.core.BlockPos
import net.minecraft.world.level.Level
import net.minecraft.world.level.block.Block
import net.minecraft.world.level.block.entity.BlockEntity
import net.minecraft.world.level.block.state.BlockState
class TerrainPlantBlockEntity(pos: BlockPos, state: BlockState): BlockEntity(CobblemonBlockEntities.TERRAIN_PLANT, pos, state) {
companion object {
fun serverTick(level: Level, pos: BlockPos, state: BlockState, terrainPlantBlockEntity: TerrainPlantBlockEntity) {
if (level.isClientSide) return
if (state.block !is TerrainPlantBlock) return
val glowing = state.getValue(TerrainPlantBlock.GLOWING)
val wantedState = if (state.`is`(CobblemonBlocks.VOLTACONIA)) level.isThundering
else if (state.`is`(CobblemonBlocks.VERDROOT)) level.isRaining && !level.isThundering
else null // We dont control glowing here
if (wantedState != null && glowing != wantedState) {
level.setBlock(pos, state.setValue(TerrainPlantBlock.GLOWING, wantedState), Block.UPDATE_CLIENTS)
}
}
}
}
@@ -1,133 +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.block.general
import com.cobblemon.mod.common.util.rotateShape
import com.mojang.serialization.MapCodec
import com.mojang.serialization.codecs.RecordCodecBuilder
import net.minecraft.core.BlockPos
import net.minecraft.core.Direction
import net.minecraft.world.item.context.BlockPlaceContext
import net.minecraft.world.level.BlockGetter
import net.minecraft.world.level.LevelAccessor
import net.minecraft.world.level.LevelReader
import net.minecraft.world.level.block.*
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.level.block.state.StateDefinition
import net.minecraft.world.level.block.HorizontalDirectionalBlock.FACING
import net.minecraft.world.level.block.state.properties.BlockStateProperties.HORIZONTAL_FACING
import net.minecraft.world.level.block.state.properties.IntegerProperty
import net.minecraft.world.phys.shapes.CollisionContext
import net.minecraft.world.phys.shapes.Shapes
import net.minecraft.world.phys.shapes.VoxelShape
import kotlin.math.min
class MintLitterBlock(properties: Properties?) : Block(properties) {
init {
registerDefaultState(stateDefinition.any()
.setValue(FACING, Direction.NORTH)
.setValue(AMOUNT, 1))
}
override fun getStateForPlacement(context: BlockPlaceContext): BlockState? {
val blockState = context.getLevel().getBlockState(context.getClickedPos())
return if (blockState.`is`(this)) blockState.setValue<Int?, Int?>(
AMOUNT,
min(4, blockState.getValue<Int?>(AMOUNT) as Int + 1)
) as BlockState else this.defaultBlockState().setValue<Direction?, Direction?>(
FACING,
context.getHorizontalDirection().getOpposite()
) as BlockState
}
override fun createBlockStateDefinition(builder: StateDefinition.Builder<Block, BlockState>) {
builder.add(FACING)
builder.add(AMOUNT)
}
override fun updateShape(
state: BlockState,
direction: Direction?,
neighborState: BlockState?,
level: LevelAccessor?,
pos: BlockPos?,
neighborPos: BlockPos?
): BlockState {
return if (!state.canSurvive(level, pos)) Blocks.AIR.defaultBlockState() else super.updateShape(
state,
direction,
neighborState,
level,
pos,
neighborPos
)
}
override fun canSurvive(state: BlockState?, level: LevelReader, pos: BlockPos): Boolean {
val posBelow = pos.below()
return level.getBlockState(posBelow).isFaceSturdy(level, posBelow, Direction.UP)
}
override fun propagatesSkylightDown(state: BlockState, level: BlockGetter?, pos: BlockPos?): Boolean {
return state.getFluidState().isEmpty()
}
public override fun canBeReplaced(state: BlockState, useContext: BlockPlaceContext): Boolean {
return if (!useContext.isSecondaryUseActive && useContext.itemInHand.`is`(this.asItem()) && state.getValue(
AMOUNT
) <= MAX_AMOUNT
) true else super.canBeReplaced(state, useContext)
}
@Deprecated("Deprecated in Java")
override fun rotate(state: BlockState, rotation: Rotation): BlockState {
return state.setValue(HORIZONTAL_FACING, rotation.rotate(state.getValue(HORIZONTAL_FACING)))
}
override fun getShape(state: BlockState, level: BlockGetter, pos: BlockPos, context: CollisionContext): VoxelShape? {
val litterBoxes = when (state.getValue(FACING)) {
Direction.SOUTH -> SOUTH_SHAPES
Direction.WEST -> WEST_SHAPES
Direction.EAST -> EAST_SHAPES
else -> NORTH_SHAPES
}
var litterBox = litterBoxes[state.getValue(AMOUNT)]
return litterBox
}
companion object {
val CODEC: MapCodec<MintLitterBlock> = RecordCodecBuilder.mapCodec {
it.group(
propertiesCodec()
).apply(it, ::MintLitterBlock) }
val AMOUNT = IntegerProperty.create("amount", 1, 4)
const val MAX_AMOUNT: Int = 4
private val NORTH_SHAPES = listOf(
Shapes.box(0.0, 0.0, 0.0, 1.0, 0.125, 1.0),
Shapes.box(0.0, 0.0, 0.0, 0.5, 0.0625, 0.5),
Shapes.box(0.0, 0.0, 0.0, 0.5, 0.0625, 1.0),
Shapes.or(
Shapes.box(0.0, 0.0, 0.0, 0.5, 0.0625, 1.0),
Shapes.box(0.5, 0.0, 0.5, 1.0, 0.0625, 1.0)
),
Shapes.box(0.0, 0.0, 0.0, 1.0, 0.0625, 1.0)
)
private val SOUTH_SHAPES = createShapes(Direction.SOUTH)
private val WEST_SHAPES = createShapes(Direction.WEST)
private val EAST_SHAPES = createShapes(Direction.EAST)
private fun createShapes(to: Direction): List<VoxelShape> {
return NORTH_SHAPES.map { rotateShape(Direction.NORTH, to, it) }
}
}
}
@@ -31,7 +31,6 @@ import com.cobblemon.mod.common.client.sound.instances.CancellableSoundInstance
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.item.PokeBallItem
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.pokemon.activestate.SentOutState
import com.cobblemon.mod.common.util.DataKeys
import com.cobblemon.mod.common.util.giveOrDropItemStack
import com.cobblemon.mod.common.util.lang
@@ -236,7 +235,6 @@ class FossilMultiblockStructure (
entity.setPos(fixedPosition.center.subtract(0.0, 0.5, 0.0))
// TODO: Find a correct way to set the new entity's Yaw rotation. (Face away from the machine)
if (world.addFreshEntity(entity)) {
pokemon.state = SentOutState(entity)
CobblemonEvents.FOSSIL_REVIVED.post(FossilRevivedEvent(pokemon, null))
return true
} else {
@@ -30,6 +30,10 @@ import com.cobblemon.mod.common.client.gui.RideControlsOverlay
import com.cobblemon.mod.common.client.gui.battle.BattleOverlay
import com.cobblemon.mod.common.client.gui.cookingpot.CookingPotScreen
import com.cobblemon.mod.common.client.gui.party.PartyTutorialToasts
import com.cobblemon.mod.common.api.molangui.MolangUiRegistry
import com.cobblemon.mod.common.client.gui.molangui.MolangUiClientManager
import com.cobblemon.mod.common.client.gui.molangui.renderer.WidgetRendererRegistry
import com.cobblemon.mod.common.client.net.molangui.MolangUiCompatibilityRequestHandler
import com.cobblemon.mod.common.client.gui.tmmachine.TMMachineScreen
import com.cobblemon.mod.common.client.particle.BedrockParticleOptionsRepository
import com.cobblemon.mod.common.client.render.ClientPlayerIcon
@@ -45,7 +49,6 @@ import com.cobblemon.mod.common.client.render.entity.PokeBobberEntityRenderer
import com.cobblemon.mod.common.client.render.generic.GenericBedrockRenderer
import com.cobblemon.mod.common.client.render.item.CobblemonBuiltinItemRendererRegistry
import com.cobblemon.mod.common.client.render.item.PokemonItemRenderer
import com.cobblemon.mod.common.client.render.layer.PlayerRecallBallRenderer
import com.cobblemon.mod.common.client.render.layer.PokemonOnShoulderRenderer
import com.cobblemon.mod.common.client.render.models.blockbench.bedrock.animation.BedrockAnimationRepository
import com.cobblemon.mod.common.client.render.models.blockbench.repository.BerryModelRepository
@@ -123,6 +126,8 @@ object CobblemonClient {
}
fun onLogout() {
MolangUiClientManager.cleanup()
MolangUiCompatibilityRequestHandler.reset()
storage.onLogout()
battle = null
battleOverlay.onLogout()
@@ -164,6 +169,8 @@ object CobblemonClient {
}
PlatformEvents.CLIENT_ENTITY_UNLOAD.subscribe { event -> EntitySoundTracker.clear(event.entity.id) }
WidgetRendererRegistry.registerDefaults()
PlatformEvents.CLIENT_TICK_POST.subscribe { MolangUiClientManager.tick() }
PlatformEvents.CLIENT_TICK_POST.subscribe { event ->
val player = event.client.player
if (player != null) {
@@ -391,7 +398,7 @@ object CobblemonClient {
CobblemonBlocks.X_SP_ATK,
CobblemonBlocks.X_SP_DEF,
CobblemonBlocks.X_SPEED,
CobblemonBlocks.TM_MACHINE,
CobblemonBlocks.TM_MACHINE,
CobblemonBlocks.GALARICA_CUFF,
CobblemonBlocks.GALARICA_WREATH,
@@ -415,108 +422,7 @@ object CobblemonClient {
CobblemonBlocks.NEVER_MELT_ICE,
CobblemonBlocks.WIDE_LENS,
CobblemonBlocks.ZOOM_LENS,
CobblemonBlocks.SCOPE_LENS,
CobblemonBlocks.MOOMOO_MILK,
CobblemonBlocks.MEDICINAL_BREW,
CobblemonBlocks.TERRAIN_EXTENDER,
CobblemonBlocks.RED_MINT_LEAF_LITTER,
CobblemonBlocks.POTTED_RED_MINT,
CobblemonBlocks.BLUE_MINT_LEAF_LITTER,
CobblemonBlocks.POTTED_BLUE_MINT,
CobblemonBlocks.CYAN_MINT_LEAF_LITTER,
CobblemonBlocks.POTTED_CYAN_MINT,
CobblemonBlocks.PINK_MINT_LEAF_LITTER,
CobblemonBlocks.POTTED_PINK_MINT,
CobblemonBlocks.GREEN_MINT_LEAF_LITTER,
CobblemonBlocks.POTTED_GREEN_MINT,
CobblemonBlocks.WHITE_MINT_LEAF_LITTER,
CobblemonBlocks.POTTED_WHITE_MINT,
CobblemonBlocks.LOBAILA,
CobblemonBlocks.POMOHANA,
CobblemonBlocks.PAUVILLEA,
CobblemonBlocks.WISTENSU,
CobblemonBlocks.WISTENSU_VINE,
CobblemonBlocks.ELECTRIC_SEED,
CobblemonBlocks.VOLTACONIA,
CobblemonBlocks.GRASSY_SEED,
CobblemonBlocks.VERDROOT,
CobblemonBlocks.MISTY_SEED,
CobblemonBlocks.MISTLEAF,
CobblemonBlocks.PSYCHIC_SEED,
CobblemonBlocks.LUCIBELL,
CobblemonBlocks.POTTED_AGUAV_BERRY,
CobblemonBlocks.POTTED_APICOT_BERRY,
CobblemonBlocks.POTTED_ASPEAR_BERRY,
CobblemonBlocks.POTTED_BABIRI_BERRY,
CobblemonBlocks.POTTED_BELUE_BERRY,
CobblemonBlocks.POTTED_BLUK_BERRY,
CobblemonBlocks.POTTED_CHARTI_BERRY,
CobblemonBlocks.POTTED_CHERI_BERRY,
CobblemonBlocks.POTTED_CHESTO_BERRY,
CobblemonBlocks.POTTED_CHILAN_BERRY,
CobblemonBlocks.POTTED_CHOPLE_BERRY,
CobblemonBlocks.POTTED_COBA_BERRY,
CobblemonBlocks.POTTED_COLBUR_BERRY,
CobblemonBlocks.POTTED_CORNN_BERRY,
CobblemonBlocks.POTTED_CUSTAP_BERRY,
CobblemonBlocks.POTTED_DURIN_BERRY,
CobblemonBlocks.POTTED_EGGANT_BERRY,
CobblemonBlocks.POTTED_ENIGMA_BERRY,
CobblemonBlocks.POTTED_FIGY_BERRY,
CobblemonBlocks.POTTED_GANLON_BERRY,
CobblemonBlocks.POTTED_GREPA_BERRY,
CobblemonBlocks.POTTED_HABAN_BERRY,
CobblemonBlocks.POTTED_HONDEW_BERRY,
CobblemonBlocks.POTTED_HOPO_BERRY,
CobblemonBlocks.POTTED_IAPAPA_BERRY,
CobblemonBlocks.POTTED_JABOCA_BERRY,
CobblemonBlocks.POTTED_KASIB_BERRY,
CobblemonBlocks.POTTED_KEBIA_BERRY,
CobblemonBlocks.POTTED_KEE_BERRY,
CobblemonBlocks.POTTED_KELPSY_BERRY,
CobblemonBlocks.POTTED_LANSAT_BERRY,
CobblemonBlocks.POTTED_LEPPA_BERRY,
CobblemonBlocks.POTTED_LIECHI_BERRY,
CobblemonBlocks.POTTED_LUM_BERRY,
CobblemonBlocks.POTTED_MAGOST_BERRY,
CobblemonBlocks.POTTED_MAGO_BERRY,
CobblemonBlocks.POTTED_MARANGA_BERRY,
CobblemonBlocks.POTTED_MICLE_BERRY,
CobblemonBlocks.POTTED_NANAB_BERRY,
CobblemonBlocks.POTTED_NOMEL_BERRY,
CobblemonBlocks.POTTED_OCCA_BERRY,
CobblemonBlocks.POTTED_ORAN_BERRY,
CobblemonBlocks.POTTED_PAMTRE_BERRY,
CobblemonBlocks.POTTED_PASSHO_BERRY,
CobblemonBlocks.POTTED_PAYAPA_BERRY,
CobblemonBlocks.POTTED_PECHA_BERRY,
CobblemonBlocks.POTTED_PERSIM_BERRY,
CobblemonBlocks.POTTED_PETAYA_BERRY,
CobblemonBlocks.POTTED_PINAP_BERRY,
CobblemonBlocks.POTTED_POMEG_BERRY,
CobblemonBlocks.POTTED_QUALOT_BERRY,
CobblemonBlocks.POTTED_RABUTA_BERRY,
CobblemonBlocks.POTTED_RAWST_BERRY,
CobblemonBlocks.POTTED_RAZZ_BERRY,
CobblemonBlocks.POTTED_RINDO_BERRY,
CobblemonBlocks.POTTED_ROSELI_BERRY,
CobblemonBlocks.POTTED_ROWAP_BERRY,
CobblemonBlocks.POTTED_SALAC_BERRY,
CobblemonBlocks.POTTED_SHUCA_BERRY,
CobblemonBlocks.POTTED_SITRUS_BERRY,
CobblemonBlocks.POTTED_SPELON_BERRY,
CobblemonBlocks.POTTED_STARF_BERRY,
CobblemonBlocks.POTTED_TAMATO_BERRY,
CobblemonBlocks.POTTED_TANGA_BERRY,
CobblemonBlocks.POTTED_TOUGA_BERRY,
CobblemonBlocks.POTTED_WACAN_BERRY,
CobblemonBlocks.POTTED_WATMEL_BERRY,
CobblemonBlocks.POTTED_WEPEAR_BERRY,
CobblemonBlocks.POTTED_WIKI_BERRY,
CobblemonBlocks.POTTED_YACHE_BERRY
CobblemonBlocks.SCOPE_LENS
)
this.createBoatModelLayers()
@@ -525,12 +431,15 @@ object CobblemonClient {
fun beforeChatRender(context: GuiGraphics, partialDeltaTicks: Float) {
val partialDeltaTicks = Minecraft.getInstance().timer // Checking that this even works
// ClientTaskTracker.update(partialDeltaTicks / 20f)
if (battle == null) {
overlay.render(context, partialDeltaTicks)
} else {
battleOverlay.render(context, partialDeltaTicks)
if (!MolangUiClientManager.hasExclusiveOverlay()) {
if (battle == null) {
overlay.render(context, partialDeltaTicks)
} else {
battleOverlay.render(context, partialDeltaTicks)
}
rideControlsOverlay.render(context, partialDeltaTicks)
}
rideControlsOverlay.render(context, partialDeltaTicks)
MolangUiClientManager.renderOverlays(context, partialDeltaTicks.getGameTimeDeltaPartialTick(false))
}
@Suppress("UNCHECKED_CAST")
@@ -538,14 +447,15 @@ object CobblemonClient {
var renderer: LivingEntityRenderer<Player, PlayerModel<Player>>? =
skinMap?.get(PlayerSkin.Model.WIDE) as LivingEntityRenderer<Player, PlayerModel<Player>>
renderer?.addLayer(PokemonOnShoulderRenderer(renderer))
renderer?.addLayer(PlayerRecallBallRenderer(renderer))
renderer = skinMap[PlayerSkin.Model.SLIM] as LivingEntityRenderer<Player, PlayerModel<Player>>?
renderer?.addLayer(PokemonOnShoulderRenderer(renderer))
renderer?.addLayer(PlayerRecallBallRenderer(renderer))
}
private fun registerMenuScreens() {
MenuScreens.register(CobblemonMenuType.COOKING_POT, ::CookingPotScreen)
MenuScreens.register(CobblemonMenuType.MOLANG_UI) { menu, playerInventory, _ ->
MolangUiClientManager.createContainerScreen(menu, playerInventory)
}
MenuScreens.register(CobblemonMenuType.TM_MACHINE, ::TMMachineScreen)
}
@@ -614,6 +524,8 @@ object CobblemonClient {
BerryModelRepository.reload(resourceManager)
MiscModelRepository.reload(resourceManager)
MolangUiRegistry.reload(resourceManager)
MolangUiClientManager.onResourceReload()
LOGGER.info("Loaded assets")
}
@@ -32,7 +32,6 @@ import com.cobblemon.mod.common.client.render.models.blockbench.PosableState
import com.cobblemon.mod.common.client.render.models.blockbench.animation.ActiveAnimation
import com.cobblemon.mod.common.client.render.models.blockbench.animation.PrimaryAnimation
import com.cobblemon.mod.common.client.render.models.blockbench.repository.VaryingModelRepository
import com.cobblemon.mod.common.client.render.player.RecallArmAnimationTracker
import com.cobblemon.mod.common.client.render.pokemon.PokemonRenderer.Companion.ease
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.net.messages.server.pokemon.update.ServerboundUpdateRidingStatePacket
@@ -321,7 +320,6 @@ class PokemonClientDelegate : PosableState(), PokemonSideDelegate {
ballOffset = 0f
ballRotOffset = 0f
sendOutPosition = null
currentEntity.ownerUUID?.let { RecallArmAnimationTracker.start(it, currentEntity.exposedBall) }
afterOnClient(seconds = BEAM_EXTEND_TIME) {
activeSendoutScale = 1F
@@ -71,7 +71,7 @@ open class VariableObjectSelectionList(x: Int, y: Int, width: Int, height: Int):
var stackHeight = -scrollAmount.toInt()
guiGraphics.enableScissor(x, y, x + width, y + height)
children.forEachIndexed { index, entry ->
if (stackHeight < height && stackHeight + entry.height > 0) {
if (stackHeight in 1..<height || stackHeight + entry.height in 1..<height) {
entry.render(guiGraphics, index, x, y + stackHeight, mouseX, mouseY, partialTick)
}
stackHeight += entry.height
@@ -0,0 +1,193 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.molangui
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.molangui.GuiMode
import com.cobblemon.mod.common.api.molangui.MolangUiDefinition
import com.cobblemon.mod.common.api.molangui.MolangUiParameters
import com.cobblemon.mod.common.api.molangui.MolangUiRegistry
import com.cobblemon.mod.common.api.molangui.container.MolangContainerMenu
import com.cobblemon.mod.common.api.molangui.container.SlotLayout
import com.cobblemon.mod.common.client.net.molangui.MolangUiCompatibilityRequestHandler
import com.cobblemon.mod.common.net.messages.server.molangui.MolangUiClosedPacket
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.screens.Screen
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen
import net.minecraft.resources.ResourceLocation
import net.minecraft.world.entity.player.Inventory
import java.util.UUID
object MolangUiClientManager {
private class Session(
var definition: MolangUiDefinition,
val tree: MolangUiWidgetTree,
val slotLayouts: Map<String, SlotLayout>,
val preparedAtNanos: Long = System.nanoTime()
) {
var screen: Screen? = null
val overlay = if (definition.mode == GuiMode.OVERLAY) {
MolangUiOverlay(tree, definition.exclusive)
} else null
}
private val sessions = linkedMapOf<UUID, Session>()
private var pendingContainerId: UUID? = null
fun open(
sessionId: UUID, guiId: ResourceLocation, contractVersion: String, mode: GuiMode, args: Map<String, String>,
exclusive: Boolean = false,
hasContainer: Boolean = false, slotLayouts: Map<String, SlotLayout> = emptyMap()
) {
if (sessionId in sessions) return
tick()
val definition = MolangUiRegistry.getDefinition(guiId)
if (definition == null || definition.contractVersion != contractVersion || definition.mode != mode ||
definition.exclusive != exclusive ||
(hasContainer && mode != GuiMode.SCREEN)) {
Cobblemon.LOGGER.error("Rejected incompatible Molang UI open for '{}'", guiId)
notifyClosed(sessionId)
return
}
if (mode == GuiMode.OVERLAY && sessions.values.count { it.overlay != null } >= 8) {
notifyClosed(sessionId)
return
}
val tree = try {
MolangUiWidgetTree(guiId, sessionId, definition.root,
MolangUiParameters.decode(definition.parameters, args), definition.variables)
} catch (error: Exception) {
Cobblemon.LOGGER.error("Failed to prepare Molang UI '{}'", guiId, error)
notifyClosed(sessionId)
return
}
if (mode == GuiMode.SCREEN) {
sessions.filterValues { it.definition.mode == GuiMode.SCREEN }.keys.toList().forEach { close(it, true) }
}
val session = Session(definition, tree, slotLayouts.toMap())
sessions[sessionId] = session
if (mode == GuiMode.SCREEN) {
if (hasContainer) {
pendingContainerId = sessionId
} else {
val screen = MolangUiScreen(sessionId, tree)
session.screen = screen
val client = Minecraft.getInstance()
client.execute {
if (sessions[sessionId] === session) client.setScreen(screen)
}
}
}
}
fun createContainerScreen(menu: MolangContainerMenu, playerInventory: Inventory): AbstractContainerScreen<MolangContainerMenu> {
tick()
val id = pendingContainerId
pendingContainerId = null
val session = id?.let(sessions::get)
?: return RejectedMolangUiContainerScreen(menu, playerInventory)
return try {
menu.initializeClient(session.slotLayouts)
MolangUiContainerScreen(menu, playerInventory, session.tree.sessionId, session.tree).also { session.screen = it }
} catch (error: Exception) {
Cobblemon.LOGGER.error("Failed to prepare Molang UI container '{}'", session.tree.guiId, error)
close(session.tree.sessionId, true)
RejectedMolangUiContainerScreen(menu, playerInventory)
}
}
fun close(sessionId: UUID) = close(sessionId, false)
private fun close(sessionId: UUID, notifyServer: Boolean) {
val session = sessions.remove(sessionId) ?: return
if (pendingContainerId == sessionId) pendingContainerId = null
session.tree.cleanup()
if (notifyServer) notifyClosed(sessionId)
val screen = session.screen ?: return
val client = Minecraft.getInstance()
client.execute {
if (client.screen === screen) {
if (screen is MolangUiContainerScreen && client.player?.containerMenu === screen.menu) {
if (notifyServer) client.player?.closeContainer() else client.player?.clientSideCloseContainer()
} else client.setScreen(null)
}
}
}
internal fun onScreenRemoved(sessionId: UUID, screen: Screen, tree: MolangUiWidgetTree) {
tree.cleanup()
if (sessions[sessionId]?.screen !== screen) return
sessions.remove(sessionId)
notifyClosed(sessionId)
}
fun updateArgs(sessionId: UUID, args: Map<String, String>, variables: Map<String, Double> = emptyMap()) {
val session = sessions[sessionId] ?: return
try {
val parameters = MolangUiParameters.decode(session.definition.parameters, args)
MolangUiParameters.validateVariables(variables)
if (variables.isNotEmpty()) session.tree.updateVariables(variables)
if (parameters.isNotEmpty()) session.tree.updateParameters(parameters)
} catch (error: Exception) {
Cobblemon.LOGGER.warn("Rejected invalid Molang UI update for '{}'", session.tree.guiId, error)
close(sessionId, true)
}
}
fun renderOverlays(guiGraphics: GuiGraphics, delta: Float) {
sessions.values.toList().forEach { it.overlay?.render(guiGraphics, delta) }
}
fun cleanup() {
val old = sessions.values.toList()
sessions.clear()
pendingContainerId = null
old.forEach { it.tree.cleanup() }
}
fun hasExclusiveOverlay(): Boolean = sessions.values.any { it.overlay?.exclusive == true }
fun onResourceReload() {
for ((id, session) in sessions.toMap()) {
val previous = session.definition
val definition = MolangUiRegistry.getDefinition(session.tree.guiId)
if (definition == null || definition.contractVersion != previous.contractVersion ||
definition.mode != previous.mode ||
definition.exclusive != previous.exclusive || definition.parameters != previous.parameters ||
definition.actions != previous.actions) {
close(id, true)
continue
}
try {
session.tree.rebuildFromDefinition(definition)
session.definition = definition
} catch (error: Exception) {
Cobblemon.LOGGER.error("Failed to reload Molang UI '{}'", session.tree.guiId, error)
close(id, true)
}
}
MolangUiCompatibilityRequestHandler.respond()
}
fun tick(nowNanos: Long = System.nanoTime()) {
val id = pendingContainerId ?: return
val session = sessions[id] ?: return
if (nowNanos - session.preparedAtNanos > 5_000_000_000L) close(id, true)
}
private fun notifyClosed(sessionId: UUID) {
if (Minecraft.getInstance().connection == null) return
try {
MolangUiClosedPacket(sessionId).sendToServer()
} catch (error: Exception) {
Cobblemon.LOGGER.warn("Failed to notify server of closed Molang UI '{}'", sessionId, error)
}
}
}
@@ -0,0 +1,64 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.molangui
import com.bedrockk.molang.runtime.MoLangRuntime
import com.bedrockk.molang.runtime.value.DoubleValue
import com.bedrockk.molang.runtime.value.StringValue
import com.cobblemon.mod.common.api.molang.function.GeneralMoLangFunctions
import com.cobblemon.mod.common.api.molangui.MolangUiParameters
import com.cobblemon.mod.common.api.molangui.ParameterValue
import com.cobblemon.mod.common.api.molangui.asMoLangValue
import net.minecraft.client.resources.language.I18n
import net.minecraft.core.registries.BuiltInRegistries
import net.minecraft.resources.ResourceLocation
internal object MolangUiClientRuntimeFactory {
fun create(guiId: ResourceLocation, parameters: Map<String, ParameterValue>, variables: Map<String, Double>) =
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("gui_id") { StringValue(guiId.toString()) }
runtime.environment.query.addFunction("translate") { params ->
StringValue(I18n.get(params.getString(0), *params.params.drop(1).map { it.asString() }.toTypedArray()))
}
runtime.environment.query.addFunction("item_name") { params ->
val id = ResourceLocation.tryParse(params.getString(0))
val item = id?.takeIf(BuiltInRegistries.ITEM::containsKey)?.let(BuiltInRegistries.ITEM::get)
StringValue(item?.description?.string ?: "")
}
updateParameters(runtime, parameters)
updateVariables(runtime, variables)
}
fun updateFrame(runtime: MoLangRuntime, guiTime: Float, delta: Float, width: Int, height: Int, mouseX: Double, mouseY: Double) {
runtime.environment.query.addFunction("gui_time") { DoubleValue(guiTime.toDouble()) }
runtime.environment.query.addFunction("delta") { DoubleValue(delta.toDouble()) }
runtime.environment.query.addFunction("screen_width") { DoubleValue(width.toDouble()) }
runtime.environment.query.addFunction("screen_height") { DoubleValue(height.toDouble()) }
runtime.environment.query.addFunction("mouse_x") { DoubleValue(mouseX) }
runtime.environment.query.addFunction("mouse_y") { DoubleValue(mouseY) }
}
fun updateParameters(runtime: MoLangRuntime, parameters: Map<String, ParameterValue>) {
MolangUiParameters.validate(parameters)
parameters.forEach { (key, value) ->
val resolved = value.asMoLangValue()
runtime.environment.query.addFunction(key) { resolved }
}
}
fun updateVariables(runtime: MoLangRuntime, variables: Map<String, Double>) {
MolangUiParameters.validateVariables(variables)
variables.forEach { (key, value) ->
runtime.environment.setSimpleVariable(key.removePrefix("v."), DoubleValue(value))
}
}
}
@@ -0,0 +1,79 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.molangui
import com.cobblemon.mod.common.api.molangui.container.MolangContainerMenu
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen
import net.minecraft.network.chat.Component
import net.minecraft.world.entity.player.Inventory
import java.util.UUID
class MolangUiContainerScreen(
menu: MolangContainerMenu,
playerInventory: Inventory,
val sessionId: UUID,
val widgetTree: MolangUiWidgetTree
) : AbstractContainerScreen<MolangContainerMenu>(menu, playerInventory, Component.empty()) {
private val widgetButtons = mutableSetOf<Int>()
init {
titleLabelX = Int.MAX_VALUE
titleLabelY = Int.MAX_VALUE
inventoryLabelX = Int.MAX_VALUE
inventoryLabelY = Int.MAX_VALUE
}
override fun render(guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
widgetTree.render(guiGraphics, width, height, mouseX.toDouble(), mouseY.toDouble(), delta)
super.render(guiGraphics, mouseX, mouseY, delta)
}
override fun renderBg(guiGraphics: GuiGraphics, delta: Float, mouseX: Int, mouseY: Int) {
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (menu.slots.any { it.isActive && isHovering(it.x, it.y, 16, 16, mouseX, mouseY) }) {
return super.mouseClicked(mouseX, mouseY, button)
}
if (widgetTree.onClick(mouseX, mouseY, button)) {
widgetButtons.add(button)
return true
}
return super.mouseClicked(mouseX, mouseY, button)
}
override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean =
widgetButtons.remove(button) || super.mouseReleased(mouseX, mouseY, button)
override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, dragX: Double, dragY: Double): Boolean =
button in widgetButtons || super.mouseDragged(mouseX, mouseY, button, dragX, dragY)
override fun mouseScrolled(mouseX: Double, mouseY: Double, horizontalAmount: Double, verticalAmount: Double): Boolean {
val handled = widgetTree.onScroll(mouseX, mouseY, verticalAmount)
return handled || super.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount)
}
override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
val handled = widgetTree.onKeyPress(keyCode, scanCode, modifiers)
if (handled) return true
return super.keyPressed(keyCode, scanCode, modifiers)
}
override fun removed() {
try {
super.removed()
} finally {
widgetButtons.clear()
MolangUiClientManager.onScreenRemoved(sessionId, this, widgetTree)
}
}
override fun isPauseScreen(): Boolean = false
}
@@ -0,0 +1,28 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.molangui
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
class MolangUiOverlay(
private val widgetTree: MolangUiWidgetTree,
val exclusive: Boolean
) {
fun render(guiGraphics: GuiGraphics, delta: Float) {
val mc = Minecraft.getInstance()
val screenWidth = mc.window.guiScaledWidth
val screenHeight = mc.window.guiScaledHeight
if (screenWidth <= 0 || screenHeight <= 0 || mc.window.screenWidth <= 0 || mc.window.screenHeight <= 0) return
val mouseX = (mc.mouseHandler.xpos() * screenWidth / mc.window.screenWidth)
val mouseY = (mc.mouseHandler.ypos() * screenHeight / mc.window.screenHeight)
widgetTree.render(guiGraphics, screenWidth, screenHeight, mouseX, mouseY, delta)
}
}
@@ -0,0 +1,50 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.molangui
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.screens.Screen
import net.minecraft.network.chat.Component
import java.util.UUID
class MolangUiScreen(
val sessionId: UUID,
val widgetTree: MolangUiWidgetTree
) : Screen(Component.empty()) {
override fun render(guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
widgetTree.render(guiGraphics, width, height, mouseX.toDouble(), mouseY.toDouble(), delta)
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
val handled = widgetTree.onClick(mouseX, mouseY, button)
return handled || super.mouseClicked(mouseX, mouseY, button)
}
override fun mouseScrolled(mouseX: Double, mouseY: Double, horizontalAmount: Double, verticalAmount: Double): Boolean {
val handled = widgetTree.onScroll(mouseX, mouseY, verticalAmount)
return handled || super.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount)
}
override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
val handled = widgetTree.onKeyPress(keyCode, scanCode, modifiers)
if (handled) return true
return super.keyPressed(keyCode, scanCode, modifiers)
}
override fun removed() {
try {
super.removed()
} finally {
MolangUiClientManager.onScreenRemoved(sessionId, this, widgetTree)
}
}
override fun isPauseScreen(): Boolean = false
}
@@ -0,0 +1,316 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.molangui
import com.bedrockk.molang.runtime.MoLangRuntime
import com.mojang.blaze3d.systems.RenderSystem
import com.cobblemon.mod.common.client.render.RenderAlphaScope
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.molangui.MolangUiDefinition
import com.cobblemon.mod.common.api.molangui.ParameterValue
import com.cobblemon.mod.common.api.molangui.event.ClientEventProcessor
import com.cobblemon.mod.common.api.molangui.event.WidgetEvent
import com.cobblemon.mod.common.api.molangui.event.WidgetEventType
import com.cobblemon.mod.common.api.molangui.layout.LayoutEngine
import com.cobblemon.mod.common.api.molangui.widget.WidgetDefinition
import com.cobblemon.mod.common.api.molangui.widget.WidgetExpansion
import com.cobblemon.mod.common.api.molangui.widget.WidgetInstance
import com.cobblemon.mod.common.api.molangui.widget.WidgetType
import com.cobblemon.mod.common.client.gui.molangui.renderer.NumberInputRenderer
import com.cobblemon.mod.common.client.gui.molangui.renderer.WidgetRendererRegistry
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.Minecraft
import com.cobblemon.mod.common.util.lang
import net.minecraft.resources.ResourceLocation
import org.lwjgl.glfw.GLFW.GLFW_KEY_ESCAPE
import java.util.UUID
class MolangUiWidgetTree(
val guiId: ResourceLocation,
val sessionId: UUID,
var rootDefinition: WidgetDefinition,
parameters: Map<String, ParameterValue>,
variables: Map<String, Double>,
private val nanoTime: () -> Long = System::nanoTime
) {
private val startedAt = nanoTime()
private var previousFrame = startedAt
private val currentParameters = parameters.toMutableMap()
val runtime: MoLangRuntime = MolangUiClientRuntimeFactory.create(guiId, parameters, variables)
var root: WidgetInstance = WidgetExpansion.refresh(rootDefinition, runtime)
private set
private var hoveredPath: List<WidgetInstance> = emptyList()
private var hoverRevision = 0L
private var expansionError: String? = null
private var closed = false
private var lastWidth = 0
private var lastHeight = 0
fun updateParameters(parameters: Map<String, ParameterValue>) {
MolangUiClientRuntimeFactory.updateParameters(runtime, parameters)
currentParameters.putAll(parameters)
}
fun updateVariables(variables: Map<String, Double>) {
MolangUiClientRuntimeFactory.updateVariables(runtime, variables)
}
fun rebuildFromDefinition(newDefinition: MolangUiDefinition) {
this.rootDefinition = newDefinition.root
val newVariables = newDefinition.variables.filterKeys { it.removePrefix("v.") !in runtime.environment.variable.map }
MolangUiClientRuntimeFactory.updateVariables(runtime, newVariables)
MolangUiClientRuntimeFactory.updateParameters(runtime, currentParameters)
prepare(lastWidth, lastHeight)
}
internal fun prepare(width: Int, height: Int): Boolean {
if (closed) return false
lastWidth = width
lastHeight = height
try {
root = WidgetExpansion.refresh(rootDefinition, runtime, root)
fun visibility(widget: WidgetInstance, parentEnabled: Boolean) {
widget.withItemContext(runtime) {
widget.visible = widget.definition.getProperty("visible")?.resolveBoolean(runtime) ?: true
widget.enabled = parentEnabled && (widget.definition.getProperty("enabled")?.resolveBoolean(runtime) ?: true)
}
widget.children.forEach { visibility(it, widget.enabled) }
}
visibility(root, true)
LayoutEngine.layout(root, width.toFloat(), height.toFloat(), runtime)
clampScrollOffsets(root)
expansionError = null
return true
} catch (error: Exception) {
clearHover()
val reason = error.message ?: error.javaClass.simpleName
if (reason != expansionError) Cobblemon.LOGGER.warn("Invalid Molang UI {}: {}", guiId, reason)
expansionError = reason
return false
}
}
fun render(guiGraphics: GuiGraphics, screenWidth: Int, screenHeight: Int, mouseX: Double, mouseY: Double, delta: Float) {
if (closed) return
val now = nanoTime()
val elapsed = ((now - startedAt).coerceAtLeast(0) / 1_000_000_000.0).toFloat()
val frameDuration = ((now - previousFrame).coerceAtLeast(0) / 1_000_000_000.0).toFloat()
previousFrame = now
MolangUiClientRuntimeFactory.updateFrame(runtime, elapsed, frameDuration, screenWidth, screenHeight, mouseX, mouseY)
if (!prepare(screenWidth, screenHeight)) {
if (!closed) guiGraphics.drawString(Minecraft.getInstance().font, lang("molang_ui.invalid_view"), 8, 8, 0xFFFFFF)
return
}
if (!updateHover(mouseX.toFloat(), mouseY.toFloat())) return
renderWidget(root, guiGraphics, mouseX.toFloat(), mouseY.toFloat(), delta)
}
private fun updateHover(mouseX: Float, mouseY: Float): Boolean {
val target = ClientEventProcessor.hitTest(root, mouseX, mouseY)
val next = generateSequence(target) { it.parent }.toList().asReversed()
if (next == hoveredPath) return true
val previous = hoveredPath
val shared = previous.zip(next).takeWhile { (old, new) -> old === new }.size
val exited = previous.drop(shared).asReversed()
val entered = next.drop(shared)
exited.forEach { it.hovered = false }
entered.forEach { it.hovered = true }
hoveredPath = next
val revision = ++hoverRevision
var handled = false
for (widget in exited) {
handled = dispatchHover(widget, WidgetEventType.HOVER_EXIT) || handled
if (closed || hoverRevision != revision) return false
}
for (widget in entered) {
handled = dispatchHover(widget, WidgetEventType.HOVER_ENTER) || handled
if (closed || hoverRevision != revision) return false
}
return !handled || prepare(lastWidth, lastHeight)
}
private fun dispatchHover(widget: WidgetInstance, type: WidgetEventType): Boolean {
val definition = widget.definition
if (type.id !in definition.events && type.clientId !in definition.clientEvents) return false
widget.withItemContext(runtime) {
ClientEventProcessor.processEvent(WidgetEvent(type, definition.id), definition, runtime, sessionId)
}
return true
}
private fun clearHover() {
hoveredPath.forEach { it.hovered = false }
hoveredPath = emptyList()
hoverRevision++
}
fun onClick(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (!prepare(lastWidth, lastHeight)) return false
val hit = ClientEventProcessor.hitTest(root, mouseX.toFloat(), mouseY.toFloat()) ?: return false
if (button == 0 && hit.definition.type == WidgetType.NUMBER_INPUT) {
var contentY = mouseY.toFloat()
var parent = hit.parent
while (parent != null) {
if (parent.definition.type == WidgetType.SCROLL_PANEL) contentY -= parent.scrollOffset
parent = parent.parent
}
if (hit.withItemContext(runtime) {
NumberInputRenderer.handleClick(hit, runtime, mouseX.toFloat(), contentY) { value ->
bubbleEvent(hit, WidgetEventType.CHANGE, mapOf("value" to value.toDouble()))
}
}) {
return true
}
}
val data = mapOf("button" to button.toDouble())
return bubbleEvent(hit, WidgetEventType.CLICK, data)
}
fun onScroll(mouseX: Double, mouseY: Double, amount: Double): Boolean {
if (!amount.isFinite() || !prepare(lastWidth, lastHeight)) return false
val hit = ClientEventProcessor.hitTest(root, mouseX.toFloat(), mouseY.toFloat()) ?: return false
val scrollPanel = findParentOfType(hit, WidgetType.SCROLL_PANEL)
if (scrollPanel != null) {
scrollPanel.scrollOffset = (scrollPanel.scrollOffset.toDouble() + amount * 10.0)
.coerceIn(minimumScroll(scrollPanel).toDouble(), 0.0).toFloat()
bubbleEvent(hit, WidgetEventType.SCROLL, mapOf("amount" to amount))
return true
}
val data = mapOf("amount" to amount)
return bubbleEvent(hit, WidgetEventType.SCROLL, data)
}
private fun findParentOfType(widget: WidgetInstance, type: WidgetType): WidgetInstance? {
var current: WidgetInstance? = widget
while (current != null) {
if (current.definition.type == type) return current
current = current.parent
}
return null
}
private fun bubbleEvent(target: WidgetInstance, type: WidgetEventType, data: Map<String, Double>): Boolean {
var current: WidgetInstance? = target
while (current != null) {
val def = current.definition
val hasServer = def.events.containsKey(type.id)
val hasClient = def.clientEvents.containsKey(type.clientId)
if (hasServer || hasClient) {
val event = WidgetEvent(type, target.definition.id, data)
target.withItemContext(runtime) { ClientEventProcessor.processEvent(event, def, runtime, sessionId) }
return true
}
current = current.parent
}
return false
}
fun onKeyPress(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
if (keyCode == GLFW_KEY_ESCAPE || !prepare(lastWidth, lastHeight) || !root.visible || !root.enabled) return false
val target = root
val hasHandler = target.definition.events.containsKey("on_key") || target.definition.clientEvents.containsKey("on_key_client")
if (!hasHandler) return false
val event = WidgetEvent(WidgetEventType.KEY, target.definition.id,
mapOf("key" to keyCode.toDouble(), "scan" to scanCode.toDouble(), "modifiers" to modifiers.toDouble()))
target.withItemContext(runtime) { ClientEventProcessor.processEvent(event, target.definition, runtime, sessionId) }
return true
}
fun cleanup() {
closed = true
clearHover()
root.children.clear()
root.rendererState = null
}
private fun renderWidget(widget: WidgetInstance, guiGraphics: GuiGraphics, mouseX: Float, mouseY: Float, delta: Float): Unit = widget.withItemContext(runtime) {
if (!widget.visible) return
val opacity = widget.definition.getProperty("opacity")?.resolveFloat(runtime) ?: 1f
if (!opacity.isFinite() || opacity <= 0f) return
if (opacity >= 1f) {
renderContents(widget, guiGraphics, mouseX, mouseY, delta)
return
}
val color = RenderSystem.getShaderColor().copyOf()
try {
guiGraphics.setColor(color[0], color[1], color[2], color[3] * opacity)
renderContents(widget, guiGraphics, mouseX, mouseY, delta)
} finally {
guiGraphics.setColor(color[0], color[1], color[2], color[3])
}
}
private fun renderContents(widget: WidgetInstance, guiGraphics: GuiGraphics, mouseX: Float, mouseY: Float, delta: Float) {
val renderer = WidgetRendererRegistry.get(widget.definition.type)
if (widget.definition.type == WidgetType.POKEMON_RENDER || widget.definition.type == WidgetType.ITEM_SLOT) {
RenderAlphaScope.render { renderer?.render(widget, guiGraphics, runtime, mouseX, mouseY, delta) }
} else {
renderer?.render(widget, guiGraphics, runtime, mouseX, mouseY, delta)
}
val isScrollPanel = widget.definition.type == WidgetType.SCROLL_PANEL
val scrollOffset = widget.scrollOffset
if (isScrollPanel) {
val x = widget.absoluteX.toInt()
val y = widget.absoluteY.toInt()
val w = widget.computedWidth.toInt()
val h = widget.computedHeight.toInt()
guiGraphics.enableScissor(x, y, x + w, y + h)
}
try {
if (isScrollPanel) applyScrollOffset(widget, scrollOffset)
for (child in widget.children) {
renderWidget(child, guiGraphics, mouseX, mouseY, delta)
}
} finally {
if (isScrollPanel) {
applyScrollOffset(widget, -scrollOffset)
guiGraphics.disableScissor()
}
}
}
private fun applyScrollOffset(widget: WidgetInstance, offset: Float) {
for (child in widget.children) {
child.absoluteY += offset
applyScrollOffset(child, offset)
}
}
private fun clampScrollOffsets(widget: WidgetInstance) {
if (!widget.visible) return
if (widget.definition.type == WidgetType.SCROLL_PANEL) {
widget.scrollOffset = if (widget.scrollOffset.isFinite()) {
widget.scrollOffset.coerceIn(minimumScroll(widget), 0f)
} else 0f
}
widget.children.forEach(::clampScrollOffsets)
}
private fun minimumScroll(widget: WidgetInstance): Float {
val viewportBottom = widget.absoluteY.toDouble() + widget.computedHeight
var contentBottom = viewportBottom
fun collect(node: WidgetInstance) {
if (!node.visible) return
contentBottom = maxOf(contentBottom, node.absoluteY.toDouble() + node.computedHeight)
if (node.definition.type != WidgetType.SCROLL_PANEL) node.children.forEach(::collect)
}
widget.children.forEach(::collect)
return (viewportBottom - contentBottom).toFloat()
}
}
@@ -0,0 +1,31 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.molangui
import com.cobblemon.mod.common.api.molangui.container.MolangContainerMenu
import com.cobblemon.mod.common.util.lang
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen
import net.minecraft.world.entity.player.Inventory
internal class RejectedMolangUiContainerScreen(menu: MolangContainerMenu, inventory: Inventory) :
AbstractContainerScreen<MolangContainerMenu>(menu, inventory, lang("molang_ui.invalid_view")) {
override fun containerTick() {
minecraft?.player?.closeContainer()
}
override fun render(graphics: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
graphics.drawString(font, title, 8, 8, 0xFFFFFF)
}
override fun renderBg(graphics: GuiGraphics, delta: Float, mouseX: Int, mouseY: Int) {}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int) = true
override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int) = true
override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, dragX: Double, dragY: Double) = true
}

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