mirror of
https://gitlab.com/cable-mc/cobblemon.git
synced 2026-10-23 23:56:24 -04:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16ff2752d1 | ||
|
|
40ece7d051 | ||
|
|
9bf26a1bbf | ||
|
|
fca8500d47 | ||
|
|
b3be1358ea | ||
|
|
7efe86e574 | ||
|
|
0a3eee798d |
@@ -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
@@ -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:
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
-4
@@ -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)))
|
||||
}
|
||||
|
||||
+1
-12
@@ -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)
|
||||
}
|
||||
+20
-24
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -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)
|
||||
|
||||
-22
@@ -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) }
|
||||
}
|
||||
}
|
||||
+50
@@ -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())
|
||||
}
|
||||
}
|
||||
+201
@@ -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()
|
||||
}
|
||||
+93
@@ -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
|
||||
}
|
||||
}
|
||||
+94
@@ -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 }
|
||||
}
|
||||
}
|
||||
+17
@@ -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
|
||||
)
|
||||
+300
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+89
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+114
@@ -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 } }
|
||||
}
|
||||
+135
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+68
@@ -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
|
||||
}
|
||||
}
|
||||
+92
@@ -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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-23
@@ -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
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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>()
|
||||
}
|
||||
}
|
||||
-62
@@ -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)
|
||||
|
||||
-2
@@ -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
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+193
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+64
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
+79
@@ -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
|
||||
}
|
||||
+28
@@ -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
|
||||
}
|
||||
+316
@@ -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()
|
||||
}
|
||||
}
|
||||
+31
@@ -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
|
||||
}
|
||||
+63
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
+32
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
+60
@@ -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()
|
||||
}
|
||||
+37
@@ -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)
|
||||
}
|
||||
}
|
||||
+51
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
+101
@@ -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())
|
||||
}
|
||||
}
|
||||
+35
@@ -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)
|
||||
}
|
||||
}
|
||||
+76
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
+44
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+24
@@ -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
|
||||
)
|
||||
}
|
||||
+34
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
-3
@@ -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
|
||||
|
||||
+9
-72
@@ -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
|
||||
|
||||
|
||||
+127
-28
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+65
-27
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
-100
@@ -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
|
||||
}
|
||||
}
|
||||
-151
@@ -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
Reference in New Issue
Block a user