Compare commits

..
1206 changed files with 22250 additions and 78327 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
+7 -107
View File
@@ -7,139 +7,39 @@
- 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.
- 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
- Updated various recipes to display in Recipe Groups within the Recipe Book
- Updated most recipes to display more logically/consistently within the Recipe Book's categories
- Make ride rotations less jittery in multiplayer.
- Changes tm shelf discs to use textures instead of tinting and overlays to line up with item display
- Blank TM can now be inserted into the Disc Shelf
- Recall animation now renders ball in hand with the player arm raised.
- Made regional and Vivillon split-evo locations now mutually exclusive
- 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
- 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()
@@ -657,11 +657,11 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<
@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))
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))
@@ -691,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
@@ -702,40 +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))
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))
@@ -755,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 }))
@@ -786,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(
@@ -809,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)))
@@ -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
@@ -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)
}
}
}
@@ -657,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"
}
}
@@ -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()}")
}
}
@@ -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)
}
}
@@ -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")
}
}
@@ -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)
@@ -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) {
@@ -424,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")
@@ -437,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)
}
@@ -513,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
}
@@ -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.client.gui.molangui.renderer
import com.cobblemon.mod.common.api.molangui.widget.WidgetInstance
import com.bedrockk.molang.runtime.MoLangRuntime
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
object ButtonRenderer : WidgetRenderer {
override fun render(
widget: WidgetInstance,
guiGraphics: GuiGraphics,
runtime: MoLangRuntime,
mouseX: Float,
mouseY: Float,
delta: Float
) {
val enabled = widget.enabled
val baseColor = widget.definition.getProperty("color")?.resolveInt(runtime) ?: 0xFF555555.toInt()
val color = when {
!enabled -> 0xFF333333.toInt()
widget.hovered -> widget.definition.hoverOverrides["color"]?.resolveInt(runtime) ?: 0xFF777777.toInt()
else -> baseColor
}
guiGraphics.fill(
widget.absoluteX.toInt(), widget.absoluteY.toInt(),
(widget.absoluteX + widget.computedWidth).toInt(),
(widget.absoluteY + widget.computedHeight).toInt(),
color
)
val text = widget.definition.getProperty("text")?.resolveString(runtime) ?: return
val contentWidth = (widget.computedWidth.toInt() - 4).coerceAtLeast(0)
if (text.isEmpty() || contentWidth == 0) return
val font = Minecraft.getInstance().font
if (widget.computedHeight < font.lineHeight + 2) return
val displayed = if (font.width(text) <= contentWidth) text else {
val suffix = "?"
val suffixWidth = font.width(suffix)
if (suffixWidth > contentWidth) return
font.plainSubstrByWidth(text, contentWidth - suffixWidth) + suffix
}
val textX = widget.absoluteX + (widget.computedWidth - font.width(displayed)) / 2f
val textY = widget.absoluteY + (widget.computedHeight - font.lineHeight) / 2f
val textColor = if (enabled) 0xFFFFFF else 0xAAAAAA
guiGraphics.enableScissor(widget.absoluteX.toInt(), widget.absoluteY.toInt(),
(widget.absoluteX + widget.computedWidth).toInt(), (widget.absoluteY + widget.computedHeight).toInt())
try {
guiGraphics.drawString(font, displayed, textX.toInt(), textY.toInt(), textColor, true)
} finally {
guiGraphics.disableScissor()
}
}
}
@@ -0,0 +1,32 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of 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.renderer
import com.cobblemon.mod.common.api.molangui.widget.WidgetInstance
import com.bedrockk.molang.runtime.MoLangRuntime
import net.minecraft.client.gui.GuiGraphics
object FillRenderer : WidgetRenderer {
override fun render(
widget: WidgetInstance,
guiGraphics: GuiGraphics,
runtime: MoLangRuntime,
mouseX: Float,
mouseY: Float,
delta: Float
) {
val color = widget.definition.getProperty("color")?.resolveInt(runtime) ?: 0xFFFFFFFF.toInt()
guiGraphics.fill(
widget.absoluteX.toInt(), widget.absoluteY.toInt(),
(widget.absoluteX + widget.computedWidth).toInt(),
(widget.absoluteY + widget.computedHeight).toInt(),
color
)
}
}
@@ -0,0 +1,60 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.molangui.renderer
import com.cobblemon.mod.common.api.molangui.widget.WidgetInstance
import com.bedrockk.molang.runtime.MoLangRuntime
import com.cobblemon.mod.common.client.render.withStandardBlend
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.resources.ResourceLocation
object ImageRenderer : WidgetRenderer {
override fun render(
widget: WidgetInstance,
guiGraphics: GuiGraphics,
runtime: MoLangRuntime,
mouseX: Float,
mouseY: Float,
delta: Float
) {
val textureStr = widget.definition.getProperty("texture")?.resolveString(runtime) ?: return
val texture = ResourceLocation.tryParse(textureStr) ?: return
if (!widget.absoluteX.isFinite() || !widget.absoluteY.isFinite()) return
val width = pixelSize(widget.computedWidth) ?: return
val height = pixelSize(widget.computedHeight) ?: return
val uvProperty = widget.definition.getProperty("uv")
val uv = uvProperty?.raw as? List<*>
if (uvProperty != null && uv == null) return
val sizeProperty = widget.definition.getProperty("texture_size")
val textureSize = sizeProperty?.raw as? List<*>
if (sizeProperty != null && textureSize == null) return
val texW = pixelSize(component(textureSize, 0, if (uv == null) width.toFloat() else 256f)) ?: return
val texH = pixelSize(component(textureSize, 1, if (uv == null) height.toFloat() else 256f)) ?: return
val u = component(uv, 0, 0f) ?: return
val v = component(uv, 1, 0f) ?: return
val uvW = pixelSize(component(uv, 2, if (uv == null) texW.toFloat() else width.toFloat())) ?: return
val uvH = pixelSize(component(uv, 3, if (uv == null) texH.toFloat() else height.toFloat())) ?: return
withStandardBlend {
guiGraphics.blit(
texture,
widget.absoluteX.toInt(), widget.absoluteY.toInt(), width, height,
u, v, uvW, uvH, texW, texH
)
}
}
private fun component(values: List<*>?, index: Int, default: Float): Float? {
if (values == null || index >= values.size) return default
return (values[index] as? Number)?.toFloat()?.takeIf { it.isFinite() }
}
private fun pixelSize(value: Float?): Int? = value?.takeIf {
it.isFinite() && it >= 1f && it.toDouble() <= Int.MAX_VALUE.toDouble()
}?.toInt()
}
@@ -0,0 +1,37 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.molangui.renderer
import com.cobblemon.mod.common.api.molangui.widget.WidgetInstance
import com.bedrockk.molang.runtime.MoLangRuntime
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.core.registries.BuiltInRegistries
import net.minecraft.resources.ResourceLocation
import net.minecraft.world.item.ItemStack
object ItemSlotRenderer : WidgetRenderer {
override fun render(
widget: WidgetInstance,
guiGraphics: GuiGraphics,
runtime: MoLangRuntime,
mouseX: Float,
mouseY: Float,
delta: Float
) {
val itemId = widget.definition.getProperty("item")?.resolveString(runtime) ?: return
val resourceLocation = ResourceLocation.tryParse(itemId) ?: return
val item = BuiltInRegistries.ITEM.get(resourceLocation)
val stack = ItemStack(item)
val x = widget.absoluteX.toInt()
val y = widget.absoluteY.toInt()
guiGraphics.renderItem(stack, x, y)
}
}
@@ -0,0 +1,51 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.molangui.renderer
import com.cobblemon.mod.common.api.molangui.widget.WidgetInstance
import com.bedrockk.molang.runtime.MoLangRuntime
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
object LabelRenderer : WidgetRenderer {
override fun render(
widget: WidgetInstance,
guiGraphics: GuiGraphics,
runtime: MoLangRuntime,
mouseX: Float,
mouseY: Float,
delta: Float
) {
val font = Minecraft.getInstance().font
val text = widget.definition.getProperty("text")?.resolveString(runtime) ?: ""
val color = widget.definition.getProperty("color")?.resolveInt(runtime) ?: 0xFFFFFF
val shadow = widget.definition.getProperty("shadow")?.resolveBoolean(runtime) ?: false
val alignment = widget.definition.getStringProperty("alignment") ?: "left"
val scale = widget.definition.getProperty("scale")?.resolveFloat(runtime) ?: 1f
if (!scale.isFinite() || scale <= 0f) return
val textWidth = font.width(text) * scale
val x = when (alignment) {
"center" -> widget.absoluteX + widget.computedWidth / 2f - textWidth / 2f
"right" -> widget.absoluteX + widget.computedWidth - textWidth
else -> widget.absoluteX
}
if (!textWidth.isFinite() || !x.isFinite() || !widget.absoluteY.isFinite()) return
val pose = guiGraphics.pose()
pose.pushPose()
try {
pose.translate(x, widget.absoluteY, 0f)
pose.scale(scale, scale, 1f)
guiGraphics.drawString(font, text, 0, 0, color, shadow)
} finally {
pose.popPose()
}
}
}
@@ -0,0 +1,101 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of 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.renderer
import com.bedrockk.molang.runtime.MoLangRuntime
import com.bedrockk.molang.runtime.value.DoubleValue
import com.cobblemon.mod.common.api.molangui.MolangUiParameters
import com.cobblemon.mod.common.api.molangui.widget.WidgetInstance
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
object NumberInputRenderer : WidgetRenderer {
private const val BUTTON_WIDTH = 20
private data class Values(val min: Int, val max: Int, val step: Int, val current: Int)
private data class Bounds(val x: Int, val y: Int, val width: Int, val height: Int) {
val buttonWidth = minOf(BUTTON_WIDTH, width / 3)
val right get() = x + width
val bottom get() = y + height
fun direction(mouseX: Float, mouseY: Float): Int = when {
!mouseX.isFinite() || !mouseY.isFinite() || mouseY < y || mouseY >= bottom -> 0
mouseX >= x && mouseX < x + buttonWidth -> -1
mouseX >= right - buttonWidth && mouseX < right -> 1
else -> 0
}
}
override fun render(widget: WidgetInstance, guiGraphics: GuiGraphics, runtime: MoLangRuntime, mouseX: Float, mouseY: Float, delta: Float) {
val bounds = bounds(widget) ?: return
val values = values(widget, runtime) ?: return
val font = Minecraft.getInstance().font
val textColor = widget.definition.getProperty("color")?.resolveInt(runtime) ?: 0xFFFFFF
val buttonColor = widget.definition.getProperty("button_color")?.resolveInt(runtime) ?: 0xFF555555.toInt()
val hoverColor = 0xFF777777.toInt()
val hovered = if (widget.enabled) bounds.direction(mouseX, mouseY) else 0
guiGraphics.fill(bounds.x, bounds.y, bounds.x + bounds.buttonWidth, bounds.bottom,
if (hovered == -1 && values.current > values.min) hoverColor else buttonColor)
guiGraphics.fill(bounds.right - bounds.buttonWidth, bounds.y, bounds.right, bounds.bottom,
if (hovered == 1 && values.current < values.max) hoverColor else buttonColor)
fun drawText(text: String, left: Int, width: Int) {
val textWidth = font.width(text)
if (textWidth > width || font.lineHeight > bounds.height) return
guiGraphics.drawString(font, text, left + (width - textWidth) / 2,
bounds.y + (bounds.height - font.lineHeight) / 2, textColor, false)
}
drawText("?", bounds.x, bounds.buttonWidth)
drawText(values.current.toString(), bounds.x + bounds.buttonWidth, bounds.width - 2 * bounds.buttonWidth)
drawText("+", bounds.right - bounds.buttonWidth, bounds.buttonWidth)
}
fun handleClick(widget: WidgetInstance, runtime: MoLangRuntime, mouseX: Float, mouseY: Float, onChange: (Int) -> Unit): Boolean {
if (!widget.visible || !widget.enabled) return false
val direction = bounds(widget)?.direction(mouseX, mouseY) ?: return false
if (direction == 0) return false
val values = values(widget, runtime) ?: return false
val variable = widget.definition.getStringProperty("variable") ?: return false
val name = try { MolangUiParameters.requireVariableName(variable) } catch (_: IllegalArgumentException) { return false }
val newValue = (values.current.toLong() + direction * values.step.toLong())
.coerceIn(values.min.toLong(), values.max.toLong()).toInt()
if (newValue != values.current) {
runtime.environment.setSimpleVariable(name, DoubleValue(newValue.toDouble()))
onChange(newValue)
}
return true
}
private fun values(widget: WidgetInstance, runtime: MoLangRuntime): Values? {
fun integer(key: String, default: Double): Int? {
val value = widget.definition.getProperty(key)?.resolveDouble(runtime) ?: default
return value.takeIf { it.isFinite() && it >= Int.MIN_VALUE && it <= Int.MAX_VALUE && it % 1.0 == 0.0 }?.toInt()
}
val min = integer("min", 1.0) ?: return null
val max = integer("max", 99.0) ?: return null
val step = integer("step", 1.0) ?: return null
val variable = widget.definition.getStringProperty("variable")?.removePrefix("v.")
val boundValue = variable?.let { runtime.environment.variable.map[it]?.asDouble() } ?: min.toDouble()
val current = integer("value", boundValue) ?: return null
if (min > max || step <= 0) return null
return Values(min, max, step, current)
}
private fun bounds(widget: WidgetInstance): Bounds? {
if (!widget.absoluteX.isFinite() || !widget.absoluteY.isFinite() ||
!widget.computedWidth.isFinite() || !widget.computedHeight.isFinite()) return null
val x = widget.absoluteX.toDouble()
val y = widget.absoluteY.toDouble()
val width = widget.computedWidth.toDouble()
val height = widget.computedHeight.toDouble()
if (width < 3 || height < 1 || width > Int.MAX_VALUE || height > Int.MAX_VALUE ||
x < Int.MIN_VALUE || y < Int.MIN_VALUE || x + width > Int.MAX_VALUE || y + height > Int.MAX_VALUE) return null
return Bounds(x.toInt(), y.toInt(), width.toInt(), height.toInt())
}
}
@@ -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.client.gui.molangui.renderer
import com.cobblemon.mod.common.api.molangui.widget.WidgetInstance
import com.bedrockk.molang.runtime.MoLangRuntime
import net.minecraft.client.gui.GuiGraphics
object PanelRenderer : WidgetRenderer {
override fun render(
widget: WidgetInstance,
guiGraphics: GuiGraphics,
runtime: MoLangRuntime,
mouseX: Float,
mouseY: Float,
delta: Float
) {
val color = widget.definition.getProperty("color")
if (color != null) {
guiGraphics.fill(
widget.absoluteX.toInt(), widget.absoluteY.toInt(),
(widget.absoluteX + widget.computedWidth).toInt(),
(widget.absoluteY + widget.computedHeight).toInt(),
color.resolveInt(runtime)
)
}
ImageRenderer.render(widget, guiGraphics, runtime, mouseX, mouseY, delta)
}
}
@@ -0,0 +1,76 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of 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.renderer
import com.cobblemon.mod.common.api.molangui.widget.WidgetInstance
import com.cobblemon.mod.common.client.gui.drawProfilePokemon
import com.cobblemon.mod.common.client.render.models.blockbench.FloatingState
import com.cobblemon.mod.common.pokemon.RenderablePokemon
import com.bedrockk.molang.runtime.MoLangRuntime
import net.minecraft.client.gui.GuiGraphics
import org.joml.Quaternionf
import org.joml.Vector3f
import com.cobblemon.mod.common.util.math.fromEulerXYZDegrees
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
object PokemonRenderRenderer : WidgetRenderer {
override fun render(
widget: WidgetInstance,
guiGraphics: GuiGraphics,
runtime: MoLangRuntime,
mouseX: Float,
mouseY: Float,
delta: Float
) {
val speciesProp = widget.definition.getProperty("species")
if (speciesProp == null) return
val speciesName = speciesProp.resolveString(runtime)
if (speciesName.isEmpty() || speciesName == "0" || speciesName == "0.0") return
val species = PokemonSpecies.getByName(speciesName)
if (species == null) return
val aspectsStr = widget.definition.getProperty("aspects")?.resolveString(runtime) ?: ""
val aspects = if (aspectsStr.isNotEmpty()) aspectsStr.split(",").map { it.trim() }.toSet() else emptySet()
val scale = widget.definition.getProperty("scale")?.resolveFloat(runtime) ?: 2.5f
val rotY = widget.definition.getProperty("rotation_y")?.resolveFloat(runtime) ?: 35f
val rotX = widget.definition.getProperty("rotation_x")?.resolveFloat(runtime) ?: 13f
if (!scale.isFinite() || scale <= 0f || !rotX.isFinite() || !rotY.isFinite()) return
val renderablePokemon = RenderablePokemon(species, aspects)
val state = (widget.rendererState as? FloatingState) ?: FloatingState().also { widget.rendererState = it }
val x = widget.absoluteX
val y = widget.absoluteY
val w = widget.computedWidth
val h = widget.computedHeight
if (!x.isFinite() || !y.isFinite() || !w.isFinite() || !h.isFinite() || w <= 0f || h <= 0f) return
val matrices = guiGraphics.pose()
guiGraphics.enableScissor(x.toInt(), y.toInt(), (x + w).toInt(), (y + h).toInt())
try {
matrices.pushPose()
try {
matrices.translate((x + w * 0.5).toDouble(), y.toDouble(), 0.0)
matrices.scale(scale, scale, scale)
drawProfilePokemon(
renderablePokemon = renderablePokemon,
matrixStack = matrices,
rotation = Quaternionf().fromEulerXYZDegrees(Vector3f(rotX, rotY, 0f)),
state = state,
partialTicks = delta
)
} finally {
matrices.popPose()
}
} finally {
guiGraphics.disableScissor()
}
}
}
@@ -0,0 +1,44 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.molangui.renderer
import com.cobblemon.mod.common.api.molangui.widget.WidgetInstance
import com.bedrockk.molang.runtime.MoLangRuntime
import net.minecraft.client.gui.GuiGraphics
object ProgressBarRenderer : WidgetRenderer {
override fun render(
widget: WidgetInstance,
guiGraphics: GuiGraphics,
runtime: MoLangRuntime,
mouseX: Float,
mouseY: Float,
delta: Float
) {
val value = widget.definition.getProperty("value")?.resolveDouble(runtime) ?: 0.0
val max = widget.definition.getProperty("max")?.resolveDouble(runtime) ?: 100.0
val bgColor = widget.definition.getProperty("background_color")?.resolveInt(runtime) ?: 0xFF333333.toInt()
val barColor = widget.definition.getProperty("color")?.resolveInt(runtime) ?: 0xFF00FF00.toInt()
val direction = widget.definition.getStringProperty("direction") ?: "left_to_right"
val x = widget.absoluteX.toInt()
val y = widget.absoluteY.toInt()
val w = widget.computedWidth.toInt()
val h = widget.computedHeight.toInt()
val fraction = if (max > 0) (value / max).coerceIn(0.0, 1.0) else 0.0
guiGraphics.fill(x, y, x + w, y + h, bgColor)
when (direction) {
"left_to_right" -> guiGraphics.fill(x, y, x + (w * fraction).toInt(), y + h, barColor)
"right_to_left" -> guiGraphics.fill(x + w - (w * fraction).toInt(), y, x + w, y + h, barColor)
"bottom_to_top" -> guiGraphics.fill(x, y + h - (h * fraction).toInt(), x + w, y + h, barColor)
"top_to_bottom" -> guiGraphics.fill(x, y, x + w, y + (h * fraction).toInt(), barColor)
}
}
}
@@ -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.client.gui.molangui.renderer
import com.cobblemon.mod.common.api.molangui.widget.WidgetInstance
import com.bedrockk.molang.runtime.MoLangRuntime
import net.minecraft.client.gui.GuiGraphics
interface WidgetRenderer {
fun render(
widget: WidgetInstance,
guiGraphics: GuiGraphics,
runtime: MoLangRuntime,
mouseX: Float,
mouseY: Float,
delta: Float
)
}
@@ -0,0 +1,34 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.molangui.renderer
import com.cobblemon.mod.common.api.molangui.widget.WidgetType
object WidgetRendererRegistry {
private val renderers = mutableMapOf<WidgetType, WidgetRenderer>()
fun register(type: WidgetType, renderer: WidgetRenderer) {
renderers[type] = renderer
}
fun get(type: WidgetType): WidgetRenderer? = renderers[type]
fun registerDefaults() {
register(WidgetType.PANEL, PanelRenderer)
register(WidgetType.LABEL, LabelRenderer)
register(WidgetType.IMAGE, ImageRenderer)
register(WidgetType.FILL, FillRenderer)
register(WidgetType.BUTTON, ButtonRenderer)
register(WidgetType.PROGRESS_BAR, ProgressBarRenderer)
register(WidgetType.NUMBER_INPUT, NumberInputRenderer)
register(WidgetType.ITEM_SLOT, ItemSlotRenderer)
register(WidgetType.SCROLL_PANEL, PanelRenderer)
register(WidgetType.POKEMON_RENDER, PokemonRenderRenderer)
}
}
@@ -36,33 +36,32 @@ import com.cobblemon.mod.common.client.gui.handleProfileDebugKeys
import com.cobblemon.mod.common.client.gui.logProfileTranslations
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.BASE_HEIGHT
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.BASE_WIDTH
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.HALF_OVERLAY_HEIGHT
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.HALF_OVERLAY_WIDTH
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.HALF_OVERLAY_WIDTH
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.HEADER_BAR_HEIGHT
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.ICONS_ARROW_PADDING_AROUND
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.ICONS_PADDING_AROUND
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.SCALE
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.TAB_ABILITIES
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.TAB_DESCRIPTION
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.TAB_DROPS
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.TAB_EVOLUTION_TREE
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.TAB_ICON_SIZE
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.TAB_MOVES
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.TAB_SIZE
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.TAB_STATS
import com.cobblemon.mod.common.client.gui.pokedex.widgets.*
import com.cobblemon.mod.common.client.gui.pokedex.widgets.evolutiontree.EvolutionTreeWidget
import com.cobblemon.mod.common.client.gui.pokedex.widgets.evolutiontree.debugEvolutionRequirementRendering
import com.cobblemon.mod.common.client.gui.pokedex.widgets.AbilitiesWidget
import com.cobblemon.mod.common.client.gui.pokedex.widgets.DescriptionWidget
import com.cobblemon.mod.common.client.gui.pokedex.widgets.DropsScrollingWidget
import com.cobblemon.mod.common.client.gui.pokedex.widgets.EntriesScrollingWidget
import com.cobblemon.mod.common.client.gui.pokedex.widgets.MovesLearnsetWidget
import com.cobblemon.mod.common.client.gui.pokedex.widgets.PokemonInfoWidget
import com.cobblemon.mod.common.client.gui.pokedex.widgets.SearchWidget
import com.cobblemon.mod.common.client.gui.pokedex.widgets.SizeWidget
import com.cobblemon.mod.common.client.gui.pokedex.widgets.StatsWidget
import com.cobblemon.mod.common.client.pokedex.PokedexType
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.net.messages.server.block.AdjustBlockEntityViewerCountPacket
import com.cobblemon.mod.common.pokemon.Gender
import com.cobblemon.mod.common.pokemon.abilities.HiddenAbility
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.isInventoryKeyPressed
import com.cobblemon.mod.common.util.lang
import com.mojang.blaze3d.platform.InputConstants
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.components.EditBox
@@ -95,10 +94,6 @@ class PokedexGUI private constructor(
private val categoryBarOverlay = cobblemonResource("textures/gui/pokedex/pokedex_screen_bar_category.png")
// TODO: separate into smaller components / widgets?
private val backgroundOverlay = cobblemonResource("textures/gui/pokedex/pokedex_screen_info_overlay.png")
private val expandedBackgroundOverlay = cobblemonResource("textures/gui/pokedex/pokedex_screen_info_overlay_expanded.png")
private val tabSelectArrow = cobblemonResource("textures/gui/pokedex/select_arrow.png")
private val tabIcons = arrayOf(
cobblemonResource("textures/gui/pokedex/tab_info.png"),
@@ -106,8 +101,7 @@ class PokedexGUI private constructor(
cobblemonResource("textures/gui/pokedex/tab_size.png"),
cobblemonResource("textures/gui/pokedex/tab_stats.png"),
cobblemonResource("textures/gui/pokedex/tab_drops.png"),
cobblemonResource("textures/gui/pokedex/tab_moves.png"),
cobblemonResource("textures/gui/pokedex/tab_evolution_tree.png"),
cobblemonResource("textures/gui/pokedex/tab_moves.png")
)
val arrowUpIcon = cobblemonResource("textures/gui/pokedex/arrow_up.png")
@@ -120,8 +114,6 @@ class PokedexGUI private constructor(
TAB_MOVES
)
private val ICONS_PADDING_BETWEEN = (BASE_WIDTH / 2 - ICONS_PADDING_AROUND * 2) / tabIcons.size
/**
* Attempts to open this screen for a client.
*/
@@ -140,7 +132,6 @@ class PokedexGUI private constructor(
var oldDragPosX = 0.0
var canDragRender = false
private var activeDragableElement: GuiEventListener? = null
private var filteredPokedex: Collection<PokedexDef> = mutableListOf()
private var seenCount = "0000"
@@ -164,29 +155,13 @@ class PokedexGUI private constructor(
private lateinit var scrollScreen: EntriesScrollingWidget
private lateinit var pokemonInfoWidget: PokemonInfoWidget
private lateinit var searchWidget: SearchWidget
private lateinit var pokemonNameWidget: PokemonNameWidget
private var selectedSearchByType: SearchByType = SearchByType.SPECIES
private var selectedCategoryFilter: PokedexCategoryFilterType = PokedexCategoryFilterType.ALL
private val tabButtons: MutableList<ScaledButton> = mutableListOf()
var seenShinyStates = setOf<String>()
var shiny = false
var maleRatio = -1F
var gender = Gender.GENDERLESS
var aspects = mutableSetOf<String>()
lateinit var tabInfoElement: GuiEventListener
var tabInfoIndex = TAB_DESCRIPTION
var evolutionTreeExpanded = false
set(value) {
field = value
if (::pokemonInfoWidget.isInitialized) {
pokemonInfoWidget.active = !value
pokemonInfoWidget.visible = !value
}
}
override fun renderBlurredBackground(delta: Float) {}
override fun renderMenuBackground(context: GuiGraphics) {}
@@ -205,7 +180,7 @@ class PokedexGUI private constructor(
//Info Widget
if (::pokemonInfoWidget.isInitialized) removeWidget(pokemonInfoWidget)
pokemonInfoWidget = PokemonInfoWidget(this, x + 180, y + 28) { newForm -> updateSelectedForm(newForm) }
pokemonInfoWidget = PokemonInfoWidget(x + 180, y + 28) { newForm -> updateSelectedForm(newForm) }
addRenderableWidget(pokemonInfoWidget)
setUpTabs()
@@ -217,10 +192,6 @@ class PokedexGUI private constructor(
searchWidget = SearchWidget(x + 26, y + 28, 120, HEADER_BAR_HEIGHT, update = ::updateFilters)
addRenderableWidget(searchWidget)
if (::pokemonNameWidget.isInitialized) removeWidget(pokemonNameWidget)
pokemonNameWidget = PokemonNameWidget(this, x + 180, y + 28)
addRenderableWidget(pokemonNameWidget)
if (::regionSelectWidgetUp.isInitialized) removeWidget(regionSelectWidgetUp)
regionSelectWidgetUp = ScaledButton(
buttonX = (x + 95).toFloat(),
@@ -396,14 +367,6 @@ class PokedexGUI private constructor(
shadow = true
)
blitk(
matrixStack = matrices,
texture = if (evolutionTreeExpanded) expandedBackgroundOverlay else backgroundOverlay,
x = pokemonInfoWidget.x, y = pokemonInfoWidget.y,
width = HALF_OVERLAY_WIDTH,
height = HALF_OVERLAY_HEIGHT
)
// Category filter
blitk(
matrixStack = matrices,
@@ -439,7 +402,7 @@ class PokedexGUI private constructor(
blitk(
matrixStack = matrices,
texture = tabSelectArrow,
x = (x + BASE_WIDTH / 2 + ICONS_ARROW_PADDING_AROUND + tabInfoIndex * ICONS_PADDING_BETWEEN) / SCALE,
x = (x + 191.5F + (22F * tabInfoIndex)) / SCALE,
y = (y + 177) / SCALE,
width = 12,
height = 6,
@@ -478,7 +441,6 @@ class PokedexGUI private constructor(
val canDisplayEntry = true //selectedForm?.unlockForms
if (::pokemonInfoWidget.isInitialized
&& pokemonInfoWidget.active
&& !pokemonInfoWidget.suppressViewport
&& pokemonInfoWidget.isWithinPortraitSpace(mouseX, mouseY)
&& canDisplayEntry == true
@@ -487,12 +449,6 @@ class PokedexGUI private constructor(
isDragging = true
oldDragPosX = mouseX
playSound(CobblemonSounds.POKEDEX_CLICK_SHORT)
activeDragableElement = pokemonInfoWidget
} else if (tabInfoIndex == TAB_EVOLUTION_TREE &&
::tabInfoElement.isInitialized &&
(tabInfoElement as EvolutionTreeWidget).isHovered(mouseX, mouseY)
) {
activeDragableElement = tabInfoElement
}
return try {
super.mouseClicked(mouseX, mouseY, button)
@@ -504,18 +460,14 @@ class PokedexGUI private constructor(
override fun mouseReleased(pMouseX: Double, pMouseY: Double, pButton: Int): Boolean {
if (canDragRender) canDragRender = false
if (isDragging) isDragging = false
activeDragableElement = null
return super.mouseReleased(pMouseX, pMouseY, pButton)
}
override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
if (isDragging && canDragRender && activeDragableElement == pokemonInfoWidget) {
if (isDragging && canDragRender) {
val dragOffsetY = (oldDragPosX - mouseX).toFloat()
pokemonInfoWidget.rotationY = (((pokemonInfoWidget.rotationY + dragOffsetY) % 360 + 360) % 360)
} else if (activeDragableElement == tabInfoElement) {
tabInfoElement.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
}
oldDragPosX = mouseX
return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
}
@@ -606,8 +558,6 @@ class PokedexGUI private constructor(
selectedForm = CobblemonClient.clientPokedexData.getEncounteredForms(newSelectedEntry).firstOrNull()
pokemonInfoWidget.setDexEntry(selectedEntry!!)
pokemonNameWidget.update(selectedEntry!!)
displaytabInfoElement(tabInfoIndex)
}
@@ -619,7 +569,7 @@ class PokedexGUI private constructor(
for (i in tabIcons.indices) {
tabButtons.add(ScaledButton(
x + BASE_WIDTH / 2 + ICONS_PADDING_AROUND + i * ICONS_PADDING_BETWEEN,
x + 190.5F + (i * 22F),
y + 181.5F,
TAB_ICON_SIZE,
TAB_ICON_SIZE,
@@ -632,8 +582,6 @@ class PokedexGUI private constructor(
}
fun displaytabInfoElement(tabIndex: Int, update: Boolean = true) {
if (tabIndex != TAB_EVOLUTION_TREE) evolutionTreeExpanded = false
val showActiveTab = selectedEntry?.let { selectedForm in getSufficientlyKnownForms(it, tabIndex) } == true
var statSubIndex = 0
@@ -685,9 +633,6 @@ class PokedexGUI private constructor(
TAB_MOVES -> {
tabInfoElement = MovesLearnsetWidget(x + 180, y + 28)
}
TAB_EVOLUTION_TREE -> {
tabInfoElement = EvolutionTreeWidget(this, x, y)
}
}
val element = tabInfoElement
if (element is Renderable && element is NarratableEntry) {
@@ -762,9 +707,6 @@ class PokedexGUI private constructor(
selectedForm = fallbackForm
) { next -> cycleSelectedForm(next) }
}
TAB_EVOLUTION_TREE -> {
(tabInfoElement as EvolutionTreeWidget).setup(selectedEntry, selectedForm)
}
}
} else {
if (tabInfoIndex != TAB_DESCRIPTION) displaytabInfoElement(TAB_DESCRIPTION)
@@ -828,10 +770,6 @@ class PokedexGUI private constructor(
}
}
if (tabInfoIndex == TAB_EVOLUTION_TREE && keyCode == InputConstants.KEY_SEMICOLON) {
debugEvolutionRequirementRendering()
}
return super.keyPressed(keyCode, scanCode, modifiers)
}
@@ -16,8 +16,6 @@ object PokedexGUIConstants {
const val HEADER_BAR_HEIGHT = 11
const val POKEMON_DESCRIPTION_HEIGHT = 42
const val POKEMON_DESCRIPTION_PADDING = 9
const val ICONS_PADDING_AROUND = 18.5f
const val ICONS_ARROW_PADDING_AROUND = ICONS_PADDING_AROUND + 1
const val TAB_ICON_SIZE = 16
@@ -38,7 +36,6 @@ object PokedexGUIConstants {
const val TAB_STATS = 3
const val TAB_DROPS = 4
const val TAB_MOVES = 5
const val TAB_EVOLUTION_TREE = 6
// RowScrollingWidget
const val SCROLL_BASE_HEIGHT = 141
@@ -13,101 +13,38 @@ import com.cobblemon.mod.common.api.text.font
import com.cobblemon.mod.common.client.CobblemonResources
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.util.cobblemonResource
import com.mojang.blaze3d.vertex.PoseStack
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.network.chat.MutableComponent
import net.minecraft.resources.ResourceLocation
private val tooltipEdge = cobblemonResource("textures/gui/pokedex/tooltip_edge.png")
private val tooltipBackground = cobblemonResource("textures/gui/pokedex/tooltip_background.png")
private const val TOOLTIP_SLICE_SIZE = 11
fun renderTooltip(
context: GuiGraphics,
text: MutableComponent,
posX: Int,
posY: Int,
delta: Float,
offsetY: Int = 0,
) {
renderTooltip(
context,
listOf(text),
posX,
posY,
delta,
offsetY,
)
}
fun renderTooltip(
context: GuiGraphics,
text: List<MutableComponent>,
posX: Int,
posY: Int,
delta: Float,
offsetY: Int = 0,
) {
fun renderTooltip(context: GuiGraphics, text: MutableComponent, posX: Int, posY: Int, delta: Float, offsetY: Int = 0) {
val poseStack = context.pose()
val textWidth = text.maxOf { Minecraft.getInstance().font.width(it.font(CobblemonResources.DEFAULT_LARGE)) }
val tooltipPadding = 6
val tooltipWidth = textWidth + tooltipPadding
val textWidth = Minecraft.getInstance().font.width(text.font(CobblemonResources.DEFAULT_LARGE))
val tooltipWidth = textWidth + 6
val tooltipHeight = 11
val tooltipTop = posY + offsetY
poseStack.pushPose()
poseStack.translate(0.0, 0.0, 1000.0)
Minecraft.getInstance().let {
it.mainRenderTarget.bindWrite(false)
context.enableScissor(
posX - (tooltipWidth / 2),
tooltipTop + 1,
(posX - (tooltipWidth / 2)) + tooltipWidth,
tooltipTop + TOOLTIP_SLICE_SIZE * text.size - 1
tooltipTop + 10
)
it.gameRenderer.processBlurEffect(delta)
context.disableScissor()
it.mainRenderTarget.bindWrite(true)
}
val totalHeight = TOOLTIP_SLICE_SIZE * text.size
blitSlicedVertical(poseStack, tooltipEdge, posX - (tooltipWidth / 2) - 1, tooltipTop, 1, totalHeight)
blitSlicedVertical(poseStack, tooltipBackground, posX - (tooltipWidth / 2), tooltipTop, tooltipWidth, totalHeight)
blitSlicedVertical(poseStack, tooltipEdge, posX + (tooltipWidth / 2), tooltipTop, 1, totalHeight)
val centered = text.size == 1
text.forEachIndexed { index, it ->
drawScaledText(
context = context,
font = CobblemonResources.DEFAULT_LARGE,
text = it,
x = if (centered) posX else posX - tooltipWidth / 2 + tooltipPadding / 2,
y = tooltipTop + 1 + TOOLTIP_SLICE_SIZE * index,
shadow = true,
centered = centered
)
}
blitk(matrixStack = poseStack, texture = tooltipEdge, x = posX - (tooltipWidth / 2) - 1, y = tooltipTop, width = 1, height = tooltipHeight)
blitk(matrixStack = poseStack, texture = tooltipBackground, x = posX - (tooltipWidth / 2), y = tooltipTop, width = tooltipWidth, height = tooltipHeight)
blitk(matrixStack = poseStack, texture = tooltipEdge, x = posX + (tooltipWidth / 2), y = tooltipTop, width = 1, height = tooltipHeight)
drawScaledText(context = context, font = CobblemonResources.DEFAULT_LARGE, text = text, x = posX, y = tooltipTop + 1, shadow = true, centered = true)
poseStack.popPose()
}
// Stretching the whole 11px image over an arbitrary height thickens the caps as more lines are added, so
// instead the caps are drawn 1:1 and only the flat fill row is stretched to cover the remaining height
private fun blitSlicedVertical(
poseStack: PoseStack,
texture: ResourceLocation,
x: Number,
y: Int,
width: Number,
totalHeight: Int,
) {
val middleHeight = totalHeight - 2
blitk(matrixStack = poseStack, texture = texture, x = x, y = y, width = width, height = 1, vOffset = 0, textureHeight = TOOLTIP_SLICE_SIZE)
blitk(matrixStack = poseStack, texture = texture, x = x, y = y + 1, width = width, height = middleHeight, vOffset = middleHeight, textureHeight = TOOLTIP_SLICE_SIZE * middleHeight)
blitk(matrixStack = poseStack, texture = texture, x = x, y = y + 1 + middleHeight, width = width, height = 1, vOffset = TOOLTIP_SLICE_SIZE - 1, textureHeight = TOOLTIP_SLICE_SIZE)
}
@@ -20,8 +20,8 @@ import net.minecraft.client.sounds.SoundManager
import net.minecraft.resources.ResourceLocation
class ScaledButton(
buttonX: Float,
buttonY: Float,
var buttonX: Float,
var buttonY: Float,
val buttonWidth: Number,
val buttonHeight: Number,
var resource: ResourceLocation? = null,
@@ -30,18 +30,6 @@ class ScaledButton(
val clickAction: Button.OnPress
): Button(buttonX.toInt(), buttonY.toInt(), buttonWidth.toInt(), buttonHeight.toInt(), "".text(), clickAction, DEFAULT_NARRATION), CobblemonRenderable {
var buttonX: Float = buttonX
set(value) {
field = value
this.x = value.toInt()
}
var buttonY: Float = buttonY
set(value) {
field = value
this.y = value.toInt()
}
var isWidgetActive = false
var isVisible = true
@@ -12,13 +12,14 @@ import com.bedrockk.molang.runtime.MoLangRuntime
import com.cobblemon.mod.common.CobblemonSounds
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.molang.MoLangFunctions.setup
import com.cobblemon.mod.common.api.pokedex.PokedexEntryProgress
import com.cobblemon.mod.common.api.pokedex.entry.PokedexEntry
import com.cobblemon.mod.common.api.pokedex.PokedexEntryProgress
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.client.ClientMoLangFunctions.setupClient
import com.cobblemon.mod.common.client.CobblemonClient
import com.cobblemon.mod.common.client.gui.ScrollingWidget
import com.cobblemon.mod.common.client.gui.drawProfilePokemon
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.SCALE
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.SCROLL_BAR_WIDTH
@@ -26,14 +27,19 @@ import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.SCROLL_BA
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.SCROLL_SLOT_SIZE
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.SCROLL_SLOT_SPACING
import com.cobblemon.mod.common.client.gui.pokedex.widgets.EntriesScrollingWidget.PokemonScrollSlotRow
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.client.render.models.blockbench.FloatingState
import com.cobblemon.mod.common.pokemon.Gender
import com.cobblemon.mod.common.pokemon.RenderablePokemon
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.math.fromEulerXYZDegrees
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.resources.sounds.SimpleSoundInstance
import net.minecraft.network.chat.Component
import net.minecraft.util.Mth
import org.joml.Quaternionf
import org.joml.Vector3f
class EntriesScrollingWidget(val pX: Int, val pY: Int, val setPokedexEntry: (PokedexEntry) -> (Unit)): ScrollingWidget<PokemonScrollSlotRow>(
width = PokedexGUIConstants.HALF_OVERLAY_WIDTH,
@@ -129,6 +135,13 @@ class EntriesScrollingWidget(val pX: Int, val pY: Int, val setPokedexEntry: (Pok
val discoveryLevelList: MutableList<PokedexEntryProgress>,
val setPokedexEntry : (PokedexEntry) -> (Unit)
): Slot<PokemonScrollSlotRow>() {
companion object {
private val slotResource = cobblemonResource("textures/gui/pokedex/pokedex_slot.png")
private val slotHighlight = cobblemonResource("textures/gui/pokedex/slot_select.png")
private val caughtIcon = cobblemonResource("textures/gui/pokedex/caught_icon_small.png")
private val unknownIcon = cobblemonResource("textures/gui/pokedex/pokedex_slot_unknown.png")
private val unimplementedIcon = cobblemonResource("textures/gui/pokedex/pokedex_slot_unimplemented.png")
}
val runtime = MoLangRuntime().setupClient().setup().also {
it.environment.query.addFunction("get_pokedex") { CobblemonClient.clientPokedexData.struct }
}
@@ -148,42 +161,128 @@ class EntriesScrollingWidget(val pX: Int, val pY: Int, val setPokedexEntry: (Pok
hovered: Boolean,
tickDelta: Float
) {
dexDataList.forEachIndexed { index, pokedexEntry ->
val species = PokemonSpecies.getByIdentifier(pokedexEntry.speciesId)
dexDataList.forEachIndexed { index, dexData ->
val state = FloatingState()
val species = PokemonSpecies.getByIdentifier(dexData.speciesId)
//FIXME: This may not work properly when accounting for custom pokemon with the same dex number
val pokemonNumber = (species?.nationalPokedexNumber?.toString() ?: "0").padStart(4, '0')
val speciesNumber = pokemonNumber.text()
val discoveryLevel = discoveryLevelList[index]
val firstVisibleForm = CobblemonClient.clientPokedexData.getEncounteredForms(pokedexEntry).firstOrNull()
val firstVisibleForm = CobblemonClient.clientPokedexData.getEncounteredForms(dexData).firstOrNull()
val shouldDrawMon = firstVisibleForm != null
if (species == null) {
return@forEachIndexed
}
val renderablePokemon =
if (firstVisibleForm != null) {
val firstVisibleGender = CobblemonClient.clientPokedexData.getSeenGenders(pokedexEntry, firstVisibleForm).firstOrNull()
val shinyStates = CobblemonClient.clientPokedexData.getSeenShinyStates(pokedexEntry, firstVisibleForm)
val firstVisibleShiny = shinyStates.count() == 1 && shinyStates.first() == "shiny"
val formAspects = species.forms.firstOrNull { it.name.equals(firstVisibleForm.displayForm, ignoreCase = true) }?.aspects ?: species.standardForm.aspects
val seenAspects = CobblemonClient.clientPokedexData.getSeenAspects(pokedexEntry)
val variationAspects = pokedexEntry.variations.mapNotNull { it.aspects.firstOrNull { it in seenAspects } }
val aspectsToDraw = (pokedexEntry.displayAspects + variationAspects + formAspects + (firstVisibleGender ?: Gender.GENDERLESS).name.lowercase()).toMutableSet()
if (firstVisibleShiny) {
aspectsToDraw.add("shiny")
}
val matrices = context.pose()
RenderablePokemon(species, aspectsToDraw)
} else {
null
}
val startPosX = x + ((SCROLL_SLOT_SPACING + SCROLL_SLOT_SIZE) * index)
val startPosY = y + SCROLL_SLOT_SPACING + 1
PokemonSlotWidget.render(
context,
x = x + (SCROLL_SLOT_SPACING + SCROLL_SLOT_SIZE) * index,
y = y + SCROLL_SLOT_SPACING + 1,
getHoveredSlotIndex(mouseX, mouseY) == index,
species,
renderablePokemon,
discoveryLevel,
blitk(
matrixStack = matrices,
texture = slotResource,
x = startPosX,
y = startPosY,
width = SCROLL_SLOT_SIZE,
height = SCROLL_SLOT_SIZE
)
if (getHoveredSlotIndex(mouseX, mouseY) == index) {
blitk(
matrixStack = matrices,
texture = slotHighlight,
x = startPosX,
y = startPosY,
width = SCROLL_SLOT_SIZE,
height = SCROLL_SLOT_SIZE,
vOffset = SCROLL_SLOT_SIZE,
textureHeight = SCROLL_SLOT_SIZE * 2
)
}
if (shouldDrawMon) {
val firstVisibleGender = CobblemonClient.clientPokedexData.getSeenGenders(dexData, firstVisibleForm).firstOrNull()
val shinyStates = CobblemonClient.clientPokedexData.getSeenShinyStates(dexData, firstVisibleForm)
val firstVisibleShiny = shinyStates.count() == 1 && shinyStates.first() == "shiny"
val formAspects = species.forms.firstOrNull { it.name.equals(firstVisibleForm.displayForm, ignoreCase = true) }?.aspects ?: species.standardForm.aspects
val seenAspects = CobblemonClient.clientPokedexData.getSeenAspects(dexData)
val variationAspects = dexData.variations.mapNotNull { it.aspects.firstOrNull { it in seenAspects } }
context.enableScissor(
startPosX + 1,
startPosY + 1,
startPosX + SCROLL_SLOT_SIZE - 1,
startPosY + SCROLL_SLOT_SIZE - 2
)
val aspectsToDraw = (dexData.displayAspects + variationAspects + formAspects + (firstVisibleGender ?: Gender.GENDERLESS).name.lowercase()).toMutableSet()
if (firstVisibleShiny) {
aspectsToDraw.add("shiny")
}
state.currentAspects = aspectsToDraw
matrices.pushPose()
matrices.translate(startPosX + (SCROLL_SLOT_SIZE / 2.0), startPosY + 1.0, 0.0)
matrices.scale(2.5F, 2.5F, 1F)
drawProfilePokemon(
renderablePokemon = RenderablePokemon(species, aspectsToDraw),
matrixStack = matrices,
rotation = Quaternionf().fromEulerXYZDegrees(Vector3f(13F, 35F, 0F)),
state = state,
partialTicks = 0F,
scale = 4.5F
)
matrices.popPose()
context.disableScissor()
} else {
blitk(
matrixStack = matrices,
texture = unknownIcon,
x = startPosX + 8.5,
y = startPosY + 9,
width = 8,
height = 10
)
if (!species.implemented) {
blitk(
matrixStack = matrices,
texture = unimplementedIcon,
x = (startPosX + 14) / SCALE,
y = (startPosY + 15.5) / SCALE,
width = 7,
height = 7,
scale = SCALE
)
}
}
// Ensure elements are not hidden behind Pokémon render
matrices.pushPose()
matrices.translate(0.0, 0.0, 100.0)
drawScaledText(
context = context,
text = speciesNumber,
x = startPosX + 1.5,//2,
y = startPosY + 2.5,//2
shadow = true,
scale = SCALE
)
if (discoveryLevel == PokedexEntryProgress.OWNED) {
blitk(
matrixStack = matrices,
texture = caughtIcon,
x = (startPosX + 18) / SCALE,
y = (startPosY + 1.5) / SCALE,
width = 11,
height = 11,
scale = SCALE
)
}
matrices.popPose()
}
}
@@ -10,10 +10,10 @@ package com.cobblemon.mod.common.client.gui.pokedex.widgets
import com.bedrockk.molang.runtime.MoLangRuntime
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.pokedex.entry.PokedexEntry
import com.cobblemon.mod.common.api.molang.MoLangFunctions.setup
import com.cobblemon.mod.common.api.pokedex.PokedexEntryProgress
import com.cobblemon.mod.common.api.pokedex.entry.PokedexCosmeticVariation
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.text.bold
@@ -25,9 +25,8 @@ import com.cobblemon.mod.common.client.CobblemonResources
import com.cobblemon.mod.common.client.gui.ProfileTransformType
import com.cobblemon.mod.common.client.gui.TypeIcon
import com.cobblemon.mod.common.client.gui.drawProfilePokemon
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUI
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.HALF_OVERLAY_WIDTH
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.HALF_OVERLAY_HEIGHT
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.HALF_OVERLAY_WIDTH
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.POKEMON_PORTRAIT_HEIGHT
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.POKEMON_PORTRAIT_WIDTH
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.PORTRAIT_POKE_BALL_HEIGHT
@@ -53,13 +52,15 @@ import com.cobblemon.mod.common.util.math.fromEulerXYZDegrees
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.resources.sounds.SimpleSoundInstance
import net.minecraft.network.chat.Component
import net.minecraft.network.chat.MutableComponent
import net.minecraft.resources.ResourceLocation
import net.minecraft.sounds.SoundEvent
import org.joml.Quaternionf
import org.joml.Vector3f
import java.io.FileNotFoundException
class PokemonInfoWidget(val parent: PokedexGUI, val pX: Int, val pY: Int, val updateForm: (PokedexForm) -> (Unit)) : SoundlessWidget(
class PokemonInfoWidget(val pX: Int, val pY: Int, val updateForm: (PokedexForm) -> (Unit)) : SoundlessWidget(
pX,
pY,
POKEMON_PORTRAIT_WIDTH,
@@ -81,6 +82,7 @@ class PokemonInfoWidget(val parent: PokedexGUI, val pX: Int, val pY: Int, val up
private val arrowFormLeft = cobblemonResource("textures/gui/pokedex/forms_arrow_left.png")
private val arrowFormRight = cobblemonResource("textures/gui/pokedex/forms_arrow_right.png")
private val caughtIcon = cobblemonResource("textures/gui/pokedex/caught_icon.png")
private val typeBar = cobblemonResource("textures/gui/pokedex/type_bar.png")
private val typeBarDouble = cobblemonResource("textures/gui/pokedex/type_bar_double.png")
@@ -98,6 +100,9 @@ class PokemonInfoWidget(val parent: PokedexGUI, val pX: Int, val pY: Int, val up
var currentEntry : PokedexEntry? = null
var speciesName: MutableComponent = Component.translatable("")
var speciesNumber: MutableComponent = "0000".text()
val runtime = MoLangRuntime().setupClient().setup().also {
it.environment.query.addFunction("get_pokedex") { CobblemonClient.clientPokedexData.struct }
}
@@ -106,6 +111,10 @@ class PokemonInfoWidget(val parent: PokedexGUI, val pX: Int, val pY: Int, val up
var selectedFormIndex: Int = 0
var type: Array<ElementalType?> = arrayOf(null, null)
var seenShinyStates: Set<String> = setOf()
var shiny = false
var maleRatio = -1F
var gender: Gender = Gender.GENDERLESS
var renderablePokemon : RenderablePokemon? = null
@@ -186,10 +195,7 @@ class PokemonInfoWidget(val parent: PokedexGUI, val pX: Int, val pY: Int, val up
20,
resource = buttonGenderMale,
clickAction = {
val maleRatio = parent.maleRatio
if (maleRatio > 0 && maleRatio < 1) {
parent.gender = if (parent.gender == Gender.MALE) Gender.FEMALE else Gender.MALE
}
if (maleRatio > 0 && maleRatio < 1) gender = if (gender == Gender.MALE) Gender.FEMALE else Gender.MALE
updateAspects()
}
).apply { addWidget(this) }
@@ -201,8 +207,8 @@ class PokemonInfoWidget(val parent: PokedexGUI, val pX: Int, val pY: Int, val up
20,
buttonNone,
clickAction = {
if (parent.seenShinyStates.size > 1) {
parent.shiny = !parent.shiny
if (seenShinyStates.size > 1) {
shiny = !shiny
updateAspects()
}
}
@@ -257,7 +263,8 @@ class PokemonInfoWidget(val parent: PokedexGUI, val pX: Int, val pY: Int, val up
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
val currentEntry = this.currentEntry ?: return
val hasKnowledge = CobblemonClient.clientPokedexData.getKnowledgeForSpecies(currentEntry.speciesId) != PokedexEntryProgress.UNREGISTERED
val hasKnowledge =
CobblemonClient.clientPokedexData.getKnowledgeForSpecies(currentEntry.speciesId) != PokedexEntryProgress.UNREGISTERED
val species = currentEntry.speciesId.let { PokemonSpecies.getByIdentifier(it) } ?: return
val matrices = context.pose()
@@ -270,6 +277,37 @@ class PokemonInfoWidget(val parent: PokedexGUI, val pX: Int, val pY: Int, val up
height = HALF_OVERLAY_HEIGHT
)
drawScaledText(
context = context,
font = CobblemonResources.DEFAULT_LARGE,
text = speciesNumber.bold(),
x = pX + 3,
y = pY + 1,
shadow = true
)
drawScaledText(
context = context,
font = CobblemonResources.DEFAULT_LARGE,
text = (if (hasKnowledge) speciesName else lang("ui.generic.question_marks")).bold(),
x = pX + 26,
y = pY + 1,
colour = 0x606B6E
)
// Caught icon
if (isSelectedPokemonOwned()) {
blitk(
matrixStack = matrices,
texture = caughtIcon,
x = (pX + 129) / SCALE,
y = (pY + 2) / SCALE,
width = 14,
height = 14,
scale = SCALE
)
}
if (!suppressViewport) {
blitk(
matrixStack = matrices,
@@ -533,6 +571,8 @@ class PokemonInfoWidget(val parent: PokedexGUI, val pX: Int, val pY: Int, val up
if (species != null) {
this.visibleForms = CobblemonClient.clientPokedexData.getEncounteredForms(pokedexEntry).toMutableList()
this.speciesNumber = species.nationalPokedexNumber.toString().padStart(4, '0').text()
this.speciesName = species.translatedName
if (forms.isNotEmpty()) {
formLeftButton.active = true
@@ -558,23 +598,23 @@ class PokemonInfoWidget(val parent: PokedexGUI, val pX: Int, val pY: Int, val up
variationButtons.forEach { removeWidget(it.getWidget()) }
variationButtons.clear()
parent.seenShinyStates = CobblemonClient.clientPokedexData.getSeenShinyStates(pokedexEntry, pokedexForm)
parent.shiny = parent.seenShinyStates.count() == 1 && parent.seenShinyStates.first() == "shiny"
shinyButton.resource = if (parent.shiny) buttonShiny else buttonNone
shinyButton.visible = parent.shiny || parent.seenShinyStates.size > 1
shinyButton.active = parent.seenShinyStates.size > 1
seenShinyStates = CobblemonClient.clientPokedexData.getSeenShinyStates(pokedexEntry, pokedexForm)
shiny = seenShinyStates.count() == 1 && seenShinyStates.first() == "shiny"
shinyButton.resource = if (shiny) buttonShiny else buttonNone
shinyButton.visible = shiny || seenShinyStates.size > 1
shinyButton.active = seenShinyStates.size > 1
val form = species.forms.find { it.name.equals(pokedexForm.displayForm, ignoreCase = true) } ?: species.standardForm
parent.maleRatio = form.maleRatio
maleRatio = form.maleRatio
val seenGenders = CobblemonClient.clientPokedexData.getSeenGenders(pokedexEntry, pokedexForm)
val visibleGenders = seenGenders.filter { it != Gender.GENDERLESS }
if (visibleGenders.isEmpty()) {
parent.gender = Gender.GENDERLESS
gender = Gender.GENDERLESS
genderButton.visible = false
genderButton.active = false
} else {
parent.gender = visibleGenders.first()
gender = visibleGenders.first()
genderButton.visible = true
genderButton.active = visibleGenders.size > 1
}
@@ -653,8 +693,8 @@ class PokemonInfoWidget(val parent: PokedexGUI, val pX: Int, val pY: Int, val up
return
}
genderButton.resource = if (parent.gender == Gender.FEMALE) buttonGenderFemale else buttonGenderMale
shinyButton.resource = if (parent.shiny) buttonShiny else buttonNone
genderButton.resource = if (gender == Gender.FEMALE) buttonGenderFemale else buttonGenderMale
shinyButton.resource = if (shiny) buttonShiny else buttonNone
val species = currentEntry?.speciesId?.let { PokemonSpecies.getByIdentifier(it) }
if (species != null) {
@@ -663,14 +703,12 @@ class PokemonInfoWidget(val parent: PokedexGUI, val pX: Int, val pY: Int, val up
updateType(species, form)
val aspects = parent.aspects
aspects.clear()
val aspects = mutableSetOf<String>()
if (shiny) aspects.add(SHINY_ASPECT.aspect)
if (parent.shiny) aspects.add(SHINY_ASPECT.aspect)
if (parent.gender == Gender.FEMALE) {
if (gender == Gender.FEMALE) {
aspects.add("female")
} else if (parent.gender == Gender.MALE) {
} else if (gender == Gender.MALE) {
aspects.add("male")
}
@@ -1,100 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.pokedex.widgets
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.pokedex.PokedexEntryProgress
import com.cobblemon.mod.common.api.pokedex.entry.PokedexEntry
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.api.text.bold
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.client.CobblemonClient
import com.cobblemon.mod.common.client.CobblemonResources
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUI
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.POKEMON_PORTRAIT_HEIGHT
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.POKEMON_PORTRAIT_WIDTH
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.SCALE
import com.cobblemon.mod.common.client.gui.summary.widgets.SoundlessWidget
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.lang
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.network.chat.Component
import net.minecraft.network.chat.MutableComponent
class PokemonNameWidget(
val parent: PokedexGUI,
val pX: Int,
val pY: Int,
) : SoundlessWidget(
pX,
pY,
POKEMON_PORTRAIT_WIDTH,
POKEMON_PORTRAIT_HEIGHT,
lang("ui.pokedex.pokemon_name"),
) {
companion object {
private val caughtIcon = cobblemonResource("textures/gui/pokedex/caught_icon.png")
}
var hasKnowledge: Boolean = false
var isOwned: Boolean = false
var speciesName: MutableComponent = Component.translatable("")
var speciesNumber: MutableComponent = "0000".text()
override fun renderWidget(
context: GuiGraphics,
mouseX: Int,
mouseY: Int,
partialTick: Float
) {
drawScaledText(
context = context,
font = CobblemonResources.DEFAULT_LARGE,
text = speciesNumber.bold(),
x = pX + 3,
y = pY + 1,
shadow = true
)
if (hasKnowledge) {
drawScaledText(
context = context,
font = CobblemonResources.DEFAULT_LARGE,
text = speciesName.bold(),
x = pX + 26,
y = pY + 1,
colour = 0x606B6E
)
}
if (isOwned) {
blitk(
matrixStack = context.pose(),
texture = caughtIcon,
x = (pX + 129) / SCALE,
y = (pY + 2) / SCALE,
width = 14,
height = 14,
scale = SCALE
)
}
}
fun update(pokedexEntry: PokedexEntry) {
val species = PokemonSpecies.getByIdentifier(pokedexEntry.speciesId) ?: return
val progress = CobblemonClient.clientPokedexData.getKnowledgeForSpecies(pokedexEntry.speciesId)
this.hasKnowledge = progress != PokedexEntryProgress.UNREGISTERED
this.isOwned = progress == PokedexEntryProgress.OWNED
this.speciesNumber = species.nationalPokedexNumber.toString().padStart(4, '0').text()
this.speciesName = species.translatedName
}
}
@@ -1,151 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.pokedex.widgets
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.pokedex.PokedexEntryProgress
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.client.gui.drawProfilePokemon
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.SCALE
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.SCROLL_SLOT_SIZE
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.client.render.models.blockbench.FloatingState
import com.cobblemon.mod.common.pokemon.RenderablePokemon
import com.cobblemon.mod.common.pokemon.Species
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.math.fromEulerXYZDegrees
import net.minecraft.client.gui.GuiGraphics
import org.joml.Quaternionf
import org.joml.Vector3f
abstract class PokemonSlotWidget {
companion object {
private val slotResource = cobblemonResource("textures/gui/pokedex/pokedex_slot.png")
private val slotHighlight = cobblemonResource("textures/gui/pokedex/slot_select.png")
private val caughtIcon = cobblemonResource("textures/gui/pokedex/caught_icon_small.png")
private val unknownIcon = cobblemonResource("textures/gui/pokedex/pokedex_slot_unknown.png")
private val unimplementedIcon = cobblemonResource("textures/gui/pokedex/pokedex_slot_unimplemented.png")
fun render(
context: GuiGraphics,
x: Int,
y: Int,
isHovered: Boolean,
species: Species,
renderablePokemon: RenderablePokemon?,
discoveryLevel: PokedexEntryProgress,
) {
val shouldDrawMon = renderablePokemon != null
// FIXME: This may not work properly when accounting for custom pokemon with the same dex number
val pokemonNumber = species.nationalPokedexNumber.toString().padStart(4, '0')
val speciesNumber = pokemonNumber.text()
val matrices = context.pose()
blitk(
matrixStack = matrices,
texture = slotResource,
x = x,
y = y,
width = SCROLL_SLOT_SIZE,
height = SCROLL_SLOT_SIZE,
)
if (isHovered) {
blitk(
matrixStack = matrices,
texture = slotHighlight,
x = x,
y = y,
width = SCROLL_SLOT_SIZE,
height = SCROLL_SLOT_SIZE,
vOffset = SCROLL_SLOT_SIZE,
textureHeight = SCROLL_SLOT_SIZE * 2,
)
}
if (shouldDrawMon && discoveryLevel != PokedexEntryProgress.UNREGISTERED) {
context.enableScissor(
x + 1,
y + 1,
x + SCROLL_SLOT_SIZE - 1,
y + SCROLL_SLOT_SIZE - 2,
)
matrices.pushPose()
matrices.translate(x + SCROLL_SLOT_SIZE / 2.0, y + 1.0, 0.0)
matrices.scale(2.5F, 2.5F, 1F)
val state = FloatingState()
state.currentAspects = renderablePokemon.aspects
drawProfilePokemon(
renderablePokemon = renderablePokemon,
matrixStack = matrices,
rotation = Quaternionf().fromEulerXYZDegrees(Vector3f(13F, 35F, 0F)),
state = state,
partialTicks = 0F,
scale = 4.5F
)
matrices.popPose()
context.disableScissor()
} else {
blitk(
matrixStack = matrices,
texture = unknownIcon,
x = x + 8.5,
y = y + 9,
width = 8,
height = 10
)
if (!species.implemented) {
blitk(
matrixStack = matrices,
texture = unimplementedIcon,
x = (x + 14) / SCALE,
y = (y + 15.5) / SCALE,
width = 7,
height = 7,
scale = SCALE
)
}
}
// Ensure elements are not hidden behind Pokémon render
matrices.pushPose()
matrices.translate(0.0, 0.0, 100.0)
drawScaledText(
context = context,
text = speciesNumber,
x = x + 1.5,
y = y + 2.5,
shadow = true,
scale = SCALE
)
if (discoveryLevel == PokedexEntryProgress.OWNED) {
blitk(
matrixStack = matrices,
texture = caughtIcon,
x = (x + 18) / SCALE,
y = (y + 1.5) / SCALE,
width = 11,
height = 11,
scale = SCALE
)
}
matrices.popPose()
}
}
}

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