mirror of
https://gitlab.com/cable-mc/cobblemon.git
synced 2026-10-25 16:46:25 -04:00
Compare commits
57
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16ff2752d1 | ||
|
|
40ece7d051 | ||
|
|
9bf26a1bbf | ||
|
|
fca8500d47 | ||
|
|
b3be1358ea | ||
|
|
7efe86e574 | ||
|
|
3cdf28ffdd | ||
|
|
c6fb18ea51 | ||
|
|
2d851cb134 | ||
|
|
f685be0aab | ||
|
|
6ab5ad52ec | ||
|
|
079b2ae766 | ||
|
|
34eea827e7 | ||
|
|
06c6ee5a9c | ||
|
|
54748c20ea | ||
|
|
02e63b6fc8 | ||
|
|
3aebcf66ec | ||
|
|
49a5f716b7 | ||
|
|
0bd7b4dab3 | ||
|
|
8cbada0cf5 | ||
|
|
f0784fec2d | ||
|
|
cfe0c5e0a5 | ||
|
|
75bc03fdcb | ||
|
|
0a3eee798d | ||
|
|
309ff3da13 | ||
|
|
3af91e2a74 | ||
|
|
2e027e3db7 | ||
|
|
fc40fb02c0 | ||
|
|
2c1c0299f9 | ||
|
|
08cbbaf68c | ||
|
|
6e911acf01 | ||
|
|
f6d82a1b28 | ||
|
|
702126aeab | ||
|
|
df566e8fcb | ||
|
|
c83e0f33b5 | ||
|
|
f279f936b6 | ||
|
|
f8e20bafd9 | ||
|
|
a5211ca7ff | ||
|
|
2f7bedf001 | ||
|
|
44c31a9ddb | ||
|
|
cfc8a15db1 | ||
|
|
d5ba22a86c | ||
|
|
a38505f1c5 | ||
|
|
3cf1ba1fb3 | ||
|
|
9f5d595827 | ||
|
|
75673f0204 | ||
|
|
b43a7571ff | ||
|
|
b071dc04d3 | ||
|
|
12f9ff1647 | ||
|
|
3256a302df | ||
|
|
edbe447477 | ||
|
|
2f43910368 | ||
|
|
c2e87e4cfa | ||
|
|
e40d2ee806 | ||
|
|
ba74e7603b | ||
|
|
19c3fe6dc4 | ||
|
|
d0778dd124 |
+38
-34
@@ -1,41 +1,45 @@
|
||||
# Changelog
|
||||
## [1.8.1 (September 13th, 2026)](#1-8-1)
|
||||
## [1.8.2 (MONTH xth, 2026)](#1-8-2)
|
||||
|
||||
### Additions
|
||||
- The following items can now be placed on the ground as blocks:
|
||||
- King's Rock, Galarica Cuff, Unremarkable Teacup, Masterpiece Teacup, Chipped Pot, Cracked Pot, Sachet, Metal Powder, Quick Powder, Bright Powder, Icy Rock, Heat Rock, Smooth Rock, Damp Rock, Destiny Knot, Luminous Moss, Never-Melt Ice, Wide Lens, Scope Lens, and Zoom Lens.
|
||||
- Iron Ball, Metal Coat, Throat Spray, HP Up, Protein, Iron, Carbos, Calcium, Zinc, PP Up, PP Max, Upgrade, Dubious Disc, and Loaded Dice.
|
||||
- The following items can now be placed on various surfaces:
|
||||
- Galarica Wreath, Magnet, Flame Orb, Life Orb, Toxic Orb, Light Ball, and Smoke Ball.
|
||||
- The following items can now be placed as blocks, and have additional functionality:
|
||||
- Ring Target, Metronome, and Eject Button.
|
||||
- The following items can now be worn by Players and Pokémon:
|
||||
- Galarica Wreath, Cleanse Tag, Spell Tag, Sharp Beak, Covert Cloak, Shell Helmet, and Never-Melt Ice.
|
||||
- Wearable items can now be equipped by a player by holding it and right-clicking (or rebound use/interact)
|
||||
- Wearable items that can be placed down are exempt from this behaviour.
|
||||
|
||||
### New rideable Pokémon
|
||||
|
||||
### Pokémon Added
|
||||
|
||||
#### Regional Forms
|
||||
|
||||
### Model updates for the following Pokémon
|
||||
|
||||
### Animation updates for the following Pokémon
|
||||
- Flapple, Appletun, Dipplin, Hydrapple (placeholder animations only).
|
||||
|
||||
#### Added sounds to the following moves
|
||||
|
||||
### Changes
|
||||
|
||||
### Fixes
|
||||
- Fixed a potential world generation stall caused by Type Gem clusters.
|
||||
- Fixed Pokémon on shoulders not animating if they are visibly holding a held item.
|
||||
- Fixed NPCs getting their own TM progress file.
|
||||
- Fixed odd NeoForge crash due to a dependency being outdated. It will now correctly flag the dependency for being outdated.
|
||||
- Fixed the TM Machine refusing move selections that the `unlockAllMoveDexMovesByDefault` config had made visible, leaving the move slot blank and the craft button unresponsive.
|
||||
- Fixed the Beast Ball not applying its 0.1× penalty to Pokémon that are not Ultra Beasts.
|
||||
- Fixed the Moon Ball reading the world's total elapsed time instead of the time of day, so its bonus applied during the day instead.
|
||||
- Fixed the Love Ball never awarding its 2.5× opposite gender bonus, and matching species by display name instead of identifier.
|
||||
- Fixed the Timer Ball being weaker than a regular Poké Ball on early turns and only reaching 4× on turn 14 instead of turn 10.
|
||||
- Fixed the Luxury Ball only doubling Friendship gains of 1, leaving level up and berry gains unaffected.
|
||||
- Fixed Rufflet, Braviary, and Hisuian Braviary flooding logs with errors about missing sound events.
|
||||
- Fixed Pokémon property typos in Pangoro and Dedenne herd spawn files.
|
||||
- Fixed unintentional watermelon cosmetic for Hisuian Electrode.
|
||||
- Fixed Blastoise having an unintentional passenger seat.
|
||||
- Fixed form typo in Rockruff-Dusk spawn, preventing it from being able to spawn.
|
||||
- Fixed the `stat_compare` and `stat_equal` evolution requirements crashing the game when given `special_attack` or `special_defence`, and made them treat any non-permanent stat as a failed requirement instead of crashing.
|
||||
- Fixed Rowap and Jacoba berries not correctly altering Pokémon sizes when used as bait.
|
||||
- Fixed Ampharos' eyes not emitting particles.
|
||||
- Fixed Alolan Raticate's cry not playing.
|
||||
- Fixed several incorrect light-based spawning conditions for ghosts and bugs.
|
||||
- Removed unused, placeholder habitat pools.
|
||||
- Fixed Mantyke, Mantine, and Quaxly getting stuck in a recoil 'expression' during battles due to having an expression assigned as a recoil animation.
|
||||
- Fixed Lucario's aura particles not emitting.
|
||||
- Fixed Lycanroc's head movement.
|
||||
- Added head movement to Archaludon.
|
||||
- Fixed Duraludon line not having their sleep animations.
|
||||
- Fixed Diglett line not having their faint animations.
|
||||
- Fixed Kingambit moving its legs while walking.
|
||||
- Correctly grants "They're Minerals!" advancement when planting a type gem
|
||||
- Type Gem Blocks now have advancements to give you the recipe in the recipe book (the data for the advancements were inside the recipes somehow??)
|
||||
- Corrected the TM Recipes for Charge Beam and Payday
|
||||
|
||||
### Developer
|
||||
|
||||
### Molang & Datapacks
|
||||
|
||||
### Particles
|
||||
|
||||
#### New Move Animations
|
||||
|
||||
### Localization
|
||||
- Updated translations for:
|
||||
- Brazilian Portuguese
|
||||
- Simplified Chinese
|
||||
- Japanese
|
||||
- Updated translations for:
|
||||
@@ -0,0 +1,41 @@
|
||||
# Changelog
|
||||
## [1.8.1 (September 13th, 2026)](#1-8-1)
|
||||
|
||||
### Animation updates for the following Pokémon
|
||||
- Flapple, Appletun, Dipplin, Hydrapple (placeholder animations only).
|
||||
|
||||
### Fixes
|
||||
- Fixed a potential world generation stall caused by Type Gem clusters.
|
||||
- Fixed Pokémon on shoulders not animating if they are visibly holding a held item.
|
||||
- Fixed NPCs getting their own TM progress file.
|
||||
- Fixed odd NeoForge crash due to a dependency being outdated. It will now correctly flag the dependency for being outdated.
|
||||
- Fixed the TM Machine refusing move selections that the `unlockAllMoveDexMovesByDefault` config had made visible, leaving the move slot blank and the craft button unresponsive.
|
||||
- Fixed the Beast Ball not applying its 0.1× penalty to Pokémon that are not Ultra Beasts.
|
||||
- Fixed the Moon Ball reading the world's total elapsed time instead of the time of day, so its bonus applied during the day instead.
|
||||
- Fixed the Love Ball never awarding its 2.5× opposite gender bonus, and matching species by display name instead of identifier.
|
||||
- Fixed the Timer Ball being weaker than a regular Poké Ball on early turns and only reaching 4× on turn 14 instead of turn 10.
|
||||
- Fixed the Luxury Ball only doubling Friendship gains of 1, leaving level up and berry gains unaffected.
|
||||
- Fixed Rufflet, Braviary, and Hisuian Braviary flooding logs with errors about missing sound events.
|
||||
- Fixed Pokémon property typos in Pangoro and Dedenne herd spawn files.
|
||||
- Fixed unintentional watermelon cosmetic for Hisuian Electrode.
|
||||
- Fixed Blastoise having an unintentional passenger seat.
|
||||
- Fixed form typo in Rockruff-Dusk spawn, preventing it from being able to spawn.
|
||||
- Fixed the `stat_compare` and `stat_equal` evolution requirements crashing the game when given `special_attack` or `special_defence`, and made them treat any non-permanent stat as a failed requirement instead of crashing.
|
||||
- Fixed Rowap and Jacoba berries not correctly altering Pokémon sizes when used as bait.
|
||||
- Fixed Ampharos' eyes not emitting particles.
|
||||
- Fixed Alolan Raticate's cry not playing.
|
||||
- Fixed several incorrect light-based spawning conditions for ghosts and bugs.
|
||||
- Removed unused, placeholder habitat pools.
|
||||
- Fixed Mantyke, Mantine, and Quaxly getting stuck in a recoil 'expression' during battles due to having an expression assigned as a recoil animation.
|
||||
- Fixed Lucario's aura particles not emitting.
|
||||
- Fixed Lycanroc's head movement.
|
||||
- Added head movement to Archaludon.
|
||||
- Fixed Duraludon line not having their sleep animations.
|
||||
- Fixed Diglett line not having their faint animations.
|
||||
- Fixed Kingambit moving its legs while walking.
|
||||
|
||||
### Localization
|
||||
- Updated translations for:
|
||||
- Brazilian Portuguese
|
||||
- Simplified Chinese
|
||||
- Japanese
|
||||
@@ -66,6 +66,8 @@ 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")
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -16,6 +16,8 @@ import com.cobblemon.mod.common.block.MintBlock.MintType
|
||||
import com.cobblemon.mod.common.block.campfirepot.CampfireBlock
|
||||
import com.cobblemon.mod.common.block.campfirepot.CampfirePotBlock
|
||||
import com.cobblemon.mod.common.block.chest.GildedChestBlock
|
||||
import com.cobblemon.mod.common.block.decor.ActivatableDecorationBlock
|
||||
import com.cobblemon.mod.common.block.decor.RingTargetBlock
|
||||
import com.cobblemon.mod.common.block.general.BaleBlock
|
||||
import com.cobblemon.mod.common.block.general.HorizontalRotationCarpetBlock
|
||||
import com.cobblemon.mod.common.block.general.HorizontalRotationalBlock
|
||||
@@ -620,6 +622,88 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<
|
||||
@JvmField
|
||||
val WEAKNESS_POLICY = create("weakness_policy", WallAttachedDirectionalShapeBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.ITEM_BLOCK_PAPER_LARGE_SOUNDS).mapColor(MapColor.NONE).noOcclusion().noCollission(), 14, 12, 1))
|
||||
|
||||
@JvmField
|
||||
val RING_TARGET = create("ring_target", RingTargetBlock(BlockBehaviour.Properties.of().sound(SoundType.WOOD).mapColor(MapColor.NONE).noOcclusion().strength(0.125f)))
|
||||
@JvmField
|
||||
val MAGNET = create("magnet", MagnetBlock(BlockBehaviour.Properties.of().sound(SoundType.METAL).mapColor(MapColor.NONE).noOcclusion().strength(0.075f)))
|
||||
@JvmField
|
||||
val GALARICA_WREATH = create("galarica_wreath", GalaricaWreathBlock(BlockBehaviour.Properties.of().sound(SoundType.AZALEA_LEAVES).mapColor(MapColor.NONE).noOcclusion().noCollission()))
|
||||
|
||||
@JvmField
|
||||
val FLAME_ORB = create("flame_orb", OrbBlock(BlockBehaviour.Properties.of().sound(SoundType.GLASS).mapColor(MapColor.NONE).noOcclusion().lightLevel{15}))
|
||||
@JvmField
|
||||
val TOXIC_ORB = create("toxic_orb", OrbBlock(BlockBehaviour.Properties.of().sound(SoundType.GLASS).mapColor(MapColor.NONE).noOcclusion()))
|
||||
@JvmField
|
||||
val LIFE_ORB = create("life_orb", OrbBlock(BlockBehaviour.Properties.of().sound(SoundType.GLASS).mapColor(MapColor.NONE).noOcclusion().lightLevel{7}))
|
||||
@JvmField
|
||||
val LIGHT_BALL = create("light_ball", OrbBlock(BlockBehaviour.Properties.of().sound(SoundType.GLASS).mapColor(MapColor.NONE).noOcclusion().lightLevel{13}))
|
||||
@JvmField
|
||||
val SMOKE_BALL = create("smoke_ball", OrbBlock(BlockBehaviour.Properties.of().sound(SoundType.GLASS).mapColor(MapColor.NONE).noOcclusion()))
|
||||
|
||||
@JvmField
|
||||
val KINGS_ROCK = create("kings_rock", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.TUMBLESTONE_BLOCK_SOUNDS).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.CENTERED_6x))
|
||||
|
||||
@JvmField
|
||||
val GALARICA_CUFF = create("galarica_cuff", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.AZALEA_LEAVES).mapColor(MapColor.NONE).noCollission().noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.FLATTENED_10x4))
|
||||
@JvmField
|
||||
val UNREMARKABLE_TEACUP = create("unremarkable_teacup", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.DECORATED_POT).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.TEACUP))
|
||||
@JvmField
|
||||
val MASTERPIECE_TEACUP = create("masterpiece_teacup", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.DECORATED_POT).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.TEACUP))
|
||||
@JvmField
|
||||
val CHIPPED_POT = create("chipped_pot", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.DECORATED_POT).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.TEAPOT))
|
||||
@JvmField
|
||||
val CRACKED_POT = create("cracked_pot", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.DECORATED_POT).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.TEAPOT))
|
||||
|
||||
@JvmField
|
||||
val SACHET = create("sachet", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.WOOL).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.POUCH))
|
||||
@JvmField
|
||||
val METAL_POWDER = create("metal_powder", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.WOOL).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.POUCH))
|
||||
@JvmField
|
||||
val QUICK_POWDER = create("quick_powder", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.WOOL).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.POUCH))
|
||||
@JvmField
|
||||
val BRIGHT_POWDER = create("bright_powder", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.WOOL).mapColor(MapColor.NONE).noOcclusion().lightLevel{3}, DecorativeItemBlock.DecorativeItemBlockType.POUCH))
|
||||
|
||||
@JvmField
|
||||
val NEVER_MELT_ICE = create("never_melt_ice", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.GLASS).mapColor(MapColor.NONE).noOcclusion().friction(0.989F), DecorativeItemBlock.DecorativeItemBlockType.NEVERMELT))
|
||||
@JvmField
|
||||
val SCOPE_LENS = create("scope_lens", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.COPPER).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.SCOPE))
|
||||
@JvmField
|
||||
val WIDE_LENS = create("wide_lens", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.COPPER).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.WIDE))
|
||||
@JvmField
|
||||
val ZOOM_LENS = create("zoom_lens", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.COPPER).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.ZOOM))
|
||||
|
||||
@JvmField
|
||||
val ICY_ROCK = create("icy_rock", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.DEEPSLATE).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.FLATTENED_8x4))
|
||||
@JvmField
|
||||
val HEAT_ROCK = create("heat_rock", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.DEEPSLATE).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.FLATTENED_8x4))
|
||||
@JvmField
|
||||
val SMOOTH_ROCK = create("smooth_rock", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.DEEPSLATE).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.FLATTENED_8x4))
|
||||
@JvmField
|
||||
val DAMP_ROCK = create("damp_rock", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.DEEPSLATE).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.FLATTENED_8x6))
|
||||
@JvmField
|
||||
val DESTINY_KNOT = create("destiny_knot", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.WOOL).mapColor(MapColor.NONE).noOcclusion(), DecorativeItemBlock.DecorativeItemBlockType.KNOT))
|
||||
@JvmField
|
||||
val LUMINOUS_MOSS = create("luminous_moss", DecorativeItemBlock(BlockBehaviour.Properties.of().sound(SoundType.MOSS).mapColor(MapColor.NONE).noOcclusion().lightLevel{5}, DecorativeItemBlock.DecorativeItemBlockType.CENTERED_8x))
|
||||
|
||||
@JvmField
|
||||
val METRONOME = create("metronome", ActivatableDecorationBlock(BlockBehaviour.Properties.of().sound(SoundType.METAL).mapColor(MapColor.NONE).pushReaction(PushReaction.DESTROY).noOcclusion(), ActivatableDecorationBlock.ActivatableDecorationBlockType.METRONOME))
|
||||
@JvmField
|
||||
val EJECT_BUTTON = create("eject_button", EjectButtonBlock(BlockBehaviour.Properties.of().sound(SoundType.METAL).mapColor(MapColor.NONE).pushReaction(PushReaction.DESTROY).noOcclusion().strength(0.125f), 40))
|
||||
|
||||
@JvmField
|
||||
val LOADED_DICE = create("loaded_dice", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.DICE))
|
||||
@JvmField
|
||||
val IRON_BALL = create("iron_ball", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.METAL).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.IRON_BALL))
|
||||
@JvmField
|
||||
val METAL_COAT = create("metal_coat", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.METAL).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.METAL_COAT))
|
||||
@JvmField
|
||||
val UPGRADE = create("upgrade", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.UPGRADE))
|
||||
@JvmField
|
||||
val DUBIOUS_DISC = create("dubious_disc", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.UPGRADE))
|
||||
|
||||
@JvmField
|
||||
val THROAT_SPRAY = create("throat_spray", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.THROAT_SPRAY))
|
||||
|
||||
@JvmField
|
||||
val FULL_HEAL = create("full_heal", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.STATUS_RESTORE))
|
||||
@JvmField
|
||||
@@ -670,6 +754,23 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<
|
||||
@JvmField
|
||||
val X_SPEED = create("x_${Stats.SPEED.identifier.path}", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.BATTLE_ITEM))
|
||||
|
||||
@JvmField
|
||||
val HP_UP = create("hp_up", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.VITAMIN))
|
||||
@JvmField
|
||||
val PROTEIN = create("protein", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.VITAMIN))
|
||||
@JvmField
|
||||
val IRON = create("iron", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.VITAMIN))
|
||||
@JvmField
|
||||
val CARBOS = create("carbos", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.VITAMIN))
|
||||
@JvmField
|
||||
val CALCIUM = create("calcium", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.VITAMIN))
|
||||
@JvmField
|
||||
val ZINC = create("zinc", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.VITAMIN))
|
||||
@JvmField
|
||||
val PP_UP = create("pp_up", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.VITAMIN))
|
||||
@JvmField
|
||||
val PP_MAX = create("pp_max", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.VITAMIN))
|
||||
|
||||
@JvmField
|
||||
val MONITOR = create("monitor", MonitorBlock(BlockBehaviour.Properties.of().mapColor(MapColor.METAL).sound(SoundType.METAL).pushReaction(PushReaction.BLOCK).requiresCorrectToolForDrops().strength(5.0F, 6.0F).lightLevel { if (it.getValue(MonitorBlock.SCREEN) != MonitorBlock.MonitorScreen.OFF) 13 else 0 }))
|
||||
@JvmField
|
||||
@@ -707,6 +808,9 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<
|
||||
@JvmField
|
||||
val DISPLAY_CASE = create("display_case", DisplayCaseBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.DISPLAY_CASE_SOUNDS).noOcclusion().pushReaction(PushReaction.BLOCK).mapColor(MapColor.STONE).strength(0.3F).requiresCorrectToolForDrops()))
|
||||
|
||||
@JvmField
|
||||
val VENDING_MACHINE = create("vending_machine", VendingMachineBlock(BlockBehaviour.Properties.of().mapColor(MapColor.METAL).sound(SoundType.METAL).pushReaction(PushReaction.BLOCK).strength(2F).requiresCorrectToolForDrops()))
|
||||
|
||||
// TODO after 1.7
|
||||
// @JvmField
|
||||
// val INCENSE_SWEET = create("incense_sweet", SweetIncenseBlock(BlockBehaviour.Properties.of().sound(CobblemonSounds.DISPLAY_CASE_SOUNDS).noOcclusion().pushReaction(PushReaction.BLOCK).mapColor(MapColor.STONE).strength(0.3F)))
|
||||
|
||||
@@ -77,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -472,9 +474,9 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
|
||||
// Evolution items
|
||||
@JvmField val LINK_CABLE = create("link_cable", LinkCableItem())
|
||||
@JvmField val DRAGON_SCALE = noSettingsItem("dragon_scale")
|
||||
@JvmField val METAL_COAT = noSettingsItem("metal_coat")
|
||||
@JvmField val UPGRADE = noSettingsItem("upgrade")
|
||||
@JvmField val DUBIOUS_DISC = noSettingsItem("dubious_disc")
|
||||
@JvmField val METAL_COAT = itemNameBlockItem("metal_coat", CobblemonBlocks.METAL_COAT)
|
||||
@JvmField val UPGRADE = itemNameBlockItem("upgrade", CobblemonBlocks.UPGRADE)
|
||||
@JvmField val DUBIOUS_DISC = itemNameBlockItem("dubious_disc", CobblemonBlocks.DUBIOUS_DISC)
|
||||
@JvmField val DEEP_SEA_SCALE = noSettingsItem("deep_sea_scale")
|
||||
@JvmField val DEEP_SEA_TOOTH = noSettingsItem("deep_sea_tooth")
|
||||
@JvmField val ELECTIRIZER = noSettingsItem("electirizer")
|
||||
@@ -483,7 +485,7 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
|
||||
@JvmField val PROTECTOR = noSettingsItem("protector")
|
||||
@JvmField val REAPER_CLOTH = noSettingsItem("reaper_cloth")
|
||||
@JvmField val PRISM_SCALE = noSettingsItem("prism_scale")
|
||||
@JvmField val SACHET = noSettingsItem("sachet")
|
||||
@JvmField val SACHET = itemNameBlockItem("sachet", CobblemonBlocks.SACHET)
|
||||
@JvmField val WHIPPED_DREAM = create("whipped_dream", foodItem(8, 0.3f))
|
||||
@JvmField val STRAWBERRY_SWEET = create("strawberry_sweet", foodItem(6, 0.125f))
|
||||
@JvmField val LOVE_SWEET = create("love_sweet", foodItem(6, 0.125f))
|
||||
@@ -492,22 +494,20 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
|
||||
@JvmField val FLOWER_SWEET = create("flower_sweet", foodItem(6, 0.125f))
|
||||
@JvmField val STAR_SWEET = create("star_sweet", foodItem(6, 0.125f))
|
||||
@JvmField val RIBBON_SWEET = create("ribbon_sweet", foodItem(6, 0.125f))
|
||||
@JvmField val CHIPPED_POT = noSettingsItem("chipped_pot")
|
||||
@JvmField val CRACKED_POT = noSettingsItem("cracked_pot")
|
||||
@JvmField val MASTERPIECE_TEACUP = noSettingsItem("masterpiece_teacup")
|
||||
@JvmField val UNREMARKABLE_TEACUP = noSettingsItem("unremarkable_teacup")
|
||||
@JvmField val CHIPPED_POT = itemNameBlockItem("chipped_pot", CobblemonBlocks.CHIPPED_POT)
|
||||
@JvmField val CRACKED_POT = itemNameBlockItem("cracked_pot", CobblemonBlocks.CRACKED_POT)
|
||||
@JvmField val MASTERPIECE_TEACUP = itemNameBlockItem("masterpiece_teacup", CobblemonBlocks.MASTERPIECE_TEACUP)
|
||||
@JvmField val UNREMARKABLE_TEACUP = itemNameBlockItem("unremarkable_teacup", CobblemonBlocks.UNREMARKABLE_TEACUP)
|
||||
@JvmField val SWEET_APPLE = compostableItem("sweet_apple", foodItem(4, 0.3f), 0.65f)
|
||||
@JvmField val TART_APPLE = compostableItem("tart_apple", foodItem(4, 0.3f), 0.65f)
|
||||
@JvmField val SYRUPY_APPLE = compostableItem("syrupy_apple", foodItem(4, 0.3f), 0.65f)
|
||||
@JvmField val GALARICA_CUFF = noSettingsItem("galarica_cuff")
|
||||
@JvmField val GALARICA_WREATH = noSettingsItem("galarica_wreath")
|
||||
@JvmField val GALARICA_CUFF = itemNameBlockItem("galarica_cuff", CobblemonBlocks.GALARICA_CUFF)
|
||||
@JvmField val BLACK_AUGURITE = noSettingsItem("black_augurite")
|
||||
@JvmField val PEAT_BLOCK = noSettingsItem("peat_block")
|
||||
@JvmField val RAZOR_CLAW = noSettingsItem("razor_claw")
|
||||
@JvmField val RAZOR_FANG = noSettingsItem("razor_fang")
|
||||
@JvmField val AUSPICIOUS_ARMOR = heldItem("auspicious_armor")
|
||||
@JvmField val MALICIOUS_ARMOR = heldItem("malicious_armor")
|
||||
@JvmField val SHELL_HELMET = heldItem("shell_helmet")
|
||||
@JvmField val METAL_ALLOY = noSettingsItem("metal_alloy")
|
||||
@JvmField val SCROLL_OF_DARKNESS = noSettingsItem("scroll_of_darkness")
|
||||
@JvmField val SCROLL_OF_WATERS = noSettingsItem("scroll_of_waters")
|
||||
@@ -651,17 +651,17 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
|
||||
@JvmField
|
||||
val EXPERIENCE_CANDY_XL = candyItem("exp_candy_xl") { _, _ -> CandyItem.DEFAULT_XL_CANDY_YIELD }
|
||||
@JvmField
|
||||
val CALCIUM = create("calcium", VitaminItem(Stats.SPECIAL_ATTACK))
|
||||
val CALCIUM = create("calcium", VitaminItem(Stats.SPECIAL_ATTACK, CobblemonBlocks.CALCIUM))
|
||||
@JvmField
|
||||
val CARBOS = create("carbos", VitaminItem(Stats.SPEED))
|
||||
val CARBOS = create("carbos", VitaminItem(Stats.SPEED, CobblemonBlocks.CARBOS))
|
||||
@JvmField
|
||||
val HP_UP = create("hp_up", VitaminItem(Stats.HP))
|
||||
val HP_UP = create("hp_up", VitaminItem(Stats.HP, CobblemonBlocks.HP_UP))
|
||||
@JvmField
|
||||
val IRON = create("iron", VitaminItem(Stats.DEFENCE))
|
||||
val IRON = create("iron", VitaminItem(Stats.DEFENCE, CobblemonBlocks.IRON))
|
||||
@JvmField
|
||||
val PROTEIN = create("protein", VitaminItem(Stats.ATTACK))
|
||||
val PROTEIN = create("protein", VitaminItem(Stats.ATTACK, CobblemonBlocks.PROTEIN))
|
||||
@JvmField
|
||||
val ZINC = create("zinc", VitaminItem(Stats.SPECIAL_DEFENCE))
|
||||
val ZINC = create("zinc", VitaminItem(Stats.SPECIAL_DEFENCE, CobblemonBlocks.ZINC))
|
||||
@JvmField
|
||||
val HEALTH_MOCHI = create("health_mochi", MochiItem(Stats.HP))
|
||||
@JvmField
|
||||
@@ -743,9 +743,9 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
|
||||
@JvmField
|
||||
val MAX_REVIVE = create("max_revive", ReviveItem(max = true))
|
||||
@JvmField
|
||||
val PP_UP = create("pp_up", PPUpItem(1))
|
||||
val PP_UP = create("pp_up", PPUpItem(1, CobblemonBlocks.PP_UP))
|
||||
@JvmField
|
||||
val PP_MAX = create("pp_max", PPUpItem(3))
|
||||
val PP_MAX = create("pp_max", PPUpItem(3, CobblemonBlocks.PP_MAX))
|
||||
|
||||
@JvmField
|
||||
val RED_MINT_SEEDS = mintSeed("red", MintType.RED.getCropBlock())
|
||||
@@ -922,11 +922,18 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
|
||||
@JvmField val CHOICE_SPECS = wearableItem("choice_specs")
|
||||
@JvmField val EXP_SHARE = wearableItem("exp_share")
|
||||
@JvmField val FOCUS_BAND = wearableItem("focus_band")
|
||||
@JvmField val KINGS_ROCK = wearableItem("kings_rock")
|
||||
@JvmField val KINGS_ROCK = wearableBlockItem("kings_rock", CobblemonBlocks.KINGS_ROCK)
|
||||
@JvmField val NEVER_MELT_ICE = wearableBlockItem("never_melt_ice", CobblemonBlocks.NEVER_MELT_ICE)
|
||||
@JvmField val MUSCLE_BAND = wearableItem("muscle_band")
|
||||
@JvmField val ROCKY_HELMET = wearableItem("rocky_helmet")
|
||||
@JvmField val SAFETY_GOGGLES = wearableItem("safety_goggles")
|
||||
@JvmField val WISE_GLASSES = wearableItem("wise_glasses")
|
||||
@JvmField val SHARP_BEAK = wearableItem("sharp_beak")
|
||||
@JvmField val COVERT_CLOAK = wearableItem("covert_cloak")
|
||||
@JvmField val SHELL_HELMET = wearableItem("shell_helmet")
|
||||
@JvmField val GALARICA_WREATH = wearableBlockItem("galarica_wreath", CobblemonBlocks.GALARICA_WREATH)
|
||||
@JvmField val CLEANSE_TAG = wearableBlockItem("cleanse_tag", CobblemonBlocks.CLEANSE_TAG)
|
||||
@JvmField val SPELL_TAG = wearableBlockItem("spell_tag", CobblemonBlocks.SPELL_TAG)
|
||||
|
||||
// Held Items
|
||||
@JvmField
|
||||
@@ -954,17 +961,13 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
|
||||
@JvmField
|
||||
val CHOICE_SCARF = heldItem("choice_scarf")
|
||||
@JvmField
|
||||
val CLEANSE_TAG = itemNameBlockItem("cleanse_tag", CobblemonBlocks.CLEANSE_TAG)
|
||||
@JvmField
|
||||
val CLEAR_AMULET = heldItem("clear_amulet")
|
||||
@JvmField
|
||||
val COVERT_CLOAK = heldItem("covert_cloak")
|
||||
@JvmField
|
||||
val DESTINY_KNOT = heldItem("destiny_knot")
|
||||
val DESTINY_KNOT = itemNameBlockItem("destiny_knot", CobblemonBlocks.DESTINY_KNOT)
|
||||
@JvmField
|
||||
val DRAGON_FANG = heldItem("dragon_fang")
|
||||
@JvmField
|
||||
val EJECT_BUTTON = heldItem("eject_button")
|
||||
val EJECT_BUTTON = itemNameBlockItem("eject_button", CobblemonBlocks.EJECT_BUTTON)
|
||||
@JvmField
|
||||
val EJECT_PACK = heldItem("eject_pack")
|
||||
@JvmField
|
||||
@@ -978,7 +981,7 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
|
||||
@JvmField
|
||||
val FAIRY_FEATHER = heldItem("fairy_feather")
|
||||
@JvmField
|
||||
val FLAME_ORB = heldItem("flame_orb")
|
||||
val FLAME_ORB = itemNameBlockItem("flame_orb", CobblemonBlocks.FLAME_ORB)
|
||||
@JvmField
|
||||
val FLOAT_STONE = heldItem("float_stone")
|
||||
@JvmField
|
||||
@@ -992,34 +995,32 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
|
||||
@JvmField
|
||||
val HEAVY_DUTY_BOOTS = heldItem("heavy_duty_boots")
|
||||
@JvmField
|
||||
val IRON_BALL = heldItem("iron_ball")
|
||||
val IRON_BALL = itemNameBlockItem("iron_ball", CobblemonBlocks.IRON_BALL)
|
||||
@JvmField
|
||||
val LAGGING_TAIL = heldItem("lagging_tail")
|
||||
@JvmField
|
||||
val LEFTOVERS = compostableHeldItem("leftovers", null, 0.50f)
|
||||
@JvmField
|
||||
val LIFE_ORB = heldItem("life_orb")
|
||||
val LIFE_ORB = itemNameBlockItem("life_orb", CobblemonBlocks.LIFE_ORB)
|
||||
@JvmField
|
||||
val LIGHT_BALL = heldItem("light_ball")
|
||||
val LIGHT_BALL = itemNameBlockItem("light_ball", CobblemonBlocks.LIGHT_BALL)
|
||||
@JvmField
|
||||
val LIGHT_CLAY = heldItem("light_clay")
|
||||
@JvmField
|
||||
val LOADED_DICE = heldItem("loaded_dice")
|
||||
val LOADED_DICE = itemNameBlockItem("loaded_dice", CobblemonBlocks.LOADED_DICE)
|
||||
@JvmField
|
||||
val LUCKY_EGG = heldItem("lucky_egg")
|
||||
@JvmField
|
||||
val LUMINOUS_MOSS = heldItem("luminous_moss")
|
||||
val LUMINOUS_MOSS = itemNameBlockItem("luminous_moss", CobblemonBlocks.LUMINOUS_MOSS)
|
||||
@JvmField
|
||||
val MAGNET = heldItem("magnet")
|
||||
val MAGNET = itemNameBlockItem("magnet", CobblemonBlocks.MAGNET)
|
||||
@JvmField
|
||||
val METRONOME = heldItem("metronome")
|
||||
val METRONOME = itemNameBlockItem("metronome", CobblemonBlocks.METRONOME)
|
||||
@JvmField
|
||||
val MIRACLE_SEED = heldItem("miracle_seed")
|
||||
@JvmField
|
||||
val MYSTIC_WATER = heldItem("mystic_water")
|
||||
@JvmField
|
||||
val NEVER_MELT_ICE = heldItem("never_melt_ice")
|
||||
@JvmField
|
||||
val POISON_BARB = heldItem("poison_barb")
|
||||
@JvmField
|
||||
val POWER_ANKLET = heldItem("power_anklet")
|
||||
@@ -1044,13 +1045,11 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
|
||||
@JvmField
|
||||
val RED_CARD = heldItem("red_card")
|
||||
@JvmField
|
||||
val RING_TARGET = heldItem("ring_target")
|
||||
val RING_TARGET = itemNameBlockItem("ring_target", CobblemonBlocks.RING_TARGET)
|
||||
@JvmField
|
||||
val ROOM_SERVICE = heldItem("room_service")
|
||||
@JvmField
|
||||
val SCOPE_LENS = heldItem("scope_lens")
|
||||
@JvmField
|
||||
val SHARP_BEAK = heldItem("sharp_beak")
|
||||
val SCOPE_LENS = itemNameBlockItem("scope_lens", CobblemonBlocks.SCOPE_LENS)
|
||||
@JvmField
|
||||
val SHED_SHELL = heldItem("shed_shell")
|
||||
@JvmField
|
||||
@@ -1062,9 +1061,7 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
|
||||
@JvmField
|
||||
val SOFT_SAND = heldItem("soft_sand")
|
||||
@JvmField
|
||||
val SPELL_TAG = itemNameBlockItem("spell_tag", CobblemonBlocks.SPELL_TAG)
|
||||
@JvmField
|
||||
val SMOKE_BALL = heldItem("smoke_ball")
|
||||
val SMOKE_BALL = itemNameBlockItem("smoke_ball", CobblemonBlocks.SMOKE_BALL)
|
||||
@JvmField
|
||||
val SOOTHE_BELL = heldItem("soothe_bell")
|
||||
@JvmField
|
||||
@@ -1072,9 +1069,9 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
|
||||
@JvmField
|
||||
val TERRAIN_EXTENDER = heldItem("terrain_extender")
|
||||
@JvmField
|
||||
val THROAT_SPRAY = heldItem("throat_spray")
|
||||
val THROAT_SPRAY = itemNameBlockItem("throat_spray", CobblemonBlocks.THROAT_SPRAY)
|
||||
@JvmField
|
||||
val TOXIC_ORB = heldItem("toxic_orb")
|
||||
val TOXIC_ORB = itemNameBlockItem("toxic_orb", CobblemonBlocks.TOXIC_ORB)
|
||||
@JvmField
|
||||
val TWISTED_SPOON = heldItem("twisted_spoon")
|
||||
@JvmField
|
||||
@@ -1082,9 +1079,9 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
|
||||
@JvmField
|
||||
val WEAKNESS_POLICY = itemNameBlockItem("weakness_policy", CobblemonBlocks.WEAKNESS_POLICY)
|
||||
@JvmField
|
||||
val WIDE_LENS = heldItem("wide_lens")
|
||||
val WIDE_LENS = itemNameBlockItem("wide_lens", CobblemonBlocks.WIDE_LENS)
|
||||
@JvmField
|
||||
val ZOOM_LENS = heldItem("zoom_lens")
|
||||
val ZOOM_LENS = itemNameBlockItem("zoom_lens", CobblemonBlocks.ZOOM_LENS)
|
||||
@JvmField
|
||||
val MENTAL_HERB = compostableHeldItem("mental_herb", null, 0.85F)
|
||||
@JvmField
|
||||
@@ -1096,19 +1093,19 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
|
||||
@JvmField
|
||||
val WHITE_HERB = compostableHeldItem("white_herb", null, 0.85F)
|
||||
@JvmField
|
||||
val BRIGHT_POWDER = heldItem("bright_powder")
|
||||
val BRIGHT_POWDER = itemNameBlockItem("bright_powder", CobblemonBlocks.BRIGHT_POWDER)
|
||||
@JvmField
|
||||
val METAL_POWDER = heldItem("metal_powder")
|
||||
val METAL_POWDER = itemNameBlockItem("metal_powder", CobblemonBlocks.METAL_POWDER)
|
||||
@JvmField
|
||||
val QUICK_POWDER = heldItem("quick_powder")
|
||||
val QUICK_POWDER = itemNameBlockItem("quick_powder", CobblemonBlocks.QUICK_POWDER)
|
||||
@JvmField
|
||||
val DAMP_ROCK = heldItem("damp_rock")
|
||||
val DAMP_ROCK = itemNameBlockItem("damp_rock", CobblemonBlocks.DAMP_ROCK)
|
||||
@JvmField
|
||||
val HEAT_ROCK = heldItem("heat_rock")
|
||||
val HEAT_ROCK = itemNameBlockItem("heat_rock", CobblemonBlocks.HEAT_ROCK)
|
||||
@JvmField
|
||||
val SMOOTH_ROCK = heldItem("smooth_rock")
|
||||
val SMOOTH_ROCK = itemNameBlockItem("smooth_rock", CobblemonBlocks.SMOOTH_ROCK)
|
||||
@JvmField
|
||||
val ICY_ROCK = heldItem("icy_rock")
|
||||
val ICY_ROCK = itemNameBlockItem("icy_rock", CobblemonBlocks.ICY_ROCK)
|
||||
|
||||
// Mulch
|
||||
@JvmField
|
||||
@@ -1599,7 +1596,16 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
|
||||
|
||||
private fun wearableItem(name: String, heldItemRemappedName: String? = null): CobblemonItem = create(
|
||||
name,
|
||||
WearableItem(name).also {
|
||||
WearableHatItem(name).also {
|
||||
wearables.add(it)
|
||||
if (heldItemRemappedName != null) {
|
||||
CobblemonHeldItemManager.registerRemap(it, heldItemRemappedName)
|
||||
}
|
||||
}
|
||||
)
|
||||
private fun wearableBlockItem(name: String, block: Block, heldItemRemappedName: String? = null): WearableBlockItem = create(
|
||||
name,
|
||||
WearableBlockItem(name, block).also {
|
||||
wearables.add(it)
|
||||
if (heldItemRemappedName != null) {
|
||||
CobblemonHeldItemManager.registerRemap(it, heldItemRemappedName)
|
||||
|
||||
@@ -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
|
||||
@@ -445,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))
|
||||
@@ -643,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))
|
||||
|
||||
@@ -656,4 +679,4 @@ object CobblemonNetwork {
|
||||
fun sendPacketToPlayer(player: ServerPlayer, packet: NetworkPacket<*>) {
|
||||
Cobblemon.implementation.networkManager.sendPacketToPlayer(player, packet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+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)
|
||||
|
||||
@@ -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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,137 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.block.decor
|
||||
|
||||
import com.cobblemon.mod.common.util.rotateShape
|
||||
import com.mojang.serialization.MapCodec
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder
|
||||
import net.minecraft.core.BlockPos
|
||||
import net.minecraft.core.Direction
|
||||
import net.minecraft.util.StringRepresentable
|
||||
import net.minecraft.world.InteractionResult
|
||||
import net.minecraft.world.entity.player.Player
|
||||
import net.minecraft.world.item.context.BlockPlaceContext
|
||||
import net.minecraft.world.level.BlockGetter
|
||||
import net.minecraft.world.level.Level
|
||||
import net.minecraft.world.level.LevelAccessor
|
||||
import net.minecraft.world.level.block.*
|
||||
import net.minecraft.world.level.block.state.BlockState
|
||||
import net.minecraft.world.level.block.state.StateDefinition
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties.HORIZONTAL_FACING
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties.WATERLOGGED
|
||||
import net.minecraft.world.level.block.state.properties.BooleanProperty
|
||||
import net.minecraft.world.level.gameevent.GameEvent
|
||||
import net.minecraft.world.level.material.FluidState
|
||||
import net.minecraft.world.level.material.Fluids
|
||||
import net.minecraft.world.phys.BlockHitResult
|
||||
import net.minecraft.world.phys.shapes.CollisionContext
|
||||
import net.minecraft.world.phys.shapes.Shapes
|
||||
import net.minecraft.world.phys.shapes.VoxelShape
|
||||
|
||||
class ActivatableDecorationBlock(settings: Properties, val type: ActivatableDecorationBlockType) : HorizontalDirectionalBlock(settings), SimpleWaterloggedBlock {
|
||||
enum class ActivatableDecorationBlockType: StringRepresentable {
|
||||
//For basic use, also ignore camelcase errors, it's just dumb
|
||||
CENTERED_8x,
|
||||
//For specific items
|
||||
METRONOME;
|
||||
|
||||
override fun getSerializedName(): String = this.name.lowercase()
|
||||
|
||||
companion object {
|
||||
val CODEC = StringRepresentable.fromEnum(::values)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val CODEC: MapCodec<ActivatableDecorationBlock> = RecordCodecBuilder.mapCodec { it.group(
|
||||
propertiesCodec(),
|
||||
ActivatableDecorationBlockType.CODEC.fieldOf("type").forGetter(ActivatableDecorationBlock::type)
|
||||
).apply(it, ::ActivatableDecorationBlock) }
|
||||
val ACTIVE = BooleanProperty.create("active")
|
||||
|
||||
val CENTERED_8x = Shapes.box(0.25, 0.0, 0.25, 0.75, 0.5, 0.75)
|
||||
val METRONOME_HITBOX = Shapes.box(0.25, 0.0, 0.375, 0.75, 0.8125, 0.75)
|
||||
|
||||
fun getShape(type: ActivatableDecorationBlockType): VoxelShape {
|
||||
return when (type) {
|
||||
ActivatableDecorationBlockType.METRONOME -> METRONOME_HITBOX
|
||||
else -> CENTERED_8x
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
registerDefaultState(stateDefinition.any()
|
||||
.setValue(HORIZONTAL_FACING, Direction.NORTH)
|
||||
.setValue(ACTIVE, false)
|
||||
.setValue(WATERLOGGED, false)
|
||||
)
|
||||
}
|
||||
|
||||
override fun codec(): MapCodec<ActivatableDecorationBlock> = CODEC
|
||||
|
||||
override fun createBlockStateDefinition(builder: StateDefinition.Builder<Block, BlockState>) {
|
||||
builder.add(HORIZONTAL_FACING, ACTIVE, WATERLOGGED)
|
||||
}
|
||||
|
||||
override fun getFluidState(blockState: BlockState): FluidState? = if (blockState.getValue(WATERLOGGED)) Fluids.WATER.getSource(false) else super.getFluidState(blockState)
|
||||
|
||||
override fun updateShape(state: BlockState, direction: Direction, neighborState: BlockState, world: LevelAccessor, pos: BlockPos, neighborPos: BlockPos): BlockState {
|
||||
if (state.getValue(WATERLOGGED)) {
|
||||
world.scheduleTick(pos, Fluids.WATER, Fluids.WATER.getTickDelay(world))
|
||||
}
|
||||
return if (state.getValue(HORIZONTAL_FACING).opposite == direction && !state.canSurvive(world, pos)) Blocks.AIR.defaultBlockState()
|
||||
else super.updateShape(state, direction, neighborState, world, pos, neighborPos)
|
||||
}
|
||||
|
||||
override fun getCollisionShape(state: BlockState, level: BlockGetter, pos: BlockPos, context: CollisionContext): VoxelShape {
|
||||
val direction = state.getValue(HORIZONTAL_FACING)
|
||||
return rotateShape(Direction.NORTH, direction, getShape(type))
|
||||
}
|
||||
|
||||
override fun getShape(state: BlockState, level: BlockGetter, pos: BlockPos, context: CollisionContext): VoxelShape {
|
||||
return getCollisionShape(state, level, pos, context).singleEncompassing()
|
||||
}
|
||||
|
||||
override fun getStateForPlacement(context: BlockPlaceContext): BlockState? {
|
||||
val fluidState = context.level.getFluidState(context.clickedPos)
|
||||
return super.getStateForPlacement(context)?.setValue(HORIZONTAL_FACING, context.horizontalDirection.opposite)?.setValue(WATERLOGGED, fluidState.type === Fluids.WATER)
|
||||
}
|
||||
|
||||
override fun useWithoutItem(
|
||||
state: BlockState,
|
||||
level: Level,
|
||||
pos: BlockPos,
|
||||
player: Player,
|
||||
hitResult: BlockHitResult
|
||||
): InteractionResult {
|
||||
if (level.isClientSide) {
|
||||
level.setBlockAndUpdate(pos, state.cycle(ACTIVE))
|
||||
return InteractionResult.SUCCESS
|
||||
} else {
|
||||
this.push(state, level, pos, null as Player?)
|
||||
return InteractionResult.CONSUME
|
||||
}
|
||||
}
|
||||
|
||||
fun push(state: BlockState, level: Level, pos: BlockPos?, player: Player?) {
|
||||
var state = state
|
||||
state = state.cycle(ACTIVE)
|
||||
level.setBlock(pos!!, state, 3)
|
||||
level.gameEvent(player, if (state.getValue(ACTIVE)) GameEvent.BLOCK_ACTIVATE else GameEvent.BLOCK_DEACTIVATE, pos)
|
||||
}
|
||||
|
||||
override fun hasAnalogOutputSignal(state: BlockState): Boolean {
|
||||
return true
|
||||
}
|
||||
//TODO: Replace this forced 4 with a value that can be set by the block, only relevant when more blocks of this class are added.
|
||||
override fun getAnalogOutputSignal(state: BlockState, level: Level, pos: BlockPos): Int {
|
||||
return if (level.getBlockState(pos).getValue(ACTIVE) as Boolean) 4 else 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of 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.util.rotateShape
|
||||
import com.mojang.serialization.MapCodec
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder
|
||||
import net.minecraft.core.BlockPos
|
||||
import net.minecraft.core.Direction
|
||||
import net.minecraft.util.StringRepresentable
|
||||
import net.minecraft.world.item.context.BlockPlaceContext
|
||||
import net.minecraft.world.level.BlockGetter
|
||||
import net.minecraft.world.level.LevelAccessor
|
||||
import net.minecraft.world.level.block.Block
|
||||
import net.minecraft.world.level.block.Blocks
|
||||
import net.minecraft.world.level.block.HorizontalDirectionalBlock
|
||||
import net.minecraft.world.level.block.SimpleWaterloggedBlock
|
||||
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.level.block.state.properties.BlockStateProperties.WATERLOGGED
|
||||
import net.minecraft.world.level.material.FluidState
|
||||
import net.minecraft.world.level.material.Fluids
|
||||
import net.minecraft.world.phys.shapes.CollisionContext
|
||||
import net.minecraft.world.phys.shapes.Shapes
|
||||
import net.minecraft.world.phys.shapes.VoxelShape
|
||||
|
||||
class DecorativeItemBlock(settings: Properties, val type: DecorativeItemBlockType): HorizontalDirectionalBlock(settings), SimpleWaterloggedBlock {
|
||||
|
||||
enum class DecorativeItemBlockType: StringRepresentable {
|
||||
//For basic use, also ignore camelcase errors, it's just dumb
|
||||
CENTERED_8x,
|
||||
CENTERED_6x,
|
||||
FLATTENED_8x6,
|
||||
FLATTENED_8x4,
|
||||
FLATTENED_10x4,
|
||||
//For specific items
|
||||
TEACUP,
|
||||
TEAPOT,
|
||||
KNOT,
|
||||
POUCH,
|
||||
NEVERMELT,
|
||||
SCOPE,
|
||||
WIDE,
|
||||
ZOOM;
|
||||
|
||||
override fun getSerializedName(): String = this.name.lowercase()
|
||||
|
||||
companion object {
|
||||
val CODEC = StringRepresentable.fromEnum(::values)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val CODEC: MapCodec<DecorativeItemBlock> = RecordCodecBuilder.mapCodec { it.group(
|
||||
propertiesCodec(),
|
||||
DecorativeItemBlockType.CODEC.fieldOf("type").forGetter(DecorativeItemBlock::type)
|
||||
).apply(it, ::DecorativeItemBlock) }
|
||||
|
||||
val CENTERED_8x = Shapes.box(0.25, 0.0, 0.25, 0.75, 0.5, 0.75)
|
||||
val CENTERED_6x = Shapes.box(0.3125, 0.0, 0.3125, 0.6875, 0.375, 0.6875)
|
||||
val FLAT6_HITBOX = Shapes.box(0.25, 0.0, 0.25, 0.75, 0.375, 0.75)
|
||||
val FLAT4_HITBOX = Shapes.box(0.25, 0.0, 0.25, 0.75, 0.25, 0.75)
|
||||
val SMOOSHED4_HITBOX = Shapes.box(0.1875, 0.0, 0.1875, 0.8125, 0.25, 0.8125)
|
||||
val TEACUP_HITBOX = Shapes.box(0.3125, 0.0, 0.3125, 0.6875, 0.25, 0.6875)
|
||||
val TEAPOT_HITBOX = Shapes.box(0.1875, 0.0, 0.1875, 0.8125, 0.625, 0.8125)
|
||||
val KNOT_HITBOX = Shapes.box(0.375, 0.0, 0.375, 0.75, 0.375, 0.75)
|
||||
val POUCH_HITBOX = Shapes.box(0.3125, 0.0, 0.3125, 0.6875, 0.3125, 0.6875)
|
||||
val NEVERMELT_HITBOX = Shapes.box(0.25, 0.0, 0.3125, 0.6875, 0.25, 0.75)
|
||||
val SCOPE_HITBOX = Shapes.box(0.625, 0.0, 0.4375, 0.9375, 0.125, 0.75)
|
||||
val WIDE_HITBOX = Shapes.box(0.625, 0.0, 0.125, 0.9375, 0.0625, 0.625)
|
||||
val ZOOM_HITBOX = Shapes.box(0.125, 0.0, 0.1875, 0.375, 0.1875, 0.5)
|
||||
|
||||
fun getShape(type: DecorativeItemBlockType): VoxelShape {
|
||||
return when (type) {
|
||||
DecorativeItemBlockType.TEAPOT -> TEAPOT_HITBOX
|
||||
DecorativeItemBlockType.TEACUP -> TEACUP_HITBOX
|
||||
DecorativeItemBlockType.KNOT -> KNOT_HITBOX
|
||||
DecorativeItemBlockType.POUCH -> POUCH_HITBOX
|
||||
DecorativeItemBlockType.NEVERMELT -> NEVERMELT_HITBOX
|
||||
DecorativeItemBlockType.SCOPE -> SCOPE_HITBOX
|
||||
DecorativeItemBlockType.WIDE -> WIDE_HITBOX
|
||||
DecorativeItemBlockType.ZOOM -> ZOOM_HITBOX
|
||||
DecorativeItemBlockType.FLATTENED_10x4 -> SMOOSHED4_HITBOX
|
||||
DecorativeItemBlockType.FLATTENED_8x4 -> FLAT4_HITBOX
|
||||
DecorativeItemBlockType.FLATTENED_8x6 -> FLAT6_HITBOX
|
||||
DecorativeItemBlockType.CENTERED_6x -> CENTERED_6x
|
||||
else -> CENTERED_8x
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
registerDefaultState(stateDefinition.any()
|
||||
.setValue(HORIZONTAL_FACING, Direction.NORTH)
|
||||
.setValue(WATERLOGGED, false))
|
||||
}
|
||||
|
||||
override fun codec(): MapCodec<DecorativeItemBlock> = CODEC
|
||||
|
||||
override fun createBlockStateDefinition(builder: StateDefinition.Builder<Block, BlockState>) {
|
||||
builder.add(HORIZONTAL_FACING, WATERLOGGED)
|
||||
}
|
||||
|
||||
override fun getFluidState(blockState: BlockState): FluidState? = if (blockState.getValue(WATERLOGGED)) Fluids.WATER.getSource(false) else super.getFluidState(blockState)
|
||||
|
||||
override fun updateShape(state: BlockState, direction: Direction, neighborState: BlockState, world: LevelAccessor, pos: BlockPos, neighborPos: BlockPos): BlockState {
|
||||
if (state.getValue(WATERLOGGED)) {
|
||||
world.scheduleTick(pos, Fluids.WATER, Fluids.WATER.getTickDelay(world))
|
||||
}
|
||||
return if (state.getValue(HORIZONTAL_FACING).opposite == direction && !state.canSurvive(world, pos)) Blocks.AIR.defaultBlockState()
|
||||
else super.updateShape(state, direction, neighborState, world, pos, neighborPos)
|
||||
}
|
||||
|
||||
override fun getShape(state: BlockState, level: BlockGetter, pos: BlockPos, context: CollisionContext): VoxelShape {
|
||||
val direction = state.getValue(HORIZONTAL_FACING)
|
||||
return rotateShape(Direction.NORTH, direction, getShape(type))
|
||||
}
|
||||
|
||||
override fun getStateForPlacement(context: BlockPlaceContext): BlockState? {
|
||||
val fluidState = context.level.getFluidState(context.clickedPos)
|
||||
return super.getStateForPlacement(context)?.setValue(HORIZONTAL_FACING, context.horizontalDirection.opposite)?.setValue(WATERLOGGED, fluidState.type === Fluids.WATER)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of 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.util.rotateShape
|
||||
import net.minecraft.core.BlockPos
|
||||
import net.minecraft.core.Direction
|
||||
import net.minecraft.world.item.context.BlockPlaceContext
|
||||
import net.minecraft.world.level.BlockGetter
|
||||
import net.minecraft.world.level.LevelAccessor
|
||||
import net.minecraft.world.level.LevelReader
|
||||
import net.minecraft.world.level.block.Block
|
||||
import net.minecraft.world.level.block.Blocks
|
||||
import net.minecraft.world.level.block.ButtonBlock
|
||||
import net.minecraft.world.level.block.SimpleWaterloggedBlock
|
||||
import net.minecraft.world.level.block.state.BlockState
|
||||
import net.minecraft.world.level.block.state.StateDefinition
|
||||
import net.minecraft.world.level.block.state.properties.AttachFace
|
||||
import net.minecraft.world.level.block.state.properties.BlockSetType
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties.WATERLOGGED
|
||||
import net.minecraft.world.level.material.FluidState
|
||||
import net.minecraft.world.level.material.Fluids
|
||||
import net.minecraft.world.phys.shapes.CollisionContext
|
||||
import net.minecraft.world.phys.shapes.Shapes
|
||||
import net.minecraft.world.phys.shapes.VoxelShape
|
||||
|
||||
class EjectButtonBlock(properties: Properties, ticksToStayPressed: Int) : ButtonBlock(BlockSetType.IRON, ticksToStayPressed, properties), SimpleWaterloggedBlock {
|
||||
companion object {
|
||||
val FLOOR_UNPOWERED = Shapes.or(
|
||||
Shapes.box(0.125, 0.0, 0.125, 0.875, 0.1875, 0.875),
|
||||
Shapes.box(0.375, 0.1875, 0.375, 0.625, 0.3125, 0.625) )
|
||||
val FLOOR_POWERED = Shapes.or(
|
||||
Shapes.box(0.125, 0.0, 0.125, 0.875, 0.1875, 0.875),
|
||||
Shapes.box(0.375, 0.1875, 0.375, 0.625, 0.25, 0.625) )
|
||||
val FLOOR_PANEL = Shapes.box(0.125, 0.0, 0.125, 0.875, 0.1875, 0.875)
|
||||
val CEILING_UNPOWERED = Shapes.or(
|
||||
Shapes.box(0.125, 0.8125, 0.125, 0.875, 1.0, 0.875),
|
||||
Shapes.box(0.375, 0.6875, 0.375, 0.625, 0.8125, 0.625) )
|
||||
val CEILING_POWERED = Shapes.or(
|
||||
Shapes.box(0.125, 0.8125, 0.125, 0.875, 1.0, 0.875),
|
||||
Shapes.box(0.375, 0.75, 0.375, 0.625, 0.8125, 0.625) )
|
||||
val CEILING_PANEL = Shapes.box(0.125, 0.8125, 0.125, 0.875, 1.0, 0.875)
|
||||
val WALL_UNPOWERED = Shapes.or(
|
||||
Shapes.box(0.125, 0.125, 0.8125, 0.875, 0.875, 1.0),
|
||||
Shapes.box(0.375, 0.375, 0.6875, 0.625, 0.625, 0.8125) )
|
||||
val WALL_POWERED = Shapes.or(
|
||||
Shapes.box(0.125, 0.125, 0.8125, 0.875, 0.875, 1.0),
|
||||
Shapes.box(0.375, 0.375, 0.75, 0.625, 0.625, 0.8125) )
|
||||
val WALL_PANEL = Shapes.box(0.125, 0.125, 0.8125, 0.875, 0.875, 1.0)
|
||||
}
|
||||
|
||||
init {
|
||||
registerDefaultState(stateDefinition.any()
|
||||
.setValue(FACING, Direction.NORTH)
|
||||
.setValue(FACE, AttachFace.WALL)
|
||||
.setValue(POWERED, false)
|
||||
.setValue(WATERLOGGED, false)
|
||||
)
|
||||
}
|
||||
|
||||
override fun createBlockStateDefinition(builder: StateDefinition.Builder<Block, BlockState>) {
|
||||
builder.add(FACING, FACE, POWERED, WATERLOGGED)
|
||||
}
|
||||
|
||||
override fun getFluidState(blockState: BlockState): FluidState? {
|
||||
return if (blockState.getValue(WATERLOGGED)) Fluids.WATER.getSource(false) else super.getFluidState(blockState);
|
||||
}
|
||||
|
||||
override fun getStateForPlacement(context: BlockPlaceContext): BlockState? {
|
||||
val fluidState = context.level.getFluidState(context.clickedPos)
|
||||
for (direction in context.nearestLookingDirections) {
|
||||
val blockState: BlockState
|
||||
if (direction.getAxis() === Direction.Axis.Y) {
|
||||
blockState = (defaultBlockState()
|
||||
.setValue(FACE, if (direction == Direction.UP) AttachFace.CEILING else AttachFace.FLOOR) as BlockState)
|
||||
.setValue(FACING, context.horizontalDirection.opposite)
|
||||
.setValue(WATERLOGGED, fluidState.type === Fluids.WATER
|
||||
) as BlockState
|
||||
} else {
|
||||
blockState = (defaultBlockState()
|
||||
.setValue(FACE, AttachFace.WALL) as BlockState)
|
||||
.setValue(FACING, direction.opposite)
|
||||
.setValue(WATERLOGGED, fluidState.type === Fluids.WATER
|
||||
) as BlockState
|
||||
}
|
||||
if (blockState.canSurvive(context.getLevel(), context.getClickedPos())) {
|
||||
return blockState
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
override fun getShape(state: BlockState, blockGetter: BlockGetter, pos: BlockPos, collisionContext: CollisionContext): VoxelShape {
|
||||
val direction = state.getValue(FACING)
|
||||
return when (state.getValue(FACE)) {
|
||||
AttachFace.FLOOR -> if (state.getValue(POWERED)) {FLOOR_POWERED} else {FLOOR_UNPOWERED}
|
||||
AttachFace.WALL ->
|
||||
when (direction) {
|
||||
Direction.NORTH, Direction.UP, Direction.DOWN -> if (state.getValue(POWERED)) {WALL_POWERED} else {WALL_UNPOWERED}
|
||||
else -> rotateShape(Direction.NORTH, direction, if (state.getValue(POWERED)) {WALL_POWERED} else {WALL_UNPOWERED})
|
||||
}
|
||||
AttachFace.CEILING -> if (state.getValue(POWERED)) {CEILING_POWERED} else {CEILING_UNPOWERED}
|
||||
}
|
||||
}
|
||||
override fun getCollisionShape(state: BlockState, level: BlockGetter, pos: BlockPos, context: CollisionContext): VoxelShape {
|
||||
val direction = state.getValue(FACING)
|
||||
return when (state.getValue(FACE)) {
|
||||
AttachFace.FLOOR -> FLOOR_PANEL
|
||||
AttachFace.WALL -> when (direction) {
|
||||
Direction.NORTH, Direction.UP, Direction.DOWN -> WALL_PANEL
|
||||
else -> rotateShape(Direction.NORTH, direction, WALL_PANEL)
|
||||
}
|
||||
AttachFace.CEILING -> CEILING_PANEL
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateShape(state: BlockState, direction: Direction, neighborState: BlockState, world: LevelAccessor, pos: BlockPos, neighborPos: BlockPos): BlockState {
|
||||
if (state.getValue(WATERLOGGED)) {
|
||||
world.scheduleTick(pos, Fluids.WATER, Fluids.WATER.getTickDelay(world))
|
||||
}
|
||||
return if (getConnectedDirection(state).opposite == direction && !state.canSurvive(world, pos)) Blocks.AIR.defaultBlockState()
|
||||
else super.updateShape(state, direction, neighborState, world, pos, neighborPos)
|
||||
}
|
||||
|
||||
override fun canSurvive(state: BlockState, level: LevelReader, pos: BlockPos): Boolean {
|
||||
return if (state.getValue(FACE) != AttachFace.WALL) {
|
||||
val direction = getConnectedDirection(state)
|
||||
canSupportCenter(level, pos.relative(direction.opposite), direction)
|
||||
} else {
|
||||
level.getBlockState(pos.relative(state.getValue(FACING).opposite)).isSolid
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.block
|
||||
|
||||
import com.cobblemon.mod.common.util.rotateShape
|
||||
import com.mojang.serialization.MapCodec
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder
|
||||
import net.minecraft.core.BlockPos
|
||||
import net.minecraft.core.Direction
|
||||
import net.minecraft.world.level.BlockGetter
|
||||
import net.minecraft.world.level.block.state.BlockState
|
||||
import net.minecraft.world.level.block.state.properties.AttachFace
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties.WATERLOGGED
|
||||
import net.minecraft.world.phys.shapes.CollisionContext
|
||||
import net.minecraft.world.phys.shapes.Shapes
|
||||
import net.minecraft.world.phys.shapes.VoxelShape
|
||||
|
||||
class GalaricaWreathBlock(properties: Properties) : SixFaceRotationalBlock(properties) {
|
||||
companion object {
|
||||
val CODEC: MapCodec<GalaricaWreathBlock> = RecordCodecBuilder.mapCodec {
|
||||
it.group(propertiesCodec()).apply(it, ::GalaricaWreathBlock)
|
||||
}
|
||||
|
||||
val FLOOR_NORTH = Shapes.box(0.125, 0.0, 0.125, 0.875, 0.25, 0.875)
|
||||
val WALL_NORTH = Shapes.box(0.125, 0.125, 0.75, 0.875, 0.875, 1.0)
|
||||
val CEILING_NORTH = Shapes.box(0.125, 0.125, 0.375, 0.875, 0.625, 0.5625)
|
||||
}
|
||||
|
||||
init {
|
||||
registerDefaultState(stateDefinition.any()
|
||||
.setValue(FACING, Direction.NORTH)
|
||||
.setValue(FACE, AttachFace.WALL)
|
||||
.setValue(WATERLOGGED, false)
|
||||
)
|
||||
}
|
||||
|
||||
override fun codec(): MapCodec<GalaricaWreathBlock> = CODEC
|
||||
|
||||
override fun getShape(state: BlockState, blockGetter: BlockGetter, pos: BlockPos, collisionContext: CollisionContext): VoxelShape {
|
||||
val direction = state.getValue(FACING)
|
||||
return when (state.getValue(FACE)) {
|
||||
AttachFace.FLOOR ->
|
||||
if (direction.axis === Direction.Axis.X) {
|
||||
rotateShape(Direction.NORTH, Direction.WEST, FLOOR_NORTH)
|
||||
} else FLOOR_NORTH
|
||||
AttachFace.WALL ->
|
||||
when (direction) {
|
||||
Direction.NORTH, Direction.UP, Direction.DOWN -> WALL_NORTH
|
||||
else -> rotateShape(Direction.NORTH, direction, WALL_NORTH)
|
||||
}
|
||||
AttachFace.CEILING ->
|
||||
if (direction.axis === Direction.Axis.X) {
|
||||
rotateShape(Direction.NORTH, Direction.WEST, CEILING_NORTH)
|
||||
} else CEILING_NORTH
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.block
|
||||
|
||||
import com.cobblemon.mod.common.util.rotateShape
|
||||
import com.mojang.serialization.MapCodec
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder
|
||||
import net.minecraft.core.BlockPos
|
||||
import net.minecraft.core.Direction
|
||||
import net.minecraft.world.level.BlockGetter
|
||||
import net.minecraft.world.level.LevelReader
|
||||
import net.minecraft.world.level.block.state.BlockState
|
||||
import net.minecraft.world.level.block.state.properties.AttachFace
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties.WATERLOGGED
|
||||
import net.minecraft.world.phys.shapes.CollisionContext
|
||||
import net.minecraft.world.phys.shapes.Shapes
|
||||
import net.minecraft.world.phys.shapes.VoxelShape
|
||||
|
||||
class MagnetBlock(properties: Properties) : SixFaceRotationalBlock(properties) {
|
||||
companion object {
|
||||
val CODEC: MapCodec<MagnetBlock> = RecordCodecBuilder.mapCodec {
|
||||
it.group(propertiesCodec()).apply(it, ::MagnetBlock)
|
||||
}
|
||||
|
||||
val FLOOR_NORTH = Shapes.box(0.25, 0.0, 0.25, 0.75, 0.1875, 0.75)
|
||||
val WALL_NORTH = Shapes.box(0.25, 0.25, 0.8125, 0.75, 0.75, 1.0)
|
||||
val CEILING_NORTH = Shapes.box(0.25, 0.8125, 0.25, 0.75, 1.0, 0.75)
|
||||
}
|
||||
|
||||
init {
|
||||
registerDefaultState(stateDefinition.any()
|
||||
.setValue(FACING, Direction.NORTH)
|
||||
.setValue(FACE, AttachFace.WALL)
|
||||
.setValue(WATERLOGGED, false)
|
||||
)
|
||||
}
|
||||
|
||||
override fun codec(): MapCodec<MagnetBlock> = CODEC
|
||||
|
||||
override fun getShape(state: BlockState, blockGetter: BlockGetter, pos: BlockPos, collisionContext: CollisionContext): VoxelShape {
|
||||
val direction = state.getValue(FACING)
|
||||
return when (state.getValue(FACE)) {
|
||||
AttachFace.FLOOR ->
|
||||
if (direction.axis === Direction.Axis.X) {
|
||||
rotateShape(Direction.NORTH, Direction.WEST, FLOOR_NORTH)
|
||||
} else FLOOR_NORTH
|
||||
AttachFace.WALL ->
|
||||
when (direction) {
|
||||
Direction.NORTH, Direction.UP, Direction.DOWN -> WALL_NORTH
|
||||
else -> rotateShape(Direction.NORTH, direction, WALL_NORTH)
|
||||
}
|
||||
AttachFace.CEILING ->
|
||||
if (direction.axis === Direction.Axis.X) {
|
||||
rotateShape(Direction.NORTH, Direction.WEST, CEILING_NORTH)
|
||||
} else CEILING_NORTH
|
||||
}
|
||||
}
|
||||
|
||||
override fun canSurvive(state: BlockState, level: LevelReader, pos: BlockPos): Boolean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -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.block
|
||||
|
||||
import com.cobblemon.mod.common.util.rotateShape
|
||||
import com.mojang.serialization.MapCodec
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder
|
||||
import net.minecraft.core.BlockPos
|
||||
import net.minecraft.core.Direction
|
||||
import net.minecraft.world.level.BlockGetter
|
||||
import net.minecraft.world.level.LevelReader
|
||||
import net.minecraft.world.level.block.state.BlockState
|
||||
import net.minecraft.world.level.block.state.properties.AttachFace
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties.WATERLOGGED
|
||||
import net.minecraft.world.phys.shapes.CollisionContext
|
||||
import net.minecraft.world.phys.shapes.Shapes
|
||||
import net.minecraft.world.phys.shapes.VoxelShape
|
||||
|
||||
class OrbBlock(properties: Properties) : SixFaceRotationalBlock(properties) {
|
||||
companion object {
|
||||
val CODEC: MapCodec<OrbBlock> = RecordCodecBuilder.mapCodec {
|
||||
it.group(propertiesCodec()).apply(it, ::OrbBlock)
|
||||
}
|
||||
|
||||
val FLOOR_NORTH = Shapes.box(0.3125, 0.0, 0.3125, 0.6875, 0.375, 0.6875)
|
||||
val WALL_NORTH = Shapes.box(0.3125, 0.3125, 0.625, 0.6875, 0.6875, 1.0)
|
||||
val CEILING_NORTH = Shapes.box(0.3125, 0.625, 0.375, 0.6875, 1.0, 0.6875)
|
||||
}
|
||||
|
||||
init {
|
||||
registerDefaultState(stateDefinition.any()
|
||||
.setValue(FACING, Direction.NORTH)
|
||||
.setValue(FACE, AttachFace.WALL)
|
||||
.setValue(WATERLOGGED, false)
|
||||
)
|
||||
}
|
||||
|
||||
override fun codec(): MapCodec<OrbBlock> = CODEC
|
||||
|
||||
override fun getShape(state: BlockState, blockGetter: BlockGetter, pos: BlockPos, collisionContext: CollisionContext): VoxelShape {
|
||||
val direction = state.getValue(FACING)
|
||||
return when (state.getValue(FACE)) {
|
||||
AttachFace.FLOOR ->
|
||||
if (direction.axis === Direction.Axis.X) {
|
||||
rotateShape(Direction.NORTH, Direction.WEST, FLOOR_NORTH)
|
||||
} else FLOOR_NORTH
|
||||
AttachFace.WALL ->
|
||||
when (direction) {
|
||||
Direction.NORTH, Direction.UP, Direction.DOWN -> WALL_NORTH
|
||||
else -> rotateShape(Direction.NORTH, direction, WALL_NORTH)
|
||||
}
|
||||
AttachFace.CEILING ->
|
||||
if (direction.axis === Direction.Axis.X) {
|
||||
rotateShape(Direction.NORTH, Direction.WEST, CEILING_NORTH)
|
||||
} else CEILING_NORTH
|
||||
}
|
||||
}
|
||||
|
||||
override fun canSurvive(state: BlockState, level: LevelReader, pos: BlockPos): Boolean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -13,25 +13,15 @@ import com.mojang.serialization.MapCodec
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder
|
||||
import net.minecraft.core.BlockPos
|
||||
import net.minecraft.core.Direction
|
||||
import net.minecraft.world.item.context.BlockPlaceContext
|
||||
import net.minecraft.world.level.BlockGetter
|
||||
import net.minecraft.world.level.LevelAccessor
|
||||
import net.minecraft.world.level.LevelReader
|
||||
import net.minecraft.world.level.block.Block
|
||||
import net.minecraft.world.level.block.Blocks
|
||||
import net.minecraft.world.level.block.FaceAttachedHorizontalDirectionalBlock
|
||||
import net.minecraft.world.level.block.SimpleWaterloggedBlock
|
||||
import net.minecraft.world.level.block.state.BlockState
|
||||
import net.minecraft.world.level.block.state.StateDefinition
|
||||
import net.minecraft.world.level.block.state.properties.AttachFace
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties.WATERLOGGED
|
||||
import net.minecraft.world.level.material.FluidState
|
||||
import net.minecraft.world.level.material.Fluids
|
||||
import net.minecraft.world.phys.shapes.CollisionContext
|
||||
import net.minecraft.world.phys.shapes.Shapes
|
||||
import net.minecraft.world.phys.shapes.VoxelShape
|
||||
|
||||
class PlaqueBlock(properties : Properties) : FaceAttachedHorizontalDirectionalBlock(properties), SimpleWaterloggedBlock {
|
||||
class PlaqueBlock(properties : Properties) : SixFaceRotationalBlock(properties) {
|
||||
companion object {
|
||||
val CODEC: MapCodec<PlaqueBlock> = RecordCodecBuilder.mapCodec {
|
||||
it.group(propertiesCodec()).apply(it, ::PlaqueBlock)
|
||||
@@ -54,40 +44,6 @@ class PlaqueBlock(properties : Properties) : FaceAttachedHorizontalDirectionalBl
|
||||
)
|
||||
}
|
||||
|
||||
override fun createBlockStateDefinition(builder: StateDefinition.Builder<Block, BlockState>) {
|
||||
builder.add(FACING, FACE, WATERLOGGED)
|
||||
}
|
||||
|
||||
override fun getFluidState(blockState: BlockState): FluidState? {
|
||||
return if (blockState.getValue(WATERLOGGED)) Fluids.WATER.getSource(false) else super.getFluidState(blockState);
|
||||
}
|
||||
|
||||
override fun getStateForPlacement(context: BlockPlaceContext): BlockState? {
|
||||
val fluidState = context.level.getFluidState(context.clickedPos)
|
||||
for (direction in context.nearestLookingDirections) {
|
||||
val blockState: BlockState
|
||||
if (direction.getAxis() === Direction.Axis.Y) {
|
||||
blockState = (defaultBlockState()
|
||||
.setValue(FACE, if (direction == Direction.UP) AttachFace.CEILING else AttachFace.FLOOR) as BlockState)
|
||||
.setValue(FACING, context.horizontalDirection.opposite)
|
||||
.setValue(WATERLOGGED, fluidState.type === Fluids.WATER
|
||||
) as BlockState
|
||||
} else {
|
||||
blockState = (defaultBlockState()
|
||||
.setValue(FACE, AttachFace.WALL) as BlockState)
|
||||
.setValue(FACING, direction.opposite)
|
||||
.setValue(WATERLOGGED, fluidState.type === Fluids.WATER
|
||||
) as BlockState
|
||||
|
||||
}
|
||||
|
||||
if (blockState.canSurvive(context.getLevel(), context.getClickedPos())) {
|
||||
return blockState
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
override fun codec(): MapCodec<PlaqueBlock> = CODEC
|
||||
|
||||
override fun getShape(state: BlockState, blockGetter: BlockGetter, pos: BlockPos, collisionContext: CollisionContext): VoxelShape {
|
||||
@@ -108,21 +64,4 @@ class PlaqueBlock(properties : Properties) : FaceAttachedHorizontalDirectionalBl
|
||||
} else CEILING_NORTH
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateShape(state: BlockState, direction: Direction, neighborState: BlockState, world: LevelAccessor, pos: BlockPos, neighborPos: BlockPos): BlockState {
|
||||
if (state.getValue(WATERLOGGED)) {
|
||||
world.scheduleTick(pos, Fluids.WATER, Fluids.WATER.getTickDelay(world))
|
||||
}
|
||||
return if (getConnectedDirection(state).opposite == direction && !state.canSurvive(world, pos)) Blocks.AIR.defaultBlockState()
|
||||
else super.updateShape(state, direction, neighborState, world, pos, neighborPos)
|
||||
}
|
||||
|
||||
override fun canSurvive(state: BlockState, level: LevelReader, pos: BlockPos): Boolean {
|
||||
return if (state.getValue(FACE) == AttachFace.CEILING) {
|
||||
val direction = getConnectedDirection(state)
|
||||
canSupportCenter(level, pos.relative(direction.opposite), direction)
|
||||
} else {
|
||||
super.canSurvive(state, level, pos)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.block.decor
|
||||
|
||||
import com.mojang.serialization.MapCodec
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder
|
||||
import net.minecraft.core.BlockPos
|
||||
import net.minecraft.core.Direction
|
||||
import net.minecraft.world.item.context.BlockPlaceContext
|
||||
import net.minecraft.world.level.BlockGetter
|
||||
import net.minecraft.world.level.LevelAccessor
|
||||
import net.minecraft.world.level.LevelReader
|
||||
import net.minecraft.world.level.block.*
|
||||
import net.minecraft.world.level.block.HorizontalDirectionalBlock.FACING
|
||||
import net.minecraft.world.level.block.state.BlockState
|
||||
import net.minecraft.world.level.block.state.StateDefinition
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties.WATERLOGGED
|
||||
import net.minecraft.world.level.block.state.properties.Property
|
||||
import net.minecraft.world.level.material.FluidState
|
||||
import net.minecraft.world.level.material.Fluids
|
||||
import net.minecraft.world.phys.shapes.CollisionContext
|
||||
import net.minecraft.world.phys.shapes.Shapes
|
||||
import net.minecraft.world.phys.shapes.VoxelShape
|
||||
|
||||
class RingTargetBlock(settings: Properties) : TargetBlock(settings), SimpleWaterloggedBlock {
|
||||
init {
|
||||
registerDefaultState(stateDefinition.any()
|
||||
.setValue(FACING, Direction.NORTH)
|
||||
.setValue(WATERLOGGED, false)
|
||||
.setValue(OUTPUT_POWER, 0))
|
||||
}
|
||||
|
||||
override fun getStateForPlacement(ctx: BlockPlaceContext): BlockState? {
|
||||
var blockState = defaultBlockState()
|
||||
.setValue(WATERLOGGED, ctx.level.getFluidState(ctx.clickedPos).type == Fluids.WATER)
|
||||
val worldView = ctx.level
|
||||
val blockPos = ctx.clickedPos
|
||||
ctx.nearestLookingDirections.forEach { direction ->
|
||||
if (direction.axis.isHorizontal) {
|
||||
blockState = blockState.setValue(FACING, direction.opposite) as BlockState
|
||||
if (blockState.canSurvive(worldView, blockPos)) {
|
||||
return blockState
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
override fun getFluidState(blockState: BlockState): FluidState? {
|
||||
return if (blockState.getValue(WATERLOGGED)) Fluids.WATER.getSource(false) else super.getFluidState(blockState)
|
||||
}
|
||||
|
||||
override fun createBlockStateDefinition(builder: StateDefinition.Builder<Block, BlockState>) {
|
||||
builder.add(FACING)
|
||||
builder.add(WATERLOGGED)
|
||||
builder.add(*arrayOf<Property<*>>(OUTPUT_POWER))
|
||||
}
|
||||
|
||||
override fun getShape(
|
||||
state: BlockState,
|
||||
blockGetter: BlockGetter,
|
||||
pos: BlockPos,
|
||||
collisionContext: CollisionContext
|
||||
): VoxelShape {
|
||||
return when (state.getValue(FACING)) {
|
||||
Direction.SOUTH -> FULL_PLANE_SOUTH
|
||||
Direction.WEST -> FULL_PLANE_WEST
|
||||
Direction.EAST -> FULL_PLANE_EAST
|
||||
else -> FULL_PLANE_NORTH
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateShape(
|
||||
state: BlockState,
|
||||
direction: Direction,
|
||||
neighborState: BlockState,
|
||||
world: LevelAccessor,
|
||||
pos: BlockPos,
|
||||
neighborPos: BlockPos
|
||||
): BlockState {
|
||||
if (state.getValue(WATERLOGGED)) {
|
||||
world.scheduleTick(pos, Fluids.WATER, Fluids.WATER.getTickDelay(world))
|
||||
}
|
||||
return if (direction.opposite == state.getValue(FACING) && !state.canSurvive(world, pos)) Blocks.AIR.defaultBlockState()
|
||||
else super.updateShape(state, direction, neighborState, world, pos, neighborPos)
|
||||
}
|
||||
|
||||
override fun rotate(state: BlockState, rotation: Rotation): BlockState {
|
||||
return state.setValue(FACING, rotation.rotate(state.getValue(FACING)))
|
||||
}
|
||||
|
||||
override fun canSurvive(state: BlockState, level: LevelReader, pos: BlockPos): Boolean {
|
||||
return level.getBlockState(pos.relative(state.getValue(FACING).opposite)).isSolid
|
||||
}
|
||||
|
||||
companion object {
|
||||
val CODEC: MapCodec<RingTargetBlock> = RecordCodecBuilder.mapCodec {
|
||||
it.group(
|
||||
propertiesCodec()
|
||||
).apply(it, ::RingTargetBlock) }
|
||||
|
||||
val OUTPUT_POWER = BlockStateProperties.POWER;
|
||||
|
||||
val FULL_PLANE_NORTH = Shapes.box(0.0, 0.0, 0.925, 1.0, 1.0, 1.0)
|
||||
val FULL_PLANE_WEST = Shapes.box(0.925, 0.0, 0.0, 1.0, 1.0, 1.0)
|
||||
val FULL_PLANE_SOUTH = Shapes.box(0.0, 0.0, 0.0, 1.0, 1.0, 0.075)
|
||||
val FULL_PLANE_EAST = Shapes.box(0.0, 0.0, 0.0, 0.075, 1.0, 1.0)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of 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.mojang.serialization.MapCodec
|
||||
import net.minecraft.core.BlockPos
|
||||
import net.minecraft.core.Direction
|
||||
import net.minecraft.world.item.context.BlockPlaceContext
|
||||
import net.minecraft.world.level.LevelAccessor
|
||||
import net.minecraft.world.level.LevelReader
|
||||
import net.minecraft.world.level.block.Block
|
||||
import net.minecraft.world.level.block.Blocks
|
||||
import net.minecraft.world.level.block.FaceAttachedHorizontalDirectionalBlock
|
||||
import net.minecraft.world.level.block.SimpleWaterloggedBlock
|
||||
import net.minecraft.world.level.block.state.BlockState
|
||||
import net.minecraft.world.level.block.state.StateDefinition
|
||||
import net.minecraft.world.level.block.state.properties.AttachFace
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties.WATERLOGGED
|
||||
import net.minecraft.world.level.material.FluidState
|
||||
import net.minecraft.world.level.material.Fluids
|
||||
|
||||
open class SixFaceRotationalBlock(properties : Properties) : FaceAttachedHorizontalDirectionalBlock(properties), SimpleWaterloggedBlock {
|
||||
companion object {
|
||||
val CODEC = simpleCodec(::SixFaceRotationalBlock)
|
||||
}
|
||||
|
||||
override fun codec(): MapCodec<out SixFaceRotationalBlock> = CODEC
|
||||
|
||||
override fun createBlockStateDefinition(builder: StateDefinition.Builder<Block, BlockState>) {
|
||||
builder.add(FACING, FACE, WATERLOGGED)
|
||||
}
|
||||
|
||||
override fun getFluidState(blockState: BlockState): FluidState? {
|
||||
return if (blockState.getValue(WATERLOGGED)) Fluids.WATER.getSource(false) else super.getFluidState(blockState);
|
||||
}
|
||||
|
||||
override fun getStateForPlacement(context: BlockPlaceContext): BlockState? {
|
||||
val fluidState = context.level.getFluidState(context.clickedPos)
|
||||
for (direction in context.nearestLookingDirections) {
|
||||
val blockState: BlockState
|
||||
if (direction.getAxis() === Direction.Axis.Y) {
|
||||
blockState = (defaultBlockState()
|
||||
.setValue(FACE, if (direction == Direction.UP) AttachFace.CEILING else AttachFace.FLOOR) as BlockState)
|
||||
.setValue(FACING, context.horizontalDirection.opposite)
|
||||
.setValue(WATERLOGGED, fluidState.type === Fluids.WATER
|
||||
) as BlockState
|
||||
} else {
|
||||
blockState = (defaultBlockState()
|
||||
.setValue(FACE, AttachFace.WALL) as BlockState)
|
||||
.setValue(FACING, direction.opposite)
|
||||
.setValue(WATERLOGGED, fluidState.type === Fluids.WATER
|
||||
) as BlockState
|
||||
|
||||
}
|
||||
|
||||
if (blockState.canSurvive(context.getLevel(), context.getClickedPos())) {
|
||||
return blockState
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
override fun updateShape(state: BlockState, direction: Direction, neighborState: BlockState, world: LevelAccessor, pos: BlockPos, neighborPos: BlockPos): BlockState {
|
||||
if (state.getValue(WATERLOGGED)) {
|
||||
world.scheduleTick(pos, Fluids.WATER, Fluids.WATER.getTickDelay(world))
|
||||
}
|
||||
return if (getConnectedDirection(state).opposite == direction && !state.canSurvive(world, pos)) Blocks.AIR.defaultBlockState()
|
||||
else super.updateShape(state, direction, neighborState, world, pos, neighborPos)
|
||||
}
|
||||
|
||||
override fun canSurvive(state: BlockState, level: LevelReader, pos: BlockPos): Boolean {
|
||||
return if (state.getValue(FACE) == AttachFace.CEILING) {
|
||||
val direction = getConnectedDirection(state)
|
||||
canSupportCenter(level, pos.relative(direction.opposite), direction)
|
||||
} else if (state.getValue(FACE) == AttachFace.WALL) {
|
||||
level.getBlockState(pos.relative(state.getValue(FACING).opposite)).isSolid
|
||||
} else {
|
||||
super.canSurvive(state, level, pos)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,13 @@ class StackableItemBlock(settings: Properties, val type: StackableItemBlockType)
|
||||
ETHER,
|
||||
MAX_POTION,
|
||||
POTION,
|
||||
STATUS_RESTORE;
|
||||
STATUS_RESTORE,
|
||||
VITAMIN,
|
||||
METAL_COAT,
|
||||
IRON_BALL,
|
||||
THROAT_SPRAY,
|
||||
UPGRADE,
|
||||
DICE;
|
||||
|
||||
override fun getSerializedName(): String = this.name.lowercase()
|
||||
|
||||
@@ -57,98 +63,78 @@ class StackableItemBlock(settings: Properties, val type: StackableItemBlockType)
|
||||
StackableItemBlockType.CODEC.fieldOf("type").forGetter(StackableItemBlock::type)
|
||||
).apply(it, ::StackableItemBlock) }
|
||||
|
||||
val northShapeMap: Map<StackableItemBlockType, List<VoxelShape>> = mapOf(
|
||||
StackableItemBlockType.BATTLE_ITEM to listOf(
|
||||
Shapes.box(0.34375, 0.0, 0.34375, 0.65625, 0.4375, 0.65625),
|
||||
Shapes.or(
|
||||
Shapes.box(0.09375, 0.0, 0.34375, 0.40625, 0.4375, 0.65625),
|
||||
Shapes.box(0.59375, 0.0, 0.40625, 0.90625, 0.4375, 0.71875)
|
||||
val northShapeMap: Map<StackableItemBlockType, List<VoxelShape>>
|
||||
get() = mapOf(
|
||||
StackableItemBlockType.BATTLE_ITEM to listOf(
|
||||
Shapes.box(0.34375, 0.0, 0.34375, 0.65625, 0.4375, 0.65625),
|
||||
Shapes.box(0.09375, 0.0, 0.34375, 0.90625, 0.4375, 0.71875),
|
||||
Shapes.box(0.09375, 0.0, 0.09375, 0.90625, 0.4375, 0.90625),
|
||||
Shapes.box(0.09375, 0.0, 0.09375, 0.96875, 0.4375, 0.90625)
|
||||
),
|
||||
Shapes.or(
|
||||
Shapes.box(0.59375, 0.0, 0.59375, 0.90625, 0.4375, 0.90625),
|
||||
Shapes.box(0.09375, 0.0, 0.53125, 0.40625, 0.4375, 0.84375),
|
||||
Shapes.box(0.34375, 0.0, 0.09375, 0.65625, 0.4375, 0.40625)
|
||||
StackableItemBlockType.ETHER to listOf(
|
||||
Shapes.box(0.375, 0.0, 0.3125, 0.625, 0.5, 0.6875),
|
||||
Shapes.box(0.125, 0.0, 0.3125, 0.875, 0.5, 0.75),
|
||||
Shapes.box(0.125, 0.0, 0.125, 0.875, 0.5, 1.0),
|
||||
Shapes.box(0.125, 0.0, 0.0625, 0.9375, 0.5, 1.0)
|
||||
),
|
||||
Shapes.or(
|
||||
Shapes.box(0.09375, 0.0, 0.09375, 0.40625, 0.4375, 0.40625),
|
||||
Shapes.box(0.59375, 0.0, 0.15625, 0.90625, 0.4375, 0.46875),
|
||||
Shapes.box(0.65625, 0.0, 0.59375, 0.96875, 0.4375, 0.90625),
|
||||
Shapes.box(0.15625, 0.0, 0.53125, 0.46875, 0.4375, 0.84375)
|
||||
)
|
||||
),
|
||||
StackableItemBlockType.ETHER to listOf(
|
||||
Shapes.box(0.375, 0.0, 0.3125, 0.625, 0.5, 0.6875),
|
||||
Shapes.or(
|
||||
Shapes.box(0.625, 0.0, 0.375, 0.875, 0.5, 0.75),
|
||||
Shapes.box(0.125, 0.0, 0.3125, 0.375, 0.5, 0.6875)
|
||||
StackableItemBlockType.MAX_POTION to listOf(
|
||||
Shapes.box(0.34375, 0.0, 0.3125, 0.65625, 0.5, 0.6875),
|
||||
Shapes.box(0.09375, 0.0, 0.3125, 0.90625, 0.5, 0.75),
|
||||
Shapes.box(0.09375, 0.0, 0.0625, 0.90625, 0.5, 1.0),
|
||||
Shapes.box(0.09375, 0.0, 0.0625, 0.96875, 0.5, 1.0)
|
||||
),
|
||||
Shapes.or(
|
||||
Shapes.box(0.375, 0.0, 0.125, 0.625, 0.5, 0.5),
|
||||
Shapes.box(0.125, 0.0, 0.5625, 0.375, 0.5, 0.9375),
|
||||
Shapes.box(0.625, 0.0, 0.625, 0.875, 0.5, 1.0)
|
||||
StackableItemBlockType.POTION to listOf(
|
||||
Shapes.box(0.375, 0.0, 0.3125, 0.625, 0.5625, 0.75),
|
||||
Shapes.box(0.125, 0.0, 0.3125, 0.875, 0.5625, 0.8125),
|
||||
Shapes.box(0.125, 0.0, 0.0625, 0.875, 0.5625, 1.0),
|
||||
Shapes.box(0.125, 0.0, 0.0, 0.9375, 0.5625, 1.0)
|
||||
),
|
||||
Shapes.or(
|
||||
Shapes.box(0.125, 0.0, 0.0625, 0.375, 0.5, 0.4375),
|
||||
Shapes.box(0.625, 0.0, 0.125, 0.875, 0.5, 0.5),
|
||||
Shapes.box(0.6875, 0.0, 0.625, 0.9375, 0.5, 1.0),
|
||||
Shapes.box(0.1875, 0.0, 0.5625, 0.4375, 0.5, 0.9375)
|
||||
)
|
||||
),
|
||||
StackableItemBlockType.MAX_POTION to listOf(
|
||||
Shapes.box(0.34375, 0.0, 0.3125, 0.65625, 0.5, 0.6875),
|
||||
Shapes.or(
|
||||
Shapes.box(0.09375, 0.0, 0.3125, 0.40625, 0.5, 0.6875),
|
||||
Shapes.box(0.59375, 0.0, 0.375, 0.90625, 0.5, 0.75)
|
||||
StackableItemBlockType.STATUS_RESTORE to listOf(
|
||||
Shapes.box(0.375, 0.0, 0.3125, 0.625, 0.4375, 0.6875),
|
||||
Shapes.box(0.125, 0.0, 0.3125, 0.875, 0.4375, 0.75),
|
||||
Shapes.box(0.125, 0.0, 0.0625, 0.875, 0.4375, 0.9375),
|
||||
Shapes.box(0.125, 0.0, 0.0, 0.9375, 0.4375, 0.9375)
|
||||
),
|
||||
Shapes.or(
|
||||
Shapes.box(0.34375, 0.0, 0.0625, 0.65625, 0.5, 0.4375),
|
||||
Shapes.box(0.09375, 0.0, 0.5625, 0.40625, 0.5, 0.9375),
|
||||
Shapes.box(0.59375, 0.0, 0.625, 0.90625, 0.5, 1.0)
|
||||
StackableItemBlockType.METAL_COAT to listOf(
|
||||
Shapes.box(0.3125, 0.0, 0.3125, 0.6875, 0.375, 0.6875),
|
||||
Shapes.box(0.125, 0.0, 0.125, 0.875, 0.375, 0.875),
|
||||
Shapes.box(0.125, 0.0, 0.0625, 0.9375, 0.375, 0.875),
|
||||
Shapes.box(0.0625, 0.0, 0.0625, 0.9375, 0.375, 0.9375)
|
||||
),
|
||||
Shapes.or(
|
||||
Shapes.box(0.09375, 0.0, 0.0625, 0.40625, 0.5, 0.4375),
|
||||
Shapes.box(0.15625, 0.0, 0.5625, 0.46875, 0.5, 0.9375),
|
||||
Shapes.box(0.65625, 0.0, 0.625, 0.96875, 0.5, 1.0),
|
||||
Shapes.box(0.59375, 0.0, 0.125, 0.90625, 0.5, 0.5)
|
||||
).singleEncompassing()
|
||||
),
|
||||
StackableItemBlockType.POTION to listOf(
|
||||
Shapes.box(0.375, 0.0, 0.3125, 0.625, 0.5625, 0.75),
|
||||
Shapes.or(
|
||||
Shapes.box(0.125, 0.0, 0.3125, 0.375, 0.5625, 0.75),
|
||||
Shapes.box(0.625, 0.0, 0.375, 0.875, 0.5625, 0.8125)
|
||||
StackableItemBlockType.VITAMIN to listOf(
|
||||
Shapes.box(0.375, 0.0, 0.375, 0.625, 0.4375, 0.625),
|
||||
Shapes.box(0.1875, 0.0, 0.3125, 0.8125, 0.4375, 0.6875),
|
||||
Shapes.box(0.1875, 0.0, 0.1875, 0.8125, 0.4375, 0.8125),
|
||||
Shapes.box(0.125, 0.0, 0.125, 0.875, 0.4375, 0.875)
|
||||
),
|
||||
Shapes.or(
|
||||
Shapes.box(0.375, 0.0, 0.0625, 0.625, 0.5625, 0.5),
|
||||
Shapes.box(0.625, 0.0, 0.5625, 0.875, 0.5625, 1.0),
|
||||
Shapes.box(0.125, 0.0, 0.5, 0.375, 0.5625, 0.9375)
|
||||
StackableItemBlockType.IRON_BALL to listOf(
|
||||
Shapes.box(0.25, 0.0, 0.25, 0.75, 0.4375, 0.75),
|
||||
Shapes.box(0.0, 0.0, 0.1875, 1.0, 0.4375, 0.8125),
|
||||
Shapes.box(0.0, 0.0, 0.0, 1.0, 0.4375, 1.0),
|
||||
Shapes.or(
|
||||
Shapes.box(0.0, 0.0, 0.0, 1.0, 0.4375, 1.0),
|
||||
Shapes.box(0.25, 0.4375, 0.3125, 0.6875, 0.875, 0.75)
|
||||
)
|
||||
),
|
||||
Shapes.or(
|
||||
Shapes.box(0.125, 0.0, 0.0, 0.375, 0.5625, 0.4375),
|
||||
Shapes.box(0.625, 0.0, 0.0625, 0.875, 0.5625, 0.5),
|
||||
Shapes.box(0.6875, 0.0, 0.5625, 0.9375, 0.5625, 1.0),
|
||||
Shapes.box(0.1875, 0.0, 0.5, 0.4375, 0.5625, 0.9375)
|
||||
)
|
||||
),
|
||||
StackableItemBlockType.STATUS_RESTORE to listOf(
|
||||
Shapes.box(0.375, 0.0, 0.3125, 0.625, 0.4375, 0.6875),
|
||||
Shapes.or(
|
||||
Shapes.box(0.125, 0.0, 0.3125, 0.375, 0.4375, 0.75),
|
||||
Shapes.box(0.625, 0.0, 0.375, 0.875, 0.4375, 0.75)
|
||||
StackableItemBlockType.THROAT_SPRAY to listOf(
|
||||
Shapes.box(0.375, 0.0, 0.1875, 0.625, 0.5625, 0.75),
|
||||
Shapes.box(0.125, 0.0, 0.1875, 0.875, 0.5625, 0.8125),
|
||||
Shapes.box(0.125, 0.0, 0.0, 0.875, 0.5625, 1.0),
|
||||
Shapes.box(0.075, 0.0, 0.0, 0.9375, 0.5625, 1.0)
|
||||
),
|
||||
Shapes.or(
|
||||
Shapes.box(0.125, 0.0, 0.5, 0.375, 0.4375, 0.875),
|
||||
Shapes.box(0.625, 0.0, 0.5625, 0.875, 0.4375, 0.9375),
|
||||
Shapes.box(0.375, 0.0, 0.0625, 0.625, 0.4375, 0.4375)
|
||||
StackableItemBlockType.UPGRADE to listOf(
|
||||
Shapes.box(0.3125, 0.0, 0.3125, 0.6875, 0.125, 0.6875),
|
||||
Shapes.box(0.3125, 0.0, 0.3125, 0.6875, 0.25, 0.6875),
|
||||
Shapes.box(0.3125, 0.0, 0.3125, 0.6875, 0.375, 0.6875),
|
||||
Shapes.box(0.3125, 0.0, 0.3125, 0.6875, 0.5, 0.6875)
|
||||
),
|
||||
Shapes.or(
|
||||
Shapes.box(0.1875, 0.0, 0.5, 0.4375, 0.4375, 0.875),
|
||||
Shapes.box(0.125, 0.0, 0.0, 0.375, 0.4375, 0.375),
|
||||
Shapes.box(0.625, 0.0, 0.0625, 0.875, 0.4375, 0.4375),
|
||||
Shapes.box(0.6875, 0.0, 0.5625, 0.9375, 0.4375, 0.9375)
|
||||
StackableItemBlockType.DICE to listOf(
|
||||
Shapes.box(0.125, 0.0, 0.6875, 0.3125, 0.1875, 0.875),
|
||||
Shapes.box(0.125, 0.0, 0.625, 0.5625, 0.1875, 0.875),
|
||||
Shapes.box(0.125, 0.0, 0.4375, 0.5625, 0.1875, 0.875),
|
||||
Shapes.box(0.125, 0.0, 0.4375, 0.5625, 0.375, 0.875)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
fun getShape(type: StackableItemBlockType, count: Int): VoxelShape = northShapeMap[type]?.let { list -> list[(count - 1).coerceIn(0, list.lastIndex)] } ?: Shapes.block()
|
||||
fun getMaxAmount(type: StackableItemBlockType): Int = min(northShapeMap[type]?.size ?: 1 , MAX_AMOUNT)
|
||||
@@ -184,7 +170,7 @@ class StackableItemBlock(settings: Properties, val type: StackableItemBlockType)
|
||||
}
|
||||
|
||||
override fun getShape(state: BlockState, level: BlockGetter, pos: BlockPos, context: CollisionContext): VoxelShape {
|
||||
return getCollisionShape(state, level, pos, context).singleEncompassing()
|
||||
return getCollisionShape(state, level, pos, context)
|
||||
}
|
||||
|
||||
override fun getStateForPlacement(context: BlockPlaceContext): BlockState? {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import com.cobblemon.mod.common.api.storage.player.client.ClientPokedexManager
|
||||
import com.cobblemon.mod.common.api.storage.player.client.ClientTMMoveManager
|
||||
import com.cobblemon.mod.common.api.tags.CobblemonItemTags
|
||||
import com.cobblemon.mod.common.block.entity.TintBlockEntity
|
||||
import com.cobblemon.mod.common.block.sign.CobblemonSignBlock
|
||||
import com.cobblemon.mod.common.client.battle.ClientBattle
|
||||
import com.cobblemon.mod.common.client.gui.PartyOverlay
|
||||
import com.cobblemon.mod.common.client.gui.PartyOverlayDataControl
|
||||
@@ -29,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
|
||||
@@ -121,6 +126,8 @@ object CobblemonClient {
|
||||
}
|
||||
|
||||
fun onLogout() {
|
||||
MolangUiClientManager.cleanup()
|
||||
MolangUiCompatibilityRequestHandler.reset()
|
||||
storage.onLogout()
|
||||
battle = null
|
||||
battleOverlay.onLogout()
|
||||
@@ -162,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) {
|
||||
@@ -238,6 +247,12 @@ object CobblemonClient {
|
||||
CobblemonBlocks.POKE_CAKE
|
||||
)
|
||||
|
||||
this.implementation.registerBlockRenderType(
|
||||
RenderType.translucent(),
|
||||
CobblemonBlocks.UPGRADE,
|
||||
CobblemonBlocks.DUBIOUS_DISC
|
||||
)
|
||||
|
||||
this.implementation.registerBlockRenderType(
|
||||
RenderType.cutout(),
|
||||
CobblemonBlocks.GILDED_CHEST,
|
||||
@@ -383,7 +398,31 @@ object CobblemonClient {
|
||||
CobblemonBlocks.X_SP_ATK,
|
||||
CobblemonBlocks.X_SP_DEF,
|
||||
CobblemonBlocks.X_SPEED,
|
||||
CobblemonBlocks.TM_MACHINE
|
||||
CobblemonBlocks.TM_MACHINE,
|
||||
|
||||
CobblemonBlocks.GALARICA_CUFF,
|
||||
CobblemonBlocks.GALARICA_WREATH,
|
||||
CobblemonBlocks.CHIPPED_POT,
|
||||
CobblemonBlocks.CRACKED_POT,
|
||||
CobblemonBlocks.UNREMARKABLE_TEACUP,
|
||||
CobblemonBlocks.MASTERPIECE_TEACUP,
|
||||
CobblemonBlocks.LUMINOUS_MOSS,
|
||||
CobblemonBlocks.ICY_ROCK,
|
||||
CobblemonBlocks.HEAT_ROCK,
|
||||
CobblemonBlocks.SMOOTH_ROCK,
|
||||
CobblemonBlocks.DAMP_ROCK,
|
||||
CobblemonBlocks.DESTINY_KNOT,
|
||||
CobblemonBlocks.METRONOME,
|
||||
CobblemonBlocks.KINGS_ROCK,
|
||||
CobblemonBlocks.FLAME_ORB,
|
||||
CobblemonBlocks.LIFE_ORB,
|
||||
CobblemonBlocks.TOXIC_ORB,
|
||||
CobblemonBlocks.LIGHT_BALL,
|
||||
CobblemonBlocks.SMOKE_BALL,
|
||||
CobblemonBlocks.NEVER_MELT_ICE,
|
||||
CobblemonBlocks.WIDE_LENS,
|
||||
CobblemonBlocks.ZOOM_LENS,
|
||||
CobblemonBlocks.SCOPE_LENS
|
||||
)
|
||||
|
||||
this.createBoatModelLayers()
|
||||
@@ -392,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")
|
||||
@@ -411,6 +453,9 @@ object CobblemonClient {
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -479,6 +524,8 @@ object CobblemonClient {
|
||||
|
||||
BerryModelRepository.reload(resourceManager)
|
||||
MiscModelRepository.reload(resourceManager)
|
||||
MolangUiRegistry.reload(resourceManager)
|
||||
MolangUiClientManager.onResourceReload()
|
||||
LOGGER.info("Loaded assets")
|
||||
}
|
||||
|
||||
|
||||
+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)
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of 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.net.molangui
|
||||
|
||||
import com.cobblemon.mod.common.api.net.ClientNetworkPacketHandler
|
||||
import com.cobblemon.mod.common.client.gui.molangui.MolangUiClientManager
|
||||
import com.cobblemon.mod.common.net.messages.client.molangui.MolangUiClosePacket
|
||||
import net.minecraft.client.Minecraft
|
||||
|
||||
object MolangUiCloseHandler : ClientNetworkPacketHandler<MolangUiClosePacket> {
|
||||
override fun handle(packet: MolangUiClosePacket, client: Minecraft) {
|
||||
MolangUiClientManager.close(packet.sessionId)
|
||||
}
|
||||
}
|
||||
+30
@@ -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.client.net.molangui
|
||||
|
||||
import com.cobblemon.mod.common.api.molangui.MolangUiRegistry
|
||||
import com.cobblemon.mod.common.api.net.ClientNetworkPacketHandler
|
||||
import com.cobblemon.mod.common.net.messages.client.molangui.MolangUiCompatibilityRequestPacket
|
||||
import com.cobblemon.mod.common.net.messages.server.molangui.MolangUiCompatibilityResponsePacket
|
||||
import net.minecraft.client.Minecraft
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
|
||||
object MolangUiCompatibilityRequestHandler : ClientNetworkPacketHandler<MolangUiCompatibilityRequestPacket> {
|
||||
private var requested: Set<ResourceLocation> = emptySet()
|
||||
override fun handle(packet: MolangUiCompatibilityRequestPacket, client: Minecraft) {
|
||||
requested = packet.contracts.keys.toSet()
|
||||
respond()
|
||||
}
|
||||
fun respond() {
|
||||
if (requested.isEmpty()) return
|
||||
val available = MolangUiRegistry.getVersionMap().filterKeys { it in requested }
|
||||
MolangUiCompatibilityResponsePacket(available).sendToServer()
|
||||
}
|
||||
fun reset() { requested = emptySet() }
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of 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.net.molangui
|
||||
|
||||
import com.cobblemon.mod.common.api.net.ClientNetworkPacketHandler
|
||||
import com.cobblemon.mod.common.client.gui.molangui.MolangUiClientManager
|
||||
import com.cobblemon.mod.common.net.messages.client.molangui.MolangUiOpenPacket
|
||||
import net.minecraft.client.Minecraft
|
||||
|
||||
object MolangUiOpenHandler : ClientNetworkPacketHandler<MolangUiOpenPacket> {
|
||||
override fun handle(packet: MolangUiOpenPacket, client: Minecraft) {
|
||||
MolangUiClientManager.open(packet.sessionId, packet.guiId, packet.contractVersion, packet.mode, packet.args, packet.exclusive, packet.hasContainer, packet.slotLayouts)
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of 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.net.molangui
|
||||
|
||||
import com.cobblemon.mod.common.api.net.ClientNetworkPacketHandler
|
||||
import com.cobblemon.mod.common.client.gui.molangui.MolangUiClientManager
|
||||
import com.cobblemon.mod.common.net.messages.client.molangui.MolangUiUpdatePacket
|
||||
import net.minecraft.client.Minecraft
|
||||
|
||||
object MolangUiUpdateHandler : ClientNetworkPacketHandler<MolangUiUpdatePacket> {
|
||||
override fun handle(packet: MolangUiUpdatePacket, client: Minecraft) {
|
||||
MolangUiClientManager.updateArgs(packet.sessionId, packet.args, packet.variables)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.client.render
|
||||
|
||||
import com.mojang.blaze3d.systems.RenderSystem
|
||||
import org.lwjgl.opengl.GL11
|
||||
import org.lwjgl.opengl.GL14
|
||||
import kotlin.math.sqrt
|
||||
|
||||
object RenderAlphaScope {
|
||||
private class MaterialState(val color: FloatArray, val blendColor: FloatArray?)
|
||||
|
||||
private class Scope {
|
||||
val materials = ArrayDeque<MaterialState>()
|
||||
}
|
||||
|
||||
private val current = ThreadLocal<Scope>()
|
||||
|
||||
fun render(draw: () -> Unit) {
|
||||
val color = RenderSystem.getShaderColor().copyOf()
|
||||
if (!color[3].isFinite() || color[3] >= 1f) {
|
||||
draw()
|
||||
return
|
||||
}
|
||||
val blend = RenderBlendState.capture()
|
||||
val previous = current.get()
|
||||
current.set(Scope())
|
||||
try {
|
||||
draw()
|
||||
} finally {
|
||||
if (previous == null) current.remove() else current.set(previous)
|
||||
RenderSystem.setShaderColor(color[0], color[1], color[2], color[3])
|
||||
blend.restore()
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun afterMaterialSetup() {
|
||||
val scope = current.get() ?: return
|
||||
val color = RenderSystem.getShaderColor().copyOf()
|
||||
val blended = GL11.glIsEnabled(GL11.GL_BLEND)
|
||||
scope.materials.addLast(MaterialState(color, if (blended) null else RenderBlendState.readColor()))
|
||||
val opacity = color[3].coerceIn(0f, 1f)
|
||||
if (!blended) {
|
||||
RenderSystem.enableBlend()
|
||||
// Cutout texels are opaque above the discard threshold; fade their coverage uniformly.
|
||||
GL14.glBlendColor(0f, 0f, 0f, opacity)
|
||||
RenderSystem.blendFuncSeparate(GL14.GL_CONSTANT_ALPHA, GL14.GL_ONE_MINUS_CONSTANT_ALPHA, GL11.GL_ONE, GL11.GL_ZERO)
|
||||
} else {
|
||||
val factor = when (GL11.glGetInteger(GL14.GL_BLEND_SRC_RGB)) {
|
||||
GL11.GL_ONE -> opacity
|
||||
// SRC_COLOR squares the source RGB, so sqrt(alpha) fades glint linearly.
|
||||
GL11.GL_SRC_COLOR -> sqrt(opacity)
|
||||
else -> 1f
|
||||
}
|
||||
if (factor != 1f) RenderSystem.setShaderColor(color[0] * factor, color[1] * factor, color[2] * factor, color[3])
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun afterMaterialClear() {
|
||||
val material = current.get()?.materials?.removeLastOrNull() ?: return
|
||||
val color = material.color
|
||||
RenderSystem.setShaderColor(color[0], color[1], color[2], color[3])
|
||||
material.blendColor?.let { RenderBlendState.restoreColor(it) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.client.render
|
||||
|
||||
import com.mojang.blaze3d.systems.RenderSystem
|
||||
import org.lwjgl.opengl.ARBImaging.GL_BLEND_COLOR
|
||||
import org.lwjgl.opengl.GL11
|
||||
import org.lwjgl.opengl.GL14
|
||||
|
||||
internal class RenderBlendState private constructor(
|
||||
private val enabled: Boolean,
|
||||
private val sourceRgb: Int,
|
||||
private val destinationRgb: Int,
|
||||
private val sourceAlpha: Int,
|
||||
private val destinationAlpha: Int,
|
||||
private val color: FloatArray
|
||||
) {
|
||||
fun restore() {
|
||||
RenderSystem.blendFuncSeparate(sourceRgb, destinationRgb, sourceAlpha, destinationAlpha)
|
||||
restoreColor(color)
|
||||
if (enabled) RenderSystem.enableBlend() else RenderSystem.disableBlend()
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun capture() = RenderBlendState(
|
||||
GL11.glIsEnabled(GL11.GL_BLEND),
|
||||
GL11.glGetInteger(GL14.GL_BLEND_SRC_RGB), GL11.glGetInteger(GL14.GL_BLEND_DST_RGB),
|
||||
GL11.glGetInteger(GL14.GL_BLEND_SRC_ALPHA), GL11.glGetInteger(GL14.GL_BLEND_DST_ALPHA), readColor()
|
||||
)
|
||||
|
||||
fun readColor() = FloatArray(4).also { GL11.glGetFloatv(GL_BLEND_COLOR, it) }
|
||||
|
||||
fun restoreColor(color: FloatArray) = GL14.glBlendColor(color[0], color[1], color[2], color[3])
|
||||
}
|
||||
}
|
||||
|
||||
internal inline fun withStandardBlend(draw: () -> Unit) {
|
||||
val state = RenderBlendState.capture()
|
||||
try {
|
||||
RenderSystem.enableBlend()
|
||||
RenderSystem.defaultBlendFunc()
|
||||
draw()
|
||||
} finally {
|
||||
state.restore()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.command
|
||||
|
||||
import com.cobblemon.mod.common.api.molangui.MolangUiContractRegistry
|
||||
import com.cobblemon.mod.common.api.molangui.MolangUiOpenRequest
|
||||
import com.cobblemon.mod.common.api.molangui.MolangUiOpenResult
|
||||
import com.cobblemon.mod.common.api.molangui.MolangUiService
|
||||
import com.cobblemon.mod.common.util.lang
|
||||
import com.mojang.brigadier.Command
|
||||
import com.mojang.brigadier.CommandDispatcher
|
||||
import net.minecraft.commands.CommandSourceStack
|
||||
import net.minecraft.commands.Commands
|
||||
import net.minecraft.commands.arguments.ResourceLocationArgument
|
||||
|
||||
object MolangUiCommand {
|
||||
private const val UI_ID = "ui"
|
||||
fun register(dispatcher: CommandDispatcher<CommandSourceStack>) {
|
||||
dispatcher.register(Commands.literal("molangui").requires { it.hasPermission(2) }
|
||||
.then(Commands.literal("open").then(Commands.argument(UI_ID, ResourceLocationArgument.id())
|
||||
.suggests { _, builder -> MolangUiContractRegistry.getAllContracts().keys.forEach { builder.suggest(it.toString()) }; builder.buildFuture() }
|
||||
.executes { context ->
|
||||
val id = ResourceLocationArgument.getId(context, UI_ID)
|
||||
when (val result = MolangUiService.open(context.source.playerOrException, MolangUiOpenRequest(id))) {
|
||||
is MolangUiOpenResult.Opened -> {
|
||||
context.source.sendSuccess({ lang("molang_ui.command.opened", id.toString(), result.session.sessionId.toString()) }, false)
|
||||
Command.SINGLE_SUCCESS
|
||||
}
|
||||
is MolangUiOpenResult.Rejected -> {
|
||||
context.source.sendFailure(lang("molang_ui.command.rejected", id.toString(),
|
||||
lang("molang_ui.rejection.${result.reason.name.lowercase()}")))
|
||||
0
|
||||
}
|
||||
}
|
||||
}))
|
||||
.then(Commands.literal("list").executes { context ->
|
||||
val contracts = MolangUiContractRegistry.getAllContracts()
|
||||
context.source.sendSuccess({ lang("molang_ui.command.list", contracts.size, contracts.keys.joinToString()) }, false)
|
||||
Command.SINGLE_SUCCESS
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,9 @@ import com.cobblemon.mod.common.api.item.HeldItems
|
||||
import com.cobblemon.mod.common.api.mark.Marks
|
||||
import com.cobblemon.mod.common.api.moves.Moves
|
||||
import com.cobblemon.mod.common.api.moves.animations.ActionEffects
|
||||
import com.cobblemon.mod.common.api.molangui.MolangUiContractRegistry
|
||||
import com.cobblemon.mod.common.api.molangui.MolangUiService
|
||||
import com.cobblemon.mod.common.api.vending.VendingMachineCatalogs
|
||||
import com.cobblemon.mod.common.api.npc.NPCClasses
|
||||
import com.cobblemon.mod.common.api.npc.NPCPresets
|
||||
import com.cobblemon.mod.common.api.pokeball.PokeBalls
|
||||
@@ -69,6 +72,14 @@ object CobblemonDataProvider : DataProvider {
|
||||
|
||||
fun registerDefaults() {
|
||||
this.register(CobblemonScripts, reloadable = true)
|
||||
this.register(MolangUiContractRegistry, reloadable = true)
|
||||
this.register(VendingMachineCatalogs, reloadable = true)
|
||||
MolangUiContractRegistry.observable.subscribe {
|
||||
server()?.execute {
|
||||
MolangUiService.closeAll()
|
||||
server()?.playerList?.players?.forEach(MolangUiService::requestCompatibility)
|
||||
}
|
||||
}
|
||||
this.register(SpeciesFeatures, reloadable = false)
|
||||
this.register(GlobalSpeciesFeatures, reloadable = false)
|
||||
this.register(SpeciesFeatureAssignments, reloadable = false)
|
||||
|
||||
@@ -30,6 +30,7 @@ import com.cobblemon.mod.common.item.TumblestoneItem
|
||||
import net.minecraft.world.item.BlockItem
|
||||
import com.cobblemon.mod.common.platform.events.ServerPlayerEvent
|
||||
import com.cobblemon.mod.common.api.storage.player.PlayerInstancedDataStoreTypes
|
||||
import com.cobblemon.mod.common.item.GemItem
|
||||
import com.cobblemon.mod.common.platform.events.PlatformEvents
|
||||
import com.cobblemon.mod.common.util.effectiveName
|
||||
import com.cobblemon.mod.common.util.getPlayer
|
||||
@@ -40,6 +41,7 @@ object AdvancementHandler : EventHandler {
|
||||
override fun registerListeners() {
|
||||
PlatformEvents.RIGHT_CLICK_BLOCK.subscribe(Priority.LOWEST, ::onTumbleStonePlaced)
|
||||
PlatformEvents.RIGHT_CLICK_BLOCK.subscribe(Priority.LOWEST, ::onTypeGemClusterPlaced)
|
||||
PlatformEvents.RIGHT_CLICK_BLOCK.subscribe(Priority.LOWEST, ::onTypeGemItemPlaced)
|
||||
POKEMON_CAPTURED.subscribe(Priority.LOWEST, ::onCapture)
|
||||
BATTLE_VICTORY.subscribe(Priority.LOWEST, ::onWinBattle)
|
||||
EVOLUTION_COMPLETE.subscribe(Priority.LOWEST, ::onEvolve)
|
||||
@@ -206,6 +208,12 @@ object AdvancementHandler : EventHandler {
|
||||
CobblemonCriteria.PLANT_TYPE_GEM.trigger(event.player, PlantTypeGemContext(event.pos, block))
|
||||
}
|
||||
|
||||
fun onTypeGemItemPlaced(event: ServerPlayerEvent.RightClickBlock){
|
||||
val item = event.player.getItemInHand(event.hand).item as? GemItem ?: return
|
||||
val block = item.block as? TypeGemClusterBlock ?: return
|
||||
CobblemonCriteria.PLANT_TYPE_GEM.trigger(event.player, PlantTypeGemContext(event.pos, block))
|
||||
}
|
||||
|
||||
fun startRiding(event: RidePokemonEvent.Post) {
|
||||
CobblemonCriteria.RIDING_STAT_BOOST.trigger(event.player, RidingStatBoostContext(event.pokemon))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.item
|
||||
|
||||
import net.minecraft.world.item.ItemNameBlockItem
|
||||
import net.minecraft.world.level.block.Block
|
||||
|
||||
class WearableBlockItem(override val name: String, block: Block): ItemNameBlockItem(block, Properties()), WearableItem{
|
||||
|
||||
}
|
||||
@@ -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.item
|
||||
|
||||
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
|
||||
import com.cobblemon.mod.common.util.traceFirstEntityCollision
|
||||
import net.minecraft.world.InteractionHand
|
||||
import net.minecraft.world.InteractionResultHolder
|
||||
import net.minecraft.world.entity.player.Player
|
||||
import net.minecraft.world.item.ItemStack
|
||||
import net.minecraft.world.level.Level
|
||||
|
||||
|
||||
class WearableHatItem(override val name: String): CobblemonItem(Properties()), WearableItem {
|
||||
override fun use(level: Level, player: Player, usedHand: InteractionHand): InteractionResultHolder<ItemStack> {
|
||||
val itemStack = player.getItemInHand(usedHand)
|
||||
val hitPokemon = player.traceFirstEntityCollision(
|
||||
entityClass = PokemonEntity::class.java,
|
||||
maxDistance = player.entityInteractionRange().toFloat()
|
||||
)
|
||||
// If the player is looking at a Pokémon within range, block the ability to swap the item onto their head
|
||||
return if (player.isCrouching && hitPokemon != null && hitPokemon.isOwnedBy(player)) {
|
||||
InteractionResultHolder.fail(itemStack)
|
||||
} else this.swapWithEquipmentSlot(this, level, player, usedHand)
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,9 @@ import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.world.entity.EquipmentSlot
|
||||
import net.minecraft.world.item.Equipable
|
||||
|
||||
class WearableItem(val name: String): CobblemonItem(Properties()), Equipable {
|
||||
interface WearableItem: Equipable {
|
||||
val name: String
|
||||
|
||||
companion object {
|
||||
const val MODEL_PATH = "item/wearable"
|
||||
}
|
||||
|
||||
@@ -330,6 +330,7 @@ object CobblemonItemGroups {
|
||||
|
||||
// entries.accept(CobblemonItems.INCENSE_SWEET) // TODO after 1.7
|
||||
entries.accept(CobblemonItems.DISPLAY_CASE)
|
||||
entries.accept(CobblemonItems.VENDING_MACHINE)
|
||||
entries.accept(CobblemonItems.APRICORN_LOG)
|
||||
entries.accept(CobblemonItems.APRICORN_WOOD)
|
||||
entries.accept(CobblemonItems.STRIPPED_APRICORN_LOG)
|
||||
|
||||
@@ -11,28 +11,27 @@ package com.cobblemon.mod.common.item.interactive
|
||||
import com.cobblemon.mod.common.api.item.PokemonSelectingItem
|
||||
import com.cobblemon.mod.common.api.pokemon.stats.ItemEvSource
|
||||
import com.cobblemon.mod.common.api.pokemon.stats.Stat
|
||||
import com.cobblemon.mod.common.item.CobblemonItem
|
||||
import com.cobblemon.mod.common.item.battle.BagItem
|
||||
import com.cobblemon.mod.common.pokemon.EVs
|
||||
import com.cobblemon.mod.common.pokemon.Pokemon
|
||||
import com.cobblemon.mod.common.util.giveOrDropItemStack
|
||||
import net.minecraft.server.level.ServerPlayer
|
||||
import net.minecraft.sounds.SoundEvent
|
||||
import net.minecraft.world.InteractionHand
|
||||
import net.minecraft.world.InteractionResultHolder
|
||||
import net.minecraft.world.entity.player.Player
|
||||
import net.minecraft.world.item.ItemStack
|
||||
import net.minecraft.world.item.Items
|
||||
import net.minecraft.world.level.Level
|
||||
|
||||
abstract class EVIncreaseItem(
|
||||
val stat: Stat,
|
||||
val evIncreaseAmount: Int,
|
||||
) : CobblemonItem(Properties()), PokemonSelectingItem {
|
||||
override val bagItem = null
|
||||
interface EVIncreaseItem: PokemonSelectingItem {
|
||||
val stat: Stat
|
||||
val evIncreaseAmount: Int
|
||||
|
||||
override val bagItem: BagItem?
|
||||
get() = null
|
||||
|
||||
override fun canUseOnPokemon(stack: ItemStack, pokemon: Pokemon) = pokemon.evs.getOrDefault(stat) < EVs.MAX_STAT_VALUE &&
|
||||
super.canUseOnPokemon(stack, pokemon)
|
||||
|
||||
abstract val sound: SoundEvent
|
||||
val sound: SoundEvent
|
||||
|
||||
override fun applyToPokemon(
|
||||
player: ServerPlayer,
|
||||
@@ -51,11 +50,4 @@ abstract class EVIncreaseItem(
|
||||
InteractionResultHolder.fail(stack)
|
||||
}
|
||||
}
|
||||
|
||||
override fun use(world: Level, user: Player, hand: InteractionHand): InteractionResultHolder<ItemStack> {
|
||||
if (user is ServerPlayer) {
|
||||
return use(user, user.getItemInHand(hand))
|
||||
}
|
||||
return InteractionResultHolder.success(user.getItemInHand(hand))
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,23 @@ package com.cobblemon.mod.common.item.interactive
|
||||
|
||||
import com.cobblemon.mod.common.CobblemonSounds
|
||||
import com.cobblemon.mod.common.api.pokemon.stats.Stat
|
||||
import com.cobblemon.mod.common.item.CobblemonItem
|
||||
import net.minecraft.server.level.ServerPlayer
|
||||
import net.minecraft.sounds.SoundEvent
|
||||
import net.minecraft.world.InteractionHand
|
||||
import net.minecraft.world.InteractionResultHolder
|
||||
import net.minecraft.world.entity.player.Player
|
||||
import net.minecraft.world.item.ItemStack
|
||||
import net.minecraft.world.level.Level
|
||||
|
||||
class FeatherItem(stat: Stat) : EVIncreaseItem(stat, 1) {
|
||||
class FeatherItem(override val stat: Stat): CobblemonItem(Properties()), EVIncreaseItem {
|
||||
override val evIncreaseAmount: Int = 1
|
||||
override val sound: SoundEvent = CobblemonSounds.MEDICINE_FEATHER_USE
|
||||
|
||||
override fun use(world: Level, user: Player, hand: InteractionHand): InteractionResultHolder<ItemStack> {
|
||||
if (user is ServerPlayer) {
|
||||
return use(user, user.getItemInHand(hand))
|
||||
}
|
||||
return InteractionResultHolder.success(user.getItemInHand(hand))
|
||||
}
|
||||
}
|
||||
@@ -9,14 +9,19 @@
|
||||
package com.cobblemon.mod.common.item.interactive
|
||||
|
||||
import com.cobblemon.mod.common.CobblemonSounds
|
||||
import com.cobblemon.mod.common.api.pokemon.stats.Stats
|
||||
import com.cobblemon.mod.common.api.pokemon.stats.Stat
|
||||
import com.cobblemon.mod.common.item.CobblemonItem
|
||||
import com.cobblemon.mod.common.pokemon.Pokemon
|
||||
import net.minecraft.server.level.ServerPlayer
|
||||
import net.minecraft.sounds.SoundEvent
|
||||
import net.minecraft.world.InteractionHand
|
||||
import net.minecraft.world.InteractionResultHolder
|
||||
import net.minecraft.world.entity.player.Player
|
||||
import net.minecraft.world.item.ItemStack
|
||||
import net.minecraft.world.level.Level
|
||||
|
||||
class MochiItem(stat: Stats): EVIncreaseItem(stat, 4) {
|
||||
class MochiItem(override val stat: Stat): CobblemonItem(Properties()), EVIncreaseItem {
|
||||
override val evIncreaseAmount: Int = 4
|
||||
override val sound: SoundEvent = CobblemonSounds.MOCHI_USE
|
||||
|
||||
override fun applyToPokemon(player: ServerPlayer, stack: ItemStack, pokemon: Pokemon): InteractionResultHolder<ItemStack> {
|
||||
@@ -28,4 +33,11 @@ class MochiItem(stat: Stats): EVIncreaseItem(stat, 4) {
|
||||
|
||||
return super.applyToPokemon(player, stack, pokemon)
|
||||
}
|
||||
|
||||
override fun use(world: Level, user: Player, hand: InteractionHand): InteractionResultHolder<ItemStack> {
|
||||
if (user is ServerPlayer) {
|
||||
return use(user, user.getItemInHand(hand))
|
||||
}
|
||||
return InteractionResultHolder.success(user.getItemInHand(hand))
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,6 @@ package com.cobblemon.mod.common.item.interactive
|
||||
import com.cobblemon.mod.common.CobblemonSounds
|
||||
import com.cobblemon.mod.common.api.item.PokemonAndMoveSelectingItem
|
||||
import com.cobblemon.mod.common.api.moves.Move
|
||||
import com.cobblemon.mod.common.item.CobblemonItem
|
||||
import com.cobblemon.mod.common.pokemon.Pokemon
|
||||
import com.cobblemon.mod.common.util.giveOrDropItemStack
|
||||
import net.minecraft.world.entity.player.Player
|
||||
@@ -19,13 +18,15 @@ import net.minecraft.world.item.ItemStack
|
||||
import net.minecraft.server.level.ServerPlayer
|
||||
import net.minecraft.world.InteractionHand
|
||||
import net.minecraft.world.InteractionResultHolder
|
||||
import net.minecraft.world.item.ItemNameBlockItem
|
||||
import net.minecraft.world.item.Items
|
||||
import net.minecraft.world.item.Rarity
|
||||
import net.minecraft.world.level.Level
|
||||
import net.minecraft.world.level.block.Block
|
||||
|
||||
class PPUpItem(
|
||||
val amount: Int
|
||||
) : CobblemonItem(Properties().apply {
|
||||
val amount: Int, block: Block
|
||||
) : ItemNameBlockItem(block, Properties().apply {
|
||||
if (amount>1) rarity(Rarity.UNCOMMON)
|
||||
}), PokemonAndMoveSelectingItem {
|
||||
|
||||
|
||||
@@ -9,9 +9,25 @@
|
||||
package com.cobblemon.mod.common.item.interactive
|
||||
|
||||
import com.cobblemon.mod.common.CobblemonSounds
|
||||
import com.cobblemon.mod.common.api.pokemon.stats.Stats
|
||||
import com.cobblemon.mod.common.api.pokemon.stats.Stat
|
||||
import net.minecraft.server.level.ServerPlayer
|
||||
import net.minecraft.sounds.SoundEvent
|
||||
import net.minecraft.world.InteractionHand
|
||||
import net.minecraft.world.InteractionResultHolder
|
||||
import net.minecraft.world.entity.player.Player
|
||||
import net.minecraft.world.item.ItemNameBlockItem
|
||||
import net.minecraft.world.item.ItemStack
|
||||
import net.minecraft.world.level.Level
|
||||
import net.minecraft.world.level.block.Block
|
||||
|
||||
class VitaminItem(stat: Stats) : EVIncreaseItem(stat, 10) {
|
||||
class VitaminItem(override val stat: Stat, block: Block): ItemNameBlockItem(block, Properties()), EVIncreaseItem {
|
||||
override val evIncreaseAmount: Int = 10
|
||||
override val sound: SoundEvent = CobblemonSounds.MEDICINE_PILLS_USE
|
||||
|
||||
override fun use(world: Level, user: Player, hand: InteractionHand): InteractionResultHolder<ItemStack> {
|
||||
if (user is ServerPlayer) {
|
||||
return use(user, user.getItemInHand(hand))
|
||||
}
|
||||
return InteractionResultHolder.success(user.getItemInHand(hand))
|
||||
}
|
||||
}
|
||||
+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.net.messages.client.molangui
|
||||
|
||||
import com.cobblemon.mod.common.api.net.NetworkPacket
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf
|
||||
import java.util.UUID
|
||||
import com.cobblemon.mod.common.api.molangui.MolangUiPacketLimits
|
||||
|
||||
class MolangUiClosePacket(
|
||||
val sessionId: UUID
|
||||
) : NetworkPacket<MolangUiClosePacket> {
|
||||
|
||||
override val id = ID
|
||||
|
||||
override fun encode(buffer: RegistryFriendlyByteBuf) {
|
||||
buffer.writeUUID(sessionId)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val ID = cobblemonResource("molang_ui_close")
|
||||
|
||||
fun decode(buffer: RegistryFriendlyByteBuf): MolangUiClosePacket {
|
||||
require(buffer.readableBytes() <= MolangUiPacketLimits.MAX_PACKET_BYTES)
|
||||
return MolangUiClosePacket(buffer.readUUID())
|
||||
}
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.net.messages.client.molangui
|
||||
|
||||
import com.cobblemon.mod.common.api.molangui.MolangUiPacketLimits
|
||||
import com.cobblemon.mod.common.api.net.NetworkPacket
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
|
||||
class MolangUiCompatibilityRequestPacket(val contracts: Map<ResourceLocation, String>) : NetworkPacket<MolangUiCompatibilityRequestPacket> {
|
||||
override val id = ID
|
||||
override fun encode(buffer: RegistryFriendlyByteBuf) {
|
||||
require(contracts.size <= MolangUiPacketLimits.MAX_CONTRACTS)
|
||||
buffer.writeVarInt(contracts.size)
|
||||
contracts.forEach { (id, version) ->
|
||||
buffer.writeUtf(id.toString(), MolangUiPacketLimits.MAX_IDENTIFIER_LENGTH)
|
||||
buffer.writeUtf(version, MolangUiPacketLimits.MAX_IDENTIFIER_LENGTH)
|
||||
}
|
||||
}
|
||||
companion object {
|
||||
val ID = cobblemonResource("molang_ui_compatibility_request")
|
||||
fun decode(buffer: RegistryFriendlyByteBuf): MolangUiCompatibilityRequestPacket {
|
||||
require(buffer.readableBytes() <= MolangUiPacketLimits.MAX_PACKET_BYTES)
|
||||
val count = buffer.readVarInt()
|
||||
require(count in 0..MolangUiPacketLimits.MAX_CONTRACTS)
|
||||
val contracts = linkedMapOf<ResourceLocation, String>()
|
||||
repeat(count) {
|
||||
val id = ResourceLocation.tryParse(buffer.readUtf(MolangUiPacketLimits.MAX_IDENTIFIER_LENGTH))
|
||||
?: throw IllegalArgumentException("invalid Molang UI id")
|
||||
contracts[id] = buffer.readUtf(MolangUiPacketLimits.MAX_IDENTIFIER_LENGTH)
|
||||
}
|
||||
return MolangUiCompatibilityRequestPacket(contracts)
|
||||
}
|
||||
}
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.net.messages.client.molangui
|
||||
|
||||
import com.cobblemon.mod.common.api.molangui.GuiMode
|
||||
import com.cobblemon.mod.common.api.molangui.MolangUiPacketLimits
|
||||
import com.cobblemon.mod.common.api.molangui.MolangUiParameters
|
||||
import com.cobblemon.mod.common.api.molangui.container.SlotLayout
|
||||
import com.cobblemon.mod.common.api.net.NetworkPacket
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import java.util.UUID
|
||||
|
||||
class MolangUiOpenPacket(
|
||||
val sessionId: UUID, val guiId: ResourceLocation, val contractVersion: String, val mode: GuiMode,
|
||||
val args: Map<String, String>, val exclusive: Boolean,
|
||||
val hasContainer: Boolean = false, val slotLayouts: Map<String, SlotLayout> = emptyMap()
|
||||
) : NetworkPacket<MolangUiOpenPacket> {
|
||||
init {
|
||||
MolangUiPacketLimits.requireIdentifier(guiId.toString(), "UI id")
|
||||
MolangUiPacketLimits.requireIdentifier(contractVersion, "contract version")
|
||||
require(args.size <= MolangUiPacketLimits.MAX_ACTION_FIELDS) { "too many UI parameters" }
|
||||
args.forEach { (name, value) ->
|
||||
MolangUiParameters.requireName(name)
|
||||
MolangUiPacketLimits.requireValue(value, "parameter '$name'")
|
||||
}
|
||||
require(hasContainer == slotLayouts.isNotEmpty()) { "container flag and slot layouts disagree" }
|
||||
require(!hasContainer || mode == GuiMode.SCREEN) { "overlay views cannot have containers" }
|
||||
SlotLayout.validateCollections(slotLayouts)
|
||||
}
|
||||
|
||||
override val id = ID
|
||||
override fun encode(buffer: RegistryFriendlyByteBuf) {
|
||||
val startIndex = buffer.writerIndex()
|
||||
buffer.writeUUID(sessionId)
|
||||
buffer.writeUtf(guiId.toString(), MolangUiPacketLimits.MAX_IDENTIFIER_LENGTH)
|
||||
buffer.writeUtf(contractVersion, MolangUiPacketLimits.MAX_IDENTIFIER_LENGTH)
|
||||
buffer.writeEnum(mode)
|
||||
buffer.writeVarInt(args.size)
|
||||
args.forEach { (key, value) -> buffer.writeUtf(key, MolangUiPacketLimits.MAX_IDENTIFIER_LENGTH).writeUtf(value, MolangUiPacketLimits.MAX_VALUE_LENGTH) }
|
||||
buffer.writeBoolean(exclusive).writeBoolean(hasContainer)
|
||||
if (hasContainer) {
|
||||
buffer.writeVarInt(slotLayouts.size)
|
||||
slotLayouts.forEach { (name, layout) ->
|
||||
buffer.writeUtf(name, MolangUiPacketLimits.MAX_IDENTIFIER_LENGTH)
|
||||
listOf(layout.offsetX, layout.offsetY, layout.columns, layout.rows, layout.slotWidth, layout.slotHeight, layout.spacingX, layout.spacingY)
|
||||
.forEach(buffer::writeVarInt)
|
||||
}
|
||||
}
|
||||
require(buffer.writerIndex() - startIndex <= MolangUiPacketLimits.MAX_PACKET_BYTES) { "UI open packet is too large" }
|
||||
}
|
||||
companion object {
|
||||
val ID = cobblemonResource("molang_ui_open")
|
||||
fun decode(buffer: RegistryFriendlyByteBuf): MolangUiOpenPacket {
|
||||
require(buffer.readableBytes() <= MolangUiPacketLimits.MAX_PACKET_BYTES)
|
||||
val sessionId = buffer.readUUID()
|
||||
val guiId = ResourceLocation.tryParse(buffer.readUtf(MolangUiPacketLimits.MAX_IDENTIFIER_LENGTH)) ?: error("invalid UI id")
|
||||
val version = buffer.readUtf(MolangUiPacketLimits.MAX_IDENTIFIER_LENGTH)
|
||||
val mode = buffer.readEnum(GuiMode::class.java)
|
||||
val argCount = buffer.readVarInt()
|
||||
require(argCount in 0..MolangUiPacketLimits.MAX_ACTION_FIELDS)
|
||||
val args = linkedMapOf<String, String>()
|
||||
repeat(argCount) {
|
||||
val name = buffer.readUtf(MolangUiPacketLimits.MAX_IDENTIFIER_LENGTH)
|
||||
require(name !in args) { "duplicate UI parameter '$name'" }
|
||||
args[name] = buffer.readUtf(MolangUiPacketLimits.MAX_VALUE_LENGTH)
|
||||
}
|
||||
val exclusive = buffer.readBoolean()
|
||||
val hasContainer = buffer.readBoolean()
|
||||
val layouts = linkedMapOf<String, SlotLayout>()
|
||||
if (hasContainer) {
|
||||
val count = buffer.readVarInt()
|
||||
require(count in 0..MolangUiPacketLimits.MAX_CONTRACTS)
|
||||
repeat(count) {
|
||||
val name = buffer.readUtf(MolangUiPacketLimits.MAX_IDENTIFIER_LENGTH)
|
||||
require(name !in layouts) { "duplicate container collection '$name'" }
|
||||
layouts[name] = SlotLayout(buffer.readVarInt(), buffer.readVarInt(), buffer.readVarInt(), buffer.readVarInt(), buffer.readVarInt(), buffer.readVarInt(), buffer.readVarInt(), buffer.readVarInt())
|
||||
}
|
||||
}
|
||||
require(!buffer.isReadable) { "unexpected trailing UI open data" }
|
||||
return MolangUiOpenPacket(sessionId, guiId, version, mode, args, exclusive, hasContainer, layouts)
|
||||
}
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.net.messages.client.molangui
|
||||
|
||||
import com.cobblemon.mod.common.api.molangui.MolangUiPacketLimits
|
||||
import com.cobblemon.mod.common.api.net.NetworkPacket
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf
|
||||
import java.util.UUID
|
||||
|
||||
class MolangUiUpdatePacket(val sessionId: UUID, val args: Map<String, String>, val variables: Map<String, Double> = emptyMap()) : NetworkPacket<MolangUiUpdatePacket> {
|
||||
override val id = ID
|
||||
override fun encode(buffer: RegistryFriendlyByteBuf) {
|
||||
require(args.size <= MolangUiPacketLimits.MAX_ACTION_FIELDS && variables.size <= MolangUiPacketLimits.MAX_ACTION_FIELDS)
|
||||
buffer.writeUUID(sessionId).writeVarInt(args.size)
|
||||
args.forEach { (key, value) -> buffer.writeUtf(key, MolangUiPacketLimits.MAX_IDENTIFIER_LENGTH).writeUtf(value, MolangUiPacketLimits.MAX_VALUE_LENGTH) }
|
||||
buffer.writeVarInt(variables.size)
|
||||
variables.forEach { (key, value) -> require(value.isFinite()); buffer.writeUtf(key, MolangUiPacketLimits.MAX_IDENTIFIER_LENGTH).writeDouble(value) }
|
||||
}
|
||||
companion object {
|
||||
val ID = cobblemonResource("molang_ui_update")
|
||||
fun decode(buffer: RegistryFriendlyByteBuf): MolangUiUpdatePacket {
|
||||
require(buffer.readableBytes() <= MolangUiPacketLimits.MAX_PACKET_BYTES)
|
||||
val sessionId = buffer.readUUID()
|
||||
val argCount = buffer.readVarInt(); require(argCount in 0..MolangUiPacketLimits.MAX_ACTION_FIELDS)
|
||||
val args = linkedMapOf<String, String>()
|
||||
repeat(argCount) { args[buffer.readUtf(MolangUiPacketLimits.MAX_IDENTIFIER_LENGTH)] = buffer.readUtf(MolangUiPacketLimits.MAX_VALUE_LENGTH) }
|
||||
val variableCount = buffer.readVarInt(); require(variableCount in 0..MolangUiPacketLimits.MAX_ACTION_FIELDS)
|
||||
val variables = linkedMapOf<String, Double>()
|
||||
repeat(variableCount) {
|
||||
val key = buffer.readUtf(MolangUiPacketLimits.MAX_IDENTIFIER_LENGTH)
|
||||
val value = buffer.readDouble(); require(value.isFinite()); variables[key] = value
|
||||
}
|
||||
return MolangUiUpdatePacket(sessionId, args, variables)
|
||||
}
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.net.messages.server.molangui
|
||||
|
||||
import com.cobblemon.mod.common.api.molangui.MolangUiPacketLimits
|
||||
import com.cobblemon.mod.common.api.net.NetworkPacket
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf
|
||||
import java.util.UUID
|
||||
|
||||
class MolangUiActionPacket(val sessionId: UUID, val action: String, val data: Map<String, String>) : NetworkPacket<MolangUiActionPacket> {
|
||||
override val id = ID
|
||||
override fun encode(buffer: RegistryFriendlyByteBuf) {
|
||||
require(data.size <= MolangUiPacketLimits.MAX_ACTION_FIELDS)
|
||||
buffer.writeUUID(sessionId)
|
||||
buffer.writeUtf(action, MolangUiPacketLimits.MAX_IDENTIFIER_LENGTH)
|
||||
buffer.writeVarInt(data.size)
|
||||
data.forEach { (key, value) ->
|
||||
buffer.writeUtf(key, MolangUiPacketLimits.MAX_IDENTIFIER_LENGTH)
|
||||
buffer.writeUtf(value, MolangUiPacketLimits.MAX_VALUE_LENGTH)
|
||||
}
|
||||
}
|
||||
companion object {
|
||||
val ID = cobblemonResource("molang_ui_action")
|
||||
fun decode(buffer: RegistryFriendlyByteBuf): MolangUiActionPacket {
|
||||
require(buffer.readableBytes() <= MolangUiPacketLimits.MAX_PACKET_BYTES)
|
||||
val sessionId = buffer.readUUID()
|
||||
val action = buffer.readUtf(MolangUiPacketLimits.MAX_IDENTIFIER_LENGTH)
|
||||
val count = buffer.readVarInt()
|
||||
require(count in 0..MolangUiPacketLimits.MAX_ACTION_FIELDS)
|
||||
val data = linkedMapOf<String, String>()
|
||||
repeat(count) { data[buffer.readUtf(MolangUiPacketLimits.MAX_IDENTIFIER_LENGTH)] = buffer.readUtf(MolangUiPacketLimits.MAX_VALUE_LENGTH) }
|
||||
return MolangUiActionPacket(sessionId, action, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
+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.net.messages.server.molangui
|
||||
|
||||
import com.cobblemon.mod.common.api.net.NetworkPacket
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf
|
||||
import java.util.UUID
|
||||
import com.cobblemon.mod.common.api.molangui.MolangUiPacketLimits
|
||||
|
||||
class MolangUiClosedPacket(
|
||||
val sessionId: UUID
|
||||
) : NetworkPacket<MolangUiClosedPacket> {
|
||||
|
||||
override val id = ID
|
||||
|
||||
override fun encode(buffer: RegistryFriendlyByteBuf) {
|
||||
buffer.writeUUID(sessionId)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val ID = cobblemonResource("molang_ui_closed")
|
||||
|
||||
fun decode(buffer: RegistryFriendlyByteBuf): MolangUiClosedPacket {
|
||||
require(buffer.readableBytes() <= MolangUiPacketLimits.MAX_PACKET_BYTES)
|
||||
return MolangUiClosedPacket(buffer.readUUID())
|
||||
}
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.net.messages.server.molangui
|
||||
|
||||
import com.cobblemon.mod.common.api.molangui.MolangUiPacketLimits
|
||||
import com.cobblemon.mod.common.api.net.NetworkPacket
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
|
||||
class MolangUiCompatibilityResponsePacket(val views: Map<ResourceLocation, String>) : NetworkPacket<MolangUiCompatibilityResponsePacket> {
|
||||
override val id = ID
|
||||
override fun encode(buffer: RegistryFriendlyByteBuf) {
|
||||
require(views.size <= MolangUiPacketLimits.MAX_CONTRACTS)
|
||||
buffer.writeVarInt(views.size)
|
||||
views.forEach { (id, version) ->
|
||||
buffer.writeUtf(id.toString(), MolangUiPacketLimits.MAX_IDENTIFIER_LENGTH)
|
||||
buffer.writeUtf(version, MolangUiPacketLimits.MAX_IDENTIFIER_LENGTH)
|
||||
}
|
||||
}
|
||||
companion object {
|
||||
val ID = cobblemonResource("molang_ui_compatibility_response")
|
||||
fun decode(buffer: RegistryFriendlyByteBuf): MolangUiCompatibilityResponsePacket {
|
||||
require(buffer.readableBytes() <= MolangUiPacketLimits.MAX_PACKET_BYTES)
|
||||
val count = buffer.readVarInt()
|
||||
require(count in 0..MolangUiPacketLimits.MAX_CONTRACTS)
|
||||
val views = linkedMapOf<ResourceLocation, String>()
|
||||
repeat(count) {
|
||||
val id = ResourceLocation.tryParse(buffer.readUtf(MolangUiPacketLimits.MAX_IDENTIFIER_LENGTH))
|
||||
?: throw IllegalArgumentException("invalid Molang UI id")
|
||||
views[id] = buffer.readUtf(MolangUiPacketLimits.MAX_IDENTIFIER_LENGTH)
|
||||
}
|
||||
return MolangUiCompatibilityResponsePacket(views)
|
||||
}
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.net.serverhandling.molangui
|
||||
|
||||
import com.cobblemon.mod.common.api.molangui.MolangUiService
|
||||
import com.cobblemon.mod.common.api.net.ServerNetworkPacketHandler
|
||||
import com.cobblemon.mod.common.net.messages.server.molangui.MolangUiActionPacket
|
||||
import net.minecraft.server.MinecraftServer
|
||||
import net.minecraft.server.level.ServerPlayer
|
||||
|
||||
object MolangUiActionHandler : ServerNetworkPacketHandler<MolangUiActionPacket> {
|
||||
override fun handle(packet: MolangUiActionPacket, server: MinecraftServer, player: ServerPlayer) {
|
||||
MolangUiService.handleAction(packet.sessionId, player, packet.action, packet.data)
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.net.serverhandling.molangui
|
||||
|
||||
import com.cobblemon.mod.common.api.molangui.MolangUiService
|
||||
import com.cobblemon.mod.common.api.net.ServerNetworkPacketHandler
|
||||
import com.cobblemon.mod.common.net.messages.server.molangui.MolangUiClosedPacket
|
||||
import net.minecraft.server.MinecraftServer
|
||||
import net.minecraft.server.level.ServerPlayer
|
||||
|
||||
object MolangUiClosedHandler : ServerNetworkPacketHandler<MolangUiClosedPacket> {
|
||||
override fun handle(packet: MolangUiClosedPacket, server: MinecraftServer, player: ServerPlayer) {
|
||||
MolangUiService.handleClosed(packet.sessionId, player)
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.net.serverhandling.molangui
|
||||
|
||||
import com.cobblemon.mod.common.api.molangui.MolangUiService
|
||||
import com.cobblemon.mod.common.api.net.ServerNetworkPacketHandler
|
||||
import com.cobblemon.mod.common.net.messages.server.molangui.MolangUiCompatibilityResponsePacket
|
||||
import net.minecraft.server.MinecraftServer
|
||||
import net.minecraft.server.level.ServerPlayer
|
||||
|
||||
object MolangUiCompatibilityResponseHandler : ServerNetworkPacketHandler<MolangUiCompatibilityResponsePacket> {
|
||||
override fun handle(packet: MolangUiCompatibilityResponsePacket, server: MinecraftServer, player: ServerPlayer) {
|
||||
MolangUiService.applyCompatibility(player, packet.views)
|
||||
}
|
||||
}
|
||||
@@ -1,63 +1,63 @@
|
||||
{
|
||||
"variants": {
|
||||
"facing=north,amount=1": {
|
||||
"model": "cobblemon:block/antidote_0"
|
||||
"model": "cobblemon:block/antidote_1"
|
||||
},
|
||||
"facing=east,amount=1": {
|
||||
"model": "cobblemon:block/antidote_0",
|
||||
"model": "cobblemon:block/antidote_1",
|
||||
"y": 90
|
||||
},
|
||||
"facing=south,amount=1": {
|
||||
"model": "cobblemon:block/antidote_0",
|
||||
"model": "cobblemon:block/antidote_1",
|
||||
"y": 180
|
||||
},
|
||||
"facing=west,amount=1": {
|
||||
"model": "cobblemon:block/antidote_0",
|
||||
"model": "cobblemon:block/antidote_1",
|
||||
"y": 270
|
||||
},
|
||||
"facing=north,amount=2": {
|
||||
"model": "cobblemon:block/antidote_1"
|
||||
"model": "cobblemon:block/antidote_2"
|
||||
},
|
||||
"facing=east,amount=2": {
|
||||
"model": "cobblemon:block/antidote_1",
|
||||
"model": "cobblemon:block/antidote_2",
|
||||
"y": 90
|
||||
},
|
||||
"facing=south,amount=2": {
|
||||
"model": "cobblemon:block/antidote_1",
|
||||
"model": "cobblemon:block/antidote_2",
|
||||
"y": 180
|
||||
},
|
||||
"facing=west,amount=2": {
|
||||
"model": "cobblemon:block/antidote_1",
|
||||
"model": "cobblemon:block/antidote_2",
|
||||
"y": 270
|
||||
},
|
||||
"facing=north,amount=3": {
|
||||
"model": "cobblemon:block/antidote_2"
|
||||
"model": "cobblemon:block/antidote_3"
|
||||
},
|
||||
"facing=east,amount=3": {
|
||||
"model": "cobblemon:block/antidote_2",
|
||||
"model": "cobblemon:block/antidote_3",
|
||||
"y": 90
|
||||
},
|
||||
"facing=south,amount=3": {
|
||||
"model": "cobblemon:block/antidote_2",
|
||||
"model": "cobblemon:block/antidote_3",
|
||||
"y": 180
|
||||
},
|
||||
"facing=west,amount=3": {
|
||||
"model": "cobblemon:block/antidote_2",
|
||||
"model": "cobblemon:block/antidote_3",
|
||||
"y": 270
|
||||
},
|
||||
"facing=north,amount=4": {
|
||||
"model": "cobblemon:block/antidote_3"
|
||||
"model": "cobblemon:block/antidote_4"
|
||||
},
|
||||
"facing=east,amount=4": {
|
||||
"model": "cobblemon:block/antidote_3",
|
||||
"model": "cobblemon:block/antidote_4",
|
||||
"y": 90
|
||||
},
|
||||
"facing=south,amount=4": {
|
||||
"model": "cobblemon:block/antidote_3",
|
||||
"model": "cobblemon:block/antidote_4",
|
||||
"y": 180
|
||||
},
|
||||
"facing=west,amount=4": {
|
||||
"model": "cobblemon:block/antidote_3",
|
||||
"model": "cobblemon:block/antidote_4",
|
||||
"y": 270
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,63 +1,63 @@
|
||||
{
|
||||
"variants": {
|
||||
"facing=north,amount=1": {
|
||||
"model": "cobblemon:block/awakening_0"
|
||||
"model": "cobblemon:block/awakening_1"
|
||||
},
|
||||
"facing=east,amount=1": {
|
||||
"model": "cobblemon:block/awakening_0",
|
||||
"model": "cobblemon:block/awakening_1",
|
||||
"y": 90
|
||||
},
|
||||
"facing=south,amount=1": {
|
||||
"model": "cobblemon:block/awakening_0",
|
||||
"model": "cobblemon:block/awakening_1",
|
||||
"y": 180
|
||||
},
|
||||
"facing=west,amount=1": {
|
||||
"model": "cobblemon:block/awakening_0",
|
||||
"model": "cobblemon:block/awakening_1",
|
||||
"y": 270
|
||||
},
|
||||
"facing=north,amount=2": {
|
||||
"model": "cobblemon:block/awakening_1"
|
||||
"model": "cobblemon:block/awakening_2"
|
||||
},
|
||||
"facing=east,amount=2": {
|
||||
"model": "cobblemon:block/awakening_1",
|
||||
"model": "cobblemon:block/awakening_2",
|
||||
"y": 90
|
||||
},
|
||||
"facing=south,amount=2": {
|
||||
"model": "cobblemon:block/awakening_1",
|
||||
"model": "cobblemon:block/awakening_2",
|
||||
"y": 180
|
||||
},
|
||||
"facing=west,amount=2": {
|
||||
"model": "cobblemon:block/awakening_1",
|
||||
"model": "cobblemon:block/awakening_2",
|
||||
"y": 270
|
||||
},
|
||||
"facing=north,amount=3": {
|
||||
"model": "cobblemon:block/awakening_2"
|
||||
"model": "cobblemon:block/awakening_3"
|
||||
},
|
||||
"facing=east,amount=3": {
|
||||
"model": "cobblemon:block/awakening_2",
|
||||
"model": "cobblemon:block/awakening_3",
|
||||
"y": 90
|
||||
},
|
||||
"facing=south,amount=3": {
|
||||
"model": "cobblemon:block/awakening_2",
|
||||
"model": "cobblemon:block/awakening_3",
|
||||
"y": 180
|
||||
},
|
||||
"facing=west,amount=3": {
|
||||
"model": "cobblemon:block/awakening_2",
|
||||
"model": "cobblemon:block/awakening_3",
|
||||
"y": 270
|
||||
},
|
||||
"facing=north,amount=4": {
|
||||
"model": "cobblemon:block/awakening_3"
|
||||
"model": "cobblemon:block/awakening_4"
|
||||
},
|
||||
"facing=east,amount=4": {
|
||||
"model": "cobblemon:block/awakening_3",
|
||||
"model": "cobblemon:block/awakening_4",
|
||||
"y": 90
|
||||
},
|
||||
"facing=south,amount=4": {
|
||||
"model": "cobblemon:block/awakening_3",
|
||||
"model": "cobblemon:block/awakening_4",
|
||||
"y": 180
|
||||
},
|
||||
"facing=west,amount=4": {
|
||||
"model": "cobblemon:block/awakening_3",
|
||||
"model": "cobblemon:block/awakening_4",
|
||||
"y": 270
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"variants": {
|
||||
"facing=south": {
|
||||
"model": "cobblemon:block/bright_powder",
|
||||
"y": 180
|
||||
},
|
||||
"facing=west": {
|
||||
"model": "cobblemon:block/bright_powder",
|
||||
"y": 270
|
||||
},
|
||||
"facing=north": {
|
||||
"model": "cobblemon:block/bright_powder"
|
||||
},
|
||||
"facing=east": {
|
||||
"model": "cobblemon:block/bright_powder",
|
||||
"y": 90
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user