Compare commits

..
Author SHA1 Message Date
Aram 9cdf91c97f Added TODO: 2026-01-04 20:38:47 +01:00
Aram 3f5b659616 Rename 2026-01-04 20:37:19 +01:00
hammy275 ee20f012e6 Interact Setup and Ball or Entity Depending on if Hovered 2025-12-16 20:55:49 -05:00
hammy275 12dcc98cf9 Select Pokemon Hovered Over 2025-12-16 20:16:14 -05:00
Aram 8c92defc90 Fixed Applying bait in VR 2025-12-15 13:21:10 +01:00
Aram 95509df022 Found the issue 2025-12-12 01:31:29 +01:00
Aram bb20890b26 Created an Locomotion Proximity Check, added TODO i need to fix once headset charges 2025-12-11 23:21:21 +01:00
hammy275 50d60674bb Pokemon Attached to Arm 2025-12-10 20:58:30 -05:00
hammy275 ff994f69f2 Render a Pokemon at the Crosshair 2025-12-10 19:26:45 -05:00
hammy275 73448d4737 Not Working Skeleton Code 2025-12-08 19:29:36 -05:00
Aram 42099d3ae5 Cleanup 2025-12-08 14:08:38 +01:00
Aram bc23ab8eb4 Merge branch 'main' into VR-support 2025-12-08 10:33:53 +01:00
Aram 6c860d8e93 Reeling in while swinging backwards also works!!!! 2025-12-07 23:06:59 +01:00
Aram de6b713adf Casting your pokerod using arm swings work in VR 2025-12-07 21:26:48 +01:00
Aram 6f5c2575f3 Refactor to Client 2025-12-07 00:41:41 +01:00
Aram b23afd17c2 Small Refactoring 2025-12-07 00:34:22 +01:00
Aram 74541c1bcb Updated Vivecraft 1 final time with the new Listener System 2025-12-07 00:00:53 +01:00
Aram 56c3ff55ca 90% + ACCURACY REACHED IN VR 2025-12-05 23:45:22 +01:00
Aram c5b82d1ad4 Current Pokeball VR Throwing - 85% Accuracy, Aiming for 90%+ 2025-12-05 23:37:33 +01:00
Aram 1237b51059 Merge branch 'main' into VR-support 2025-12-05 23:07:42 +01:00
Aram 9529fe66ac Updated Vivecraft internally 2025-12-05 21:14:35 +01:00
Aram 7058ac21cb Improved Pokeball throwing in VR!!!!!!! 2025-12-05 20:34:14 +01:00
Aram b5d0cddc82 Throwing a Pokeball works!!!! 2025-12-05 18:47:31 +01:00
Aram 2f918220e9 Setup the first prototype of Pokeball Throwing in VR 2025-12-05 01:15:46 +01:00
1704 changed files with 11595 additions and 84314 deletions
+12 -47
View File
@@ -1,60 +1,25 @@
# Changelog
## [1.7.4 (MONTH Xth, 2026)](#1-7-4)
## [1.7.2 (MONTH Xth, YEAR)](#1-7-2)
### Additions
- Added config options to award experience to fainted Pokémon for opponents they helped defeat (`awardExperienceToFaintedPokemon`) and to still award experience for defeated enemies even after battle loss/forfeit (`awardExperienceOnBattleLoss`)
- Party Overlay Exp Gained Animation will now display the amount of EXP the Pokémon gained
- Added a toast to notify players of how they can scroll through their party. Did you know you can hold R and scroll instead of using arrow keys??!
- Added new layer property that makes a layer scroll in a direction. "scrolling": {"speedU": 0.1, "speedV": 0.1}
- Added missing crops to Botany Pots integration
### New rideable Pokémon
### Pokémon Added
### Model updates for the following Pokémon
### Animation updates for the following Pokémon
### Changes
- Restructured Botany Pots integration folder structure
- Decrease default pastured mon wander radius to 32.
- Factor in the weight of pokemon when calculating pushing forces between hitboxes.
- Remade the Starter Selection Screen with new assets
- Refactored berry trees to be less impactful on TPS (more performant)
### Fixes
- Fixes https://gitlab.com/cable-mc/cobblemon/-/issues/1943
- Fix occasional crash on retrieving revived fossils
- Fixed a compatibility issue with World Game Rules which caused some log spam in specific cases.
- Fixed improper validation of Pokémon when performing a trade.
- Fixed issue where particle like beam can disappear when you don't see the beginning of the beam or you look away
- Fixes https://gitlab.com/cable-mc/cobblemon/-/issues/1943
- Fix occasional crash on retrieving revived fossils
- Fix brushing not working on Neoforge
- [Various Submarine Ride Fixes](https://gitlab.com/cable-mc/cobblemon/-/merge_requests/2157)
- Fix Pastured mons despawning and running away.
- Fix Apricorn trees crashing when other mods bonemeal it
- [Various UFO Ride Improvements](https://gitlab.com/cable-mc/cobblemon/-/merge_requests/2156)
- Fixed NeoForge brewing stand unexpectedly crashing users when shift clicking items within it's menu
- Fixed NPCs switching pokemon very often during battles
- Fixed Campfire pot causing crashes under certain circumstances
- Fixed Type Gems not being consumed upon use
- Fixed Ancient Pokeball's to their respective modifiers
- Fixed a Server warning when recalling Pokemon
- Fixed sliding particles incorrectly handling block collision
- Fixed Full Heal not curing status conditions in battle
- Fixed orientation not getting reset after crashing while flying and getting back on the Pokémon.
- Fixed Furfrou not being trimmable on NeoForge.
- Fixed honey from Saccharine Leaves being harvestable at less than the max age.
- Items given from interacting with Saccharine Leaves are placed in the active hotbar slot if possible.
- Fix all Pokémon being saved to chunks and never despawning.
- Fixed PokeSnacks crashing if there was no available spawn.
- Fixed Pokémon spawn influences not being specific to regional forms
- Fixed crashing when viewing another player in spectator mode
### Developer
- Added `PokeSnackSpawnerFactory` which allows influence registration on PokeSnack encounters
- Added `FishingSpawnerFactory` which allows influence registration on fishing encounters
- Added `BattleFleeAttemptEvent` fired from `PokemonBattle.checkFleeAttempt`, allowing developers to intercept and control flee attempts
### Molang & Datapacks
- q.item.is_enchanted()
- q.item.has_enchantment(minecraft:sharpness, 3)
- Added `has_chosen_starter` & `get_starter_uuid` as available Molang functions
- Added `marks`, `has_mark`, & `remove_marks` as available Molang functions for pokemon
### Particles
### Localization
- Updated translations for:
- Updated translations for:
-510
View File
@@ -1,510 +0,0 @@
{
"format_version": "1.21.11",
"credit": "Made with Blockbench",
"texture_size": [128, 128],
"textures": {
"2": "block/texture"
},
"elements": [
{
"from": [9.999, 10.999, -0.001],
"to": [15.001, 12.001, 0.001],
"rotation": {"angle": 0, "axis": "y", "origin": [1, -1, 1]},
"faces": {
"north": {"uv": [0.25, 1.125, 0.875, 1.25], "texture": "#2"},
"east": {"uv": [0.25, 1.125, 0.25, 1.25], "texture": "#2"},
"south": {"uv": [0.875, 1.125, 1.5, 1.25], "texture": "#2"},
"west": {"uv": [0.875, 1.125, 0.875, 1.25], "texture": "#2"},
"up": {"uv": [0.875, 1.125, 0.25, 1.125], "texture": "#2"},
"down": {"uv": [1.5, 1.125, 0.875, 1.125], "texture": "#2"}
}
},
{
"from": [-0.0001, 8.9999, 2.9999],
"to": [2.0001, 13.0001, 13.0001],
"rotation": {"angle": 0, "axis": "y", "origin": [1, -1, 2]},
"faces": {
"north": {"uv": [4.25, 6, 4.5, 6.5], "texture": "#2"},
"east": {"uv": [3, 6, 4.25, 6.5], "texture": "#2"},
"south": {"uv": [5.75, 6, 6, 6.5], "texture": "#2"},
"west": {"uv": [4.5, 6, 5.75, 6.5], "texture": "#2"},
"up": {"uv": [4.5, 6, 4.25, 4.75], "texture": "#2"},
"down": {"uv": [4.75, 4.75, 4.5, 6], "texture": "#2"}
}
},
{
"from": [-0.0001, 8.9999, -0.0001],
"to": [16.0001, 13.0001, 3.0001],
"rotation": {"angle": 0, "axis": "y", "origin": [1, -1, 1]},
"faces": {
"north": {"uv": [0.375, 3.375, 2.375, 3.875], "texture": "#2"},
"east": {"uv": [0, 3.375, 0.375, 3.875], "texture": "#2"},
"south": {"uv": [2.75, 3.375, 4.75, 3.875], "texture": "#2"},
"west": {"uv": [2.375, 3.375, 2.75, 3.875], "texture": "#2"},
"up": {"uv": [2.375, 3.375, 0.375, 3], "texture": "#2"},
"down": {"uv": [4.375, 3, 2.375, 3.375], "texture": "#2"}
}
},
{
"from": [2.995, 10.995, -0.005],
"to": [15.005, 13.005, 2.005],
"rotation": {"angle": 0, "axis": "y", "origin": [1, -1, 1]},
"faces": {
"north": {"uv": [0.25, 6.75, 1.75, 7], "texture": "#2"},
"east": {"uv": [0, 6.75, 0.25, 7], "texture": "#2"},
"south": {"uv": [2, 6.75, 3.5, 7], "texture": "#2"},
"west": {"uv": [1.75, 6.75, 2, 7], "texture": "#2"},
"up": {"uv": [1.75, 6.75, 0.25, 6.5], "texture": "#2"},
"down": {"uv": [3.25, 6.5, 1.75, 6.75], "texture": "#2"}
}
},
{
"from": [9.999, 10.999, -0.001],
"to": [15.001, 12.001, 0.001],
"rotation": {"angle": 0, "axis": "y", "origin": [1, -1, 1]},
"faces": {
"north": {"uv": [0.25, 1.375, 0.875, 1.5], "texture": "#2"},
"east": {"uv": [0.25, 1.375, 0.25, 1.5], "texture": "#2"},
"south": {"uv": [0.875, 1.375, 1.5, 1.5], "texture": "#2"},
"west": {"uv": [0.875, 1.375, 0.875, 1.5], "texture": "#2"},
"up": {"uv": [0.875, 1.375, 0.25, 1.375], "texture": "#2"},
"down": {"uv": [1.5, 1.375, 0.875, 1.375], "texture": "#2"}
}
},
{
"from": [13.9999, 8.9999, 2.9999],
"to": [16.0001, 13.0001, 13.0001],
"rotation": {"angle": 0, "axis": "y", "origin": [1, -1, 2]},
"faces": {
"north": {"uv": [1.25, 6, 1.5, 6.5], "texture": "#2"},
"east": {"uv": [0, 6, 1.25, 6.5], "texture": "#2"},
"south": {"uv": [2.75, 6, 3, 6.5], "texture": "#2"},
"west": {"uv": [1.5, 6, 2.75, 6.5], "texture": "#2"},
"up": {"uv": [1.5, 6, 1.25, 4.75], "texture": "#2"},
"down": {"uv": [1.75, 4.75, 1.5, 6], "texture": "#2"}
}
},
{
"from": [-0.0001, 8.9999, 12.9999],
"to": [16.0001, 13.0001, 16.0001],
"rotation": {"angle": 0, "axis": "y", "origin": [1, -1, 1]},
"faces": {
"north": {"uv": [0.375, 4.25, 2.375, 4.75], "texture": "#2"},
"east": {"uv": [0, 4.25, 0.375, 4.75], "texture": "#2"},
"south": {"uv": [2.75, 4.25, 4.75, 4.75], "texture": "#2"},
"west": {"uv": [2.375, 4.25, 2.75, 4.75], "texture": "#2"},
"up": {"uv": [2.375, 4.25, 0.375, 3.875], "texture": "#2"},
"down": {"uv": [4.375, 3.875, 2.375, 4.25], "texture": "#2"}
}
},
{
"from": [1, 0, 1],
"to": [15, 10, 15],
"rotation": {"angle": 0, "axis": "y", "origin": [1, 0, 1]},
"faces": {
"north": {"uv": [1.75, 1.75, 3.5, 3], "texture": "#2"},
"east": {"uv": [0, 1.75, 1.75, 3], "texture": "#2"},
"south": {"uv": [5.25, 1.75, 7, 3], "texture": "#2"},
"west": {"uv": [3.5, 1.75, 5.25, 3], "texture": "#2"},
"up": {"uv": [3.5, 1.75, 1.75, 0], "texture": "#2"},
"down": {"uv": [5.25, 0, 3.5, 1.75], "texture": "#2"}
}
},
{
"from": [3.999, 1.999, 0.999],
"to": [12.001, 10.001, 1.001],
"rotation": {"angle": 0, "axis": "y", "origin": [1, 0, 1]},
"faces": {
"north": {"uv": [0, 7.375, 1, 8.375], "texture": "#2"},
"east": {"uv": [0, 7.375, 0, 8.375], "texture": "#2"},
"south": {"uv": [1, 7.375, 2, 8.375], "texture": "#2"},
"west": {"uv": [1, 7.375, 1, 8.375], "texture": "#2"},
"up": {"uv": [1, 7.375, 0, 7.375], "texture": "#2"},
"down": {"uv": [2, 7.375, 1, 7.375], "texture": "#2"}
}
},
{
"from": [2, 11, 5],
"to": [14, 12, 6],
"rotation": {"angle": 0, "axis": "y", "origin": [1, 11, 1]},
"faces": {
"north": {"uv": [5.125, 3.375, 6.625, 3.5], "texture": "#2"},
"east": {"uv": [4.75, 3.375, 4.875, 3.5], "texture": "#2"},
"south": {"uv": [7, 3.375, 8.5, 3.5], "texture": "#2"},
"west": {"uv": [6.875, 3.375, 7, 3.5], "texture": "#2"},
"up": {"uv": [6.625, 3.125, 5.125, 3], "texture": "#2"},
"down": {"uv": [8.125, 3, 6.625, 3.125], "texture": "#2"}
}
},
{
"from": [2, 11, 4],
"to": [14, 12, 5],
"rotation": {"angle": 0, "axis": "y", "origin": [1, 11, 1]},
"faces": {
"north": {"uv": [5.125, 3.375, 6.625, 3.5], "texture": "#2"},
"east": {"uv": [4.875, 3.375, 5, 3.5], "texture": "#2"},
"south": {"uv": [7, 3.375, 8.5, 3.5], "texture": "#2"},
"west": {"uv": [6.75, 3.375, 6.875, 3.5], "texture": "#2"},
"up": {"uv": [6.625, 3.25, 5.125, 3.125], "texture": "#2"},
"down": {"uv": [8.125, 3.125, 6.625, 3.25], "texture": "#2"}
}
},
{
"from": [2, 11, 3],
"to": [14, 12, 4],
"rotation": {"angle": 0, "axis": "y", "origin": [1, 11, 1]},
"faces": {
"north": {"uv": [5.125, 3.375, 6.625, 3.5], "texture": "#2"},
"east": {"uv": [5, 3.375, 5.125, 3.5], "texture": "#2"},
"south": {"uv": [7, 3.375, 8.5, 3.5], "texture": "#2"},
"west": {"uv": [6.625, 3.375, 6.75, 3.5], "texture": "#2"},
"up": {"uv": [6.625, 3.375, 5.125, 3.25], "texture": "#2"},
"down": {"uv": [8.125, 3.25, 6.625, 3.375], "texture": "#2"}
}
},
{
"from": [10, 11, 6],
"to": [14, 12, 8],
"rotation": {"angle": 0, "axis": "y", "origin": [1, 11, 1]},
"faces": {
"north": {"uv": [5, 4.25, 5.5, 4.375], "texture": "#2"},
"east": {"uv": [4.75, 4.25, 5, 4.375], "texture": "#2"},
"south": {"uv": [5.75, 4.25, 6.25, 4.375], "texture": "#2"},
"west": {"uv": [5.5, 4.25, 5.75, 4.375], "texture": "#2"},
"up": {"uv": [5.5, 4.25, 5, 4], "texture": "#2"},
"down": {"uv": [6, 4, 5.5, 4.25], "texture": "#2"}
}
},
{
"from": [2, 11, 6],
"to": [6, 12, 8],
"rotation": {"angle": 0, "axis": "y", "origin": [1, 11, 1]},
"faces": {
"north": {"uv": [5, 4.625, 5.5, 4.75], "texture": "#2"},
"east": {"uv": [4.75, 4.625, 5, 4.75], "texture": "#2"},
"south": {"uv": [5.75, 4.625, 6.25, 4.75], "texture": "#2"},
"west": {"uv": [5.5, 4.625, 5.75, 4.75], "texture": "#2"},
"up": {"uv": [5.5, 4.625, 5, 4.375], "texture": "#2"},
"down": {"uv": [6, 4.375, 5.5, 4.625], "texture": "#2"}
}
},
{
"from": [2, 11, 10],
"to": [14, 12, 11],
"rotation": {"angle": 0, "axis": "y", "origin": [1, 11, 6]},
"faces": {
"north": {"uv": [5.125, 3.875, 6.625, 4], "texture": "#2"},
"east": {"uv": [5, 3.875, 5.125, 4], "texture": "#2"},
"south": {"uv": [7, 3.875, 8.5, 4], "texture": "#2"},
"west": {"uv": [6.625, 3.875, 6.75, 4], "texture": "#2"},
"up": {"uv": [6.625, 3.875, 5.125, 3.75], "texture": "#2"},
"down": {"uv": [8.125, 3.75, 6.625, 3.875], "texture": "#2"}
}
},
{
"from": [2, 11, 11],
"to": [14, 12, 12],
"rotation": {"angle": 0, "axis": "y", "origin": [1, 11, 6]},
"faces": {
"north": {"uv": [5.125, 3.875, 6.625, 4], "texture": "#2"},
"east": {"uv": [4.875, 3.875, 5, 4], "texture": "#2"},
"south": {"uv": [7, 3.875, 8.5, 4], "texture": "#2"},
"west": {"uv": [6.75, 3.875, 6.875, 4], "texture": "#2"},
"up": {"uv": [6.625, 3.75, 5.125, 3.625], "texture": "#2"},
"down": {"uv": [8.125, 3.625, 6.625, 3.75], "texture": "#2"}
}
},
{
"from": [2, 11, 12],
"to": [14, 12, 13],
"rotation": {"angle": 0, "axis": "y", "origin": [1, 11, 6]},
"faces": {
"north": {"uv": [5.125, 3.875, 6.625, 4], "texture": "#2"},
"east": {"uv": [4.75, 3.875, 4.875, 4], "texture": "#2"},
"south": {"uv": [7, 3.875, 8.5, 4], "texture": "#2"},
"west": {"uv": [6.875, 3.875, 7, 4], "texture": "#2"},
"up": {"uv": [6.625, 3.625, 5.125, 3.5], "texture": "#2"},
"down": {"uv": [8.125, 3.5, 6.625, 3.625], "texture": "#2"}
}
},
{
"from": [10, 11, 8],
"to": [14, 12, 10],
"rotation": {"angle": 0, "axis": "y", "origin": [1, 11, 6]},
"faces": {
"north": {"uv": [6.25, 5, 6.75, 5.125], "texture": "#2"},
"east": {"uv": [6, 5, 6.25, 5.125], "texture": "#2"},
"south": {"uv": [7, 5, 7.5, 5.125], "texture": "#2"},
"west": {"uv": [6.75, 5, 7, 5.125], "texture": "#2"},
"up": {"uv": [6.75, 5, 6.25, 4.75], "texture": "#2"},
"down": {"uv": [7.25, 4.75, 6.75, 5], "texture": "#2"}
}
},
{
"from": [6, 11, 9],
"to": [10, 12, 10],
"rotation": {"angle": 0, "axis": "y", "origin": [1, 11, 6]},
"faces": {
"north": {"uv": [6.125, 5.625, 6.625, 5.75], "texture": "#2"},
"east": {"uv": [6, 5.625, 6.125, 5.75], "texture": "#2"},
"south": {"uv": [6.75, 5.625, 7.25, 5.75], "texture": "#2"},
"west": {"uv": [6.625, 5.625, 6.75, 5.75], "texture": "#2"},
"up": {"uv": [6.625, 5.625, 6.125, 5.5], "texture": "#2"},
"down": {"uv": [7.125, 5.5, 6.625, 5.625], "texture": "#2"}
}
},
{
"from": [2, 11, 8],
"to": [6, 12, 10],
"rotation": {"angle": 0, "axis": "y", "origin": [-3, 11, 6]},
"faces": {
"north": {"uv": [6.25, 5.375, 6.75, 5.5], "texture": "#2"},
"east": {"uv": [6, 5.375, 6.25, 5.5], "texture": "#2"},
"south": {"uv": [7, 5.375, 7.5, 5.5], "texture": "#2"},
"west": {"uv": [6.75, 5.375, 7, 5.5], "texture": "#2"},
"up": {"uv": [6.75, 5.375, 6.25, 5.125], "texture": "#2"},
"down": {"uv": [7.25, 5.125, 6.75, 5.375], "texture": "#2"}
}
},
{
"from": [9, 11, 7],
"to": [10, 12, 9],
"rotation": {"angle": 0, "axis": "y", "origin": [1, 11, 6]},
"faces": {
"north": {"uv": [6.5, 4.25, 6.625, 4.375], "texture": "#2"},
"east": {"uv": [6.25, 4.25, 6.5, 4.375], "texture": "#2"},
"south": {"uv": [6.875, 4.25, 7, 4.375], "texture": "#2"},
"west": {"uv": [6.625, 4.25, 6.875, 4.375], "texture": "#2"},
"up": {"uv": [6.625, 4.25, 6.5, 4], "texture": "#2"},
"down": {"uv": [6.75, 4, 6.625, 4.25], "texture": "#2"}
}
},
{
"from": [6, 11, 7],
"to": [7, 12, 9],
"rotation": {"angle": 0, "axis": "y", "origin": [1, 11, 6]},
"faces": {
"north": {"uv": [6.5, 4.625, 6.625, 4.75], "texture": "#2"},
"east": {"uv": [6.25, 4.625, 6.5, 4.75], "texture": "#2"},
"south": {"uv": [6.875, 4.625, 7, 4.75], "texture": "#2"},
"west": {"uv": [6.625, 4.625, 6.875, 4.75], "texture": "#2"},
"up": {"uv": [6.625, 4.625, 6.5, 4.375], "texture": "#2"},
"down": {"uv": [6.75, 4.375, 6.625, 4.625], "texture": "#2"}
}
},
{
"from": [7, 11, 7],
"to": [9, 12, 9],
"rotation": {"angle": 0, "axis": "y", "origin": [1, 11, 6]},
"faces": {
"north": {"uv": [6.25, 6.25, 6.5, 6.375], "texture": "#2"},
"east": {"uv": [6, 6.25, 6.25, 6.375], "texture": "#2"},
"south": {"uv": [6.75, 6.25, 7, 6.375], "texture": "#2"},
"west": {"uv": [6.5, 6.25, 6.75, 6.375], "texture": "#2"},
"up": {"uv": [6.5, 6.25, 6.25, 6], "texture": "#2"},
"down": {"uv": [6.75, 6, 6.5, 6.25], "texture": "#2"}
}
},
{
"from": [6, 11, 6],
"to": [10, 12, 7],
"rotation": {"angle": 0, "axis": "y", "origin": [1, 11, 6]},
"faces": {
"north": {"uv": [6.125, 5.875, 6.625, 6], "texture": "#2"},
"east": {"uv": [6, 5.875, 6.125, 6], "texture": "#2"},
"south": {"uv": [6.75, 5.875, 7.25, 6], "texture": "#2"},
"west": {"uv": [6.625, 5.875, 6.75, 6], "texture": "#2"},
"up": {"uv": [6.625, 5.875, 6.125, 5.75], "texture": "#2"},
"down": {"uv": [7.125, 5.75, 6.625, 5.875], "texture": "#2"}
}
},
{
"from": [0, 13, 11],
"to": [0, 15, 12],
"rotation": {"angle": 0, "axis": "y", "origin": [1, 0, -2]},
"faces": {
"north": {"uv": [6.375, 6.5, 6.375, 6.75], "texture": "#2"},
"east": {"uv": [6.25, 6.5, 6.375, 6.75], "texture": "#2"},
"south": {"uv": [6.5, 6.5, 6.5, 6.75], "texture": "#2"},
"west": {"uv": [6.375, 6.5, 6.5, 6.75], "texture": "#2"},
"up": {"uv": [6.375, 6.5, 6.375, 6.375], "texture": "#2"},
"down": {"uv": [6.375, 6.375, 6.375, 6.5], "texture": "#2"}
}
},
{
"from": [0, 13, 14],
"to": [0, 20, 15],
"rotation": {"angle": 0, "axis": "y", "origin": [1, 0, 1]},
"faces": {
"north": {"uv": [6.125, 6.5, 6.125, 7.375], "texture": "#2"},
"east": {"uv": [6, 6.5, 6.125, 7.375], "texture": "#2"},
"south": {"uv": [6.25, 6.5, 6.25, 7.375], "texture": "#2"},
"west": {"uv": [6.125, 6.5, 6.25, 7.375], "texture": "#2"},
"up": {"uv": [6.125, 6.5, 6.125, 6.375], "texture": "#2"},
"down": {"uv": [6.125, 6.375, 6.125, 6.5], "texture": "#2"}
}
},
{
"from": [0, 13, 14],
"to": [0, 20, 15],
"rotation": {"angle": 0, "axis": "y", "origin": [1, 0, 1]},
"faces": {
"north": {"uv": [6.625, 6.5, 6.625, 7.375], "texture": "#2"},
"east": {"uv": [6.5, 6.5, 6.625, 7.375], "texture": "#2"},
"south": {"uv": [6.75, 6.5, 6.75, 7.375], "texture": "#2"},
"west": {"uv": [6.625, 6.5, 6.75, 7.375], "texture": "#2"},
"up": {"uv": [6.625, 6.5, 6.625, 6.375], "texture": "#2"},
"down": {"uv": [6.625, 6.375, 6.625, 6.5], "texture": "#2"}
}
},
{
"from": [0, 13, 14],
"to": [0, 20, 15],
"rotation": {"angle": 0, "axis": "y", "origin": [1, 0, 1]},
"faces": {
"north": {"uv": [6.875, 6.5, 6.875, 7.375], "texture": "#2"},
"east": {"uv": [6.75, 6.5, 6.875, 7.375], "texture": "#2"},
"south": {"uv": [7, 6.5, 7, 7.375], "texture": "#2"},
"west": {"uv": [6.875, 6.5, 7, 7.375], "texture": "#2"},
"up": {"uv": [6.875, 6.5, 6.875, 6.375], "texture": "#2"},
"down": {"uv": [6.875, 6.375, 6.875, 6.5], "texture": "#2"}
}
}
],
"groups": [
{
"name": "bone",
"origin": [1, 0, 1],
"color": 0,
"children": [
{
"name": "bone9",
"origin": [12.5, 11.5, 0],
"color": 0,
"children": [0]
},
1,
2,
{
"name": "bone5",
"origin": [9, 12, 1],
"color": 0,
"children": [3]
},
{
"name": "bone4",
"origin": [12.5, 11.5, 0],
"color": 0,
"children": [4]
},
5,
6,
7,
{
"name": "bone8",
"origin": [8, 6, 1],
"color": 0,
"children": [8]
},
{
"name": "opening",
"origin": [8, 11.5, 6],
"color": 0,
"children": [
{
"name": "bone3",
"origin": [8, 11.5, 3],
"color": 0,
"children": [
{
"name": "bone10",
"origin": [8, 11.5, 5.5],
"color": 0,
"children": [
9,
{
"name": "bone11",
"origin": [8, 11.5, 4.5],
"color": 0,
"children": [
10,
{
"name": "bone12",
"origin": [8, 11.5, 3.5],
"color": 0,
"children": [11]
}
]
}
]
},
12,
13
]
},
{
"name": "bone2",
"origin": [8, 11.5, 13.0375],
"color": 0,
"children": [
{
"name": "bone13",
"origin": [8, 11.5, 10.5],
"color": 0,
"children": [
14,
{
"name": "bone14",
"origin": [8, 11.5, 11.5],
"color": 0,
"children": [15]
},
{
"name": "bone15",
"origin": [8, 11.5, 12.5],
"color": 0,
"children": [16]
}
]
},
17,
18,
19,
20,
21,
22,
23
]
}
]
},
24,
{
"name": "bone7",
"origin": [1, 0, 1],
"color": 0,
"children": [
{
"name": "bone6",
"origin": [0, 16.5, 14.5],
"color": 0,
"children": [25]
},
{
"name": "bone5red",
"origin": [0, 16.5, 14.5],
"color": 0,
"children": [26]
},
{
"name": "bone8green",
"origin": [0, 16.5, 14.5],
"color": 0,
"children": [27]
}
]
}
]
}
]
}
-137
View File
@@ -1,137 +0,0 @@
# Changelog
## [1.7.2 (January 25th, 2026)](#1-7-2)
### Additions
- Added Saccharine wood furniture for Adorn.
- Added the Partner Mark, which party Pokémon have a very small chance to earn as the player walks, provided they have enough friendship.
- Added decorative block models for existing items: Potion, Super Potion, Hyper Potion, Max Potion, Full Restore, Antidote, Awakening, Burn Heal, Ice Heal, Paralyze Heal, Full Heal, Ether, Elixir, Max Ether, Max Elixir, X Accuracy, X Attack, X Defence, X Special Attack, X Special Defence, X Speed, Dire Hit, Guard Spec, Weakness Policy, Blunder Policy, Cleanse Tag, and Spell Tag.
- Added decorative sign blocks called Plaques.
- Added ride style labels to ride stats in Pokédex, as well as ride style icons in the summary GUI.
- Added more seasonings for sinister tea:
- Mental Herb - Mental Restoration I for 10 seconds, which staves off Insomnia.
- White Herb - Removes negative effects.
- Milk, Moomoo Milk - Removes effects.
- Added dispenser support for:
- Applying a Honey Bottle to Saccharine Logs.
- Washing honey off of Saccharine Slathered Logs.
- Applying and removing honey on Saccharine Leaves.
- Added riding statistics: `Distance by Pokémon on Land`, `Distance by Pokémon in Air` and `Distance by Pokémon in Liquid`.
### New rideable Pokémon
- Honchkrow
- Mantine
### Pokémon Added
#### Gen 5
- Throh
- Sawk
#### Gen 6
- Litleo
- Pyroar
- Spritzee
- Aromatisse
- Swirlix
- Slurpuff
### Model updates for the following Pokémon
- Falinks
- Cottonee
- Whimsicott
- Ferroseed
- Ferrothorn
- Chingling
- Chimecho
### Animation updates for the following Pokémon
- Mantine (ride animations)
- Flygon (ride jump animation)
- Honchkrow (ride animations)
- Croagunk
- Toxicroak
### Changes
- Added a new config option, `Enable In-Flight Dismounting` (default: off), which lets you dismount while riding a Pokémon in the air. This is the textbook definition of caveat emptor.
- Ride sounds have been separated into stereo for passengers and mono for other players. Riding will now sound more spacious.
- Battle AI now uses a smarter threshold for switching, reducing unnecessary switches.
- AI will always use the most damaging move when at low HP and when it is not switching, improving endgame decision-making.
- Items given from interacting with Saccharine Leaves are placed in the active hotbar slot if possible.
- Added `label` as an alternate key to the `tag` property.
### Fixes
- Fixed crashing when viewing another player in spectator mode. Some said this was reason to release this update a month ago. They were probably right.
- Fixed crashing when sending a Pokémon out after editing its form.
- Fixed all Pokémon being saved to chunks and never despawning even when they should be.
- Fixed Poké Snacks crashing if there was no available spawn.
- Fixed Poké Snack / Bait effects so that EV Yield, Type, and Egg Group filters now all apply together when weighting spawns. And you thought you were going crazy.
- Fixed Furfrou not being trimmable on NeoForge specifically.
- Fixed honey from Saccharine Leaves being harvestable at less than the max age.
- Fixed possible error on world generation when Combee are placed in naturally generated bee nests.
- Fixed Galarian Weezing crashing the world if it decides to blink the wrong way. The same could happen to you!
- Fixed Galarian Mr. Mime incorrectly being rideable. Some would argue that regular Mr. Mime shouldn't be rideable, but that would make them wrong.
- Fixed missing apricorn textures for Adorn blocks.
- Fixed Apricorn block model rotation for counters/sinks/cupboards/drawers to match that of most recent version of Adorn.
- Fixed item interaction sometimes not working properly when playing on servers.
- Fixed Mint Leaves not being usable for filling the Resurrection Machine.
- Fixed Super Potion recipes incorrectly using Hondew Berry instead of Aguav Berry. They look similar, to be fair.
- Fixed Wishiwashi not schooling properly.
- Fixed battle item duplication issue. How did it work? Wouldn't you like to know, weather boy.
- Fixed the ordering of Aprijuice's riding stat boosts so they always have the same order.
- Fixed the movesets of certain Pokémon being incorrectly sanitized upon reload, causing some forms to keep losing moves.
- Fixed selected leading Pokémon in battle not being used when levels are set.
- Fixed the bottom half of pasture blocks having an incorrect hit-box when facing north or south.
- Fixed gimmicks not being usable in double and triple battles.
- Fixed gimmick buttons showing when already used in the same turn in double and triple battles.
- Fixed mochi items consuming two items at a time.
- Fixed Fresh Start Mochi's EV reset not syncing health changes properly.
- Fixed Pokémon spawn influences not being specific to regional forms.
- Fixed memory leak from battles not being cleaned up after ending.
- Fixed orientation not getting reset after crashing while flying and getting back on the Pokémon.
- Fixed shiny Pokémon not respecting the `silent` flag on spawn.
- Fixed 'Plain' prefix not being applied to all low-quality Aprijuice.
- Fixed Pokémon interactions not playing sound effects when the volume was not specified.
- Fixed Pokémon interaction cooldowns taking longer than intended when the Pokémon was in the player’s party.
- Fixed Pokémon interactions occasionally crashing the game when spamming an interaction with a 0 cooldown.
- Fixed Pokémon fullness decreasing more than intended when the Pokémon was pastured.
- Fixed Scraggy line, Milcery line, and Morpeko spawning on unnatural blocks.
- Fixed invulnerability-bypassed damage being resisted by invulnerable NPC entities.
- Fixed Fabric network packets not being handled immediately, fixing incompatibilities with Supplementaries, Prometheus, Sleep Tight, and others.
- Fixed killer not being set early enough on a wild Pokémon entity from `WinInstruction`.
- Fixed requirements such as `biomeCondition` crashing the game when used to define Pokémon interactions.
- Fixed an issue that prevented registering custom dispenser behaviours (no longer overrides vanilla behaviour).
- Fixed an issue navigation that prevented Combee from pathing into and out of Saccharine Leaves.
- Added Saccharine Boats and Saccharine Boat with Chests to relevant boat tags.
- Fixed Soothe Bell not being properly tagged as a held item.
- Fixed `/calculateseatpositions` expecting a locator format that even we don't use. It needs an underscore after "seat".
- Fixed the `run_script` Molang function to not fail if the environment's context is null.
- Fixed the Molang functions `date_local_time`, `date_of`, `date_is_after` to use the correct date format.
### Developer
- Changed the `owner` parameter in the `OwnerQueryRequirement` interface from `ServerPlayer` to `Player`. This method is now also called on the client to verify whether a Pokémon interaction succeeded, so make sure to update your implementations to handle both server and client contexts.
- Added the `pnx` to the `BattleFaintedEvent` and `FormeChangeEvent`.
- Added register methods for custom instructions to `ShowdownInterpreter`.
- Changed callback operations in `BattlePokemon` to allow multiple callbacks and be mutable.
- Added `display_name`, `description`, and `max_pp` functions to the `MoveTemplate` struct.
- Changed the implementations of `AIBattleActor` to be open classes to allow extension.
- Changed the resource location of Poké Ball icons in the GUI to use the balls namespace instead of Cobblemon's, allowing mods to properly use their own namespace for these textures.
### Molang & Datapacks
- Ride sounds can now be set as exclusive to passengers.
- Ride sounds no longer play when submerged unless specified with a new setting.
- Exception handling has been added to `run_molang`, resolving some crashes caused by malformed MoLang expressions.
- Added the `create_itemstack`, `has_inventory_space`, `set_inventory_slot`, and `give_item` MoLang functions for item and inventory utility.
- Added the `get_move_from_id` MoLang function for general move info queries without requiring a Moveset object to access.
- Added support for optional message variants in battle activate instructions for more context-specific battle text.
- Example: `this.add('-activate', pokemon, 'ability: example', '[msg]message1');` will parse to lang key `cobblemon.battle.activate.example.message1`.
### Localization
- Updated translations for:
- French
- Canadian French
- Simplified Chinese
- Ukrainian
- Brazilian Portuguese
- Spanish
- Korean
- Japanese
-22
View File
@@ -1,22 +0,0 @@
# Changelog
## [1.7.3 (January 31st, 2026)](#1-7-3)
### Fixes
- Fixed mounted Pokémon not playing sounds.
- Fixed players failing to join servers randomly.
- Fixed some Pokémon having no stamina for flying.
- Fixed `/pctake` not respecting the actual size of the PC.
### Developer
- Poké Ball GUI textures now respect the ball's namespace, enabling proper texture loading for custom balls from other mods.
### Molang & Datapacks
- Added a `chance` requirement type for Pokémon interactions.
### Localization
- Updated translations for:
- French
- Canadian French
- Korean
- Portuguese
- Simplified Chinese
- Spanish
+3 -1
View File
@@ -54,8 +54,10 @@ dependencies {
// Flywheel has no common dep so just pick one and don't use any platform specific code in common
// modCompileOnly(libs.flywheelFabric)
modCompileOnly(files("temp/vivecraft-1.21.1-1.3.4.jar")) // TODO: Switch this to a proper dependency when Vivecraft gets a maven repo
// Showdown
modCompileOnly(libs.graal.core)
modCompileOnly(libs.graal)
// Data Storage
modCompileOnly(libs.bundles.mongo)
@@ -1,146 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.camera;
import com.cobblemon.mod.common.client.util.compat.SodiumCompat;
import com.cobblemon.mod.common.platform.events.ClientChunkEvent;
import com.cobblemon.mod.common.platform.events.PlatformEvents;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import kotlin.Unit;
import kotlin.jvm.functions.Function1;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.protocol.game.ClientboundLevelChunkPacketData;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.chunk.LevelChunk;
import java.util.function.Consumer;
public class CameraClientChunkCacheExtension {
private static final int MIN_RENDER_DISTANCE = 2;
private static final int MAX_RENDER_DISTANCE = 64;
private static final int RENDER_DISTANCE_MARGIN = 2;
private static final Long2ObjectOpenHashMap<LevelChunk> CHUNK_MAP = new Long2ObjectOpenHashMap<>();
private static final Long2ObjectOpenHashMap<LevelChunk> CHUNK_MAP_OTHER_THREADS = new Long2ObjectOpenHashMap<>();
private static volatile ChunkPos CAMERA_CENTER;
private CameraClientChunkCacheExtension() {}
public static void drop(ClientLevel level, ChunkPos chunkPos) {
if (Minecraft.getInstance().isSameThread()) {
long chunkPosLong = chunkPos.toLong();
LevelChunk chunk = CHUNK_MAP.get(chunkPosLong);
if (chunk != null) {
modifyChunkMaps(map -> map.remove(chunkPosLong));
SodiumCompat.getInstance().notifyChunkRemoved(level, chunkPos.x, chunkPos.z);
PlatformEvents.CLIENT_CHUNK_UNLOAD.postEvents(new ClientChunkEvent.Unload(chunk, level));
level.unload(chunk);
}
}
}
public static LevelChunk getChunk(int x, int z) {
long chunkPos = ChunkPos.asLong(x, z);
if (Minecraft.getInstance().isSameThread())
return CHUNK_MAP.get(chunkPos);
else {
synchronized (CHUNK_MAP_OTHER_THREADS) {
return CHUNK_MAP_OTHER_THREADS.get(chunkPos);
}
}
}
public static LevelChunk replaceWithPacketData(ClientLevel level, int x, int z, FriendlyByteBuf packetData, CompoundTag chunkTag, Consumer<ClientboundLevelChunkPacketData.BlockEntityTagOutput> tagOutput) {
if (!Minecraft.getInstance().isSameThread())
throw new UnsupportedOperationException("replaceWithPacketData called off-thread, this shouldn't happen!");
ChunkPos chunkPos = new ChunkPos(x, z);
long longChunkPos = chunkPos.toLong();
LevelChunk chunk = CHUNK_MAP.get(longChunkPos);
if (chunk == null) {
LevelChunk newChunk = new LevelChunk(level, chunkPos);
chunk = newChunk;
chunk.replaceWithPacketData(packetData, chunkTag, tagOutput);
modifyChunkMaps(map -> map.put(longChunkPos, newChunk));
}
level.onChunkLoaded(chunkPos);
SodiumCompat.getInstance().notifyChunkAdded(level, x, z);
PlatformEvents.CLIENT_CHUNK_LOAD.postEvents(new ClientChunkEvent.Load(chunk, level));
return chunk;
}
private static void modifyChunkMaps(Consumer<Long2ObjectOpenHashMap<LevelChunk>> operation) {
operation.accept(CHUNK_MAP);
synchronized (CHUNK_MAP_OTHER_THREADS) {
operation.accept(CHUNK_MAP_OTHER_THREADS);
}
}
public static void clear() {
CHUNK_MAP.clear();
CHUNK_MAP_OTHER_THREADS.clear();
CAMERA_CENTER = null;
}
public static boolean hasCameraChunks() {
if (Minecraft.getInstance().isSameThread()) {
return !CHUNK_MAP.isEmpty();
} else {
synchronized (CHUNK_MAP_OTHER_THREADS) {
return !CHUNK_MAP_OTHER_THREADS.isEmpty();
}
}
}
/**
* Computes render distance using Chebyshev distance with a safety margin.
*/
public static int getSuggestedRenderDistance(ChunkPos center) {
int maxDistance = 0;
if (Minecraft.getInstance().isSameThread()) {
for (long key : CHUNK_MAP.keySet()) {
int x = ChunkPos.getX(key);
int z = ChunkPos.getZ(key);
int dx = Math.abs(x - center.x);
int dz = Math.abs(z - center.z);
maxDistance = Math.max(maxDistance, Math.max(dx, dz));
}
} else {
synchronized (CHUNK_MAP_OTHER_THREADS) {
for (long key : CHUNK_MAP_OTHER_THREADS.keySet()) {
int x = ChunkPos.getX(key);
int z = ChunkPos.getZ(key);
int dx = Math.abs(x - center.x);
int dz = Math.abs(z - center.z);
maxDistance = Math.max(maxDistance, Math.max(dx, dz));
}
}
}
return Math.max(MIN_RENDER_DISTANCE, Math.min(MAX_RENDER_DISTANCE, maxDistance + RENDER_DISTANCE_MARGIN));
}
public static void setCameraCenter(ChunkPos center) {
CAMERA_CENTER = center;
}
public static ChunkPos getCameraCenter() {
return CAMERA_CENTER;
}
}
@@ -1,86 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.render.camera;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import net.minecraft.client.renderer.chunk.SectionRenderDispatcher;
import net.minecraft.core.BlockPos;
import net.minecraft.core.SectionPos;
import net.minecraft.world.level.Level;
public final class CameraViewAreaExtension {
private static final Long2ObjectOpenHashMap<SectionRenderDispatcher.RenderSection> SECTIONS = new Long2ObjectOpenHashMap<>();
private static SectionRenderDispatcher sectionRenderDispatcher;
private static int minSectionY;
private static int maxSectionY;
private CameraViewAreaExtension() {
}
public static void allChanged(SectionRenderDispatcher dispatcher, Level level) {
clear();
sectionRenderDispatcher = dispatcher;
minSectionY = level.getMinSection();
maxSectionY = level.getMaxSection();
}
public static SectionRenderDispatcher.RenderSection rawFetch(int cx, int cy, int cz, boolean generateNew) {
if (cy < minSectionY || cy >= maxSectionY) {
return null;
}
long sectionPos = SectionPos.asLong(cx, cy, cz);
return generateNew ? provideSection(sectionPos) : SECTIONS.get(sectionPos);
}
public static void setDirty(int cx, int cy, int cz, boolean playerChanged) {
SectionRenderDispatcher.RenderSection section = rawFetch(cx, cy, cz, false);
if (section != null) {
section.setDirty(playerChanged);
}
}
public static void onChunkUnload(int sectionX, int sectionZ) {
for (int sectionY = minSectionY; sectionY < maxSectionY; sectionY++) {
long sectionPos = SectionPos.asLong(sectionX, sectionY, sectionZ);
SectionRenderDispatcher.RenderSection section = SECTIONS.remove(sectionPos);
if (section != null) {
section.releaseBuffers();
}
}
}
public static void clear() {
for (SectionRenderDispatcher.RenderSection section : SECTIONS.values()) {
section.releaseBuffers();
}
SECTIONS.clear();
}
public static int minSectionY() {
return minSectionY;
}
public static int maxSectionY() {
return maxSectionY;
}
private static SectionRenderDispatcher.RenderSection provideSection(long sectionPos) {
return SECTIONS.computeIfAbsent(sectionPos, CameraViewAreaExtension::createSection);
}
private static SectionRenderDispatcher.RenderSection createSection(long sectionPos) {
if (sectionRenderDispatcher == null) {
throw new IllegalStateException("SectionRenderDispatcher not initialised");
}
BlockPos origin = SectionPos.of(sectionPos).origin();
return sectionRenderDispatcher.new RenderSection(0, origin.getX(), origin.getY(), origin.getZ());
}
}
@@ -1,15 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.render.camera.accessor;
import net.minecraft.client.renderer.culling.Frustum;
public interface CameraLevelRendererAccessor {
Frustum cobblemonGetFrustum();
}
@@ -11,9 +11,7 @@ package com.cobblemon.mod.common.mixin;
import com.cobblemon.mod.common.CobblemonMemories;
import com.cobblemon.mod.common.api.ai.config.task.PathToBeeHiveTaskConfig;
import com.cobblemon.mod.common.api.pokemon.PokemonProperties;
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity;
import com.cobblemon.mod.common.world.feature.SaccharineTreeFeature;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.tags.BlockTags;
@@ -44,21 +42,7 @@ public abstract class BeeOccupantMixin {
if (cir.getReturnValue() == null) {
final BeehiveBlockEntity.Occupant occupant = (BeehiveBlockEntity.Occupant) (Object) this;
CompoundTag compoundTag = occupant.entityData().copyTag();
String id = compoundTag.getString("id");
Entity entity = null;
if (id.equals(SaccharineTreeFeature.Companion.getPOKEMON_PLACEHOLD_ID())) {
// Special case for Pokemon stuffed into a beenest upon world gen.
// For technical reasons we cannot create them as full entities during world gen.
// So we look for this special id tag and complete the entity when it attempts to exit a hive.
if (compoundTag.contains("pokemonProperties")) {
String properties = compoundTag.getString("pokemonProperties");
PokemonProperties pokemon = PokemonProperties.Companion.parse(properties);
entity = pokemon.createEntity(level);
}
} else {
entity = EntityType.loadEntityRecursive(compoundTag, level, (entityx) -> entityx);
}
Entity entity = EntityType.loadEntityRecursive(compoundTag, level, (entityx) -> entityx);
if (entity instanceof PokemonEntity pokemonEntity) {
var state = level.getBlockState(pos);
var newPos = Vec3.ZERO;
@@ -15,8 +15,6 @@ import com.cobblemon.mod.common.api.events.CobblemonEvents;
import com.cobblemon.mod.common.api.events.item.LeftoversCreatedEvent;
import com.cobblemon.mod.common.api.orientation.OrientationController;
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies;
import com.cobblemon.mod.common.api.riding.RidingStyle;
import com.cobblemon.mod.common.api.riding.behaviour.types.air.HoverBehaviour;
import com.cobblemon.mod.common.api.storage.party.PartyStore;
import com.cobblemon.mod.common.api.storage.party.PlayerPartyStore;
import com.cobblemon.mod.common.api.tags.CobblemonItemTags;
@@ -61,7 +59,6 @@ import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@@ -172,32 +169,6 @@ public abstract class PlayerMixin extends LivingEntity implements ScannableEntit
}
}
@Redirect(
method = "getDestroySpeed",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/world/entity/player/Player;onGround()Z"
)
)
private boolean cobblemon$considerHoverOnGround(Player player) {
// If riding a Hover Mount, return true to bypass the 5x slowdown
if (player.getVehicle() instanceof PokemonEntity vehicle) {
var ridingController = vehicle.getRidingController();
if (ridingController == null) player.onGround();
var activeContext = ridingController.getContext();
if (activeContext == null) player.onGround();
if (activeContext.getStyle() == RidingStyle.AIR &&
activeContext.getBehaviour().equals(HoverBehaviour.Companion.getKEY())) {
return true;
}
}
// Otherwise, return the actual value
return player.onGround();
}
@Inject(
method = "eat",
at = @At(
@@ -11,15 +11,11 @@ package com.cobblemon.mod.common.mixin;
import com.cobblemon.mod.common.Cobblemon;
import com.cobblemon.mod.common.OrientationControllable;
import com.cobblemon.mod.common.PlayerSpawnerAccessor;
import com.cobblemon.mod.common.api.riding.RidingStyle;
import com.cobblemon.mod.common.api.spawning.spawner.PlayerSpawner;
import com.cobblemon.mod.common.api.spawning.spawner.PlayerSpawnerFactory;
import com.cobblemon.mod.common.api.stats.CobblemonStats;
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity;
import com.cobblemon.mod.common.world.gamerules.CobblemonGameRules;
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.llamalad7.mixinextras.sugar.Local;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
@@ -93,32 +89,4 @@ public abstract class ServerPlayerMixin implements PlayerSpawnerAccessor {
private boolean cobblemon$cancelSavingPokemonMounts(boolean original, @Local(ordinal=0) Entity entity) {
return original && !(entity instanceof PokemonEntity);
}
@WrapOperation(method = "checkRidingStatistics", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/level/ServerPlayer;getVehicle()Lnet/minecraft/world/entity/Entity;"))
public Entity cobblemon$checkRidingStatistics(ServerPlayer player, Operation<Entity> original, @Local int distance) {
var entity = original.call(player);
if (!(entity instanceof PokemonEntity pokemonEntity)) {
return entity;
}
var ridingController = pokemonEntity.getRidingController();
if (ridingController == null) return entity;
var activeContext = ridingController.getContext();
if (activeContext == null) return entity;
var ridingStyle = activeContext.getStyle();
CobblemonStats.CobblemonStat stat = switch (ridingStyle) {
case RidingStyle.LAND -> CobblemonStats.RIDING_LAND;
case RidingStyle.AIR -> CobblemonStats.RIDING_AIR;
case RidingStyle.LIQUID -> CobblemonStats.RIDING_LIQUID;
default -> null;
};
if (stat != null) {
player.awardStat(CobblemonStats.getStat(stat), distance);
}
return entity;
}
}
@@ -1,23 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.mixin.accessor;
import net.minecraft.client.Camera;
import net.minecraft.world.level.BlockGetter;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(Camera.class)
public interface CameraAccessor {
@Accessor("level")
BlockGetter getLevel();
@Accessor("detached")
void setDetached(boolean detached);
}
@@ -1,26 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.mixin.accessor;
import net.minecraft.world.item.Item;
import net.minecraft.core.dispenser.DispenseItemBehavior;
import net.minecraft.world.level.block.DispenserBlock;
import java.util.Map;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(DispenserBlock.class)
public interface DispenserBlockAccessor {
@Accessor("DISPENSER_REGISTRY")
static Map<Item, DispenseItemBehavior> getItemBehaviors() {
throw new AssertionError();
}
}
@@ -1,23 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.mixin.accessor;
import net.minecraft.client.Camera;
import net.minecraft.client.renderer.GameRenderer;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(GameRenderer.class)
public interface GameRendererAccessor {
@Accessor("mainCamera")
Camera cobblemon$getMainCamera();
@Accessor("mainCamera")
void cobblemon$setMainCamera(Camera camera);
}
@@ -1,105 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.mixin.camera;
import java.util.HashSet;
import java.util.Set;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import com.cobblemon.mod.common.api.camera.CameraManager;
import com.cobblemon.mod.common.api.camera.CameraManager.ChunkViewUpdate;
import com.llamalad7.mixinextras.sugar.Local;
import com.mojang.logging.LogUtils;
import net.minecraft.server.level.ChunkMap;
import net.minecraft.server.level.ChunkTrackingView;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.chunk.LevelChunk;
import org.slf4j.Logger;
@Mixin(value = ChunkMap.class, priority = 1100)
public abstract class ChunkMapMixin {
private static final Logger COBBLEMON$CAMERA_LOGGER = LogUtils.getLogger();
@Shadow
protected abstract void markChunkPendingToSend(ServerPlayer player, ChunkPos pos);
@Shadow
private static void markChunkPendingToSend(ServerPlayer player, LevelChunk chunk) {}
@Shadow
private static void dropChunk(ServerPlayer player, ChunkPos chunkPos) {}
@Inject(method = "updateChunkTracking", at = @At("HEAD"))
private void cobblemon$handleCameraChunkUpdates(ServerPlayer player, CallbackInfo ci) {
ChunkViewUpdate update = CameraManager.consumeChunkViewUpdate(player);
if (update != null) {
ChunkTrackingView playerView = player.getChunkTrackingView();
ChunkTrackingView current = update.getCurrent();
ChunkTrackingView previous = update.getPrevious();
if (current != null) {
ChunkTrackingView.difference(playerView, current, chunkPos -> cobblemon$queueChunkForPlayer(player, chunkPos), chunkPos -> {});
}
if (previous != null) {
Set<ChunkPos> droppingChunks = new HashSet<>();
ChunkTrackingView targetComparison = current != null ? current : playerView;
ChunkTrackingView.difference(previous, targetComparison, chunkPos -> {}, droppingChunks::add);
for (ChunkPos chunkPos : droppingChunks) {
if (playerView == null || !playerView.contains(chunkPos)) {
dropChunk(player, chunkPos);
}
}
}
}
}
@Inject(method = "onChunkReadyToSend", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/level/ServerPlayer;getChunkTrackingView()Lnet/minecraft/server/level/ChunkTrackingView;"))
private void cobblemon$queueCameraChunk(LevelChunk chunk, CallbackInfo ci, @Local ServerPlayer player) {
ChunkTrackingView view = CameraManager.getPlayerChunkView(player);
if (view != null && view.contains(chunk.getPos())) {
markChunkPendingToSend(player, chunk);
}
}
@Inject(method = "isChunkTracked", at = @At("HEAD"), cancellable = true)
private void cobblemon$isCameraChunkTracked(ServerPlayer player, int x, int z, CallbackInfoReturnable<Boolean> cir) {
ChunkTrackingView view = CameraManager.getPlayerChunkView(player);
if (view != null && view.contains(x, z) && !player.connection.chunkSender.isPending(ChunkPos.asLong(x, z))) {
cir.setReturnValue(true);
}
}
@Inject(method = "dropChunk", at = @At("HEAD"), cancellable = true)
private static void cobblemon$preventCameraChunkDrop(ServerPlayer player, ChunkPos pos, CallbackInfo ci) {
ChunkTrackingView view = CameraManager.getPlayerChunkView(player);
if (view != null && view.contains(pos)) {
ci.cancel();
}
}
private void cobblemon$queueChunkForPlayer(ServerPlayer player, ChunkPos chunkPos) {
markChunkPendingToSend(player, chunkPos);
}
}
@@ -1,135 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.mixin.client;
import com.cobblemon.mod.common.client.render.camera.CameraFeedHandler;
import com.cobblemon.mod.common.client.render.camera.CameraViewAreaExtension;
import com.cobblemon.mod.common.client.render.camera.accessor.CameraLevelRendererAccessor;
import com.cobblemon.mod.common.client.util.compat.SodiumCompat;
import com.llamalad7.mixinextras.injector.v2.WrapWithCondition;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import net.minecraft.client.Camera;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.world.entity.Entity;
import net.minecraft.client.renderer.LevelRenderer;
import net.minecraft.client.renderer.ViewArea;
import net.minecraft.client.renderer.chunk.SectionRenderDispatcher;
import net.minecraft.client.renderer.culling.Frustum;
import net.minecraft.core.BlockPos;
import net.minecraft.core.SectionPos;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.ModifyVariable;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(value = LevelRenderer.class, priority = 1100)
public abstract class CameraLevelRendererMixin implements CameraLevelRendererAccessor {
private static final int CINEMATIC_RENDER_DISTANCE_CHUNKS = 32;
private static final float CINEMATIC_FOG_DISTANCE = CINEMATIC_RENDER_DISTANCE_CHUNKS * 16;
@Shadow
@Final
private Minecraft minecraft;
@Shadow
private SectionRenderDispatcher sectionRenderDispatcher;
@Shadow
private ClientLevel level;
@Shadow private Frustum capturedFrustum;
@Shadow public Frustum cullingFrustum;
@WrapWithCondition(method = "setupRender", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/ViewArea;repositionCamera(DD)V"))
private boolean cobblemon$shouldRepositionCamera(ViewArea viewArea, double x, double z) {
return !CameraFeedHandler.isCapturing();
}
/**
* Cancel vanilla setupRender during feed capture to avoid overwriting
* the camera feed's visible sections. When Sodium is active, let it
* proceed since Sodium manages its own chunk rendering pipeline and
* needs setupRender to run.
*/
@Inject(method = "setupRender", at = @At("HEAD"), cancellable = true)
private void cobblemon$onSetupRender(Camera camera, Frustum frustum, boolean hasCapturedFrustum, boolean isSpectator, CallbackInfo ci) {
if (CameraFeedHandler.isCapturing() && !SodiumCompat.getInstance().isActive())
ci.cancel();
}
@Inject(method = "allChanged", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/SectionOcclusionGraph;waitAndReset(Lnet/minecraft/client/renderer/ViewArea;)V"))
private void cobblemon$onAllChanged(CallbackInfo ci) {
CameraViewAreaExtension.allChanged(sectionRenderDispatcher, level);
}
@Inject(method = "isSectionCompiled", at = @At("HEAD"), cancellable = true)
private void cobblemon$onIsSectionCompiled(BlockPos pos, CallbackInfoReturnable<Boolean> cir) {
if (CameraFeedHandler.isCapturing()) {
SectionPos sectionPos = SectionPos.of(pos);
SectionRenderDispatcher.RenderSection section = CameraViewAreaExtension.rawFetch(sectionPos.x(), sectionPos.y(), sectionPos.z(), false);
if (section != null && section.getCompiled() != SectionRenderDispatcher.CompiledSection.UNCOMPILED) {
cir.setReturnValue(true);
}
}
}
@ModifyVariable(method = "renderLevel", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/FogRenderer;setupColor(Lnet/minecraft/client/Camera;FLnet/minecraft/client/multiplayer/ClientLevel;IF)V"), ordinal = 1)
private float cobblemon$modifyFogRenderDistance(float original) {
if (CameraFeedHandler.isCapturing())
return CINEMATIC_FOG_DISTANCE;
return original;
}
/**
* During feed capture the camera entity is a Marker, which causes the vanilla condition
* {@code !(entity instanceof LocalPlayer) || camera.getEntity() == entity} to fail for
* the local player — preventing them from rendering.
*
* By returning the local player from {@code getEntity()} and {@code true} from
* {@code isDetached()}, both condition2 and condition3 in the entity rendering check pass:
* <ul>
* <li>condition2: {@code entity != player || isDetached()==true} → passes</li>
* <li>condition3: {@code player == player} → passes</li>
* </ul>
*/
@WrapOperation(
method = "renderLevel",
at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Camera;getEntity()Lnet/minecraft/world/entity/Entity;")
)
private Entity cobblemon$getCameraEntityDuringCapture(Camera camera, Operation<Entity> original) {
if (CameraFeedHandler.isCapturing()) {
Entity player = Minecraft.getInstance().player;
if (player != null) return player;
}
return original.call(camera);
}
@WrapOperation(
method = "renderLevel",
at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Camera;isDetached()Z")
)
private boolean cobblemon$isCameraDetachedDuringCapture(Camera camera, Operation<Boolean> original) {
if (CameraFeedHandler.isCapturing()) {
return true;
}
return original.call(camera);
}
@Override
public Frustum cobblemonGetFrustum() {
return this.capturedFrustum != null ? this.capturedFrustum : this.cullingFrustum;
}
}
@@ -8,32 +8,22 @@
package com.cobblemon.mod.common.mixin.client;
import com.cobblemon.mod.common.Cobblemon;
import com.cobblemon.mod.common.OrientationControllable;
import com.cobblemon.mod.common.api.camera.CameraRotation;
import com.cobblemon.mod.common.api.orientation.OrientationController;
import com.cobblemon.mod.common.api.riding.Rideable;
import com.cobblemon.mod.common.client.MountedPokemonAnimationRenderController;
import com.cobblemon.mod.common.client.camera.ClientCameraController;
import com.cobblemon.mod.common.client.render.camera.CameraFeedHandler;
import com.cobblemon.mod.common.client.render.camera.MountedCameraRenderer;
import com.cobblemon.mod.common.duck.CameraDuck;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.llamalad7.mixinextras.sugar.Local;
import net.minecraft.client.Camera;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.LevelRenderer;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.phys.Vec3;
import org.joml.Quaternionf;
import org.joml.Vector3f;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
@Mixin(Camera.class)
@@ -42,7 +32,6 @@ public abstract class CameraMixin implements CameraDuck {
@Shadow private float eyeHeightOld;
@Shadow private BlockGetter level;
@Shadow private Entity entity;
@Final @Shadow private Quaternionf rotation;
@Final @Shadow private Vector3f forwards;
@Final @Shadow private Vector3f up;
@Final @Shadow private Vector3f left;
@@ -54,11 +43,6 @@ public abstract class CameraMixin implements CameraDuck {
@Override public BlockGetter cobblemon$getLevel() {
return level;
}
@Shadow protected abstract void setRotation(float yRot, float xRot);
@Unique private float returnTimer = 0;
@Unique private float rollAngleStart = 0;
@Unique Minecraft minecraft = Minecraft.getInstance();
@Override public Entity cobblemon$getEntity() {
return entity;
@@ -82,28 +66,6 @@ public abstract class CameraMixin implements CameraDuck {
Entity vehicle = entity.getVehicle();
MountedPokemonAnimationRenderController.INSTANCE.reset();
// Check for cinematic camera override
if (ClientCameraController.isActive()) {
// When capturing a feed, use that specific camera's transform
// instead of the primary camera to avoid both feeds sharing the same position.
java.util.UUID capturedId = CameraFeedHandler.getCurrentlyCapturedCamera();
ClientCameraController.ActiveTransform cameraTransform = capturedId != null
? ClientCameraController.getActiveCameraTransform(capturedId)
: ClientCameraController.getCameraTransform();
if (cameraTransform != null) {
Vec3 position = cameraTransform.getPosition();
CameraRotation rotation = cameraTransform.getRotation();
setPosition(new Vec3(position.x, position.y, position.z));
setRotation(rotation.getYaw(), rotation.getPitch());
OrientationController.Companion.getFORWARDS().rotate(this.rotation, this.forwards);
OrientationController.Companion.getUP().rotate(this.rotation, this.up);
OrientationController.Companion.getLEFT().rotate(this.rotation, this.left);
return;
}
}
if(vehicle instanceof Rideable){
Vec3 position = MountedCameraRenderer.INSTANCE.getCameraPosition(
instance,
@@ -1,83 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.mixin.client;
import com.cobblemon.mod.common.client.camera.CameraClientChunkCacheExtension;
import com.cobblemon.mod.common.client.camera.CameraChunkVisibilityManager;
import com.cobblemon.mod.common.client.render.camera.CameraFeedHandler;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ClientChunkCache;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.protocol.game.ClientboundLevelChunkPacketData;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.util.function.Consumer;
@Mixin(value = ClientChunkCache.class, priority = 1100)
public abstract class ClientChunkCacheMixin {
@Shadow
volatile ClientChunkCache.Storage storage;
@Shadow
@Final
ClientLevel level;
/**
* Intercept updateViewCenter to use camera position when active.
*/
@Inject(method = "drop", at = @At("HEAD"))
private void cobblemon$onDrop(ChunkPos chunkPos, CallbackInfo ci) {
int renderDistance = Minecraft.getInstance().options.renderDistance().get();
if (CameraChunkVisibilityManager.shouldKeepChunk(chunkPos, renderDistance)
|| CameraFeedHandler.shouldAddChunk(chunkPos, renderDistance)) {
return;
}
CameraClientChunkCacheExtension.drop(level, chunkPos);
}
/**
* Override replaceWithPacketData to accept chunks based on camera position.
*/
@Inject(method = "replaceWithPacketData", at = @At("HEAD"), cancellable = true)
public void cobblemon$onReplaceWithPacketData(int x, int z, FriendlyByteBuf buffer, CompoundTag chunkTag, Consumer<ClientboundLevelChunkPacketData.BlockEntityTagOutput> tagOutputConsumer, CallbackInfoReturnable<LevelChunk> cir) {
int renderDistance = Minecraft.getInstance().options.renderDistance().get();
ChunkPos chunkPos = new ChunkPos(x, z);
boolean isInPlayerRange = storage.inRange(x, z);
boolean shouldAddChunk = CameraChunkVisibilityManager.shouldKeepChunk(chunkPos, renderDistance)
|| CameraFeedHandler.shouldAddChunk(chunkPos, renderDistance);
if (shouldAddChunk) {
LevelChunk newChunk = CameraClientChunkCacheExtension.replaceWithPacketData(level, x, z, new FriendlyByteBuf(buffer.copy()), chunkTag, tagOutputConsumer);
if (!isInPlayerRange)
cir.setReturnValue(newChunk);
}
}
@Inject(method = "getChunk(IILnet/minecraft/world/level/chunk/status/ChunkStatus;Z)Lnet/minecraft/world/level/chunk/LevelChunk;", at = @At("TAIL"), cancellable = true)
private void cobblemon$onGetChunk(int x, int z, ChunkStatus chunkStatus, boolean requireChunk, CallbackInfoReturnable<LevelChunk> cir) {
if (!storage.inRange(x, z)) {
LevelChunk chunk = CameraClientChunkCacheExtension.getChunk(x, z);
if (chunk != null)
cir.setReturnValue(chunk);
}
}
}
@@ -9,122 +9,43 @@
package com.cobblemon.mod.common.mixin.client;
import com.cobblemon.mod.common.api.scheduling.ClientTaskTracker;
import com.cobblemon.mod.common.api.tween.TweenManager;
import com.cobblemon.mod.common.client.camera.GameCamera;
import com.cobblemon.mod.common.client.camera.ClientCameraController;
import com.cobblemon.mod.common.client.render.camera.CameraFeedHandler;
import com.cobblemon.mod.common.client.render.camera.compositor.CompositorManager;
import net.minecraft.client.Camera;
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity;
import com.llamalad7.mixinextras.sugar.Local;
import net.minecraft.client.DeltaTracker;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.GameRenderer;
import net.minecraft.world.entity.Entity;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Mutable;
import org.spongepowered.asm.mixin.Shadow;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.BlockGetter;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.ModifyArg;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.function.Predicate;
@Mixin(GameRenderer.class)
public abstract class GameRendererMixin {
@Shadow @Final @Mutable
private Camera mainCamera;
public class GameRendererMixin {
@Unique
private Camera cobblemon$playerCamera;
long lastTime = -1;
@Unique
private final GameCamera cobblemon$gameCamera = new GameCamera();
@Unique
private boolean cobblemon$usingCinematic;
@Unique
private long cobblemon$lastTime = -1;
@Inject(method = "render", at = @At("HEAD"))
private void cobblemon$swapCamera(DeltaTracker counter, boolean tick, CallbackInfo ci) {
@Inject(
method = "render",
at = @At(value = "TAIL")
)
public void render(DeltaTracker counter, boolean tick, CallbackInfo ci) {
Minecraft client = Minecraft.getInstance();
if (client.level == null) {
cobblemon$restoreCamera();
return;
}
if (ClientCameraController.shouldUseCinematicCamera()) {
if (!cobblemon$usingCinematic) {
cobblemon$playerCamera = mainCamera;
mainCamera = cobblemon$gameCamera;
cobblemon$usingCinematic = true;
}
ClientCameraController.prepareCinematicCamera(client, counter.getGameTimeDeltaPartialTick(tick));
} else if (cobblemon$usingCinematic) {
cobblemon$restoreCamera();
}
}
/**
* Skip the vanilla renderLevel() call when a fullscreen compositor is active since
* the compositor covers the entire screen with camera feeds.
* This avoids a full wasted scene render every frame.
*/
@Redirect(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/GameRenderer;renderLevel(Lnet/minecraft/client/DeltaTracker;)V"))
private void cobblemon$skipVanillaRenderLevel(GameRenderer instance, DeltaTracker deltaTracker) {
if (!CompositorManager.isFullscreenActive()) {
instance.renderLevel(deltaTracker);
}
}
@Inject(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/GameRenderer;tryTakeScreenshotIfNeeded()V"))
private void cobblemon$afterLevelRendering(DeltaTracker deltaTracker, boolean renderLevel, CallbackInfo ci) {
// Only capture feeds when a compositor is actively consuming them.
// This avoids expensive full-resolution renders (especially with Iris)
// when feeds aren't being displayed.
boolean compositorNeedsFeeds = CompositorManager.isFullscreenActive()
|| !CompositorManager.INSTANCE.getRequiredFeeds().isEmpty();
if (compositorNeedsFeeds && CameraFeedHandler.hasFeeds()) {
CameraFeedHandler.captureFeeds(deltaTracker);
}
if (CompositorManager.isFullscreenActive()) {
CompositorManager.INSTANCE.compositeAll();
CompositorManager.renderFullscreen();
}
}
@Inject(method = "pick", at = @At("HEAD"), cancellable = true)
private void cobblemon$preventFramePick(float partialTicks, CallbackInfo ci) {
if (CameraFeedHandler.isCapturing())
ci.cancel();
}
@Inject(method = "render", at = @At(value = "TAIL"))
private void cobblemon$afterRender(DeltaTracker counter, boolean tick, CallbackInfo ci) {
Minecraft client = Minecraft.getInstance();
long newTime = System.currentTimeMillis();
// Don't play scheduled animations when the game is paused
if (client.isPaused()) {
cobblemon$lastTime = newTime;
lastTime = newTime;
return;
}
if (cobblemon$lastTime != -1) {
float delta = (newTime - cobblemon$lastTime) / 1000F;
ClientTaskTracker.INSTANCE.update(delta);
TweenManager.Companion.getClient().update(delta);
if (lastTime != -1) {
ClientTaskTracker.INSTANCE.update((newTime - lastTime) / 1000F);
}
cobblemon$lastTime = newTime;
lastTime = newTime;
}
@ModifyArg(
@@ -139,12 +60,4 @@ public abstract class GameRendererMixin {
return (entityX) -> !(entity.getVehicle() == entityX && entityX instanceof PokemonEntity) && original.test(entityX);
}
@Unique
private void cobblemon$restoreCamera() {
if (cobblemon$usingCinematic) {
mainCamera = cobblemon$playerCamera;
cobblemon$playerCamera = null;
cobblemon$usingCinematic = false;
}
}
}
@@ -9,7 +9,6 @@
package com.cobblemon.mod.common.mixin.client;
import com.cobblemon.mod.common.OrientationControllable;
import com.cobblemon.mod.common.client.camera.ClientCameraController;
import com.cobblemon.mod.common.item.PokedexItem;
import com.mojang.blaze3d.vertex.PoseStack;
import net.minecraft.client.Minecraft;
@@ -29,12 +28,6 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
public class ItemInHandRendererMixin {
@Inject(method = "renderArmWithItem", at = @At(value = "HEAD"), cancellable = true)
private void cobblemon$renderArmWithItem(AbstractClientPlayer abstractClientPlayer, float f, float g, InteractionHand interactionHand, float h, ItemStack itemStack, float i, PoseStack poseStack, MultiBufferSource multiBufferSource, int j, CallbackInfo ci) {
// Hide arms when cinematic camera is active
if (ClientCameraController.isActive()) {
ci.cancel();
return;
}
if (abstractClientPlayer.isUsingItem() && abstractClientPlayer.getUseItem().getItem() instanceof PokedexItem) {
ci.cancel();
}
@@ -35,7 +35,6 @@ import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import com.cobblemon.mod.common.client.gui.party.PartyTutorialToasts;
import static com.google.common.primitives.Floats.min;
import static net.minecraft.util.Mth.lerp;
@@ -86,13 +85,11 @@ public abstract class MouseHandlerMixin {
CobblemonClient.INSTANCE.getStorage().shiftSelected(false);
ci.cancel();
PartySendBinding.INSTANCE.actioned();
PartyTutorialToasts.INSTANCE.onHoldKeyScrollSwitchedSlot();
} else if (i < 0) {
accumulatedScrollY -= i;
CobblemonClient.INSTANCE.getStorage().shiftSelected(true);
ci.cancel();
PartySendBinding.INSTANCE.actioned();
PartyTutorialToasts.INSTANCE.onHoldKeyScrollSwitchedSlot();
}
}
}
@@ -313,7 +310,7 @@ public abstract class MouseHandlerMixin {
var playerRotater = (RidePassenger)player;
playerRotater.cobblemon$setRideXRot(playerRotater.cobblemon$getRideXRot() + f);
playerRotater.cobblemon$setRideYRot(playerRotater.cobblemon$getRideYRot() + g);
playerRotater.cobblemon$setRideXRot(Mth.clamp(playerRotater.cobblemon$getRideXRot(), -89.0F, 89.0F));
playerRotater.cobblemon$setRideXRot(Mth.clamp(playerRotater.cobblemon$getRideXRot(), -90.0F, 90.0F));
playerRotater.cobblemon$setRideYRot(Mth.clamp(playerRotater.cobblemon$getRideYRot(), -105.0F, 105.0F));
if (player.getVehicle() != null) {
player.getVehicle().onPassengerTurned(player);
@@ -8,14 +8,11 @@
package com.cobblemon.mod.common.mixin.client;
import com.cobblemon.mod.common.client.camera.ClientCameraController;
import com.cobblemon.mod.common.client.render.ClientPlayerIcon;
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.mojang.blaze3d.vertex.PoseStack;
import net.minecraft.client.CameraType;
import net.minecraft.client.Minecraft;
import net.minecraft.client.model.PlayerModel;
import net.minecraft.client.player.AbstractClientPlayer;
import net.minecraft.client.renderer.MultiBufferSource;
@@ -25,7 +22,6 @@ import net.minecraft.client.renderer.entity.player.PlayerRenderer;
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.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(PlayerRenderer.class)
@@ -35,27 +31,6 @@ public abstract class PlayerRendererMixin extends LivingEntityRenderer<AbstractC
super(context, model, shadowRadius);
}
/**
* Override the camera type check to force player body rendering when cinematic camera is active.
* This makes Minecraft think we're not in first person mode (for rendering purposes only),
* so the local player's body is rendered even though the camera entity is still the player.
*/
@Redirect(
method = "render(Lnet/minecraft/client/player/AbstractClientPlayer;FFLcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/MultiBufferSource;I)V",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/client/CameraType;isFirstPerson()Z"
)
)
private boolean cobblemon$allowBodyRenderInCinematic(CameraType instance) {
// When cinematic camera is active and configured to show player body,
// pretend we're not in first person so the player body renders
if (ClientCameraController.isActive() && ClientCameraController.shouldRenderPlayerBody()) {
return false;
}
return instance.isFirstPerson();
}
@Inject(method = "render", at = @At(value = "TAIL"))
public void renderPlayer(AbstractClientPlayer player, float f, float g, PoseStack poseStack, MultiBufferSource multiBufferSource, int i, CallbackInfo ci) {
ClientPlayerIcon.Companion.onRenderPlayer(player);
@@ -1,25 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.mixin.client;
import com.cobblemon.mod.common.client.render.camera.CameraViewAreaExtension;
import net.minecraft.client.renderer.ViewArea;
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(ViewArea.class)
public class ViewAreaMixin {
@Inject(method = "setDirty", at = @At("HEAD"))
private void cobblemon$markCinematicDirty(int cx, int cy, int cz, boolean reRenderOnMainThread, CallbackInfo ci) {
CameraViewAreaExtension.setDirty(cx, cy, cz, reRenderOnMainThread);
}
}
@@ -14,14 +14,9 @@ import com.cobblemon.mod.common.entity.pokemon.PokemonEntity;
import com.cobblemon.mod.common.pokemon.Pokemon;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.llamalad7.mixinextras.sugar.Local;
import com.llamalad7.mixinextras.sugar.Share;
import com.llamalad7.mixinextras.sugar.ref.LocalBooleanRef;
import com.llamalad7.mixinextras.sugar.ref.LocalFloatRef;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.util.Mth;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.Level;
@@ -94,61 +89,4 @@ public abstract class EntityMixin {
cir.setReturnValue(customEyePos);
}
@WrapOperation(
method = "push(Lnet/minecraft/world/entity/Entity;)V",
at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/Entity;push(DDD)V", ordinal = 0)
)
public void cobblemon$customPusherLogic(Entity pusher, double x, double y, double z,
Operation<Void> original,
@Local(argsOnly = true) Entity pushee,
@Local(name = "e") double e, @Local(name = "d") double d,
@Share("pusherIsPlayerOrPokemon") LocalBooleanRef pusherIsPlayerOrPokemon,
@Share("pusheeIsPlayerOrPokemon") LocalBooleanRef pusheeIsPlayerOrPokemon,
@Share("pusheeReceivedForce")LocalFloatRef pusheeReceivedForce) {
pusherIsPlayerOrPokemon.set(pusher instanceof PokemonEntity || pusher instanceof Player);
pusheeIsPlayerOrPokemon.set(pushee instanceof PokemonEntity || pushee instanceof Player);
if (!pusheeIsPlayerOrPokemon.get() || !pusherIsPlayerOrPokemon.get()) {
original.call(pusher, x, y, z);
return;
}
/**********************************************
* Custom portion of the code:
* Handle collisions with weight in mind. Use
* the weight value of pokemon to determine
* which side gets what portion of the total
* force.
*********************************************/
// Weights in hectograms
// Check to see if the pusher or pushee is a player. 200ish pounds. Steve's like a 6'6 miner
var pusherWeight = pusher instanceof PokemonEntity pusherPokemon ? pusherPokemon.getPokemon().getSpecies().getWeight() : 900F;
var pusheeWeight = pushee instanceof PokemonEntity pusheePokemon ? pusheePokemon.getPokemon().getSpecies().getWeight() : 900F;
// Calculate the portion of force received for both entities. Bigger receives less from the
// collision than the smaller one.
var totalWeight = pusherWeight + pusheeWeight;
var pusherReceivedForce = pusheeWeight / totalWeight * 2F;
pusheeReceivedForce.set((pusherWeight / totalWeight) * 2F);
original.call(pusher, -d * pusherReceivedForce, 0.0, -e * pusherReceivedForce);
}
@WrapOperation(
method = "push(Lnet/minecraft/world/entity/Entity;)V",
at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/Entity;push(DDD)V", ordinal = 1)
)
public void cobblemon$customPusheeLogic(Entity pusher, double x, double y, double z, Operation<Void> original,
@Local(name = "e") double e, @Local(name = "d") double d,
@Share("pusherIsPlayerOrPokemon") LocalBooleanRef pusherIsPlayerOrPokemon,
@Share("pusheeIsPlayerOrPokemon") LocalBooleanRef pusheeIsPlayerOrPokemon,
@Share("pusheeReceivedForce")LocalFloatRef pusheeReceivedForce) {
if (!pusheeIsPlayerOrPokemon.get() || !pusherIsPlayerOrPokemon.get()) {
original.call(pusher, x, y, z);
return;
}
original.call(pusher, d * pusheeReceivedForce.get(), 0.0, e * pusheeReceivedForce.get());
}
}
@@ -1,21 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.mixin.invoker;
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
import net.minecraft.world.inventory.Slot;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Invoker;
@Mixin(AbstractContainerScreen.class)
public interface AbstractContainerScreenInvoker {
@Invoker("findSlot")
@Nullable Slot callFindSlot(double mouseX, double mouseY);
}
@@ -65,30 +65,19 @@ import com.cobblemon.mod.common.api.storage.player.adapter.PlayerDataMongoBacken
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.battles.BagItems
import com.cobblemon.mod.common.battles.BattleFormat
import com.cobblemon.mod.common.battles.BattleRegistry
import com.cobblemon.mod.common.battles.BattleSide
import com.cobblemon.mod.common.battles.ShowdownThread
import com.cobblemon.mod.common.battles.*
import com.cobblemon.mod.common.battles.actor.PokemonBattleActor
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.api.camera.track.CameraTrackDeserializers
import com.cobblemon.mod.common.block.dispenser.DispenserBehaviorRegistry
import com.cobblemon.mod.common.block.DispenserBehaviorRegistry
import com.cobblemon.mod.common.client.CobblemonPack
import com.cobblemon.mod.common.api.tween.EasingRegistry
import com.cobblemon.mod.common.api.tween.TweenManager
import com.cobblemon.mod.common.command.argument.*
import com.cobblemon.mod.common.config.CobblemonConfig
import com.cobblemon.mod.common.config.LastChangedVersion
import com.cobblemon.mod.common.config.constraint.IntConstraint
import com.cobblemon.mod.common.config.starter.StarterConfig
import com.cobblemon.mod.common.data.CobblemonDataProvider
import com.cobblemon.mod.common.events.AdvancementHandler
import com.cobblemon.mod.common.events.CallbackHandler
import com.cobblemon.mod.common.events.EntityCallbackHandler
import com.cobblemon.mod.common.events.PokedexHandler
import com.cobblemon.mod.common.events.ServerTickHandler
import com.cobblemon.mod.common.events.StatHandler
import com.cobblemon.mod.common.events.*
import com.cobblemon.mod.common.integration.vivecraft.CobblemonVR
import com.cobblemon.mod.common.net.messages.client.settings.ServerSettingsPacket
import com.cobblemon.mod.common.permission.LaxPermissionValidator
import com.cobblemon.mod.common.platform.events.PlatformEvents
@@ -102,23 +91,11 @@ import com.cobblemon.mod.common.pokemon.feature.SlowpokeTailRegrowthSpeciesFeatu
import com.cobblemon.mod.common.pokemon.feature.SlowpokeTailRegrowthSpeciesFeatureProvider
import com.cobblemon.mod.common.pokemon.feature.TagSeasonResolver
import com.cobblemon.mod.common.pokemon.helditem.CobblemonHeldItemManager
import com.cobblemon.mod.common.pokemon.properties.AspectPropertyType
import com.cobblemon.mod.common.pokemon.properties.BattleCloneProperty
import com.cobblemon.mod.common.pokemon.properties.FreezeFrameProperty
import com.cobblemon.mod.common.pokemon.properties.HiddenAbilityPropertyType
import com.cobblemon.mod.common.pokemon.properties.LabelProperty
import com.cobblemon.mod.common.pokemon.properties.NoAIProperty
import com.cobblemon.mod.common.pokemon.properties.UnaspectPropertyType
import com.cobblemon.mod.common.pokemon.properties.UncatchableProperty
import com.cobblemon.mod.common.pokemon.properties.*
import com.cobblemon.mod.common.pokemon.properties.tags.PokemonFlagProperty
import com.cobblemon.mod.common.pokemon.stat.CobblemonStatProvider
import com.cobblemon.mod.common.starter.CobblemonStarterHandler
import com.cobblemon.mod.common.util.DataKeys
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.ifDedicatedServer
import com.cobblemon.mod.common.util.isLaterVersion
import com.cobblemon.mod.common.util.party
import com.cobblemon.mod.common.util.requestWallpapers
import com.cobblemon.mod.common.util.server
import com.cobblemon.mod.common.util.*
import com.cobblemon.mod.common.world.feature.CobblemonPlacedFeatures
import com.cobblemon.mod.common.world.feature.ore.CobblemonOrePlacedFeatures
import com.cobblemon.mod.common.world.gamerules.CobblemonGameRules
@@ -126,17 +103,6 @@ import com.mongodb.ConnectionString
import com.mongodb.MongoClientSettings
import com.mongodb.client.MongoClient
import com.mongodb.client.MongoClients
import java.io.File
import java.io.FileReader
import java.io.FileWriter
import java.io.PrintWriter
import java.util.UUID
import java.util.concurrent.TimeUnit
import kotlin.math.roundToInt
import kotlin.properties.Delegates
import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.isAccessible
import kotlin.reflect.jvm.javaField
import net.minecraft.client.Minecraft
import net.minecraft.commands.synchronization.SingletonArgumentInfo
import net.minecraft.resources.ResourceKey
@@ -149,15 +115,28 @@ import net.minecraft.world.level.Level
import net.minecraft.world.level.storage.LevelResource
import org.apache.logging.log4j.LogManager
import org.apache.logging.log4j.Logger
import java.io.File
import java.io.FileReader
import java.io.FileWriter
import java.io.PrintWriter
import java.util.*
import java.util.concurrent.TimeUnit
import kotlin.math.roundToInt
import kotlin.properties.Delegates
import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.isAccessible
import kotlin.reflect.jvm.javaField
object Cobblemon {
const val MODID = CobblemonBuildDetails.MOD_ID
const val VERSION = CobblemonBuildDetails.VERSION
const val CONFIG_PATH = "config/$MODID/main.json"
@JvmField
val LOGGER: Logger = LogManager.getLogger()
lateinit var implementation: CobblemonImplementation
@Deprecated("This field is now a config value", ReplaceWith("Cobblemon.config.captureCalculator"))
var captureCalculator: CaptureCalculator
get() = this.config.captureCalculator
@@ -187,21 +166,49 @@ object Cobblemon {
val serverPlayerStructs = mutableMapOf<UUID, ObjectValue<Player>>()
val statistics: CobblemonStats = CobblemonStats
var cobblemonVR: CobblemonVR? = null
@JvmStatic
val builtinPacks = listOf(
CobblemonPack(id = "adorncompatibility", name = "Adorn Compatibility", packType = PackType.CLIENT_RESOURCES, activationBehaviour = ResourcePackActivationBehaviour.ALWAYS_ENABLED, neededMods = setOf("adorn")),
CobblemonPack(id = "gyaradosjump", name = "Gyarados Jump Patterns", packType = PackType.CLIENT_RESOURCES, activationBehaviour = ResourcePackActivationBehaviour.DEFAULT_ENABLED),
CobblemonPack(id = "regionbiasforms", name = "Region Bias Forms", packType = PackType.CLIENT_RESOURCES, activationBehaviour = ResourcePackActivationBehaviour.DEFAULT_ENABLED),
CobblemonPack(id = "uniqueshinyforms", name = "Shinies for Magikarp Jump", packType = PackType.CLIENT_RESOURCES, activationBehaviour = ResourcePackActivationBehaviour.NORMAL),
val builtinPacks = listOf<CobblemonPack>(
CobblemonPack(
id = "adorncompatibility",
name = "Adorn Compatibility",
packType = PackType.CLIENT_RESOURCES,
activationBehaviour = ResourcePackActivationBehaviour.ALWAYS_ENABLED,
neededMods = setOf("adorn")
),
CobblemonPack(
id = "gyaradosjump",
name = "Gyarados Jump Patterns",
packType = PackType.CLIENT_RESOURCES,
activationBehaviour = ResourcePackActivationBehaviour.DEFAULT_ENABLED
),
CobblemonPack(
id = "regionbiasforms",
name = "Region Bias Forms",
packType = PackType.CLIENT_RESOURCES,
activationBehaviour = ResourcePackActivationBehaviour.DEFAULT_ENABLED
),
CobblemonPack(
id = "uniqueshinyforms",
name = "Shinies for Magikarp Jump",
packType = PackType.CLIENT_RESOURCES,
activationBehaviour = ResourcePackActivationBehaviour.NORMAL
),
CobblemonPack(id = "repurposedstructurescobblemon", name = "Repurposed Structures Cobblemon", packType = PackType.SERVER_DATA, activationBehaviour = ResourcePackActivationBehaviour.DEFAULT_ENABLED, neededMods = setOf("repurposed_structures")),
CobblemonPack(
id = "repurposedstructurescobblemon",
name = "Repurposed Structures Cobblemon",
packType = PackType.SERVER_DATA,
activationBehaviour = ResourcePackActivationBehaviour.DEFAULT_ENABLED,
neededMods = setOf("repurposed_structures")
),
)
fun preInitialize(implementation: CobblemonImplementation) {
this.implementation = implementation
this.LOGGER.info("Launching Cobblemon ${CobblemonBuildDetails.VERSION}${if(CobblemonBuildDetails.SNAPSHOT) "-SNAPSHOT" else ""} ")
this.LOGGER.info("Launching Cobblemon ${CobblemonBuildDetails.VERSION}${if (CobblemonBuildDetails.SNAPSHOT) "-SNAPSHOT" else ""} ")
if (CobblemonBuildDetails.SNAPSHOT) {
this.LOGGER.info(" - Git Commit: ${smallCommitHash()} (https://gitlab.com/cable-mc/cobblemon/-/commit/${CobblemonBuildDetails.GIT_COMMIT})")
this.LOGGER.info(" - Branch: ${CobblemonBuildDetails.BRANCH}")
@@ -225,7 +232,6 @@ object Cobblemon {
implementation.registerEntityDataSerializers()
implementation.registerCriteria()
implementation.registerEntitySubPredicates()
implementation.registerMobEffects()
DispenserBehaviorRegistry.registerDispenserBehaviors()
DropEntry.register("command", CommandDropEntry::class.java)
@@ -259,7 +265,6 @@ object Cobblemon {
playerDataManager.onPlayerDisconnect(it.player)
RequestManager.onLogoff(it.player)
serverPlayerStructs.remove(it.player.uuid)
com.cobblemon.mod.common.api.molangui.MolangUiSessionManager.closeAllForPlayer(it.player.uuid)
}
PlatformEvents.PLAYER_DEATH.subscribe {
PCLinkManager.removeLink(it.player.uuid)
@@ -267,7 +272,10 @@ object Cobblemon {
}
PlatformEvents.RIGHT_CLICK_ENTITY.subscribe { event ->
if (event.player.getItemInHand(event.hand).item is NameTagItem && event.entity.type.`is`(CobblemonEntityTypeTags.CANNOT_HAVE_NAME_TAG)) {
if (event.player.getItemInHand(event.hand).item is NameTagItem && event.entity.type.`is`(
CobblemonEntityTypeTags.CANNOT_HAVE_NAME_TAG
)
) {
event.cancel()
}
}
@@ -278,7 +286,10 @@ object Cobblemon {
pokemon.lockedEvolutions
.filterIsInstance<BlockClickEvolution>()
.forEach { evolution ->
evolution.attemptEvolution(pokemon, BlockClickEvolution.BlockInteractionContext(block, player.level()))
evolution.attemptEvolution(
pokemon,
BlockClickEvolution.BlockInteractionContext(block, player.level())
)
}
}
}
@@ -302,7 +313,6 @@ object Cobblemon {
HeldItemProvider.register(CobblemonHeldItemManager, Priority.LOWEST)
}
fun sendServerSettingsPacketToPlayer(player: ServerPlayer) {
ServerSettingsPacket(
this.config.preventCompletePartyDeposit,
@@ -339,19 +349,20 @@ object Cobblemon {
SlowpokeTailRegrowthSpeciesFeatureProvider
)
if (implementation.isModInstalled("vivecraft")) { // VR integration
cobblemonVR = CobblemonVR()
cobblemonVR?.init()
}
CustomPokemonProperty.register(UncatchableProperty)
CustomPokemonProperty.register(BattleCloneProperty)
CustomPokemonProperty.register(LabelProperty)
CustomPokemonProperty.register(PokemonFlagProperty)
CustomPokemonProperty.register(HiddenAbilityPropertyType)
CustomPokemonProperty.register(AspectPropertyType)
CustomPokemonProperty.register(UnaspectPropertyType)
CustomPokemonProperty.register(FreezeFrameProperty)
CustomPokemonProperty.register(NoAIProperty)
// Tween & camera system initialization
EasingRegistry.registerBuiltinEasings()
CameraTrackDeserializers.registerBuiltinTracks()
CobblemonEvents.POKEMON_PROPERTY_INITIALISED.emit(Unit)
CallbackHandler.setup()
@@ -362,9 +373,8 @@ object Cobblemon {
}
PlatformEvents.SERVER_TICK_POST.subscribe {
ServerTaskTracker.update(1/20F)
ServerTaskTracker.update(1 / 20F)
ServerRealTimeTaskTracker.update()
TweenManager.server.update(1/20F)
}
PlatformEvents.SERVER_TICK_PRE.subscribe {
ServerRealTimeTaskTracker.update()
@@ -407,10 +417,22 @@ object Cobblemon {
.applyConnectionString(ConnectionString(config.mongoDBConnectionString))
.build()
mongoClient = MongoClients.create(mongoClientSettings)
val generalMongoFactory = CachedPlayerDataStoreFactory(PlayerDataMongoBackend(mongoClient, config.mongoDBDatabaseName, "PlayerDataCollection"))
val generalMongoFactory = CachedPlayerDataStoreFactory(
PlayerDataMongoBackend(
mongoClient,
config.mongoDBDatabaseName,
"PlayerDataCollection"
)
)
generalMongoFactory.setup(server)
val pokedexMongoFactory = CachedPlayerDataStoreFactory(DexDataMongoBackend(mongoClient, config.mongoDBDatabaseName, "PokeDexCollection"))
val pokedexMongoFactory = CachedPlayerDataStoreFactory(
DexDataMongoBackend(
mongoClient,
config.mongoDBDatabaseName,
"PokeDexCollection"
)
)
pokedexMongoFactory.setup(server)
playerDataManager.setFactory(generalMongoFactory, PlayerInstancedDataStoreTypes.GENERAL)
@@ -482,7 +504,7 @@ object Cobblemon {
server()?.getLevel(dimension)
} else {
val mc = Minecraft.getInstance()
mc.singleplayerServer?.getLevel(dimension) ?: mc.level
return mc.singleplayerServer?.getLevel(dimension) ?: mc.level
}
}
@@ -532,6 +554,7 @@ object Cobblemon {
field.set(config, field.get(defaultConfig))
}
}
is IntConstraint -> {
var value = field.get(config)
if (value is Int) {
@@ -584,20 +607,76 @@ object Cobblemon {
}
private fun registerArgumentTypes() {
this.implementation.registerCommandArgument(cobblemonResource("pokemon"), SpeciesArgumentType::class, SingletonArgumentInfo.contextFree(SpeciesArgumentType::species))
this.implementation.registerCommandArgument(cobblemonResource("pokemon_properties"), PokemonPropertiesArgumentType::class, SingletonArgumentInfo.contextFree(PokemonPropertiesArgumentType::properties))
this.implementation.registerCommandArgument(cobblemonResource("spawn_bucket"), SpawnBucketArgumentType::class, SingletonArgumentInfo.contextFree(SpawnBucketArgumentType::spawnBucket))
this.implementation.registerCommandArgument(cobblemonResource("move"), MoveArgumentType::class, SingletonArgumentInfo.contextFree(MoveArgumentType::move))
this.implementation.registerCommandArgument(cobblemonResource("party_slot"), PartySlotArgumentType::class, SingletonArgumentInfo.contextFree(PartySlotArgumentType::partySlot))
this.implementation.registerCommandArgument(cobblemonResource("pokemon_store"), PokemonStoreArgumentType::class, SingletonArgumentInfo.contextFree(PokemonStoreArgumentType::pokemonStore))
this.implementation.registerCommandArgument(cobblemonResource("dialogue"), DialogueArgumentType::class, SingletonArgumentInfo.contextFree(DialogueArgumentType::dialogue))
this.implementation.registerCommandArgument(cobblemonResource("form"), FormArgumentType::class, SingletonArgumentInfo.contextFree(FormArgumentType::form))
this.implementation.registerCommandArgument(cobblemonResource("dex"), DexArgumentType::class, SingletonArgumentInfo.contextFree (DexArgumentType::dex))
this.implementation.registerCommandArgument(cobblemonResource("npc_class"), NPCClassArgumentType::class, SingletonArgumentInfo.contextFree(NPCClassArgumentType::npcClass))
this.implementation.registerCommandArgument(cobblemonResource("wallpaper"), UnlockablePCBoxWallpaperArgumentType::class, SingletonArgumentInfo.contextFree(UnlockablePCBoxWallpaperArgumentType::wallpaper))
this.implementation.registerCommandArgument(cobblemonResource("mark"), MarkArgumentType::class, SingletonArgumentInfo.contextFree(MarkArgumentType::mark))
this.implementation.registerCommandArgument(cobblemonResource("transform_type"), TransformTypeArgumentType::class, SingletonArgumentInfo.contextFree(TransformTypeArgumentType::transformType))
this.implementation.registerCommandArgument(cobblemonResource("model_part"), ModelPartArgumentType::class, SingletonArgumentInfo.contextFree(ModelPartArgumentType::modelPart))
this.implementation.registerCommandArgument(
cobblemonResource("pokemon"),
SpeciesArgumentType::class,
SingletonArgumentInfo.contextFree(SpeciesArgumentType::species)
)
this.implementation.registerCommandArgument(
cobblemonResource("pokemon_properties"),
PokemonPropertiesArgumentType::class,
SingletonArgumentInfo.contextFree(PokemonPropertiesArgumentType::properties)
)
this.implementation.registerCommandArgument(
cobblemonResource("spawn_bucket"),
SpawnBucketArgumentType::class,
SingletonArgumentInfo.contextFree(SpawnBucketArgumentType::spawnBucket)
)
this.implementation.registerCommandArgument(
cobblemonResource("move"),
MoveArgumentType::class,
SingletonArgumentInfo.contextFree(MoveArgumentType::move)
)
this.implementation.registerCommandArgument(
cobblemonResource("party_slot"),
PartySlotArgumentType::class,
SingletonArgumentInfo.contextFree(PartySlotArgumentType::partySlot)
)
this.implementation.registerCommandArgument(
cobblemonResource("pokemon_store"),
PokemonStoreArgumentType::class,
SingletonArgumentInfo.contextFree(PokemonStoreArgumentType::pokemonStore)
)
this.implementation.registerCommandArgument(
cobblemonResource("dialogue"),
DialogueArgumentType::class,
SingletonArgumentInfo.contextFree(DialogueArgumentType::dialogue)
)
this.implementation.registerCommandArgument(
cobblemonResource("form"),
FormArgumentType::class,
SingletonArgumentInfo.contextFree(FormArgumentType::form)
)
this.implementation.registerCommandArgument(
cobblemonResource("dex"),
DexArgumentType::class,
SingletonArgumentInfo.contextFree(DexArgumentType::dex)
)
this.implementation.registerCommandArgument(
cobblemonResource("npc_class"),
NPCClassArgumentType::class,
SingletonArgumentInfo.contextFree(NPCClassArgumentType::npcClass)
)
this.implementation.registerCommandArgument(
cobblemonResource("wallpaper"),
UnlockablePCBoxWallpaperArgumentType::class,
SingletonArgumentInfo.contextFree(UnlockablePCBoxWallpaperArgumentType::wallpaper)
)
this.implementation.registerCommandArgument(
cobblemonResource("mark"),
MarkArgumentType::class,
SingletonArgumentInfo.contextFree(MarkArgumentType::mark)
)
this.implementation.registerCommandArgument(
cobblemonResource("transform_type"),
TransformTypeArgumentType::class,
SingletonArgumentInfo.contextFree(TransformTypeArgumentType::transformType)
)
this.implementation.registerCommandArgument(
cobblemonResource("model_part"),
ModelPartArgumentType::class,
SingletonArgumentInfo.contextFree(ModelPartArgumentType::modelPart)
)
}
}
@@ -106,10 +106,4 @@ object CobblemonBlockEntities : PlatformRegistry<Registry<BlockEntityType<*>>, R
"poke_snack",
BlockEntityType.Builder.of(::PokeSnackBlockEntity, CobblemonBlocks.POKE_SNACK, CobblemonBlocks.POKE_CAKE).build(null)
)
@JvmField
val STADIUM_BLOCK: BlockEntityType<StadiumBlockEntity> = this.create(
"stadium_block",
BlockEntityType.Builder.of(::StadiumBlockEntity, CobblemonBlocks.STADIUM_BLOCK).build(null)
)
}
@@ -9,7 +9,6 @@
package com.cobblemon.mod.common
import com.cobblemon.mod.common.api.apricorn.Apricorn
import com.cobblemon.mod.common.api.pokemon.stats.Stats
import com.cobblemon.mod.common.block.*
import com.cobblemon.mod.common.block.campfirepot.CampfireBlock
import com.cobblemon.mod.common.block.LecternBlock
@@ -42,11 +41,11 @@ import net.minecraft.world.level.block.state.BlockBehaviour
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.level.block.state.properties.BlockSetType
import net.minecraft.world.level.block.state.properties.BlockStateProperties
import net.minecraft.world.level.block.state.properties.BlockStateProperties.LIT
import net.minecraft.world.level.block.state.properties.NoteBlockInstrument
import net.minecraft.world.level.block.state.properties.WoodType
import net.minecraft.world.level.material.MapColor
import net.minecraft.world.level.material.PushReaction
import net.minecraft.world.phys.shapes.Shapes
@Suppress("SameParameterValue", "HasPlatformType", "MemberVisibilityCanBePrivate", "unused")
object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<Block>>, Block>() {
@@ -244,9 +243,9 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<
val REVIVAL_HERB = this.create("revival_herb", RevivalHerbBlock(BlockBehaviour.Properties.of().pushReaction(PushReaction.DESTROY).mapColor(MapColor.PLANT).ignitedByLava().noCollission().instabreak().sound(CobblemonSounds.REVIVAL_HERB_SOUNDS)))
@JvmField
val POKE_CAKE = this.create("poke_cake", PokeSnackBlock(BlockBehaviour.Properties.of().forceSolidOn().strength(0.5F).sound(SoundType.WOOL).pushReaction(PushReaction.DESTROY).lightLevel { if (it.getValue(BlockStateProperties.LIT)) 3 else 0 }, false))
val POKE_CAKE = this.create("poke_cake", PokeSnackBlock(BlockBehaviour.Properties.of().forceSolidOn().strength(0.5F).sound(SoundType.WOOL).pushReaction(PushReaction.DESTROY).lightLevel { if (it.getValue(LIT)) 3 else 0 }, false))
@JvmField
val POKE_SNACK = this.create("poke_snack", PokeSnackBlock(BlockBehaviour.Properties.of().forceSolidOn().strength(0.5F).sound(SoundType.WOOL).pushReaction(PushReaction.DESTROY).lightLevel { if (it.getValue(BlockStateProperties.LIT)) 3 else 0 }, true))
val POKE_SNACK = this.create("poke_snack", PokeSnackBlock(BlockBehaviour.Properties.of().forceSolidOn().strength(0.5F).sound(SoundType.WOOL).pushReaction(PushReaction.DESTROY).lightLevel { if (it.getValue(LIT)) 3 else 0 }, true))
@JvmField
val TUMBLESTONE_CLUSTER = tumblestoneBlock("tumblestone_cluster", GrowableStoneBlock.STAGE_3, 7, 3, null)
@@ -478,99 +477,6 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<
@JvmField
val GIMMIGHOUL_CHEST = create("gimmighoul_chest", GildedChestBlock(BlockBehaviour.Properties.ofFullCopy(Blocks.CHEST).noOcclusion().sound(CobblemonSounds.GILDED_CHEST_SOUNDS), GildedChestBlock.Type.FAKE))
@JvmField
val WHITE_PLAQUE = create("white_plaque", PlaqueBlock(BlockBehaviour.Properties.of().sound(SoundType.METAL).mapColor(MapColor.METAL).noOcclusion().pushReaction(PushReaction.DESTROY).strength(5.0F)))
@JvmField
val LIGHT_GRAY_PLAQUE = create("light_gray_plaque", PlaqueBlock(BlockBehaviour.Properties.of().sound(SoundType.METAL).mapColor(MapColor.METAL).noOcclusion().pushReaction(PushReaction.DESTROY).strength(5.0F)))
@JvmField
val GRAY_PLAQUE = create("gray_plaque", PlaqueBlock(BlockBehaviour.Properties.of().sound(SoundType.METAL).mapColor(MapColor.METAL).noOcclusion().pushReaction(PushReaction.DESTROY).strength(5.0F)))
@JvmField
val BLACK_PLAQUE = create("black_plaque", PlaqueBlock(BlockBehaviour.Properties.of().sound(SoundType.METAL).mapColor(MapColor.METAL).noOcclusion().pushReaction(PushReaction.DESTROY).strength(5.0F)))
@JvmField
val BROWN_PLAQUE = create("brown_plaque", PlaqueBlock(BlockBehaviour.Properties.of().sound(SoundType.METAL).mapColor(MapColor.METAL).noOcclusion().pushReaction(PushReaction.DESTROY).strength(5.0F)))
@JvmField
val RED_PLAQUE = create("red_plaque", PlaqueBlock(BlockBehaviour.Properties.of().sound(SoundType.METAL).mapColor(MapColor.METAL).noOcclusion().pushReaction(PushReaction.DESTROY).strength(5.0F)))
@JvmField
val ORANGE_PLAQUE = create("orange_plaque", PlaqueBlock(BlockBehaviour.Properties.of().sound(SoundType.METAL).mapColor(MapColor.METAL).noOcclusion().pushReaction(PushReaction.DESTROY).strength(5.0F)))
@JvmField
val YELLOW_PLAQUE = create("yellow_plaque", PlaqueBlock(BlockBehaviour.Properties.of().sound(SoundType.METAL).mapColor(MapColor.METAL).noOcclusion().pushReaction(PushReaction.DESTROY).strength(5.0F)))
@JvmField
val LIME_PLAQUE = create("lime_plaque", PlaqueBlock(BlockBehaviour.Properties.of().sound(SoundType.METAL).mapColor(MapColor.METAL).noOcclusion().pushReaction(PushReaction.DESTROY).strength(5.0F)))
@JvmField
val GREEN_PLAQUE = create("green_plaque", PlaqueBlock(BlockBehaviour.Properties.of().sound(SoundType.METAL).mapColor(MapColor.METAL).noOcclusion().pushReaction(PushReaction.DESTROY).strength(5.0F)))
@JvmField
val CYAN_PLAQUE = create("cyan_plaque", PlaqueBlock(BlockBehaviour.Properties.of().sound(SoundType.METAL).mapColor(MapColor.METAL).noOcclusion().pushReaction(PushReaction.DESTROY).strength(5.0F)))
@JvmField
val LIGHT_BLUE_PLAQUE = create("light_blue_plaque", PlaqueBlock(BlockBehaviour.Properties.of().sound(SoundType.METAL).mapColor(MapColor.METAL).noOcclusion().pushReaction(PushReaction.DESTROY).strength(5.0F)))
@JvmField
val BLUE_PLAQUE = create("blue_plaque", PlaqueBlock(BlockBehaviour.Properties.of().sound(SoundType.METAL).mapColor(MapColor.METAL).noOcclusion().pushReaction(PushReaction.DESTROY).strength(5.0F)))
@JvmField
val PURPLE_PLAQUE = create("purple_plaque", PlaqueBlock(BlockBehaviour.Properties.of().sound(SoundType.METAL).mapColor(MapColor.METAL).noOcclusion().pushReaction(PushReaction.DESTROY).strength(5.0F)))
@JvmField
val MAGENTA_PLAQUE = create("magenta_plaque", PlaqueBlock(BlockBehaviour.Properties.of().sound(SoundType.METAL).mapColor(MapColor.METAL).noOcclusion().pushReaction(PushReaction.DESTROY).strength(5.0F)))
@JvmField
val PINK_PLAQUE = create("pink_plaque", PlaqueBlock(BlockBehaviour.Properties.of().sound(SoundType.METAL).mapColor(MapColor.METAL).noOcclusion().pushReaction(PushReaction.DESTROY).strength(5.0F)))
@JvmField
val CLEANSE_TAG = create("cleanse_tag", WallAttachedStackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.WOOD).mapColor(MapColor.NONE).pushReaction(PushReaction.DESTROY).noOcclusion().noCollission(), WallAttachedStackableItemBlock.WallStackableItemBlockType.TAG))
@JvmField
val SPELL_TAG = create("spell_tag", WallAttachedStackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.WOOD).mapColor(MapColor.NONE).pushReaction(PushReaction.DESTROY).noOcclusion().noCollission(), WallAttachedStackableItemBlock.WallStackableItemBlockType.TAG))
@JvmField
val BLUNDER_POLICY = create("blunder_policy", WallAttachedDirectionalShapeBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion().noCollission(), 14, 12, 1))
@JvmField
val WEAKNESS_POLICY = create("weakness_policy", WallAttachedDirectionalShapeBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion().noCollission(), 14, 12, 1))
@JvmField
val FULL_HEAL = create("full_heal", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.STATUS_RESTORE))
@JvmField
val ANTIDOTE = create("antidote", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.STATUS_RESTORE))
@JvmField
val AWAKENING = create("awakening", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.STATUS_RESTORE))
@JvmField
val BURN_HEAL = create("burn_heal", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.STATUS_RESTORE))
@JvmField
val ICE_HEAL = create("ice_heal", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.STATUS_RESTORE))
@JvmField
val PARALYZE_HEAL = create("paralyze_heal", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.STATUS_RESTORE))
@JvmField
val ETHER = create("ether", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.ETHER))
@JvmField
val MAX_ETHER = create("max_ether", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.ETHER))
@JvmField
val ELIXIR = create("elixir", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.ETHER))
@JvmField
val MAX_ELIXIR = create("max_elixir", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.ETHER))
@JvmField
val POTION = create("potion", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.POTION))
@JvmField
val SUPER_POTION = create("super_potion", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.POTION))
@JvmField
val HYPER_POTION = create("hyper_potion", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.POTION))
@JvmField
val MAX_POTION = create("max_potion", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.MAX_POTION))
@JvmField
val FULL_RESTORE = create("full_restore", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.MAX_POTION))
@JvmField
val DIRE_HIT = create("dire_hit", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.BATTLE_ITEM))
@JvmField
val GUARD_SPEC = create("guard_spec", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.BATTLE_ITEM))
@JvmField
val X_ACCURACY = create("x_${Stats.ACCURACY.identifier.path}", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.BATTLE_ITEM))
@JvmField
val X_ATTACK = create("x_${Stats.ATTACK.identifier.path}", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.BATTLE_ITEM))
@JvmField
val X_DEFENSE = create("x_${Stats.DEFENCE.identifier.path}", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.BATTLE_ITEM))
@JvmField
val X_SP_ATK = create("x_${Stats.SPECIAL_ATTACK.identifier.path}", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.BATTLE_ITEM))
@JvmField
val X_SP_DEF = create("x_${Stats.SPECIAL_DEFENCE.identifier.path}", StackableItemBlock(BlockBehaviour.Properties.of().sound(SoundType.CANDLE).mapColor(MapColor.NONE).noOcclusion(), StackableItemBlock.StackableItemBlockType.BATTLE_ITEM))
@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 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
@@ -581,9 +487,6 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<
@JvmField
val HEALING_MACHINE = create("healing_machine", HealingMachineBlock(BlockBehaviour.Properties.of().mapColor(MapColor.METAL).sound(SoundType.METAL).pushReaction(PushReaction.BLOCK).strength(2f).noOcclusion().lightLevel { if (it.getValue(HealingMachineBlock.CHARGE_LEVEL) >= HealingMachineBlock.MAX_CHARGE_LEVEL) 12 else 5 }))
@JvmField
val STADIUM_BLOCK = create("stadium_block", StadiumBlock(BlockBehaviour.Properties.of().mapColor(MapColor.METAL).sound(SoundType.METAL).strength(3.5F, 6.0F).noOcclusion().pushReaction(PushReaction.BLOCK)))
@JvmField
val PC = create("pc", PCBlock(BlockBehaviour.Properties.of().mapColor(MapColor.METAL).sound(SoundType.METAL).pushReaction(PushReaction.BLOCK).strength(2F).noOcclusion().lightLevel { if ((it.getValue(PCBlock.ON) as Boolean) && (it.getValue(PCBlock.PART) == PCBlock.PCPart.TOP)) 13 else 0 }))
@@ -15,11 +15,7 @@ import net.minecraft.commands.CommandSourceStack
import net.minecraft.commands.Commands
object CobblemonCommands {
fun register(
dispatcher: CommandDispatcher<CommandSourceStack>,
registry: CommandBuildContext,
selection: Commands.CommandSelection
) {
fun register(dispatcher: CommandDispatcher<CommandSourceStack>, registry: CommandBuildContext, selection: Commands.CommandSelection) {
SpawnPokemon.register(dispatcher)
SpawnNPCCommand.register(dispatcher)
GivePokemon.register(dispatcher)
@@ -80,8 +76,5 @@ object CobblemonCommands {
// with a certain system environment variable set
CobblemonInfoCommand.register(dispatcher)
CalculateSeatPositionCommand.register(dispatcher)
CameraDebugCommand.register(dispatcher)
StadiumBattleCommand.register(dispatcher)
MolangUiCommand.register(dispatcher)
}
}
@@ -117,11 +117,6 @@ interface CobblemonImplementation {
fun registerEntitySubPredicates()
/**
* TODO
*/
fun registerMobEffects()
/**
* Add a feature to the current platform implementation.
@@ -39,7 +39,6 @@ import com.cobblemon.mod.common.item.interactive.PonigiriItem
import com.cobblemon.mod.common.item.food.SinisterTeaItem
import com.cobblemon.mod.common.item.interactive.*
import com.cobblemon.mod.common.item.interactive.ability.AbilityChangeItem
import com.cobblemon.mod.common.item.stadium.StadiumPlacementWandItem
import com.cobblemon.mod.common.platform.PlatformRegistry
import com.cobblemon.mod.common.pokeball.PokeBall
import com.cobblemon.mod.common.pokemon.IVs
@@ -76,9 +75,6 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val NPC_EDITOR = create("npc_editor", CobblemonItem(Item.Properties().stacksTo(1)))
@JvmField
val STADIUM_PLACEMENT_WAND = create("stadium_placement_wand", StadiumPlacementWandItem())
@JvmField
val pokeBalls = mutableListOf<PokeBallItem>()
@JvmField
@@ -464,8 +460,6 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
val PASTURE = blockItem("pasture", CobblemonBlocks.PASTURE)
@JvmField
val DISPLAY_CASE = blockItem("display_case", CobblemonBlocks.DISPLAY_CASE)
@JvmField
val STADIUM_BLOCK = blockItem("stadium_block", CobblemonBlocks.STADIUM_BLOCK)
// @JvmField
// val INCENSE_SWEET = blockItem("incense_sweet", CobblemonBlocks.INCENSE_SWEET) // TODO after 1.7
@@ -513,23 +507,6 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField val SCROLL_OF_DARKNESS = noSettingsItem("scroll_of_darkness")
@JvmField val SCROLL_OF_WATERS = noSettingsItem("scroll_of_waters")
@JvmField val WHITE_PLAQUE = blockItem("white_plaque", CobblemonBlocks.WHITE_PLAQUE)
@JvmField val LIGHT_GRAY_PLAQUE = blockItem("light_gray_plaque", CobblemonBlocks.LIGHT_GRAY_PLAQUE)
@JvmField val GRAY_PLAQUE = blockItem("gray_plaque", CobblemonBlocks.GRAY_PLAQUE)
@JvmField val BLACK_PLAQUE = blockItem("black_plaque", CobblemonBlocks.BLACK_PLAQUE)
@JvmField val BROWN_PLAQUE = blockItem("brown_plaque", CobblemonBlocks.BROWN_PLAQUE)
@JvmField val RED_PLAQUE = blockItem("red_plaque", CobblemonBlocks.RED_PLAQUE)
@JvmField val ORANGE_PLAQUE = blockItem("orange_plaque", CobblemonBlocks.ORANGE_PLAQUE)
@JvmField val YELLOW_PLAQUE = blockItem("yellow_plaque", CobblemonBlocks.YELLOW_PLAQUE)
@JvmField val LIME_PLAQUE = blockItem("lime_plaque", CobblemonBlocks.LIME_PLAQUE)
@JvmField val GREEN_PLAQUE = blockItem("green_plaque", CobblemonBlocks.GREEN_PLAQUE)
@JvmField val CYAN_PLAQUE = blockItem("cyan_plaque", CobblemonBlocks.CYAN_PLAQUE)
@JvmField val LIGHT_BLUE_PLAQUE = blockItem("light_blue_plaque", CobblemonBlocks.LIGHT_BLUE_PLAQUE)
@JvmField val BLUE_PLAQUE = blockItem("blue_plaque", CobblemonBlocks.BLUE_PLAQUE)
@JvmField val PURPLE_PLAQUE = blockItem("purple_plaque", CobblemonBlocks.PURPLE_PLAQUE)
@JvmField val MAGENTA_PLAQUE = blockItem("magenta_plaque", CobblemonBlocks.MAGENTA_PLAQUE)
@JvmField val PINK_PLAQUE = blockItem("pink_plaque", CobblemonBlocks.PINK_PLAQUE)
private val berries = mutableMapOf<ResourceLocation, BerryItem>()
// Plants
@JvmField val CHERI_BERRY = berryItem("cheri", StatusCuringBerryItem(CobblemonBlocks.CHERI_BERRY, Statuses.PARALYSIS))
@@ -722,15 +699,15 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
val MOOMOO_MILK = create("moomoo_milk", MoomooMilk(Item.Properties().craftRemainder(Items.GLASS_BOTTLE).stacksTo((16))))
@JvmField
val POTION = create("potion", PotionItem(PotionType.POTION, CobblemonBlocks.POTION))
val POTION = create("potion", PotionItem(PotionType.POTION))
@JvmField
val SUPER_POTION = create("super_potion", PotionItem(PotionType.SUPER_POTION, CobblemonBlocks.SUPER_POTION))
val SUPER_POTION = create("super_potion", PotionItem(PotionType.SUPER_POTION))
@JvmField
val HYPER_POTION = create("hyper_potion", PotionItem(PotionType.HYPER_POTION, CobblemonBlocks.HYPER_POTION))
val HYPER_POTION = create("hyper_potion", PotionItem(PotionType.HYPER_POTION))
@JvmField
val MAX_POTION = create("max_potion", PotionItem(PotionType.MAX_POTION, CobblemonBlocks.MAX_POTION))
val MAX_POTION = create("max_potion", PotionItem(PotionType.MAX_POTION))
@JvmField
val FULL_RESTORE = create("full_restore", PotionItem(PotionType.FULL_RESTORE, CobblemonBlocks.FULL_RESTORE))
val FULL_RESTORE = create("full_restore", PotionItem(PotionType.FULL_RESTORE))
@JvmField
val HEAL_POWDER = create("heal_powder", HealPowderItem())
@@ -815,27 +792,28 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val SERIOUS_MINT = mintItem("serious_mint", MintItem(Natures.SERIOUS))
@JvmField val X_ACCURACY = create("x_${Stats.ACCURACY.identifier.path}", XStatItem(Stats.ACCURACY, block = CobblemonBlocks.X_ACCURACY))
@JvmField val X_ATTACK = create("x_${Stats.ATTACK.identifier.path}", XStatItem(Stats.ATTACK, block = CobblemonBlocks.X_ATTACK))
@JvmField val X_DEFENSE = create("x_${Stats.DEFENCE.identifier.path}", XStatItem(Stats.DEFENCE, block = CobblemonBlocks.X_DEFENSE))
@JvmField val X_SP_ATK = create("x_${Stats.SPECIAL_ATTACK.identifier.path}", XStatItem(Stats.SPECIAL_ATTACK, block = CobblemonBlocks.X_SP_ATK))
@JvmField val X_SP_DEF = create("x_${Stats.SPECIAL_DEFENCE.identifier.path}", XStatItem(Stats.SPECIAL_DEFENCE, block = CobblemonBlocks.X_SP_DEF))
@JvmField val X_SPEED = create("x_${Stats.SPEED.identifier.path}", XStatItem(Stats.SPEED, block = CobblemonBlocks.X_SPEED))
@JvmField val X_ACCURACY = create("x_${Stats.ACCURACY.identifier.path}", XStatItem(Stats.ACCURACY))
@JvmField val X_ATTACK = create("x_${Stats.ATTACK.identifier.path}", XStatItem(Stats.ATTACK))
@JvmField val X_DEFENSE = create("x_${Stats.DEFENCE.identifier.path}", XStatItem(Stats.DEFENCE))
@JvmField val X_SP_ATK = create("x_${Stats.SPECIAL_ATTACK.identifier.path}", XStatItem(Stats.SPECIAL_ATTACK))
@JvmField val X_SP_DEF = create("x_${Stats.SPECIAL_DEFENCE.identifier.path}", XStatItem(Stats.SPECIAL_DEFENCE))
@JvmField val X_SPEED = create("x_${Stats.SPEED.identifier.path}", XStatItem(Stats.SPEED))
@JvmField val DIRE_HIT = create("dire_hit", DireHitItem(CobblemonBlocks.DIRE_HIT))
@JvmField val GUARD_SPEC = create("guard_spec", GuardSpecItem(CobblemonBlocks.GUARD_SPEC))
@JvmField val DIRE_HIT = create("dire_hit", DireHitItem())
@JvmField val GUARD_SPEC = create("guard_spec", GuardSpecItem())
@JvmField val FULL_HEAL = create("full_heal", StatusCureItem("item.cobblemon.full_heal", *Statuses.getPersistentStatuses().toTypedArray(), block = CobblemonBlocks.FULL_HEAL))
@JvmField val ANTIDOTE = create("antidote", StatusCureItem("item.cobblemon.antidote", Statuses.POISON, Statuses.POISON_BADLY, block = CobblemonBlocks.ANTIDOTE))
@JvmField val AWAKENING = create("awakening", StatusCureItem("item.cobblemon.awakening", Statuses.SLEEP, block = CobblemonBlocks.AWAKENING))
@JvmField val BURN_HEAL = create("burn_heal", StatusCureItem("item.cobblemon.burn_heal", Statuses.BURN, block = CobblemonBlocks.BURN_HEAL))
@JvmField val ICE_HEAL = create("ice_heal", StatusCureItem("item.cobblemon.ice_heal", Statuses.FROZEN, block = CobblemonBlocks.ICE_HEAL))
@JvmField val PARALYZE_HEAL = create("paralyze_heal", StatusCureItem("item.cobblemon.paralyze_heal", Statuses.PARALYSIS, block = CobblemonBlocks.PARALYZE_HEAL))
@JvmField val BURN_HEAL = create("burn_heal", StatusCureItem("item.cobblemon.burn_heal", Statuses.BURN))
@JvmField val PARALYZE_HEAL = create("paralyze_heal", StatusCureItem("item.cobblemon.paralyze_heal", Statuses.PARALYSIS))
@JvmField val ICE_HEAL = create("ice_heal", StatusCureItem("item.cobblemon.ice_heal", Statuses.FROZEN))
@JvmField val ANTIDOTE = create("antidote", StatusCureItem("item.cobblemon.antidote", Statuses.POISON, Statuses.POISON_BADLY))
@JvmField val AWAKENING = create("awakening", StatusCureItem("item.cobblemon.awakening", Statuses.SLEEP))
@JvmField val ETHER = create("ether", EtherItem(max = false, CobblemonBlocks.ETHER))
@JvmField val MAX_ETHER = create("max_ether", EtherItem(max = true, CobblemonBlocks.MAX_ETHER))
@JvmField val ELIXIR = create("elixir", ElixirItem(max = false, CobblemonBlocks.ELIXIR))
@JvmField val MAX_ELIXIR = create("max_elixir", ElixirItem(max = true, CobblemonBlocks.MAX_ELIXIR))
@JvmField val FULL_HEAL = create("full_heal", StatusCureItem("item.cobblemon.full_heal"))
@JvmField val ETHER = create("ether", EtherItem(max = false))
@JvmField val MAX_ETHER = create("max_ether", EtherItem(max = true))
@JvmField val ELIXIR = create("elixir", ElixirItem(max = false))
@JvmField val MAX_ELIXIR = create("max_elixir", ElixirItem(max = true))
@JvmField
val ABILITY_CAPSULE = this.create("ability_capsule", AbilityChangeItem(AbilityChanger.COMMON_ABILITY))
@@ -944,7 +922,7 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val BLACK_SLUDGE = heldItem("black_sludge")
@JvmField
val BLUNDER_POLICY = itemNameBlockItem("blunder_policy", CobblemonBlocks.BLUNDER_POLICY)
val BLUNDER_POLICY = heldItem("blunder_policy")
@JvmField
val CELL_BATTERY = heldItem("cell_battery")
@JvmField
@@ -952,7 +930,7 @@ 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)
val CLEANSE_TAG = heldItem("cleanse_tag")
@JvmField
val CLEAR_AMULET = heldItem("clear_amulet")
@JvmField
@@ -1060,7 +1038,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)
val SPELL_TAG = heldItem("spell_tag")
@JvmField
val SMOKE_BALL = heldItem("smoke_ball")
@JvmField
@@ -1078,7 +1056,7 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val UTILITY_UMBRELLA = heldItem("utility_umbrella")
@JvmField
val WEAKNESS_POLICY = itemNameBlockItem("weakness_policy", CobblemonBlocks.WEAKNESS_POLICY)
val WEAKNESS_POLICY = heldItem("weakness_policy")
@JvmField
val WIDE_LENS = heldItem("wide_lens")
@JvmField
@@ -1575,7 +1553,6 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
}
}
)
private fun heldItem(name: String, item: Item, remappedName: String? = null) = create(
name = name,
entry = item.also {
@@ -9,7 +9,6 @@
package com.cobblemon.mod.common
import com.cobblemon.mod.common.api.molangui.container.MolangContainerMenu
import com.cobblemon.mod.common.block.campfirepot.CookingPotMenu
import com.cobblemon.mod.common.platform.PlatformRegistry
import net.minecraft.core.Registry
@@ -21,7 +20,6 @@ import net.minecraft.world.inventory.MenuType
object CobblemonMenuType : PlatformRegistry<Registry<MenuType<*>>, ResourceKey<Registry<MenuType<*>>>, MenuType<*>>() {
val COOKING_POT = MenuType.register("cooking_pot", ::CookingPotMenu)
val MOLANG_UI = MenuType.register("molang_ui", ::MolangContainerMenu)
override val registry: Registry<MenuType<*>>
get() = BuiltInRegistries.MENU
@@ -1,33 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common
import com.cobblemon.mod.common.api.cooking.effects.CleanseAllEffect
import com.cobblemon.mod.common.api.cooking.effects.CleanseNegativeEffect
import com.cobblemon.mod.common.api.cooking.effects.MentalRestorationEffect
import com.cobblemon.mod.common.platform.PlatformRegistry
import net.minecraft.core.Registry
import net.minecraft.core.registries.BuiltInRegistries
import net.minecraft.core.registries.Registries
import net.minecraft.resources.ResourceKey
import net.minecraft.world.effect.MobEffect
object CobblemonMobEffects : PlatformRegistry<Registry<MobEffect>, ResourceKey<Registry<MobEffect>>, MobEffect>() {
override val registry: Registry<MobEffect> = BuiltInRegistries.MOB_EFFECT
override val resourceKey: ResourceKey<Registry<MobEffect>> = Registries.MOB_EFFECT
@JvmField
val CLEANSE_NEGATIVE = create("cleanse_negative", CleanseNegativeEffect())
@JvmField
val MENTAL_RESTORATION = create("mental_restoration", MentalRestorationEffect())
@JvmField
val CLEANSE_ALL = create("cleanse_all", CleanseAllEffect())
}
File diff suppressed because it is too large Load Diff
@@ -17,7 +17,6 @@ import com.cobblemon.mod.common.api.molang.MoLangFunctions.asMostSpecificMoLangV
import com.cobblemon.mod.common.entity.OmniPathingEntity
import com.cobblemon.mod.common.entity.ai.CobblemonRandomSurfacePos
import com.cobblemon.mod.common.entity.ai.CobblemonWalkTarget
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.util.asExpression
import com.cobblemon.mod.common.util.mainThreadRuntime
import com.cobblemon.mod.common.util.resolveFloat
@@ -185,12 +184,6 @@ class WanderTaskConfig : SingleTaskConfig {
if (targetVec == null) continue
// Ignore pathing targets outside of the pasture zone if the pokemon is pastured.
val tethering = (entity as? PokemonEntity)?.tethering
if (tethering?.canRoamTo(BlockPos.containing(targetVec)) == false) {
continue
}
pos = applyHeightConstraints(
pos = BlockPos.containing(targetVec),
minimumHeight = minimumHeight,
@@ -22,21 +22,18 @@ import com.cobblemon.mod.common.api.battles.model.actor.EntityBackedBattleActor
import com.cobblemon.mod.common.api.battles.model.actor.FleeableBattleActor
import com.cobblemon.mod.common.api.events.CobblemonEvents
import com.cobblemon.mod.common.api.events.battles.BattleFledEvent
import com.cobblemon.mod.common.api.events.battles.BattleFleeAttemptEvent
import com.cobblemon.mod.common.api.molang.MoLangFunctions.asMoLangValue
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.api.pokemon.stats.BattleEvSource
import com.cobblemon.mod.common.api.storage.party.PlayerPartyStore
import com.cobblemon.mod.common.api.tags.CobblemonItemTags
import com.cobblemon.mod.common.api.text.red
import com.cobblemon.mod.common.api.text.white
import com.cobblemon.mod.common.api.text.yellow
import com.cobblemon.mod.common.battles.ActiveBattlePokemon
import com.cobblemon.mod.common.battles.BattleCaptureAction
import com.cobblemon.mod.common.battles.BattleFormat
import com.cobblemon.mod.common.battles.BattleRegistry
import com.cobblemon.mod.common.battles.BattleSide
import com.cobblemon.mod.common.battles.FleeAttemptActionResponse
import com.cobblemon.mod.common.battles.ForfeitActionResponse
import com.cobblemon.mod.common.battles.actor.PlayerBattleActor
import com.cobblemon.mod.common.battles.dispatch.BattleDispatch
@@ -51,9 +48,7 @@ import com.cobblemon.mod.common.entity.npc.NPCBattleActor
import com.cobblemon.mod.common.entity.npc.NPCEntity
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.net.messages.client.battle.BattleEndPacket
import com.cobblemon.mod.common.net.messages.client.battle.BattleMakeChoicePacket
import com.cobblemon.mod.common.net.messages.client.battle.BattleMessagePacket
import com.cobblemon.mod.common.net.messages.client.battle.BattleQueueRequestPacket
import com.cobblemon.mod.common.pokemon.evolution.progress.DefeatEvolutionProgress
import com.cobblemon.mod.common.pokemon.evolution.progress.LastBattleCriticalHitsEvolutionProgress
import com.cobblemon.mod.common.pokemon.requirements.DefeatRequirement
@@ -85,9 +80,7 @@ open class PokemonBattle(
/** Whether logging will be silenced for this battle. */
var mute = !CobblemonBuildDetails.SNAPSHOT
val struct = this.asMoLangValue()
val runtime = MoLangRuntime().also {
it.environment.query = struct
}
val runtime = MoLangRuntime().also { it.environment.query = struct }
val onEndHandlers: MutableList<(PokemonBattle) -> Unit> = mutableListOf()
@@ -144,9 +137,6 @@ open class PokemonBattle(
val captureActions = mutableListOf<BattleCaptureAction>()
val majorBattleActions = hashMapOf<UUID, BattleMessage>()
/** Monotonically increasing counter to track faint ordering within this battle. */
var faintCounter = 0
val minorBattleActions = hashMapOf<UUID, BattleMessage>()
val contextManager = ContextManager()
@@ -254,23 +244,12 @@ open class PokemonBattle(
fun end() {
ended = true
val awardToFainted = Cobblemon.config.awardExperienceToFaintedPokemon
val awardOnLoss = Cobblemon.config.awardExperienceOnBattleLoss
this.actors.forEach { actor ->
val faintedPokemons = actor.pokemonList.filter { it.health <= 0 }
actor.getSide().getOppositeSide().actors.forEach { opponent ->
val opponentPokemons = if (awardToFainted) opponent.pokemonList else opponent.pokemonList.filter { it.health > 0 }
val opponentNonFaintedPokemons = opponent.pokemonList.filter { it.health > 0 }
faintedPokemons.forEach { faintedPokemon ->
for (opponentPokemon in opponentPokemons) {
// Faint-order check: if awarding XP to fainted pokemon, only award for enemies that fainted before this pokemon did
if (awardToFainted) {
val enemyFaintedAt = faintedPokemon.faintedAt ?: continue
val opponentFaintedAt = opponentPokemon.faintedAt
if (opponentFaintedAt != null && opponentFaintedAt <= enemyFaintedAt) {
continue
}
}
for (opponentPokemon in opponentNonFaintedPokemons) {
val facedFainted = opponentPokemon.facedOpponents.contains(faintedPokemon)
val pokemon = opponentPokemon.effectedPokemon
if (facedFainted) {
@@ -291,8 +270,7 @@ open class PokemonBattle(
else -> continue
}
val experience = Cobblemon.experienceCalculator.calculate(opponentPokemon, faintedPokemon, multiplier)
val enemyTeamWiped = actor.pokemonList.all { it.health <= 0 }
if (experience > 0 && (enemyTeamWiped || awardOnLoss)) {
if (experience > 0 && actor.pokemonList.all { it.health <= 0 }) {
opponent.awardExperience(opponentPokemon, experience)
}
Cobblemon.evYieldCalculator.calculate(opponentPokemon, faintedPokemon).forEach { (stat, amount) ->
@@ -326,8 +304,7 @@ open class PokemonBattle(
.forEach{it.pokemon.heal()}
actors.forEach { actor ->
actor.pokemonList.forEach { battlePokemon ->
battlePokemon.postBattlePokemonOperations.forEach { it(battlePokemon) }
battlePokemon.entity?.let { entity -> battlePokemon.postBattleEntityOperations.forEach { it(entity) } }
battlePokemon.entity?.let { entity -> battlePokemon.postBattleEntityOperation(entity) }
if (battlePokemon.effectedPokemon.entity != null
&& battlePokemon.effectedPokemon.entity?.beamMode == 0
&& battlePokemon.effectedPokemon.entity?.platform != PlatformType.NONE) {
@@ -521,13 +498,12 @@ open class PokemonBattle(
}
fun stop() {
writeShowdownAction(">forcetie") // This will terminate the Showdown connection
end()
writeShowdownAction(">forcetie") // This will terminate the Showdown connection
}
fun checkForInputDispatch() {
if (checkForfeit()) return // ignore actors that are still choosing, their choices don't matter anymore
if (checkFleeAttempt()) return
val readyToInput = (actors.any { !it.mustChoose && it.responses.isNotEmpty() } && actors.none { it.mustChoose })
if (readyToInput && captureActions.isEmpty()) {
actors.filter { it.responses.isNotEmpty() }.forEach { it.writeShowdownResponse() }
@@ -535,20 +511,6 @@ open class PokemonBattle(
}
}
private fun checkFleeAttempt(): Boolean {
val runner = actors.find { it.responses.any { it is FleeAttemptActionResponse } } as? PlayerBattleActor ?: return false
CobblemonEvents.BATTLE_FLEE_ATTEMPT.post(BattleFleeAttemptEvent(this, runner))
runner.responses.clear()
runner.mustChoose = true
runner.request?.let { runner.sendUpdate(BattleQueueRequestPacket(it)) }
runner.sendUpdate(BattleMakeChoicePacket())
runner.entity?.sendSystemMessage(battleLang("run_prompt").white())
return true
}
/** Forces Showdown to end the battle when a [BattleActor] chooses to forfeit. */
private fun checkForfeit(): Boolean {
val forfeit = actors.find { it.responses.any { it is ForfeitActionResponse } }
@@ -26,9 +26,8 @@ import com.cobblemon.mod.common.net.messages.client.battle.BattleMakeChoicePacke
import com.cobblemon.mod.common.net.messages.client.battle.BattleMessagePacket
import net.minecraft.network.chat.Component
import net.minecraft.network.chat.MutableComponent
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.phys.Vec3
import java.util.UUID
import net.minecraft.server.level.ServerPlayer
abstract class BattleActor(
val uuid: UUID,
@@ -55,11 +54,6 @@ abstract class BattleActor(
var itemsUsed = mutableListOf<BagItem>()
/** For when battles start, it's the number of Pokémon that are still in the process of being sent out (animation wise) */
var stillSendingOutCount = 0
/**
* Optional override for send-out positions. When present the indexes follow the
* actor's active slot order (e.g. slot `a` -> index 0).
*/
var stadiumSendOutPositions: List<Vec3>? = null
abstract val type: ActorType
@@ -1,200 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.camera
import com.cobblemon.mod.common.Cobblemon
import net.minecraft.resources.ResourceKey
import net.minecraft.server.MinecraftServer
import net.minecraft.server.level.ServerLevel
import net.minecraft.world.level.ChunkPos
import net.minecraft.world.level.Level
import net.minecraft.world.level.chunk.LevelChunk
import net.minecraft.world.level.chunk.status.ChunkStatus
import java.util.ArrayDeque
import java.util.UUID
/**
* Handles chunk forceloading for cameras.
* Chunks are queued and processed in small batches each tick to avoid overwhelming the server.
*/
object CameraChunkLoader {
private const val MAX_CHUNKS_PER_TICK = 16
private data class CameraState(
var dimension: ResourceKey<Level>,
val chunks: MutableSet<Long>
)
private data class ChunkRequest(
val dimension: ResourceKey<Level>,
val chunkPos: ChunkPos,
val load: Boolean
)
private val cameraStates = mutableMapOf<UUID, CameraState>()
private val chunkOwners = mutableMapOf<ResourceKey<Level>, MutableMap<Long, MutableSet<UUID>>>()
private val pendingRequests: ArrayDeque<ChunkRequest> = ArrayDeque()
private val pendingAsyncLoads = mutableSetOf<Pair<ResourceKey<Level>, Long>>()
fun ensureArea(level: ServerLevel, cameraId: UUID, centerChunk: ChunkPos, radius: Int) {
val dimension = level.dimension()
val state = cameraStates[cameraId]?.takeIf { it.dimension == dimension } ?: run {
cameraStates.remove(cameraId)?.let { oldState ->
oldState.chunks.forEach { chunkLong ->
removeOwnership(oldState.dimension, chunkLong, cameraId)
}
}
CameraState(dimension, mutableSetOf()).also { cameraStates[cameraId] = it }
}
val newChunks = buildChunkSet(centerChunk, radius)
val currentChunks = state.chunks
val toRemove = currentChunks.minus(newChunks)
val toAdd = newChunks.minus(currentChunks)
if (toRemove.isNotEmpty()) {
toRemove.forEach { chunkLong ->
removeOwnership(dimension, chunkLong, cameraId)
currentChunks.remove(chunkLong)
}
}
if (toAdd.isNotEmpty()) {
toAdd.forEach { chunkLong ->
addOwnership(dimension, chunkLong, cameraId)
currentChunks.add(chunkLong)
}
}
}
fun clear(cameraId: UUID) {
val state = cameraStates.remove(cameraId) ?: return
val dimension = state.dimension
state.chunks.forEach { chunkLong ->
removeOwnership(dimension, chunkLong, cameraId)
}
}
fun tick(server: MinecraftServer) {
if (pendingRequests.isEmpty()) {
return
}
var processed = 0
while (processed < MAX_CHUNKS_PER_TICK && pendingRequests.isNotEmpty()) {
val request = pendingRequests.removeFirst()
val level = server.getLevel(request.dimension) ?: continue
level.setChunkForced(request.chunkPos.x, request.chunkPos.z, request.load)
if (request.load) {
scheduleAsyncLoad(level, request.dimension, request.chunkPos)
}
processed++
}
}
private fun addOwnership(dimension: ResourceKey<Level>, chunkLong: Long, cameraId: UUID) {
val ownersForDimension = chunkOwners.computeIfAbsent(dimension) { mutableMapOf() }
val owners = ownersForDimension.computeIfAbsent(chunkLong) { mutableSetOf() }
if (owners.add(cameraId) && owners.size == 1) {
pendingRequests.add(
ChunkRequest(
dimension,
ChunkPos(chunkLong),
true
)
)
}
}
private fun removeOwnership(dimension: ResourceKey<Level>, chunkLong: Long, cameraId: UUID) {
val ownersForDimension = chunkOwners[dimension] ?: return
val owners = ownersForDimension[chunkLong] ?: return
if (!owners.remove(cameraId)) {
return
}
if (owners.isEmpty()) {
ownersForDimension.remove(chunkLong)
if (ownersForDimension.isEmpty()) {
chunkOwners.remove(dimension)
}
pendingRequests.add(
ChunkRequest(
dimension,
ChunkPos(chunkLong),
false
)
)
}
}
private fun scheduleAsyncLoad(level: ServerLevel, dimension: ResourceKey<Level>, chunkPos: ChunkPos) {
val key = dimension to chunkPos.toLong()
if (!pendingAsyncLoads.add(key)) {
return
}
if (Cobblemon.LOGGER.isDebugEnabled) {
Cobblemon.LOGGER.debug(
"[CobblemonCamera] Scheduling async load for chunk {} in {}",
chunkPos,
dimension.location()
)
}
level.chunkSource
.getChunkFuture(chunkPos.x, chunkPos.z, ChunkStatus.FULL, true)
.thenAcceptAsync({ result ->
result.ifSuccess { chunkAccess ->
if (chunkAccess is LevelChunk && Cobblemon.LOGGER.isDebugEnabled) {
Cobblemon.LOGGER.debug(
"[CobblemonCamera] Async loaded chunk {} (light={}, entities={})",
chunkPos,
chunkAccess.isLightCorrect,
chunkAccess.blockEntities.size
)
}
}
pendingAsyncLoads.remove(key)
}, level.server)
.exceptionally { throwable ->
level.server.execute {
pendingAsyncLoads.remove(key)
Cobblemon.LOGGER.warn(
"[CobblemonCamera] Exception while async loading chunk {}: {}",
chunkPos,
throwable.message
)
}
null
}
}
private fun buildChunkSet(centerChunk: ChunkPos, radius: Int): MutableSet<Long> {
val chunks = mutableSetOf<Long>()
for (x in centerChunk.x - radius..centerChunk.x + radius) {
for (z in centerChunk.z - radius..centerChunk.z + radius) {
chunks.add(ChunkPos.asLong(x, z))
}
}
return chunks
}
}
@@ -1,203 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.camera
import com.bedrockk.molang.runtime.MoLangRuntime
import com.bedrockk.molang.runtime.MoParams
import com.bedrockk.molang.runtime.value.MoValue
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.camera.integration.server.CameraExecutor
import com.cobblemon.mod.common.api.camera.integration.server.CameraInstance
import com.cobblemon.mod.common.api.camera.integration.server.CameraNetworkSync
import com.cobblemon.mod.common.api.camera.registry.TimelineRegistry
import com.cobblemon.mod.common.api.molang.MoLangFunctions.setup
import net.minecraft.core.SectionPos
import net.minecraft.server.level.ChunkTrackingView
import net.minecraft.server.level.ServerLevel
import net.minecraft.server.level.ServerPlayer
import net.minecraft.util.Mth
import net.minecraft.world.level.ChunkPos
import net.minecraft.world.phys.Vec3
import java.util.UUID
/**
* Server-side camera controller.
*
* Orchestrates camera instance, executor, network sync, and chunk/viewer management.
* Delegates responsibilities to focused components.
*/
class CameraController(
override val id: UUID = UUID.randomUUID(),
val context: Map<String, MoValue> = emptyMap(),
override val origin: Vec3 = Vec3.ZERO,
val queryFunctions: Map<String, java.util.function.Function<MoParams, Any>> = emptyMap()
) : ICameraController {
private val instance: CameraInstance
private val runtime: MoLangRuntime
private lateinit var executor: CameraExecutor
private lateinit var networkSync: CameraNetworkSync
private val viewers = mutableSetOf<UUID>()
private val viewerChunkStates = mutableMapOf<UUID, ViewerChunkState>()
private var level: ServerLevel? = null
init {
runtime = MoLangRuntime().setup()
runtime.environment.query.functions.putAll(queryFunctions)
instance = CameraInstance(id, origin, context, queryFunctions)
executor = CameraExecutor(instance, runtime, context)
networkSync = CameraNetworkSync(id, viewers)
Cobblemon.LOGGER.debug("[CameraController] Initialized $id")
}
private data class ViewerChunkState(
val viewDistance: Int,
var centerChunk: ChunkPos,
var chunkView: ChunkTrackingView
)
override fun startTimeline(timelineId: String): Boolean {
val timeline = TimelineRegistry.getTimeline(timelineId)
if (timeline == null) {
Cobblemon.LOGGER.error("[CameraController] Timeline '$timelineId' not found")
return false
}
return if (instance.startTimeline(timeline)) {
Cobblemon.LOGGER.info("[CameraController] Started '$timelineId'")
true
} else {
Cobblemon.LOGGER.error("[CameraController] Engine start failed for '$timelineId'")
false
}
}
override fun tick() {
val currentLevel = level ?: return
val stillActive = executor.tick(currentLevel, networkSync) { chunkPos ->
updateViewerChunkViews(chunkPos)
}
// If timeline ended, stop the camera and notify all viewers
if (!stillActive && viewers.isNotEmpty()) {
Cobblemon.LOGGER.info("[CameraController] Timeline ended naturally, stopping camera")
stop()
}
}
private fun updateViewerChunkViews(chunkPos: ChunkPos) {
viewerChunkStates.forEach { (viewerId, state) ->
if (state.centerChunk == chunkPos) return@forEach
state.centerChunk = chunkPos
val newView = ChunkTrackingView.of(chunkPos, state.viewDistance)
state.chunkView = newView
CameraManager.registerViewerChunkView(viewerId, newView)
}
val serverLevel = level
if (serverLevel != null && viewerChunkStates.isNotEmpty()) {
val maxDistance = viewerChunkStates.values.maxOf { it.viewDistance }
CameraChunkLoader.ensureArea(serverLevel, id, chunkPos, maxDistance)
}
}
override fun addViewer(player: ServerPlayer) {
viewers.add(player.uuid)
registerViewerChunkView(player, origin)
networkSync.sendStart(origin)
}
/**
* Adds a player as a feed viewer without sending CameraStartPacket.
*
* The player receives transform/focus updates like a normal viewer, but
* the camera will NOT become the primary camera on the client. This is
* used for secondary cameras (e.g. Camera B in split-screen) that render
* to an off-screen RenderTarget via CameraFeedHandler.
*/
fun addFeedViewer(player: ServerPlayer) {
viewers.add(player.uuid)
registerViewerChunkView(player, origin)
// No sendStart() — intentionally skipped so the client doesn't set this as primary
}
private fun registerViewerChunkView(player: ServerPlayer, centerPosition: Vec3) {
val playerLevel = player.serverLevel()
if (level == null || level != playerLevel) {
level?.let { CameraChunkLoader.clear(id) }
level = playerLevel
}
val maxViewDistance = player.server.playerList.viewDistance
val viewDistance = Mth.clamp(player.requestedViewDistance(), 2, maxViewDistance)
val chunkPos = chunkPosFrom(centerPosition)
val chunkView = ChunkTrackingView.of(chunkPos, viewDistance)
viewerChunkStates[player.uuid] = ViewerChunkState(viewDistance, chunkPos, chunkView)
CameraManager.registerViewerChunkView(player.uuid, chunkView)
val maxDistance = viewerChunkStates.values.maxOfOrNull { it.viewDistance } ?: viewDistance
CameraChunkLoader.ensureArea(playerLevel, id, chunkPos, maxDistance)
}
override fun removeViewer(player: ServerPlayer) {
// Send end packet to THIS player specifically
networkSync.sendEndToPlayer(player)
viewers.remove(player.uuid)
viewerChunkStates.remove(player.uuid)
CameraManager.clearViewerChunkView(player.uuid)
if (viewers.isEmpty()) {
CameraChunkLoader.clear(id)
level = null
} else {
val serverLevel = level
if (serverLevel != null && viewerChunkStates.isNotEmpty()) {
val state = viewerChunkStates.values.first()
val maxDistance = viewerChunkStates.values.maxOf { it.viewDistance }
CameraChunkLoader.ensureArea(serverLevel, id, state.centerChunk, maxDistance)
}
}
}
override fun stop() {
instance.stop()
networkSync.sendEnd()
viewerChunkStates.keys.toList().forEach { CameraManager.clearViewerChunkView(it) }
viewerChunkStates.clear()
CameraChunkLoader.clear(id)
level = null
viewers.clear()
}
override fun isActive() = instance.isActive()
fun setFlag(name: String, value: Boolean) = setVariable(name, if (value) 1.0 else 0.0)
override fun setVariable(name: String, value: Double) = instance.setVariable(name, value)
override fun setFocusOverride(position: Vec3?) {
instance.focusOverride = position
}
override fun triggerInstantTransition() = instance.triggerInstantTransition()
override fun addQueryFunctions(functions: Map<String, java.util.function.Function<MoParams, Any>>) {
runtime.environment.query.functions.putAll(functions)
instance.addQueryFunctions(functions)
}
private fun chunkPosFrom(position: Vec3): ChunkPos {
val chunkX = SectionPos.blockToSectionCoord(Mth.floor(position.x))
val chunkZ = SectionPos.blockToSectionCoord(Mth.floor(position.z))
return ChunkPos(chunkX, chunkZ)
}
}
@@ -1,164 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.camera
import com.bedrockk.molang.runtime.MoParams
import com.bedrockk.molang.runtime.value.MoValue
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.camera.components.CameraChunkManager
import com.cobblemon.mod.common.api.camera.components.CameraLifecycleManager
import com.cobblemon.mod.common.api.camera.components.CameraViewerManager
import com.cobblemon.mod.common.api.events.CobblemonEvents
import com.cobblemon.mod.common.api.events.camera.CameraStartedEvent
import com.cobblemon.mod.common.api.events.camera.CameraStoppedEvent
import com.cobblemon.mod.common.api.events.camera.CameraViewerAddedEvent
import com.cobblemon.mod.common.api.events.camera.CameraViewerRemovedEvent
import com.cobblemon.mod.common.battles.stadium.StadiumBattleCameraCoordinator
import net.minecraft.server.level.ChunkTrackingView
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.phys.Vec3
import java.util.UUID
import java.util.function.Function
object CameraManager : ICameraManager {
private val lifecycleManager = CameraLifecycleManager()
private val viewerManager = CameraViewerManager()
private val chunkManager = CameraChunkManager()
override fun startCamera(
timelineId: String,
context: Map<String, MoValue>,
viewers: List<ServerPlayer>,
origin: Vec3,
queryFunctions: Map<String, Function<MoParams, Any>>
): UUID? {
val camera = CameraController(
context = context,
origin = origin,
queryFunctions = queryFunctions
)
val preEvent = CameraStartedEvent.Pre(camera.id, camera, timelineId, viewers)
CobblemonEvents.CAMERA_STARTED_PRE.emit(preEvent)
if (preEvent.isCanceled) {
Cobblemon.LOGGER.info("[CobblemonCamera] Camera start canceled by event listener")
return null
}
if (!camera.startTimeline(timelineId)) {
Cobblemon.LOGGER.warn("[CobblemonCamera] Failed to start timeline '$timelineId'")
return null
}
lifecycleManager.registerCamera(camera)
viewers.forEach { player ->
camera.addViewer(player)
viewerManager.assignViewerToCamera(player.uuid, camera.id)
CobblemonEvents.CAMERA_VIEWER_ADDED.emit(
CameraViewerAddedEvent(camera.id, camera, player)
)
}
CobblemonEvents.CAMERA_STARTED_POST.emit(
CameraStartedEvent.Post(camera.id, camera, timelineId, viewers)
)
Cobblemon.LOGGER.info("[CobblemonCamera] Started camera ${camera.id} with timeline '$timelineId' for ${viewers.size} viewers")
return camera.id
}
override fun addViewerToCamera(player: ServerPlayer, cameraId: UUID): Boolean {
val camera = lifecycleManager.getCamera(cameraId) ?: return false
camera.addViewer(player)
viewerManager.assignViewerToCamera(player.uuid, camera.id)
CobblemonEvents.CAMERA_VIEWER_ADDED.emit(
CameraViewerAddedEvent(camera.id, camera, player)
)
return true
}
override fun stopCamera(cameraId: UUID) {
lifecycleManager.getCamera(cameraId)?.let { camera ->
CobblemonEvents.CAMERA_STOPPED_PRE.emit(
CameraStoppedEvent.Pre(cameraId, camera, CameraStoppedEvent.StopReason.MANUAL)
)
camera.stop()
lifecycleManager.unregisterCamera(cameraId)
viewerManager.removeViewersForCamera(cameraId)
CobblemonEvents.CAMERA_STOPPED_POST.emit(
CameraStoppedEvent.Post(cameraId, camera, CameraStoppedEvent.StopReason.MANUAL)
)
Cobblemon.LOGGER.info("[CobblemonCamera] Stopped camera $cameraId")
}
}
override fun stopCameraForPlayer(player: ServerPlayer) {
viewerManager.getCameraForPlayer(player.uuid)?.let { cameraId ->
lifecycleManager.getCamera(cameraId)?.let { camera ->
camera.removeViewer(player)
viewerManager.removeViewer(player.uuid)
CobblemonEvents.CAMERA_VIEWER_REMOVED.emit(
CameraViewerRemovedEvent(cameraId, camera, player)
)
}
}
}
override fun getPlayerCamera(player: ServerPlayer): ICameraController? {
val cameraId = viewerManager.getCameraForPlayer(player.uuid) ?: return null
return lifecycleManager.getCamera(cameraId)
}
override fun getCamera(cameraId: UUID): ICameraController? {
return lifecycleManager.getCamera(cameraId)
}
override fun tick() {
lifecycleManager.tick()
StadiumBattleCameraCoordinator.tickSecondaryCameras()
}
override fun clear() {
lifecycleManager.clear()
viewerManager.clear()
chunkManager.clear()
}
internal fun registerViewerChunkView(playerId: UUID, newView: ChunkTrackingView) {
chunkManager.registerViewerChunkView(playerId, newView)
}
internal fun clearViewerChunkView(playerId: UUID) {
chunkManager.clearViewerChunkView(playerId)
}
@JvmStatic
fun getPlayerChunkView(player: ServerPlayer): ChunkTrackingView? {
return chunkManager.getPlayerChunkView(player.uuid)
}
@JvmStatic
fun consumeChunkViewUpdate(player: ServerPlayer): ChunkViewUpdate? {
return chunkManager.consumeChunkViewUpdate(player.uuid)?.let {
ChunkViewUpdate(it.previous, it.current)
}
}
data class ChunkViewUpdate(val previous: ChunkTrackingView?, val current: ChunkTrackingView?)
}
@@ -1,20 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.camera
/**
* Represents a camera rotation in 3D space.
*
* @property yaw Horizontal rotation (left/right) in degrees
* @property pitch Vertical rotation (up/down) in degrees
*/
data class CameraRotation(
val yaw: Float,
val pitch: Float
)
@@ -1,43 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.camera
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.camera.model.CameraTrack
import com.google.gson.JsonObject
object CameraTrackRegistry {
private val trackDeserializers = mutableMapOf<String, (JsonObject) -> CameraTrack>()
fun registerTrackType(trackType: String, deserializer: (JsonObject) -> CameraTrack) {
if (trackDeserializers.containsKey(trackType)) {
Cobblemon.LOGGER.warn("[CameraTrackRegistry] Track type '$trackType' is already registered, overwriting")
}
trackDeserializers[trackType] = deserializer
Cobblemon.LOGGER.debug("[CameraTrackRegistry] Registered camera track type: $trackType")
}
fun deserializeTrack(trackType: String, json: JsonObject): CameraTrack? {
val deserializer = trackDeserializers[trackType]
if (deserializer == null) {
Cobblemon.LOGGER.warn("[CameraTrackRegistry] Unknown track type: $trackType")
return null
}
return try {
deserializer(json)
} catch (e: Exception) {
Cobblemon.LOGGER.error("[CameraTrackRegistry] Failed to deserialize track type '$trackType': ${e.message}", e)
null
}
}
fun getRegisteredTrackTypes(): Set<String> = trackDeserializers.keys.toSet()
fun isTrackTypeRegistered(trackType: String): Boolean = trackDeserializers.containsKey(trackType)
}
@@ -1,32 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.camera
import com.bedrockk.molang.runtime.MoParams
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.phys.Vec3
import java.util.UUID
import java.util.function.Function
interface ICameraController {
val id: UUID
val origin: Vec3
fun startTimeline(timelineId: String): Boolean
fun tick()
fun stop()
fun isActive(): Boolean
fun addViewer(player: ServerPlayer)
fun removeViewer(player: ServerPlayer)
fun setVariable(name: String, value: Double)
fun setFocusOverride(position: Vec3?)
/** Triggers an instant camera transition (no client-side interpolation) on the next tick. */
fun triggerInstantTransition()
/** Injects additional MoLang query functions into the camera's runtime after creation. */
fun addQueryFunctions(functions: Map<String, Function<MoParams, Any>>)
}
@@ -1,17 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.camera
import java.util.UUID
interface ICameraCoordinator {
fun bind(cameraId: UUID)
fun release()
fun getCameraId(): UUID?
}
@@ -1,34 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.camera
import com.bedrockk.molang.runtime.MoParams
import com.bedrockk.molang.runtime.value.MoValue
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.phys.Vec3
import java.util.UUID
import java.util.function.Function
interface ICameraManager {
fun startCamera(
timelineId: String,
context: Map<String, MoValue> = emptyMap(),
viewers: List<ServerPlayer> = emptyList(),
origin: Vec3 = Vec3.ZERO,
queryFunctions: Map<String, Function<MoParams, Any>> = emptyMap()
): UUID?
fun addViewerToCamera(player: ServerPlayer, cameraId: UUID): Boolean
fun stopCamera(cameraId: UUID)
fun stopCameraForPlayer(player: ServerPlayer)
fun getPlayerCamera(player: ServerPlayer): ICameraController?
fun getCamera(cameraId: UUID): ICameraController?
fun tick()
fun clear()
}
@@ -1,45 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.camera.components
import net.minecraft.server.level.ChunkTrackingView
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
class CameraChunkManager {
private val playerChunkViews = ConcurrentHashMap<UUID, ChunkTrackingView>()
private val chunkViewUpdates = ConcurrentHashMap<UUID, ChunkViewUpdate>()
data class ChunkViewUpdate(val previous: ChunkTrackingView?, val current: ChunkTrackingView?)
fun registerViewerChunkView(playerId: UUID, newView: ChunkTrackingView) {
val previous = playerChunkViews.put(playerId, newView)
if (previous == null || previous != newView) {
chunkViewUpdates[playerId] = ChunkViewUpdate(previous, newView)
}
}
fun clearViewerChunkView(playerId: UUID) {
val previous = playerChunkViews.remove(playerId)
if (previous != null) {
chunkViewUpdates[playerId] = ChunkViewUpdate(previous, null)
}
}
fun getPlayerChunkView(playerId: UUID): ChunkTrackingView? = playerChunkViews[playerId]
fun consumeChunkViewUpdate(playerId: UUID): ChunkViewUpdate? = chunkViewUpdates.remove(playerId)
fun clear() {
playerChunkViews.clear()
chunkViewUpdates.clear()
}
}
@@ -1,58 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.camera.components
import com.cobblemon.mod.common.api.camera.ICameraController
import com.cobblemon.mod.common.api.events.CobblemonEvents
import com.cobblemon.mod.common.api.events.camera.CameraStoppedEvent
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
class CameraLifecycleManager {
private val cameras = ConcurrentHashMap<UUID, ICameraController>()
fun registerCamera(camera: ICameraController) {
cameras[camera.id] = camera
}
fun unregisterCamera(cameraId: UUID): ICameraController? {
return cameras.remove(cameraId)
}
fun getCamera(cameraId: UUID): ICameraController? = cameras[cameraId]
fun getAllCameras(): Collection<ICameraController> = cameras.values
fun tick() {
cameras.values.forEach { it.tick() }
cameras.entries.removeIf { (id, camera) ->
if (!camera.isActive()) {
CobblemonEvents.CAMERA_STOPPED_POST.emit(
CameraStoppedEvent.Post(id, camera, CameraStoppedEvent.StopReason.COMPLETED)
)
true
} else {
false
}
}
}
fun clear() {
cameras.values.forEach {
it.stop()
CobblemonEvents.CAMERA_STOPPED_POST.emit(
CameraStoppedEvent.Post(it.id, it, CameraStoppedEvent.StopReason.MANUAL)
)
}
cameras.clear()
}
fun size(): Int = cameras.size
}
@@ -1,36 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.camera.components
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
class CameraViewerManager {
private val playerCameras = ConcurrentHashMap<UUID, UUID>()
fun assignViewerToCamera(playerId: UUID, cameraId: UUID) {
playerCameras[playerId] = cameraId
}
fun removeViewer(playerId: UUID): UUID? {
return playerCameras.remove(playerId)
}
fun getCameraForPlayer(playerId: UUID): UUID? {
return playerCameras[playerId]
}
fun removeViewersForCamera(cameraId: UUID) {
playerCameras.entries.removeIf { it.value == cameraId }
}
fun clear() {
playerCameras.clear()
}
}
@@ -1,591 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.camera.engine
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.Cobblemon
import com.cobblemon.mod.common.api.camera.CameraRotation
import com.cobblemon.mod.common.api.camera.model.*
import com.cobblemon.mod.common.api.camera.result.ValidationError
import com.cobblemon.mod.common.api.camera.track.EventTrack
import com.cobblemon.mod.common.api.camera.track.FiredGuiAction
import com.cobblemon.mod.common.api.camera.track.GuiTrack
import com.cobblemon.mod.common.api.tween.EasingRegistry
import com.cobblemon.mod.common.client.camera.CameraInterpolationUtils
import com.cobblemon.mod.common.util.resolve
import com.cobblemon.mod.common.util.resolveBoolean
import net.minecraft.world.phys.Vec3
import java.util.*
import java.util.concurrent.ConcurrentHashMap
/**
* Thread-safe timeline execution engine.
*
* Uses immutable state snapshots for thread safety. MoLang evaluation is NOT
* thread-safe - callers must ensure tick() is called from the same thread.
*
* @param id Unique identifier for this engine instance
* @param timeline The timeline to execute
* @param origin The world position origin for relative coordinates
* @param runtime The MoLang runtime for expression evaluation
* @param providedContext Initial context values (parameter values) to inject into MoLang
*/
class TimelineEngine(
val id: UUID,
private val timeline: Timeline,
val origin: Vec3,
private val runtime: MoLangRuntime,
private val providedContext: Map<String, MoValue> = emptyMap()
) {
@Volatile
private var state: ExecutionState = ExecutionState.Stopped
private var previousExecutionState: ExecutionState.Playing? = null
private var instantTransitionOccurred = false
private val customVariables = ConcurrentHashMap<String, Double>()
/** GUI actions fired during the current tick, consumed by the executor. */
private val pendingGuiActions = mutableListOf<FiredGuiAction>()
private val timeOnCurrentPathValue = DoubleValue(0.0)
private val totalTimeValue = DoubleValue(0.0)
private val runnerIdValue = StringValue(id.toString())
fun start(): Boolean {
if (state !is ExecutionState.Stopped) {
Cobblemon.LOGGER.warn("[TimelineEngine] Cannot start: already active (id=$id)")
return false
}
val errors = timeline.validate()
if (errors.any { it.severity == ValidationError.Severity.ERROR }) {
Cobblemon.LOGGER.error("[TimelineEngine] Timeline '${timeline.id}' validation failed: ${errors.joinToString { it.toString() }}")
return false
}
if (!validateAndInjectParameters()) {
return false
}
val startState = timeline.getState(timeline.startStateId)
if (startState == null) {
Cobblemon.LOGGER.error("[TimelineEngine] Start state '${timeline.startStateId}' not found in timeline '${timeline.id}'")
return false
}
state = ExecutionState.Playing(
currentState = startState,
stateTime = 0f,
totalTime = 0f,
lastTickNanos = System.nanoTime(),
transitionState = TransitionState.None,
progressMemory = emptyMap(),
executedEvents = emptySet()
)
Cobblemon.LOGGER.info("[TimelineEngine] Started timeline '${timeline.id}' in state '${startState.id}'")
return true
}
/**
* Validates that all required parameters are provided and injects parameters
* (including defaults) into the MoLang runtime.
*/
private fun validateAndInjectParameters(): Boolean {
if (timeline.parameters.isEmpty()) return true
val providedVarNames = providedContext.keys
val missingParams = timeline.validateParameters(providedVarNames)
if (missingParams.isNotEmpty()) {
Cobblemon.LOGGER.error("[TimelineEngine] Timeline '${timeline.id}' missing required parameters: ${missingParams.joinToString(", ")}")
return false
}
providedContext.forEach { (name, value) ->
runtime.environment.variable.map[name] = value
}
for ((paramName, definition) in timeline.parameters) {
val default = definition.default ?: continue
when (default) {
is ParameterDefault.NumberDefault -> {
if (paramName !in providedVarNames) {
runtime.environment.variable.map[paramName] = DoubleValue(default.value)
Cobblemon.LOGGER.debug("[TimelineEngine] Using default for parameter '$paramName': ${default.value}")
}
}
is ParameterDefault.Vec3Default -> {
val varNames = definition.getVariableNames(paramName)
val xVar = varNames[0]
val yVar = varNames[1]
val zVar = varNames[2]
if (xVar !in providedVarNames) {
runtime.environment.variable.map[xVar] = DoubleValue(default.x)
}
if (yVar !in providedVarNames) {
runtime.environment.variable.map[yVar] = DoubleValue(default.y)
}
if (zVar !in providedVarNames) {
runtime.environment.variable.map[zVar] = DoubleValue(default.z)
}
if (xVar !in providedVarNames || yVar !in providedVarNames || zVar !in providedVarNames) {
Cobblemon.LOGGER.debug("[TimelineEngine] Using default for parameter '$paramName': (${default.x}, ${default.y}, ${default.z})")
}
}
}
}
return true
}
fun tick(): CameraTransform? {
val current = state
if (current !is ExecutionState.Playing) {
return null
}
previousExecutionState = current
instantTransitionOccurred = false
val nowNanos = System.nanoTime()
val deltaNanos = nowNanos - current.lastTickNanos
val deltaSeconds = (deltaNanos / 1_000_000_000.0).toFloat()
.coerceIn(0f, 0.5f)
val newStateTime = current.stateTime + deltaSeconds * current.currentState.speed
val newTotalTime = current.totalTime + deltaSeconds
val transitionResult = checkTransitions(current, newStateTime, newTotalTime)
if (transitionResult is TransitionCheckResult.TransitionNow) {
val targetState = timeline.getState(transitionResult.targetStateId)
?: run {
Cobblemon.LOGGER.error("[TimelineEngine] Transition target state '${transitionResult.targetStateId}' not found")
return null
}
state = when (transitionResult.type) {
TransitionType.INSTANT -> {
instantTransitionOccurred = true
val savedSnapshot = if (targetState.rememberProgress) {
current.progressMemory[targetState.id]
} else null
val restoredTime = savedSnapshot?.normalizedProgress?.let {
it * targetState.getEffectiveDuration()
} ?: 0f
val restoredFocus = savedSnapshot?.lastFocus
val currentSnapshot = StateSnapshot(
normalizedProgress = normalizeProgress(current.stateTime, current.currentState),
lastFocus = current.lastEvaluatedFocus
)
ExecutionState.Playing(
currentState = targetState,
stateTime = restoredTime,
totalTime = newTotalTime,
lastTickNanos = nowNanos,
transitionState = TransitionState.None,
progressMemory = current.progressMemory + (current.currentState.id to currentSnapshot),
executedEvents = if (targetState.rememberProgress) current.executedEvents else emptySet(),
lastEvaluatedFocus = restoredFocus
)
}
TransitionType.BLEND -> {
val currentTransform = evaluateState(current.currentState, current.stateTime)
ExecutionState.Playing(
currentState = current.currentState,
stateTime = newStateTime,
totalTime = newTotalTime,
lastTickNanos = nowNanos,
transitionState = TransitionState.Blending(
targetState = targetState,
startTime = newTotalTime,
duration = transitionResult.blendDuration,
startTransform = currentTransform
),
progressMemory = current.progressMemory,
executedEvents = current.executedEvents
)
}
}
} else {
val effectiveDuration = current.currentState.getEffectiveDuration()
val (finalStateTime, finalEvents) = if (current.currentState.isLooping && effectiveDuration > 0f && newStateTime >= effectiveDuration) {
newStateTime % effectiveDuration to emptySet<EventKey>()
} else {
val clampedTime = if (!current.currentState.isLooping && effectiveDuration > 0f) {
newStateTime.coerceAtMost(effectiveDuration)
} else {
newStateTime
}
clampedTime to current.executedEvents
}
state = current.copy(
stateTime = finalStateTime,
totalTime = newTotalTime,
lastTickNanos = nowNanos,
executedEvents = finalEvents
)
}
val updatedState = state
if (updatedState is ExecutionState.Playing && updatedState.transitionState is TransitionState.Blending) {
val blend = updatedState.transitionState
val blendProgress = if (blend.duration > 0f) {
((updatedState.totalTime - blend.startTime) / blend.duration).coerceIn(0f, 1f)
} else 1f
if (blendProgress >= 1f) {
val savedSnapshot = updatedState.progressMemory[blend.targetState.id]
val restoredTime = if (blend.targetState.rememberProgress && savedSnapshot != null) {
savedSnapshot.normalizedProgress * blend.targetState.getEffectiveDuration()
} else 0f
val currentSnapshot = StateSnapshot(
normalizedProgress = normalizeProgress(updatedState.stateTime, updatedState.currentState),
lastFocus = null
)
state = ExecutionState.Playing(
currentState = blend.targetState,
stateTime = restoredTime,
totalTime = updatedState.totalTime,
lastTickNanos = nowNanos,
transitionState = TransitionState.None,
progressMemory = updatedState.progressMemory + (updatedState.currentState.id to currentSnapshot),
executedEvents = if (blend.targetState.rememberProgress) updatedState.executedEvents else emptySet()
)
}
}
executeEvents()
executeGuiActions()
return calculateTransform()
}
fun stop() {
state = ExecutionState.Stopped
customVariables.clear()
// Clear MoLang runtime to prevent memory leaks
runtime.environment.variable.map.clear()
runtime.environment.query.functions.clear()
}
fun setVariable(name: String, value: Double) {
customVariables[name] = value
}
/** Triggers an instant transition on the next update (no client-side interpolation). */
fun triggerInstantTransition() {
instantTransitionOccurred = true
}
fun isActive(): Boolean = state is ExecutionState.Playing
fun getCurrentStateId(): String? = (state as? ExecutionState.Playing)?.currentState?.id
fun getCurrentRenderPlayerBody(): Boolean = (state as? ExecutionState.Playing)?.currentState?.renderPlayerBody ?: true
fun getCurrentHiddenHudElements(): Set<HudElement> = (state as? ExecutionState.Playing)?.currentState?.hiddenHudElements ?: emptySet()
fun consumeInstantTransitionFlag(): Boolean {
val result = instantTransitionOccurred
instantTransitionOccurred = false
return result
}
private fun checkTransitions(
current: ExecutionState.Playing,
newStateTime: Float,
newTotalTime: Float
): TransitionCheckResult {
if (current.transitionState !is TransitionState.None) {
return TransitionCheckResult.NoTransition
}
val context = createMoLangContext(current.currentState, newStateTime, newTotalTime)
for (transition in timeline.getTransitionsFrom(current.currentState.id)) {
try {
val shouldTransition = runtime.resolveBoolean(transition.condition, context)
if (shouldTransition) {
return TransitionCheckResult.TransitionNow(
targetStateId = transition.targetStateId,
type = transition.type,
blendDuration = transition.blendDuration
)
}
} catch (e: Exception) {
Cobblemon.LOGGER.error("[TimelineEngine] Error evaluating transition condition: ${e.message}", e)
}
}
return TransitionCheckResult.NoTransition
}
private fun executeEvents() {
val current = state as? ExecutionState.Playing ?: return
val previousState = previousExecutionState ?: return
val context = createMoLangContext(current.currentState, current.stateTime, current.totalTime)
val previousTime = previousState.stateTime
val currentTime = current.stateTime
current.currentState.tracks.filterIsInstance<EventTrack>().forEach { track ->
track.events.forEach { timedEvent ->
val eventKey = EventKey(current.currentState.id, timedEvent.eventId, timedEvent.time.toInt())
if (eventKey in current.executedEvents) return@forEach
val crossedEvent = if (previousTime <= currentTime) {
timedEvent.time in previousTime..currentTime
} else {
timedEvent.time <= currentTime
}
if (crossedEvent) {
try {
runtime.resolve(timedEvent.expression, context)
state = current.copy(
executedEvents = current.executedEvents + eventKey
)
if (timedEvent.eventId != null) {
Cobblemon.LOGGER.debug("[TimelineEngine] Executed event '${timedEvent.eventId}' at time ${timedEvent.time}s")
}
} catch (e: Exception) {
Cobblemon.LOGGER.error("[TimelineEngine] Failed to execute event: ${e.message}", e)
}
}
}
}
}
/**
* Checks GUI tracks for actions whose time has been crossed and queues them.
* Follows the same pattern as [executeEvents].
*/
private fun executeGuiActions() {
val current = state as? ExecutionState.Playing ?: return
val previousState = previousExecutionState ?: return
val previousTime = previousState.stateTime
val currentTime = current.stateTime
current.currentState.tracks.filterIsInstance<GuiTrack>().forEach { track ->
track.actions.forEach { guiAction ->
val actionKey = EventKey(current.currentState.id, "gui_${guiAction.guiId}_${guiAction.action}", guiAction.time.toInt())
if (actionKey in current.executedEvents) return@forEach
val crossedAction = if (previousTime <= currentTime) {
guiAction.time in previousTime..currentTime
} else {
guiAction.time <= currentTime
}
if (crossedAction) {
pendingGuiActions.add(FiredGuiAction(
action = guiAction.action,
guiId = guiAction.guiId,
args = guiAction.args,
argsExpressions = guiAction.argsExpressions
))
state = current.copy(
executedEvents = current.executedEvents + actionKey
)
Cobblemon.LOGGER.debug("[TimelineEngine] Fired GUI action ${guiAction.action} for '${guiAction.guiId}' at time ${guiAction.time}s")
}
}
}
}
/**
* Consumes and returns all pending GUI actions since the last tick.
* Called by the CameraExecutor after tick().
*/
fun consumeGuiActions(): List<FiredGuiAction> {
if (pendingGuiActions.isEmpty()) return emptyList()
val actions = pendingGuiActions.toList()
pendingGuiActions.clear()
return actions
}
private fun calculateTransform(): CameraTransform {
val current = state as? ExecutionState.Playing ?: return CameraTransform(origin, null, null)
return when (val transition = current.transitionState) {
is TransitionState.None -> {
evaluateState(current.currentState, current.stateTime)
}
is TransitionState.Blending -> {
val targetTransform = evaluateState(transition.targetState, 0f)
val progress = if (transition.duration > 0f) {
((current.totalTime - transition.startTime) / transition.duration).coerceIn(0f, 1f)
} else 1f
val easedProgress = EasingRegistry.easeOutQuad(progress)
blendTransforms(transition.startTransform, targetTransform, easedProgress)
}
}
}
private fun evaluateState(state: State, time: Float): CameraTransform {
val context = createMoLangContext(state, time, (this.state as? ExecutionState.Playing)?.totalTime ?: 0f)
var position: Vec3? = null
var focus: CameraFocus? = null
var rotation: CameraRotation? = null
for (track in state.tracks) {
val outputs = track.evaluate(time, context, runtime, origin)
for (output in outputs) {
when (output) {
is TrackOutput.Position -> {
if (position == null) position = output.position
}
is TrackOutput.Focus -> {
if (focus == null) focus = output.target
}
is TrackOutput.Rotation -> {
if (rotation == null) rotation = output.rotation
}
is TrackOutput.Event -> {
// Events handled separately
}
}
}
}
return CameraTransform(
position = position ?: origin,
focus = focus,
rotation = rotation
)
}
private fun blendTransforms(
from: CameraTransform,
to: CameraTransform,
progress: Float
): CameraTransform {
val blendedPosition = CameraInterpolationUtils.lerpPosition(from.position, to.position, progress)
val blendedRotation = if (from.rotation != null && to.rotation != null) {
CameraInterpolationUtils.slerpRotation(from.rotation, to.rotation, progress)
} else {
to.rotation ?: from.rotation
}
return CameraTransform(
position = blendedPosition,
focus = to.focus ?: from.focus,
rotation = blendedRotation
)
}
private fun createMoLangContext(
state: State,
stateTime: Float,
totalTime: Float
): Map<String, MoValue> {
timeOnCurrentPathValue.value = stateTime.toDouble()
totalTimeValue.value = totalTime.toDouble()
runtime.environment.variable.apply {
map["time_on_current_path"] = timeOnCurrentPathValue
map["total_time"] = totalTimeValue
map["runner_id"] = runnerIdValue
customVariables.forEach { (name, value) ->
map[name] = DoubleValue(value)
}
}
return emptyMap() // Context passed via runtime.environment
}
private fun normalizeProgress(time: Float, state: State): Float {
val duration = state.getEffectiveDuration()
return if (duration > 0f) {
(time / duration).coerceIn(0f, 1f)
} else 0f
}
}
data class CameraTransform(
val position: Vec3,
val focus: CameraFocus?,
val rotation: CameraRotation?
)
private sealed class ExecutionState {
object Stopped : ExecutionState()
data class Playing(
val currentState: State,
val stateTime: Float,
val totalTime: Float,
val lastTickNanos: Long,
val transitionState: TransitionState,
val progressMemory: Map<String, StateSnapshot>,
val executedEvents: Set<EventKey>,
val lastEvaluatedFocus: CameraFocus? = null
) : ExecutionState()
}
private sealed class TransitionState {
object None : TransitionState()
data class Blending(
val targetState: State,
val startTime: Float,
val duration: Float,
val startTransform: CameraTransform
) : TransitionState()
}
private sealed class TransitionCheckResult {
object NoTransition : TransitionCheckResult()
data class TransitionNow(
val targetStateId: String,
val type: TransitionType,
val blendDuration: Float
) : TransitionCheckResult()
}
private data class EventKey(
val stateId: String,
val eventId: String?,
val timeMillis: Int
)
private data class StateSnapshot(
val normalizedProgress: Float,
val lastFocus: CameraFocus?
)
@@ -1,129 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.camera.integration.server
import com.bedrockk.molang.runtime.MoLangRuntime
import com.bedrockk.molang.runtime.value.MoValue
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.camera.model.CameraFocus
import com.cobblemon.mod.common.api.camera.track.FiredGuiAction
import com.cobblemon.mod.common.api.camera.track.GuiActionType
import net.minecraft.core.SectionPos
import net.minecraft.server.level.ServerLevel
import net.minecraft.util.Mth
import net.minecraft.world.level.ChunkPos
import net.minecraft.world.phys.Vec3
/**
* Executes camera timeline and manages focus resolution.
*
* Coordinates between CameraInstance, viewer/chunk managers, and network sync.
*/
class CameraExecutor(
private val instance: CameraInstance,
private val runtime: MoLangRuntime,
private val context: Map<String, MoValue>,
private val guiActionHandler: ((FiredGuiAction) -> Unit)? = null
) {
companion object {
private const val CHUNK_UPDATE_DISTANCE_SQ = (16 * 16).toDouble()
}
private var lastCenterChunkUpdate: Vec3? = null
fun tick(
level: ServerLevel,
networkSync: CameraNetworkSync,
onChunkUpdate: (ChunkPos) -> Unit
): Boolean {
if (!instance.isActive()) return false
val transform = instance.tick() ?: run {
if (!instance.isActive()) instance.stop()
return false
}
val shouldUpdateCenter = lastCenterChunkUpdate?.let { last ->
transform.position.distanceToSqr(last) > CHUNK_UPDATE_DISTANCE_SQ
} ?: true
if (shouldUpdateCenter) {
val chunkPos = chunkPosFrom(transform.position)
onChunkUpdate(chunkPos)
lastCenterChunkUpdate = transform.position
}
val instantTransition = instance.consumeInstantTransitionFlag()
networkSync.sendTransform(transform, shouldUpdateCenter)
when (val focus = transform.focus) {
null -> networkSync.sendFocusNone()
is CameraFocus.Bone -> networkSync.sendFocusBone(focus, context, runtime)
else -> {
val resolvedFocus = resolveFocusPosition(focus)
val finalFocusPos = instance.focusOverride ?: resolvedFocus
networkSync.sendFocusPoint(finalFocusPos)
}
}
networkSync.sendState(
instantTransition = instantTransition,
renderPlayerBody = instance.getCurrentRenderPlayerBody(),
hiddenHudElements = instance.getCurrentHiddenHudElements()
)
if (guiActionHandler != null) {
val guiActions = instance.consumeGuiActions()
for (action in guiActions) {
guiActionHandler.invoke(action)
}
}
return true
}
fun resolveFocusPosition(focus: CameraFocus): Vec3? {
return when (focus) {
is CameraFocus.Entity -> {
val entity = CameraFocus.resolveEntity(focus.selector, context, runtime)
if (entity != null) {
val position = entity.position()
instance.updateLastValidFocusPosition(position)
position
} else {
val cached = instance.lastValidFocusPosition
if (cached != null) {
Cobblemon.LOGGER.debug("[CameraExecutor] Entity focus invalid, using last valid position")
cached
} else {
Cobblemon.LOGGER.warn("[CameraExecutor] Entity focus invalid and no cached position, using origin")
instance.origin
}
}
}
is CameraFocus.Point -> {
val position = focus.position.toVec3(context, runtime, instance.origin, false)
instance.updateLastValidFocusPosition(position)
position
}
is CameraFocus.Bone -> {
// Bone focus is resolved client-side, so return entity position as fallback
CameraFocus.resolveEntity(focus.selector, context, runtime)?.position()
?: instance.lastValidFocusPosition
}
}
}
private fun chunkPosFrom(position: Vec3): ChunkPos {
val chunkX = SectionPos.blockToSectionCoord(Mth.floor(position.x))
val chunkZ = SectionPos.blockToSectionCoord(Mth.floor(position.z))
return ChunkPos(chunkX, chunkZ)
}
}
@@ -1,95 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.camera.integration.server
import com.bedrockk.molang.runtime.MoLangRuntime
import com.bedrockk.molang.runtime.MoParams
import com.bedrockk.molang.runtime.value.MoValue
import com.cobblemon.mod.common.api.camera.engine.TimelineEngine
import com.cobblemon.mod.common.api.camera.model.State
import com.cobblemon.mod.common.api.camera.model.Timeline
import com.cobblemon.mod.common.api.molang.MoLangFunctions.setup
import net.minecraft.world.phys.Vec3
import java.util.UUID
/**
* Server-side camera instance - pure state and identity.
*
* Represents a single camera with its configuration and execution engine.
* Does NOT handle networking, chunk loading, or viewer management.
*
* @param id Unique identifier for this camera instance
* @param origin The world position origin for relative coordinates
* @param initialContext Initial context values (parameter values) to inject into MoLang.
* These are validated against the timeline's declared parameters when starting.
* @param queryFunctions Custom MoLang query functions to register
*/
class CameraInstance(
val id: UUID = UUID.randomUUID(),
val origin: Vec3,
private val initialContext: Map<String, MoValue> = emptyMap(),
queryFunctions: Map<String, java.util.function.Function<MoParams, Any>> = emptyMap()
) {
private val runtime: MoLangRuntime = MoLangRuntime().setup().apply {
environment.query.functions.putAll(queryFunctions)
}
private var engine: TimelineEngine = TimelineEngine(id, createStubTimeline(), origin, runtime, initialContext)
@Volatile
var focusOverride: Vec3? = null
var lastValidFocusPosition: Vec3? = null
private set
fun startTimeline(timeline: Timeline): Boolean {
engine = TimelineEngine(id, timeline, origin, runtime, initialContext)
return engine.start()
}
fun tick() = engine.tick()
fun stop() = engine.stop()
fun isActive() = engine.isActive()
fun getCurrentRenderPlayerBody() = engine.getCurrentRenderPlayerBody()
fun getCurrentHiddenHudElements() = engine.getCurrentHiddenHudElements()
fun setVariable(name: String, value: Double) = engine.setVariable(name, value)
fun triggerInstantTransition() = engine.triggerInstantTransition()
fun addQueryFunctions(functions: Map<String, java.util.function.Function<MoParams, Any>>) {
runtime.environment.query.functions.putAll(functions)
}
fun consumeInstantTransitionFlag() = engine.consumeInstantTransitionFlag()
fun consumeGuiActions() = engine.consumeGuiActions()
fun updateLastValidFocusPosition(position: Vec3) {
lastValidFocusPosition = position
}
private fun createStubTimeline(): Timeline {
return Timeline(
id = "stub",
startStateId = "stub",
states = mapOf(
"stub" to State(
id = "stub",
tracks = emptyList()
)
),
transitions = emptyList()
)
}
}
@@ -1,184 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.camera.integration.server
import com.bedrockk.molang.runtime.MoLangRuntime
import com.bedrockk.molang.runtime.value.MoValue
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.CobblemonNetwork.sendPacket
import com.cobblemon.mod.common.api.camera.engine.CameraTransform
import com.cobblemon.mod.common.api.camera.model.CameraFocus
import com.cobblemon.mod.common.api.camera.model.HudElement
import com.cobblemon.mod.common.net.messages.client.camera.CameraEndPacket
import com.cobblemon.mod.common.net.messages.client.camera.CameraFocusPacket
import com.cobblemon.mod.common.net.messages.client.camera.CameraStartPacket
import com.cobblemon.mod.common.net.messages.client.camera.CameraStatePacket
import com.cobblemon.mod.common.net.messages.client.camera.CameraTransformPacket
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.phys.Vec3
import java.util.UUID
/**
* Handles camera packet transmission to clients.
*
* Manages synchronization of camera state with connected viewers.
* Uses modular packets with change tracking to minimize network traffic.
*/
class CameraNetworkSync(
private val cameraId: UUID,
private val viewers: Set<UUID>
) {
private var lastFocusPosition: Vec3? = null
private var lastRenderPlayerBody: Boolean = true
private var lastHiddenHudElements: Set<HudElement> = emptySet()
private var lastInstantTransition: Boolean = false
fun sendStart(startPosition: Vec3) {
getPlayers().forEach { player ->
player.sendPacket(CameraStartPacket(cameraId, startPosition))
}
sendFocusNone(force = true)
sendState(
instantTransition = false,
renderPlayerBody = true,
hiddenHudElements = emptySet(),
force = true
)
}
fun sendTransform(
transform: CameraTransform,
updateCenterChunk: Boolean
) {
val packet = CameraTransformPacket(
cameraId = cameraId,
position = transform.position,
rotation = transform.rotation,
updateCenterChunk = updateCenterChunk
)
getPlayers().forEach { it.sendPacket(packet) }
}
fun sendFocusNone(force: Boolean = false) {
if (!force && lastFocusPosition == null) {
return
}
val packet = CameraFocusPacket(
cameraId = cameraId,
focus = CameraFocusPacket.FocusData.None
)
getPlayers().forEach { it.sendPacket(packet) }
lastFocusPosition = null
}
fun sendFocusPoint(focusPosition: Vec3?, force: Boolean = false) {
if (!force && focusPosition == lastFocusPosition) {
return
}
val focusData = when (focusPosition) {
null -> CameraFocusPacket.FocusData.None
else -> CameraFocusPacket.FocusData.Point(focusPosition)
}
val packet = CameraFocusPacket(
cameraId = cameraId,
focus = focusData
)
getPlayers().forEach { it.sendPacket(packet) }
lastFocusPosition = focusPosition
}
fun sendFocusBone(
boneFocus: CameraFocus.Bone,
context: Map<String, MoValue>,
runtime: MoLangRuntime,
force: Boolean = false
) {
val entity = CameraFocus.resolveEntity(boneFocus.selector, context, runtime)
if (entity == null) {
Cobblemon.LOGGER.debug("[CameraNetworkSync] Bone focus entity invalid")
sendFocusNone(force)
return
}
val offset = boneFocus.offset.toVec3(context, runtime)
Cobblemon.LOGGER.debug("[CameraNetworkSync] Sending bone focus: entityId=${entity.id}, bone=${boneFocus.boneName}, offset=$offset, smooth=${boneFocus.smooth}")
val focusData = CameraFocusPacket.FocusData.Bone(
entityId = entity.id,
boneName = boneFocus.boneName,
offset = offset,
smooth = boneFocus.smooth
)
val packet = CameraFocusPacket(
cameraId = cameraId,
focus = focusData
)
getPlayers().forEach { it.sendPacket(packet) }
// Invalidate lastFocusPosition when switching to bone focus.
// This ensures that when transitioning back to a point focus,
// the packet will always be sent (not incorrectly deduplicated).
lastFocusPosition = null
}
fun sendState(
instantTransition: Boolean,
renderPlayerBody: Boolean,
hiddenHudElements: Set<HudElement>,
force: Boolean = false
) {
if (!force &&
instantTransition == lastInstantTransition &&
renderPlayerBody == lastRenderPlayerBody &&
hiddenHudElements == lastHiddenHudElements) {
return
}
val packet = CameraStatePacket(
cameraId = cameraId,
instantTransition = instantTransition,
renderPlayerBody = renderPlayerBody,
hiddenHudElements = hiddenHudElements
)
getPlayers().forEach { it.sendPacket(packet) }
lastInstantTransition = instantTransition
lastRenderPlayerBody = renderPlayerBody
lastHiddenHudElements = hiddenHudElements
}
fun sendEnd() {
val packet = CameraEndPacket(cameraId)
getPlayers().forEach { it.sendPacket(packet) }
}
/**
* Send end packet to a specific player.
* Used when removing a single viewer without stopping the entire camera.
*/
fun sendEndToPlayer(player: ServerPlayer) {
player.sendPacket(CameraEndPacket(cameraId))
}
private fun getPlayers(): List<ServerPlayer> {
val server = Cobblemon.implementation.server() ?: return emptyList()
return viewers.mapNotNull { server.playerList.getPlayer(it) }
}
}
@@ -1,38 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.camera.integration.server
import com.cobblemon.mod.common.CobblemonNetwork.sendPacket
import com.cobblemon.mod.common.client.render.camera.compositor.CompositorLayout
import com.cobblemon.mod.common.net.messages.client.camera.CompositorPacket
import net.minecraft.server.level.ServerPlayer
import java.util.UUID
class CompositorSession(
val compositorId: UUID = UUID.randomUUID(),
private val viewers: () -> Collection<ServerPlayer>,
val fullscreen: Boolean = true,
val width: Int = 1920,
val height: Int = 1080
) {
fun sendCreate(layout: CompositorLayout) {
val packet = CompositorPacket.create(compositorId, layout, fullscreen, width, height)
viewers().forEach { it.sendPacket(packet) }
}
fun sendLayoutUpdate(layout: CompositorLayout) {
val packet = CompositorPacket.updateLayout(compositorId, layout)
viewers().forEach { it.sendPacket(packet) }
}
fun sendDestroy() {
val packet = CompositorPacket.destroy(compositorId)
viewers().forEach { it.sendPacket(packet) }
}
}
@@ -1,252 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.camera.model
import com.bedrockk.molang.runtime.MoLangRuntime
import com.bedrockk.molang.runtime.value.MoValue
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.molang.ExpressionLike
import com.cobblemon.mod.common.api.molang.ObjectValue
import com.cobblemon.mod.common.util.asExpressionLike
import com.cobblemon.mod.common.util.resolve
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import net.minecraft.world.entity.Entity
import net.minecraft.world.phys.Vec3
/**
* Represents where the camera should look.
*/
sealed class CameraFocus {
abstract fun resolve(
context: Map<String, MoValue>,
runtime: MoLangRuntime,
fallback: Vec3?
): Vec3?
data class Point(val position: PositionExpression) : CameraFocus() {
override fun resolve(
context: Map<String, MoValue>,
runtime: MoLangRuntime,
fallback: Vec3?
): Vec3 {
return position.toVec3(context, runtime)
}
}
data class Entity(val selector: ExpressionLike) : CameraFocus() {
override fun resolve(
context: Map<String, MoValue>,
runtime: MoLangRuntime,
fallback: Vec3?
): Vec3? {
return resolveEntity(selector, context, runtime)?.position() ?: fallback
}
}
data class Bone(
val selector: ExpressionLike,
val boneName: String,
val offset: PositionExpression = PositionExpression(
CoordinateValue.Static(0.0),
CoordinateValue.Static(0.0),
CoordinateValue.Static(0.0)
),
val smooth: Boolean = true
) : CameraFocus() {
override fun resolve(
context: Map<String, MoValue>,
runtime: MoLangRuntime,
fallback: Vec3?
): Vec3? {
// Server-side: just resolve the entity position as fallback
// Client-side bone resolution happens in BoneFocusResolver
return resolveEntity(selector, context, runtime)?.position() ?: fallback
}
}
companion object {
/**
* Resolves a MoLang entity selector to a live Entity, or null if the selector
* doesn't resolve, the entity is removed, or the entity is dead.
*/
fun resolveEntity(
selector: ExpressionLike,
context: Map<String, MoValue>,
runtime: MoLangRuntime
): net.minecraft.world.entity.Entity? {
return try {
val result = runtime.resolve(selector, context)
if (result is ObjectValue<*>) {
val entity = result.obj as? net.minecraft.world.entity.Entity
if (entity != null && !entity.isRemoved && entity.isAlive) entity else null
} else null
} catch (e: Exception) {
Cobblemon.LOGGER.error("[CameraFocus] Failed to resolve entity from selector", e)
null
}
}
fun fromJson(obj: JsonObject): CameraFocus {
val type = obj.get("type")?.asString
?: throw IllegalArgumentException("CameraFocus missing 'type' field")
return when (type) {
"entity" -> {
val selector = obj.get("selector")?.asString
?: throw IllegalArgumentException("Entity focus missing 'selector' field")
Entity(selector.asExpressionLike())
}
"position", "point" -> {
val posElement = obj.get("position")
?: obj // If no nested position, use the object itself
Point(PositionExpression.fromJson(posElement))
}
"bone" -> {
val selector = obj.get("selector")?.asString
?: throw IllegalArgumentException("Bone focus missing 'selector' field")
val boneName = obj.get("bone")?.asString
?: obj.get("boneName")?.asString
?: throw IllegalArgumentException("Bone focus missing 'bone' field")
val offset = if (obj.has("offset")) {
PositionExpression.fromJson(obj.get("offset"))
} else {
PositionExpression(
CoordinateValue.Static(0.0),
CoordinateValue.Static(0.0),
CoordinateValue.Static(0.0)
)
}
val smooth = obj.get("smooth")?.asBoolean ?: true
Bone(selector.asExpressionLike(), boneName, offset, smooth)
}
else -> {
throw IllegalArgumentException("Unknown CameraFocus type: $type")
}
}
}
}
}
/**
* Position defined by three coordinates (x, y, z), each either static or dynamic.
*/
data class PositionExpression(
val x: CoordinateValue,
val y: CoordinateValue,
val z: CoordinateValue
) {
fun toVec3(
context: Map<String, MoValue> = emptyMap(),
runtime: MoLangRuntime,
origin: Vec3 = Vec3.ZERO,
isRelative: Boolean = false
): Vec3 {
val base = Vec3(
x.evaluate(context, runtime),
y.evaluate(context, runtime),
z.evaluate(context, runtime)
)
return if (isRelative) base.add(origin) else base
}
fun isFinite(context: Map<String, MoValue>, runtime: MoLangRuntime): Boolean {
val vec = toVec3(context, runtime)
return vec.x.isFinite() && vec.y.isFinite() && vec.z.isFinite()
}
companion object {
fun fromJson(element: JsonElement): PositionExpression {
return when {
element.isJsonObject -> {
val obj = element.asJsonObject
PositionExpression(
x = deserializeCoordinate(obj.get("x")),
y = deserializeCoordinate(obj.get("y")),
z = deserializeCoordinate(obj.get("z"))
)
}
element.isJsonArray -> {
val arr = element.asJsonArray
if (arr.size() != 3) {
throw IllegalArgumentException("Position array must have exactly 3 elements [x, y, z], got ${arr.size()}")
}
PositionExpression(
x = deserializeCoordinate(arr[0]),
y = deserializeCoordinate(arr[1]),
z = deserializeCoordinate(arr[2])
)
}
else -> {
throw IllegalArgumentException("Position must be either an object {x, y, z} or array [x, y, z]")
}
}
}
private fun deserializeCoordinate(element: JsonElement?): CoordinateValue {
if (element == null) {
return CoordinateValue.Static(0.0)
}
return when {
element.isJsonPrimitive && element.asJsonPrimitive.isNumber -> {
CoordinateValue.Static(element.asDouble)
}
element.isJsonPrimitive && element.asJsonPrimitive.isString -> {
// String is treated as expression
CoordinateValue.Dynamic(element.asString.asExpressionLike())
}
element.isJsonObject -> {
val obj = element.asJsonObject
val type = obj.get("type")?.asString
val expression = obj.get("expression")?.asString
when (type) {
"expression" -> {
if (expression == null) {
throw IllegalArgumentException("Expression coordinate missing 'expression' field")
}
CoordinateValue.Dynamic(expression.asExpressionLike())
}
else -> {
throw IllegalArgumentException("Unknown coordinate type: $type")
}
}
}
else -> {
throw IllegalArgumentException("Invalid coordinate value: $element")
}
}
}
}
}
sealed class CoordinateValue {
data class Static(val value: Double) : CoordinateValue()
data class Dynamic(val expression: ExpressionLike) : CoordinateValue()
fun evaluate(
context: Map<String, MoValue>,
runtime: MoLangRuntime
): Double {
return when (this) {
is Static -> value
is Dynamic -> {
try {
runtime.resolve(expression, context).asDouble()
} catch (e: Exception) {
// Log error but don't crash - coordinate expressions shouldn't break cameras
0.0
}
}
}
}
}
@@ -1,34 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.camera.model
enum class HudElement(val id: String) {
CROSSHAIR("crosshair"),
HOTBAR("hotbar"),
EXPERIENCE_BAR("experience_bar"),
PLAYER_HEALTH("player_health"),
ARMOR("armor"),
FOOD("food"),
AIR("air"),
EFFECTS("effects"),
BOSS_BAR("boss_bar"),
SCOREBOARD("scoreboard"),
CHAT("chat"),
PLAYER_LIST("player_list"),
SELECTED_ITEM_NAME("selected_item_name"),
COBBLEMON_OVERLAY("cobblemon_overlay");
companion object {
val ALL: Set<HudElement> = entries.toSet()
private val BY_ID = entries.associateBy { it.id }
fun fromId(id: String): HudElement? = BY_ID[id]
}
}
@@ -1,79 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.camera.model
/**
* Defines a parameter that can be passed to a camera timeline.
*
* Parameters allow timelines to be reusable with different values.
* For example, an arena camera can accept `arena_center` as a Vec3 parameter
* to position the camera relative to any arena location.
*
* @property type The type of the parameter (NUMBER or VEC3)
* @property description Optional description for documentation
* @property default Optional default value. If null, the parameter is required.
*/
data class ParameterDefinition(
val type: ParameterType,
val description: String? = null,
val default: ParameterDefault? = null
) {
/**
* Whether this parameter must be provided when starting the timeline.
* Parameters with defaults are optional.
*/
val isRequired: Boolean get() = default == null
/**
* Gets the variable names this parameter will inject into the MoLang runtime.
* - NUMBER type: returns just the parameter name
* - VEC3 type: returns name_x, name_y, name_z
*/
fun getVariableNames(paramName: String): List<String> {
return when (type) {
ParameterType.NUMBER -> listOf(paramName)
ParameterType.VEC3 -> listOf("${paramName}_x", "${paramName}_y", "${paramName}_z")
}
}
}
/**
* Types of parameters supported by camera timelines.
*/
enum class ParameterType {
/**
* A single numeric value (Double).
* Accessible in MoLang as `variable.param_name`
*/
NUMBER,
/**
* A 3D vector value.
* Expands to three variables in MoLang:
* - `variable.param_name_x`
* - `variable.param_name_y`
* - `variable.param_name_z`
*/
VEC3
}
/**
* Default value for a parameter.
*/
sealed class ParameterDefault {
/**
* Default value for a NUMBER parameter.
*/
data class NumberDefault(val value: Double) : ParameterDefault()
/**
* Default value for a VEC3 parameter.
*/
data class Vec3Default(val x: Double, val y: Double, val z: Double) : ParameterDefault()
}
@@ -1,243 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.camera.model
import com.cobblemon.mod.common.api.camera.result.ValidationError
import com.cobblemon.mod.common.api.molang.ExpressionLike
/**
* Immutable timeline definition - a state machine of camera animations with transitions.
*
* Timelines can declare required and optional parameters that must be provided when
* starting the camera. Parameters are injected into the MoLang runtime as variables,
* allowing tracks to use dynamic values like `variable.arena_center_x`.
*
* @property id Unique identifier for this timeline
* @property startStateId The ID of the initial state when the timeline starts
* @property parameters Map of parameter names to their definitions. Parameters without
* defaults are required and must be provided when starting the camera.
* @property states Map of state IDs to state definitions
* @property transitions List of transitions between states
*/
data class Timeline(
val id: String,
val startStateId: String,
val parameters: Map<String, ParameterDefinition> = emptyMap(),
val states: Map<String, State>,
val transitions: List<Transition>
) {
fun getState(stateId: String): State? = states[stateId]
fun getTransitionsFrom(stateId: String): List<Transition> =
transitions.filter { it.sourceStateId == stateId }
/**
* Gets the names of all required parameters (those without defaults).
*/
fun getRequiredParameterNames(): Set<String> =
parameters.filter { it.value.isRequired }.keys
/**
* Gets all variable names that this timeline's parameters will inject.
* For NUMBER parameters, returns the parameter name.
* For VEC3 parameters, returns name_x, name_y, name_z.
*/
fun getAllParameterVariableNames(): Set<String> =
parameters.flatMap { (name, def) -> def.getVariableNames(name) }.toSet()
/**
* Validates that all required parameters are provided in the given context.
*
* @param providedVariables Set of variable names provided in the context
* @return List of missing parameter names, empty if all required params are present
*/
fun validateParameters(providedVariables: Set<String>): List<String> {
val missingParams = mutableListOf<String>()
for ((paramName, definition) in parameters) {
if (!definition.isRequired) continue
val requiredVars = definition.getVariableNames(paramName)
val allProvided = requiredVars.all { it in providedVariables }
if (!allProvided) {
missingParams.add(paramName)
}
}
return missingParams
}
fun validate(): List<ValidationError> {
val errors = mutableListOf<ValidationError>()
// Check start state exists
if (startStateId !in states) {
errors.add(ValidationError(
path = "startStateId",
message = "Start state '$startStateId' not found in states map"
))
}
// Check all transition states exist
transitions.forEachIndexed { index, transition ->
if (transition.sourceStateId !in states) {
errors.add(ValidationError(
path = "transitions[$index].sourceStateId",
message = "Source state '${transition.sourceStateId}' not found"
))
}
if (transition.targetStateId !in states) {
errors.add(ValidationError(
path = "transitions[$index].targetStateId",
message = "Target state '${transition.targetStateId}' not found"
))
}
}
states.forEach { (stateId, state) ->
if (state.tracks.isEmpty()) {
errors.add(ValidationError(
path = "states[$stateId].tracks",
message = "State has no tracks defined",
severity = ValidationError.Severity.WARNING
))
}
}
val reachableStates = findReachableStates()
states.keys.forEach { stateId ->
if (stateId !in reachableStates) {
errors.add(ValidationError(
path = "states[$stateId]",
message = "State is unreachable from start state '$startStateId'",
severity = ValidationError.Severity.WARNING
))
}
}
val cycles = detectCycles()
if (cycles.isNotEmpty()) {
errors.add(ValidationError(
path = "transitions",
message = "Detected transition cycles: ${cycles.joinToString(", ") { it.joinToString(" -> ") }}",
severity = ValidationError.Severity.WARNING
))
}
return errors
}
private fun findReachableStates(): Set<String> {
val reachable = mutableSetOf<String>()
val queue = ArrayDeque<String>()
if (startStateId in states) {
queue.add(startStateId)
reachable.add(startStateId)
}
while (queue.isNotEmpty()) {
val current = queue.removeFirst()
transitions
.filter { it.sourceStateId == current }
.forEach { transition ->
if (transition.targetStateId !in reachable && transition.targetStateId in states) {
reachable.add(transition.targetStateId)
queue.add(transition.targetStateId)
}
}
}
return reachable
}
private fun detectCycles(): List<List<String>> {
val cycles = mutableListOf<List<String>>()
val visited = mutableSetOf<String>()
val recStack = mutableSetOf<String>()
fun dfs(node: String, path: MutableList<String>): Boolean {
if (node !in states) return false
visited.add(node)
recStack.add(node)
path.add(node)
for (transition in getTransitionsFrom(node)) {
val target = transition.targetStateId
if (target !in visited) {
if (dfs(target, path)) return true
} else if (target in recStack) {
val cycleStart = path.indexOf(target)
if (cycleStart >= 0) {
cycles.add(path.subList(cycleStart, path.size) + target)
}
}
}
path.removeAt(path.size - 1)
recStack.remove(node)
return false
}
states.keys.forEach { stateId ->
if (stateId !in visited) {
dfs(stateId, mutableListOf())
}
}
return cycles
}
}
/**
* Immutable camera state definition - a collection of tracks that play together.
*/
data class State(
val id: String,
val tracks: List<CameraTrack>,
val duration: Float? = null,
val isLooping: Boolean = false,
val speed: Float = 1.0f,
val rememberProgress: Boolean = false,
val renderPlayerBody: Boolean = true,
val hiddenHudElements: Set<HudElement> = emptySet()
) {
init {
require(speed > 0f) { "State speed must be positive, got $speed" }
duration?.let { d ->
require(d >= 0f) { "State duration must be non-negative, got $d" }
}
}
fun getEffectiveDuration(): Float {
return duration ?: tracks.maxOfOrNull { it.duration } ?: 0f
}
}
/**
* Immutable transition definition - defines when and how to move from one state to another.
*/
data class Transition(
val sourceStateId: String,
val targetStateId: String,
val condition: ExpressionLike,
val type: TransitionType = TransitionType.BLEND,
val blendDuration: Float = 0.4f
) {
init {
require(blendDuration >= 0f) { "Blend duration must be non-negative, got $blendDuration" }
}
}
enum class TransitionType {
INSTANT,
BLEND
}
@@ -1,59 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.camera.model
import com.bedrockk.molang.runtime.MoLangRuntime
import com.bedrockk.molang.runtime.value.MoValue
import com.cobblemon.mod.common.api.camera.CameraRotation
import com.cobblemon.mod.common.api.molang.ExpressionLike
import net.minecraft.world.phys.Vec3
/**
* Type-safe track evaluation result.
*/
sealed class TrackOutput {
data class Position(val position: Vec3) : TrackOutput()
data class Focus(val target: CameraFocus) : TrackOutput()
data class Rotation(val rotation: CameraRotation) : TrackOutput()
data class Event(val expression: ExpressionLike, val eventId: String?) : TrackOutput()
}
/**
* Camera track interface - produces outputs over time.
*/
interface CameraTrack {
val trackType: String
val duration: Float
fun evaluate(
time: Float,
context: Map<String, MoValue>,
runtime: MoLangRuntime,
origin: Vec3
): List<TrackOutput>
}
enum class TrackCapability {
POSITION,
FOCUS,
ROTATION,
EVENTS
}
fun CameraTrack.capabilities(): Set<TrackCapability> {
val outputs = evaluate(0f, emptyMap(), MoLangRuntime(), Vec3.ZERO)
return outputs.mapTo(mutableSetOf()) { output ->
when (output) {
is TrackOutput.Position -> TrackCapability.POSITION
is TrackOutput.Focus -> TrackCapability.FOCUS
is TrackOutput.Rotation -> TrackCapability.ROTATION
is TrackOutput.Event -> TrackCapability.EVENTS
}
}
}
@@ -1,222 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.camera.registry
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.camera.CameraTrackRegistry
import com.cobblemon.mod.common.api.camera.model.*
import com.cobblemon.mod.common.util.asExpressionLike
import com.google.gson.*
import java.lang.reflect.Type
/**
* JSON deserializer for Timeline (V2 format).
*
* Deserializes directly into the immutable Timeline model without legacy conversion.
*/
object TimelineDeserializer : JsonDeserializer<Timeline> {
override fun deserialize(json: JsonElement, typeOfT: Type, ctx: JsonDeserializationContext): Timeline {
val obj = json.asJsonObject
val id = obj.get("timelineId")?.asString
?: throw JsonParseException("Timeline missing 'timelineId' field")
val startStateId = obj.get("startStateId")?.asString
?: throw JsonParseException("Timeline '$id' missing 'startStateId' field")
val statesObj = obj.get("states")?.asJsonObject
?: throw JsonParseException("Timeline '$id' missing 'states' object")
val transitionsArray = obj.get("transitions")?.asJsonArray ?: JsonArray()
// Parse parameters (optional)
val parametersObj = obj.get("parameters")?.asJsonObject
val parameters = parametersObj?.entrySet()?.associate { (paramName, paramValue) ->
try {
paramName to deserializeParameter(paramValue.asJsonObject, paramName, id)
} catch (e: Exception) {
Cobblemon.LOGGER.error("[Timeline] Error parsing parameter '$paramName' in timeline '$id': ${e.message}")
throw JsonParseException("Invalid parameter '$paramName' in timeline '$id'", e)
}
} ?: emptyMap()
val states = statesObj.entrySet().associate { (key, value) ->
try {
key to deserializeState(value.asJsonObject, id)
} catch (e: Exception) {
Cobblemon.LOGGER.error("[Timeline] Error parsing state '$key' in timeline '$id': ${e.message}")
throw JsonParseException("Invalid state '$key' in timeline '$id'", e)
}
}
val transitions = transitionsArray.mapIndexed { index, element ->
try {
deserializeTransition(element.asJsonObject, id)
} catch (e: Exception) {
Cobblemon.LOGGER.error("[Timeline] Error parsing transition $index in timeline '$id': ${e.message}")
throw JsonParseException("Invalid transition $index in timeline '$id'", e)
}
}
return Timeline(
id = id,
startStateId = startStateId,
parameters = parameters,
states = states,
transitions = transitions
)
}
private fun deserializeParameter(obj: JsonObject, paramName: String, timelineId: String): ParameterDefinition {
val typeStr = obj.get("type")?.asString
?: throw JsonParseException("Parameter '$paramName' missing 'type' field")
val type = when (typeStr.lowercase()) {
"number" -> ParameterType.NUMBER
"vec3" -> ParameterType.VEC3
else -> throw JsonParseException("Parameter '$paramName' has unknown type '$typeStr'. Valid types: number, vec3")
}
val description = obj.get("description")?.asString
val default: ParameterDefault? = when {
!obj.has("default") -> null
type == ParameterType.NUMBER -> {
val defaultValue = obj.get("default")
if (defaultValue.isJsonPrimitive && defaultValue.asJsonPrimitive.isNumber) {
ParameterDefault.NumberDefault(defaultValue.asDouble)
} else {
throw JsonParseException("Parameter '$paramName' default must be a number")
}
}
type == ParameterType.VEC3 -> {
val defaultObj = obj.get("default")
if (defaultObj.isJsonObject) {
val vec = defaultObj.asJsonObject
val x = vec.get("x")?.asDouble
?: throw JsonParseException("Parameter '$paramName' vec3 default missing 'x' field")
val y = vec.get("y")?.asDouble
?: throw JsonParseException("Parameter '$paramName' vec3 default missing 'y' field")
val z = vec.get("z")?.asDouble
?: throw JsonParseException("Parameter '$paramName' vec3 default missing 'z' field")
ParameterDefault.Vec3Default(x, y, z)
} else {
throw JsonParseException("Parameter '$paramName' vec3 default must be an object with x, y, z fields")
}
}
else -> null
}
return ParameterDefinition(
type = type,
description = description,
default = default
)
}
private fun deserializeState(obj: JsonObject, timelineId: String): State {
val stateId = obj.get("stateId")?.asString
?: throw JsonParseException("State missing 'stateId' field")
val tracksArray = obj.get("tracks")?.asJsonArray
?: throw JsonParseException("State '$stateId' missing 'tracks' array")
val tracks = tracksArray.mapIndexedNotNull { index, element ->
try {
deserializeTrack(element.asJsonObject)
} catch (e: Exception) {
Cobblemon.LOGGER.error("[Timeline] Error parsing track $index in state '$stateId': ${e.message}", e)
null
}
}
val duration = obj.get("duration")?.asFloat
val isLooping = obj.get("isLooping")?.asBoolean ?: false
val speed = obj.get("speed")?.asFloat ?: 1.0f
val rememberProgress = obj.get("rememberProgress")?.asBoolean ?: false
val renderPlayerBody = obj.get("renderPlayerBody")?.asBoolean ?: true
val hiddenHudElements = when (val hideHudElement = obj.get("hideHud")) {
null -> emptySet()
is JsonPrimitive -> {
if (hideHudElement.isBoolean) {
if (hideHudElement.asBoolean) HudElement.ALL else emptySet()
} else {
Cobblemon.LOGGER.warn("[Timeline] Invalid hideHud value in state '$stateId', expected boolean or array")
emptySet()
}
}
is JsonArray -> {
hideHudElement.mapNotNull { element ->
val id = element.asString
HudElement.fromId(id) ?: run {
Cobblemon.LOGGER.warn("[Timeline] Unknown HUD element '$id' in state '$stateId'")
null
}
}.toSet()
}
else -> {
Cobblemon.LOGGER.warn("[Timeline] Invalid hideHud value in state '$stateId', expected boolean or array")
emptySet()
}
}
return State(
id = stateId,
tracks = tracks,
duration = duration,
isLooping = isLooping,
speed = speed,
rememberProgress = rememberProgress,
renderPlayerBody = renderPlayerBody,
hiddenHudElements = hiddenHudElements
)
}
private fun deserializeTrack(obj: JsonObject): CameraTrack {
val trackType = obj.get("trackType")?.asString
?: throw JsonParseException("Track missing 'trackType' field")
return CameraTrackRegistry.deserializeTrack(trackType, obj)
?: throw JsonParseException("Unknown track type: $trackType")
}
private fun deserializeTransition(obj: JsonObject, timelineId: String): Transition {
val sourceStateId = obj.get("sourceStateId")?.asString
?: throw JsonParseException("Transition missing 'sourceStateId' field")
val targetStateId = obj.get("targetStateId")?.asString
?: throw JsonParseException("Transition missing 'targetStateId' field")
val conditionStr = obj.get("condition")?.asString
?: throw JsonParseException("Transition from '$sourceStateId' to '$targetStateId' missing 'condition' field")
val blendDuration = obj.get("blendDuration")?.asFloat ?: 0.4f
val transitionType = obj.get("transitionType")?.asString?.let { typeStr ->
when (typeStr.lowercase()) {
"blend" -> TransitionType.BLEND
"instant" -> TransitionType.INSTANT
else -> {
Cobblemon.LOGGER.warn("[Timeline] Unknown transition type '$typeStr' in timeline '$timelineId', using BLEND")
TransitionType.BLEND
}
}
} ?: TransitionType.BLEND
return Transition(
sourceStateId = sourceStateId,
targetStateId = targetStateId,
condition = conditionStr.asExpressionLike(),
type = transitionType,
blendDuration = blendDuration
)
}
}
@@ -1,117 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.camera.registry
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.camera.model.Timeline
import com.cobblemon.mod.common.api.camera.result.ValidationError
import com.cobblemon.mod.common.api.data.DataRegistry
import com.cobblemon.mod.common.api.reactive.SimpleObservable
import com.cobblemon.mod.common.util.cobblemonResource
import com.google.gson.GsonBuilder
import net.minecraft.server.level.ServerPlayer
import net.minecraft.server.packs.PackType
import net.minecraft.server.packs.resources.ResourceManager
import java.io.InputStreamReader
/**
* Registry for loading Timeline (V2 format) from JSON files.
*
* Loads timelines directly into the new immutable Timeline model
* without legacy conversion.
*/
object TimelineRegistry : DataRegistry {
override val id = cobblemonResource("camera_timelines")
override val type = PackType.SERVER_DATA
override val observable = SimpleObservable<TimelineRegistry>()
private val gson = GsonBuilder()
.registerTypeAdapter(Timeline::class.java, TimelineDeserializer)
.create()
private val timelines = mutableMapOf<String, Timeline>()
override fun reload(manager: ResourceManager) {
timelines.clear()
val resources = manager.listResources("camera_timelines") { path ->
path.path.endsWith(".json")
}
for ((identifier, resource) in resources) {
try {
resource.open().use { stream ->
val reader = InputStreamReader(stream)
val timeline = gson.fromJson(reader, Timeline::class.java)
val errors = timeline.validate()
val hasErrors = errors.any { it.severity == ValidationError.Severity.ERROR }
if (hasErrors) {
Cobblemon.LOGGER.error("[TimelineRegistry] Validation failed for '$identifier':")
errors.forEach { error ->
val level = if (error.severity == ValidationError.Severity.ERROR) "ERROR" else "WARN"
Cobblemon.LOGGER.error(" [$level] ${error.path}: ${error.message}")
}
continue
}
if (errors.isNotEmpty()) {
Cobblemon.LOGGER.warn("[TimelineRegistry] Warnings for timeline '${timeline.id}':")
errors.forEach { Cobblemon.LOGGER.warn(" ${it.path}: ${it.message}") }
}
if (timelines.containsKey(timeline.id)) {
Cobblemon.LOGGER.warn("[TimelineRegistry] Duplicate timeline ID '${timeline.id}' in $identifier, overwriting")
}
timelines[timeline.id] = timeline
Cobblemon.LOGGER.debug("[TimelineRegistry] Loaded timeline '${timeline.id}' from $identifier")
}
} catch (e: Exception) {
Cobblemon.LOGGER.error("[TimelineRegistry] Error loading timeline from $identifier", e)
}
}
Cobblemon.LOGGER.info("[TimelineRegistry] Loaded ${timelines.size} timelines")
observable.emit(this)
}
override fun sync(player: ServerPlayer) {
// Timelines are server-driven, no sync needed
}
fun getTimeline(id: String): Timeline? {
val timeline = timelines[id]
if (timeline == null) {
Cobblemon.LOGGER.error("[TimelineRegistry] Timeline '$id' not found")
}
return timeline
}
fun hasTimeline(id: String): Boolean = timelines.containsKey(id)
fun registerTimeline(timeline: Timeline) {
val errors = timeline.validate()
if (errors.any { it.severity == ValidationError.Severity.ERROR }) {
Cobblemon.LOGGER.error("[TimelineRegistry] Cannot register invalid timeline '${timeline.id}':")
errors.forEach { Cobblemon.LOGGER.error(" ${it.path}: ${it.message}") }
return
}
if (timelines.containsKey(timeline.id)) {
Cobblemon.LOGGER.warn("[TimelineRegistry] Timeline '${timeline.id}' already registered, overwriting")
}
timelines[timeline.id] = timeline
Cobblemon.LOGGER.debug("[TimelineRegistry] Registered timeline '${timeline.id}' programmatically")
}
fun getAllTimelines(): Collection<Timeline> = timelines.values
}
@@ -1,22 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.camera.result
/**
* Validation error with location context.
*/
data class ValidationError(
val path: String,
val message: String,
val severity: Severity = Severity.ERROR
) {
enum class Severity { WARNING, ERROR }
override fun toString(): String = "[$severity] $path: $message"
}
@@ -1,243 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.camera.track
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.camera.CameraTrackRegistry
import com.cobblemon.mod.common.api.tween.InterpolationMethod
import com.cobblemon.mod.common.api.camera.model.CameraFocus
import com.cobblemon.mod.common.api.camera.model.PositionExpression
import com.cobblemon.mod.common.util.asExpressionLike
import com.google.gson.JsonObject
object CameraTrackDeserializers {
/** Should be called during mod initialization. */
fun registerBuiltinTracks() {
CameraTrackRegistry.registerTrackType("position", ::deserializePositionTrack)
CameraTrackRegistry.registerTrackType("focus", ::deserializeFocusTrack)
CameraTrackRegistry.registerTrackType("event", ::deserializeEventTrack)
CameraTrackRegistry.registerTrackType("reference", ::deserializeReferenceTrack)
CameraTrackRegistry.registerTrackType("gui", ::deserializeGuiTrack)
Cobblemon.LOGGER.debug("[CameraTrackDeserializers] Registered built-in track types: position, focus, event, reference, gui")
}
/**
* Deserializes a position track from JSON.
*
* Expected JSON format:
* ```json
* {
* "trackType": "position",
* "keyframes": [
* {
* "time": 0.0,
* "position": { "x": 10.0, "y": 64.0, "z": 10.0 },
* "easing": "ease_in_out_quad"
* }
* ],
* "interpolationType": "bezier",
* "easing": "linear",
* "isRelative": false
* }
* ```
*/
private fun deserializePositionTrack(json: JsonObject): PositionTrack {
val keyframesArray = json.getAsJsonArray("keyframes")
?: throw IllegalArgumentException("Position track missing 'keyframes' array")
val keyframes = keyframesArray.map { element ->
val kfObj = element.asJsonObject
val time = kfObj.get("time")?.asFloat
?: throw IllegalArgumentException("Position keyframe missing 'time' field")
val positionElement = kfObj.get("position")
?: throw IllegalArgumentException("Position keyframe missing 'position' field")
val position = PositionExpression.fromJson(positionElement)
val easing = kfObj.get("easing")?.asString
PositionKeyframe(time, position, easing)
}
val interpolationTypeStr = json.get("interpolationType")?.asString ?: "linear"
val interpolationType = when (interpolationTypeStr.lowercase()) {
"linear" -> InterpolationMethod.LINEAR
"bezier", "cubic_bezier" -> InterpolationMethod.BEZIER
"hermite", "catmull_rom" -> InterpolationMethod.BEZIER // Map legacy types to available ones
else -> {
Cobblemon.LOGGER.warn("[CameraTrackDeserializers] Unknown interpolation type '$interpolationTypeStr', using LINEAR")
InterpolationMethod.LINEAR
}
}
val defaultEasing = json.get("easing")?.asString ?: "linear"
val isRelative = json.get("isRelative")?.asBoolean ?: false
return PositionTrack(keyframes, interpolationType, defaultEasing, isRelative)
}
/**
* Deserializes a focus track from JSON.
*
* Expected JSON format:
* ```json
* {
* "trackType": "focus",
* "keyframes": [
* {
* "time": 0.0,
* "target": { "type": "position", "x": 20.0, "y": 60.0, "z": 20.0 },
* "easing": "linear"
* }
* ],
* "easing": "linear"
* }
* ```
*/
private fun deserializeFocusTrack(json: JsonObject): FocusTrack {
val keyframesArray = json.getAsJsonArray("keyframes")
?: throw IllegalArgumentException("Focus track missing 'keyframes' array")
val keyframes = keyframesArray.map { element ->
val kfObj = element.asJsonObject
val time = kfObj.get("time")?.asFloat
?: throw IllegalArgumentException("Focus keyframe missing 'time' field")
val targetObj = kfObj.getAsJsonObject("target")
?: throw IllegalArgumentException("Focus keyframe missing 'target' field")
val target = CameraFocus.fromJson(targetObj)
val easing = kfObj.get("easing")?.asString
FocusKeyframe(time, target, easing)
}
val defaultEasing = json.get("easing")?.asString ?: "linear"
return FocusTrack(keyframes, defaultEasing)
}
/**
* Deserializes an event track from JSON.
*
* Expected JSON format:
* ```json
* {
* "trackType": "event",
* "events": [
* {
* "time": 0.5,
* "expression": "q.play_sound('cobblemon:poke_ball.open')",
* "eventId": "ball_open"
* }
* ]
* }
* ```
*/
private fun deserializeEventTrack(json: JsonObject): EventTrack {
val eventsArray = json.getAsJsonArray("events")
?: throw IllegalArgumentException("Event track missing 'events' array")
val events = eventsArray.map { element ->
val eventObj = element.asJsonObject
val time = eventObj.get("time")?.asFloat
?: throw IllegalArgumentException("Event missing 'time' field")
val expressionStr = eventObj.get("expression")?.asString
?: throw IllegalArgumentException("Event missing 'expression' field")
val eventId = eventObj.get("eventId")?.asString
TimedEvent(time, expressionStr.asExpressionLike(), eventId)
}
return EventTrack(events)
}
/**
* Deserializes a reference track from JSON.
*
* Expected JSON format:
* ```json
* {
* "trackType": "reference",
* "trackId": "intro_camera_movement"
* }
* ```
*/
private fun deserializeReferenceTrack(json: JsonObject): ReferenceTrack {
val trackId = json.get("trackId")?.asString
?: throw IllegalArgumentException("Reference track missing 'trackId' field")
return ReferenceTrack(trackId)
}
/**
* Deserializes a GUI track from JSON.
*
* Expected JSON format:
* ```json
* {
* "trackType": "gui",
* "actions": [
* {
* "time": 0.0,
* "action": "OPEN",
* "guiId": "cobblemon:battle_intro",
* "args": { "title": "Stadium Battle" },
* "argsExpressions": {
* "player_party_count": "variable.player_party_count"
* }
* },
* {
* "time": 5.0,
* "action": "CLOSE",
* "guiId": "cobblemon:battle_intro"
* }
* ]
* }
* ```
*/
private fun deserializeGuiTrack(json: JsonObject): GuiTrack {
val actionsArray = json.getAsJsonArray("actions")
?: throw IllegalArgumentException("GUI track missing 'actions' array")
val actions = actionsArray.map { element ->
val actionObj = element.asJsonObject
val time = actionObj.get("time")?.asFloat
?: throw IllegalArgumentException("GUI action missing 'time' field")
val actionType = actionObj.get("action")?.asString
?: throw IllegalArgumentException("GUI action missing 'action' field")
val guiId = actionObj.get("guiId")?.asString
?: throw IllegalArgumentException("GUI action missing 'guiId' field")
val args = mutableMapOf<String, String>()
actionObj.getAsJsonObject("args")?.entrySet()?.forEach { (key, value) ->
args[key] = value.asString
}
val argsExpressions = mutableMapOf<String, String>()
actionObj.getAsJsonObject("argsExpressions")?.entrySet()?.forEach { (key, value) ->
argsExpressions[key] = value.asString
}
TimedGuiAction(
time = time,
action = GuiActionType.valueOf(actionType.uppercase()),
guiId = guiId,
args = args,
argsExpressions = argsExpressions
)
}
return GuiTrack(actions)
}
}
@@ -1,56 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.camera.track
import com.bedrockk.molang.runtime.MoLangRuntime
import com.bedrockk.molang.runtime.value.MoValue
import com.cobblemon.mod.common.api.camera.model.CameraTrack
import com.cobblemon.mod.common.api.camera.model.TrackOutput
import com.cobblemon.mod.common.api.molang.ExpressionLike
import net.minecraft.world.phys.Vec3
/**
* Track that executes MoLang expressions at specific times.
*/
data class EventTrack(
val events: List<TimedEvent>
) : CameraTrack {
override val trackType = "event"
override val duration: Float
get() = events.maxOfOrNull { it.time } ?: 0f
override fun evaluate(
time: Float,
context: Map<String, MoValue>,
runtime: MoLangRuntime,
origin: Vec3
): List<TrackOutput> {
return emptyList()
}
init {
require(events.zipWithNext().all { (a, b) -> a.time <= b.time }) {
"EventTrack events must be sorted by time"
}
}
}
/**
* Timed event - execute expression at specific time.
*/
data class TimedEvent(
val time: Float,
val expression: ExpressionLike,
val eventId: String? = null
) {
init {
require(time >= 0f) { "Event time must be non-negative, got $time" }
}
}
@@ -1,126 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.camera.track
import com.bedrockk.molang.runtime.MoLangRuntime
import com.bedrockk.molang.runtime.value.MoValue
import com.cobblemon.mod.common.api.tween.EasingRegistry
import com.cobblemon.mod.common.api.tween.Interpolator
import com.cobblemon.mod.common.api.tween.Keyframe
import com.cobblemon.mod.common.api.camera.model.*
import net.minecraft.util.Mth
import net.minecraft.world.phys.Vec3
/**
* Track that animates camera look-at target over time.
* Point targets interpolate smoothly; entity targets switch instantly.
*/
data class FocusTrack(
val keyframes: List<FocusKeyframe>,
val defaultEasing: String = "linear"
) : CameraTrack {
override val trackType = "focus"
override val duration: Float
get() = keyframes.maxOfOrNull { it.time } ?: 0f
override fun evaluate(
time: Float,
context: Map<String, MoValue>,
runtime: MoLangRuntime,
origin: Vec3
): List<TrackOutput> {
if (keyframes.isEmpty()) return emptyList()
val segment = findFocusSegment(time) ?: return emptyList()
val focus = when {
// Both are Point targets - interpolate
segment.start is CameraFocus.Point && segment.end is CameraFocus.Point -> {
val startPos = segment.start.position.toVec3(context, runtime)
val endPos = segment.end.position.toVec3(context, runtime)
// Interpolate positions
val easedProgress = EasingRegistry.getEasingFunction(segment.easing)(segment.progress)
val interpolatedPos = Vec3(
Mth.lerp(easedProgress.toDouble(), startPos.x, endPos.x),
Mth.lerp(easedProgress.toDouble(), startPos.y, endPos.y),
Mth.lerp(easedProgress.toDouble(), startPos.z, endPos.z)
)
CameraFocus.Point(
PositionExpression(
CoordinateValue.Static(interpolatedPos.x),
CoordinateValue.Static(interpolatedPos.y),
CoordinateValue.Static(interpolatedPos.z)
)
)
}
// Entity targets or mixed - use instant switching
else -> {
if (segment.progress < 0.5f) segment.start else segment.end
}
}
return listOf(TrackOutput.Focus(focus))
}
private fun findFocusSegment(time: Float): FocusSegment? {
if (keyframes.isEmpty()) return null
if (keyframes.size == 1) {
val kf = keyframes[0]
return FocusSegment(kf.target, kf.target, kf.time, kf.time, 1f, defaultEasing)
}
for (i in 0 until keyframes.size - 1) {
val start = keyframes[i]
val end = keyframes[i + 1]
if (time <= end.time) {
val duration = end.time - start.time
val progress = if (duration > 0f) {
((time - start.time) / duration).coerceIn(0f, 1f)
} else 1f
val easing = end.easing ?: defaultEasing
return FocusSegment(start.target, end.target, start.time, end.time, progress, easing)
}
}
val last = keyframes.last()
return FocusSegment(last.target, last.target, last.time, last.time, 1f, defaultEasing)
}
init {
require(keyframes.isNotEmpty()) { "FocusTrack must have at least one keyframe" }
require(keyframes.zipWithNext().all { (a, b) -> a.time <= b.time }) {
"FocusTrack keyframes must be sorted by time"
}
}
private data class FocusSegment(
val start: CameraFocus,
val end: CameraFocus,
val startTime: Float,
val endTime: Float,
val progress: Float,
val easing: String
)
}
data class FocusKeyframe(
val time: Float,
val target: CameraFocus,
val easing: String? = null
) {
init {
require(time >= 0f) { "Keyframe time must be non-negative, got $time" }
}
}
@@ -1,86 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.camera.track
import com.bedrockk.molang.runtime.MoLangRuntime
import com.bedrockk.molang.runtime.value.MoValue
import com.cobblemon.mod.common.api.camera.model.CameraTrack
import com.cobblemon.mod.common.api.camera.model.TrackOutput
import net.minecraft.world.phys.Vec3
/**
* Track that fires timed GUI actions (open, close, update) during camera timelines.
*
* This track produces no camera outputs itself (no position/focus/rotation).
* The actions are executed by the CameraExecutor when their time is crossed.
*/
data class GuiTrack(
val actions: List<TimedGuiAction>
) : CameraTrack {
override val trackType = "gui"
override val duration: Float
get() = actions.maxOfOrNull { it.time } ?: 0f
override fun evaluate(
time: Float,
context: Map<String, MoValue>,
runtime: MoLangRuntime,
origin: Vec3
): List<TrackOutput> {
// GUI actions are handled by the engine/executor, not via TrackOutput
return emptyList()
}
init {
require(actions.zipWithNext().all { (a, b) -> a.time <= b.time }) {
"GuiTrack actions must be sorted by time"
}
}
}
/**
* Determines the type of GUI action to fire.
*/
enum class GuiActionType {
OPEN,
CLOSE,
UPDATE_ARGS
}
/**
* A timed GUI action within a timeline.
*
* @param time The time in seconds when this action should fire
* @param action The type of action
* @param guiId The scripted GUI resource location (e.g., "cobblemon:battle_intro")
* @param args Static argument values
* @param argsExpressions MoLang expressions to evaluate at fire time, merged with static args
*/
data class TimedGuiAction(
val time: Float,
val action: GuiActionType,
val guiId: String,
val args: Map<String, String> = emptyMap(),
val argsExpressions: Map<String, String> = emptyMap()
) {
init {
require(time >= 0f) { "GUI action time must be non-negative, got $time" }
}
}
/**
* Represents a GUI action that has fired (time crossed).
*/
data class FiredGuiAction(
val action: GuiActionType,
val guiId: String,
val args: Map<String, String>,
val argsExpressions: Map<String, String>
)
@@ -1,79 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.camera.track
import com.bedrockk.molang.runtime.MoLangRuntime
import com.bedrockk.molang.runtime.value.MoValue
import com.cobblemon.mod.common.api.tween.*
import com.cobblemon.mod.common.api.camera.model.*
import net.minecraft.world.phys.Vec3
/**
* Track that animates camera position over time using keyframes.
*/
data class PositionTrack(
val keyframes: List<PositionKeyframe>,
val interpolation: InterpolationMethod = InterpolationMethod.LINEAR,
val defaultEasing: String = "linear",
val isRelative: Boolean = false
) : CameraTrack {
override val trackType = "position"
override val duration: Float
get() = keyframes.maxOfOrNull { it.time } ?: 0f
override fun evaluate(
time: Float,
context: Map<String, MoValue>,
runtime: MoLangRuntime,
origin: Vec3
): List<TrackOutput> {
if (keyframes.isEmpty()) return emptyList()
val interpolatableKeyframes = keyframes.map { kf ->
val vec = kf.position.toVec3(context, runtime, origin, isRelative)
Keyframe(kf.time, vec.asInterpolatable())
}
val interpolated = Interpolator.interpolate(
keyframes = interpolatableKeyframes,
time = time,
method = interpolation,
defaultEasing = defaultEasing,
getEasing = { value ->
// Find original keyframe to get easing
val index = interpolatableKeyframes.indexOfFirst { it.value == value }
keyframes.getOrNull(index)?.easing
}
)
return if (interpolated != null) {
listOf(TrackOutput.Position(interpolated.vec))
} else {
emptyList()
}
}
init {
require(keyframes.isNotEmpty()) { "PositionTrack must have at least one keyframe" }
require(keyframes.zipWithNext().all { (a, b) -> a.time <= b.time }) {
"PositionTrack keyframes must be sorted by time"
}
}
}
data class PositionKeyframe(
val time: Float,
val position: PositionExpression,
val easing: String? = null
) {
init {
require(time >= 0f) { "Keyframe time must be non-negative, got $time" }
}
}
@@ -1,50 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.camera.track
import com.bedrockk.molang.runtime.MoLangRuntime
import com.bedrockk.molang.runtime.value.MoValue
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.camera.model.CameraTrack
import com.cobblemon.mod.common.api.camera.model.TrackOutput
import com.cobblemon.mod.common.client.camera.tracks.CameraTrackDefinitionRegistry
import net.minecraft.world.phys.Vec3
/**
* Track that references another track definition by ID.
*/
data class ReferenceTrack(
val trackId: String
) : CameraTrack {
override val trackType = "reference"
override val duration: Float
get() {
val referencedTrack = CameraTrackDefinitionRegistry.getTrack(trackId)
return referencedTrack?.duration ?: 0f
}
override fun evaluate(
time: Float,
context: Map<String, MoValue>,
runtime: MoLangRuntime,
origin: Vec3
): List<TrackOutput> {
val referencedTrack = CameraTrackDefinitionRegistry.getTrack(trackId)
if (referencedTrack == null) {
// Log warning but don't crash
Cobblemon.LOGGER.warn("[ReferenceTrack] Track '$trackId' not found in registry")
return emptyList()
}
// Delegate to referenced track
return referencedTrack.evaluate(time, context, runtime, origin)
}
}
@@ -1,22 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cooking.effects
import net.minecraft.world.effect.MobEffect
import net.minecraft.world.effect.MobEffectCategory
import net.minecraft.world.entity.Entity
import net.minecraft.world.entity.LivingEntity
class CleanseAllEffect : MobEffect(MobEffectCategory.BENEFICIAL, 0xFFFFFF) {
override fun isInstantenous() = true
override fun applyInstantenousEffect(source: Entity?, indirect: Entity?, target: LivingEntity, amplifier: Int, proximity: Double) {
target.removeAllEffects()
}
override fun shouldApplyEffectTickThisTick(duration: Int, amplifier: Int) = false
}
@@ -1,27 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cooking.effects
import net.minecraft.world.effect.MobEffect
import net.minecraft.world.effect.MobEffectCategory
import net.minecraft.world.entity.Entity
import net.minecraft.world.entity.LivingEntity
class CleanseNegativeEffect : MobEffect(MobEffectCategory.BENEFICIAL, 0xFFFFFF) {
override fun isInstantenous() = true
override fun applyInstantenousEffect(source: Entity?, indirect: Entity?, target: LivingEntity, amplifier: Int, proximity: Double) {
val snapshot = target.activeEffects.toList()
for (ei in snapshot) {
if (ei.effect.value().category == MobEffectCategory.HARMFUL) {
target.removeEffect(ei.effect)
}
}
}
override fun shouldApplyEffectTickThisTick(duration: Int, amplifier: Int) = false
}
@@ -1,33 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.cooking.effects
import net.minecraft.server.level.ServerPlayer
import net.minecraft.stats.Stats
import net.minecraft.world.effect.MobEffect
import net.minecraft.world.effect.MobEffectCategory
import net.minecraft.world.entity.LivingEntity
class MentalRestorationEffect : MobEffect(MobEffectCategory.BENEFICIAL, 0xAA66FF) {
override fun applyEffectTick(entity: LivingEntity, amplifier: Int): Boolean {
if (entity is ServerPlayer) {
val stats = entity.stats
val statType = Stats.CUSTOM.get(Stats.TIME_SINCE_REST)
val current = stats.getValue(statType)
val reduction = 31 * (amplifier + 1)// reduce by half a minute every second per level (30 ticks removed per tick + 1 to account for the tick passing)
val newValue = Math.max(current - reduction, 0)
stats.setValue(entity, statType, newValue)
}
return true
}
override fun shouldApplyEffectTickThisTick(duration: Int, amplifier: Int): Boolean {
return true
}
}
@@ -10,7 +10,6 @@ package com.cobblemon.mod.common.api.events
import com.cobblemon.mod.common.api.events.battles.BattleFaintedEvent
import com.cobblemon.mod.common.api.events.battles.BattleFledEvent
import com.cobblemon.mod.common.api.events.battles.BattleFleeAttemptEvent
import com.cobblemon.mod.common.api.events.battles.BattleStartedEvent
import com.cobblemon.mod.common.api.events.battles.BattleVictoryEvent
import com.cobblemon.mod.common.api.events.battles.instruction.FormeChangeEvent
@@ -22,7 +21,6 @@ import com.cobblemon.mod.common.api.events.berry.BerryMutationOfferEvent
import com.cobblemon.mod.common.api.events.berry.BerryMutationResultEvent
import com.cobblemon.mod.common.api.events.berry.BerryYieldCalculationEvent
import com.cobblemon.mod.common.api.events.cooking.PokeSnackSpawnPokemonEvent
import com.cobblemon.mod.common.api.events.camera.*
import com.cobblemon.mod.common.api.events.drops.LootDroppedEvent
import com.cobblemon.mod.common.api.events.entity.*
import com.cobblemon.mod.common.api.events.farming.ApricornHarvestEvent
@@ -112,9 +110,6 @@ object CobblemonEvents {
@JvmField
val BATTLE_STARTED_POST = EventObservable<BattleStartedEvent.Post>()
@JvmField
val BATTLE_FLEE_ATTEMPT = EventObservable<BattleFleeAttemptEvent>()
@JvmField
val BATTLE_FLED = EventObservable<BattleFledEvent>()
@@ -366,33 +361,4 @@ object CobblemonEvents {
// -------------------------------------------------------------------------------------
@JvmField
val SELECT_DRIVER = EventObservable<SelectDriverEvent>()
// -------------------------------------------------------------------------------------
//
// Camera System
//
// -------------------------------------------------------------------------------------
@JvmField
val CAMERA_STARTED_PRE = CancelableObservable<CameraStartedEvent.Pre>()
@JvmField
val CAMERA_STARTED_POST = EventObservable<CameraStartedEvent.Post>()
@JvmField
val CAMERA_STOPPED_PRE = EventObservable<CameraStoppedEvent.Pre>()
@JvmField
val CAMERA_STOPPED_POST = EventObservable<CameraStoppedEvent.Post>()
@JvmField
val CAMERA_STATE_TRANSITION = EventObservable<CameraStateTransitionEvent>()
@JvmField
val CAMERA_VIEWER_ADDED = EventObservable<CameraViewerAddedEvent>()
@JvmField
val CAMERA_VIEWER_REMOVED = EventObservable<CameraViewerRemovedEvent>()
@JvmField
val CAMERA_TICK = EventObservable<CameraTickEvent>()
}
@@ -9,7 +9,6 @@
package com.cobblemon.mod.common.api.events.battles
import com.bedrockk.molang.runtime.value.MoValue
import com.bedrockk.molang.runtime.value.StringValue
import com.cobblemon.mod.common.api.battles.interpreter.BattleContext
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.battles.model.actor.ActorType
@@ -26,8 +25,7 @@ import com.cobblemon.mod.common.util.asArrayValue
data class BattleFaintedEvent(
override val battle: PokemonBattle,
val killed: BattlePokemon,
val context: BattleContext,
val pnx: String
val context: BattleContext
) : BattleEvent {
val structContext = mutableMapOf<String, MoValue>(
"battle" to battle.struct,
@@ -35,7 +33,6 @@ data class BattleFaintedEvent(
"npcs" to battle.actors.filter { it.type == ActorType.NPC }.asArrayValue { it.struct },
"wild_pokemon" to battle.actors.filter { it.type == ActorType.WILD }.asArrayValue { it.struct },
"pokemon" to killed.struct,
"context" to context.getStruct(),
"pnx" to StringValue(pnx)
"context" to context.getStruct()
)
}
}
@@ -1,34 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.events.battles
import com.bedrockk.molang.runtime.value.MoValue
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.battles.model.actor.ActorType
import com.cobblemon.mod.common.battles.actor.PlayerBattleActor
import com.cobblemon.mod.common.util.asArrayValue
/**
* Event fired when a [PokemonBattle] is fled by a [PlayerBattleActor].
*
* @author saultsc
* @since March 31th 2026
*/
data class BattleFleeAttemptEvent(
override val battle: PokemonBattle,
val player: PlayerBattleActor
) : BattleEvent {
val context = mutableMapOf<String, MoValue>(
"battle" to battle.struct,
"players" to battle.actors.filter { it.type == ActorType.PLAYER }.asArrayValue { it.struct },
"npcs" to battle.actors.filter { it.type == ActorType.NPC }.asArrayValue { it.struct },
"wild_pokemon" to battle.actors.filter { it.type == ActorType.WILD }.asArrayValue { it.struct }
)
}
@@ -25,8 +25,7 @@ import com.cobblemon.mod.common.util.asArrayValue
data class FormeChangeEvent(
override val battle: PokemonBattle,
val pokemon: BattlePokemon,
val formeName: String,
val pnx: String
val formeName: String
) : BattleEvent {
val context = mutableMapOf(
"battle" to battle.struct,
@@ -34,7 +33,6 @@ data class FormeChangeEvent(
"npcs" to battle.actors.filter { it.type == ActorType.NPC }.asArrayValue { it.struct },
"wild_pokemon" to battle.actors.filter { it.type == ActorType.WILD }.asArrayValue { it.struct },
"pokemon" to pokemon.struct,
"forme" to StringValue(formeName),
"pnx" to StringValue(pnx)
"forme" to StringValue(formeName)
)
}
}
@@ -1,100 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.events.camera
import com.cobblemon.mod.common.api.camera.ICameraController
import com.cobblemon.mod.common.api.events.Cancelable
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.phys.Vec3
import java.util.UUID
/** Base interface for all camera events. */
interface CameraEvent {
val cameraId: UUID
val controller: ICameraController
}
/** Events fired when a camera starts. Pre is cancelable. */
interface CameraStartedEvent : CameraEvent {
val timelineId: String
val viewers: List<ServerPlayer>
/** Pre event - fired before camera starts. */
data class Pre(
override val cameraId: UUID,
override val controller: ICameraController,
override val timelineId: String,
override val viewers: List<ServerPlayer>
) : CameraStartedEvent, Cancelable()
/** Post event - fired after camera starts. */
data class Post(
override val cameraId: UUID,
override val controller: ICameraController,
override val timelineId: String,
override val viewers: List<ServerPlayer>
) : CameraStartedEvent
}
/** Events fired when a camera stops. */
interface CameraStoppedEvent : CameraEvent {
val reason: StopReason
enum class StopReason {
MANUAL,
COMPLETED,
VIEWER_LEFT,
ERROR
}
/** Pre event - fired before camera stops. */
data class Pre(
override val cameraId: UUID,
override val controller: ICameraController,
override val reason: StopReason
) : CameraStoppedEvent
/** Post event - fired after camera stops. */
data class Post(
override val cameraId: UUID,
override val controller: ICameraController,
override val reason: StopReason
) : CameraStoppedEvent
}
/** Fired when a camera transitions between states. */
data class CameraStateTransitionEvent(
override val cameraId: UUID,
override val controller: ICameraController,
val fromStateId: String,
val toStateId: String,
val transitionType: String
) : CameraEvent
/** Fired when a viewer is added to a camera. */
data class CameraViewerAddedEvent(
override val cameraId: UUID,
override val controller: ICameraController,
val viewer: ServerPlayer
) : CameraEvent
/** Fired when a viewer is removed from a camera. */
data class CameraViewerRemovedEvent(
override val cameraId: UUID,
override val controller: ICameraController,
val viewer: ServerPlayer
) : CameraEvent
/** Fired every tick while a camera is active. Fires 20 times per second. */
data class CameraTickEvent(
override val cameraId: UUID,
override val controller: ICameraController,
val currentPosition: Vec3,
val currentFocus: Vec3?
) : CameraEvent
@@ -9,14 +9,13 @@
package com.cobblemon.mod.common.api.interaction
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.conditional.RegistryLikeCondition
import com.cobblemon.mod.common.api.data.JsonDataRegistry
import com.cobblemon.mod.common.api.molang.ExpressionLike
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.api.pokemon.requirement.OwnerQueryRequirement
import com.cobblemon.mod.common.api.pokemon.requirement.Requirement
import com.cobblemon.mod.common.api.reactive.SimpleObservable
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.net.messages.client.data.PokemonInteractionsSyncPacket
import com.cobblemon.mod.common.pokemon.adapters.CobblemonRequirementAdapter
import com.cobblemon.mod.common.pokemon.evolution.adapters.LegacyItemConditionWrapperAdapter
import com.cobblemon.mod.common.pokemon.requirements.PokemonPropertiesRequirement
@@ -30,11 +29,6 @@ import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import net.minecraft.server.packs.PackType
import net.minecraft.util.LowerCaseEnumTypeAdapterFactory
import net.minecraft.world.item.Item
import net.minecraft.world.level.biome.Biome
import net.minecraft.world.level.block.Block
import net.minecraft.world.level.levelgen.structure.Structure
import net.minecraft.world.level.material.Fluid
object PokemonInteractions : JsonDataRegistry<PokemonInteractionSet> {
override val id = cobblemonResource("pokemon_interactions")
@@ -49,42 +43,40 @@ object PokemonInteractions : JsonDataRegistry<PokemonInteractionSet> {
.registerTypeAdapter(Requirement::class.java, CobblemonRequirementAdapter)
.registerTypeAdapter(InteractionEffect::class.java, InteractionEffectAdapter)
.registerTypeAdapter(ExpressionLike::class.java, ExpressionLikeAdapter)
.registerTypeAdapter(TypeToken.getParameterized(RegistryLikeCondition::class.java, Biome::class.java).type, BiomeLikeConditionAdapter)
.registerTypeAdapter(TypeToken.getParameterized(RegistryLikeCondition::class.java, Block::class.java).type, BlockLikeConditionAdapter)
.registerTypeAdapter(TypeToken.getParameterized(RegistryLikeCondition::class.java, Item::class.java).type, ItemLikeConditionAdapter)
.registerTypeAdapter(TypeToken.getParameterized(RegistryLikeCondition::class.java, Structure::class.java).type, StructureLikeConditionAdapter)
.registerTypeAdapter(TypeToken.getParameterized(RegistryLikeCondition::class.java, Fluid::class.java).type, FluidLikeConditionAdapter)
.registerTypeAdapter(IntRange::class.java, IntRangeAdapter)
.registerTypeAdapterFactory(LowerCaseEnumTypeAdapterFactory())
.setPrettyPrinting()
.create()
val interactions = mutableListOf<PokemonInteractionSet>()
val speciesInteractions = mutableListOf<PokemonInteractionSet>()
val generalInteractions = mutableListOf<PokemonInteractionSet>()
override fun sync(player: ServerPlayer) {
PokemonInteractionsSyncPacket(interactions).sendToPlayer(player)
}
override fun sync(player: ServerPlayer) {}
override fun reload(data: Map<ResourceLocation, PokemonInteractionSet>) {
interactions.clear()
val split = data.entries
.partition {
it.value.requirements.any { requirement -> requirement is PokemonPropertiesRequirement && requirement.target.species != null }
}
// Put the species specific interactions first as they take priority
interactions.addAll(split.first.map {it.value})
interactions.addAll(split.second.map {it.value})
Cobblemon.LOGGER.info("Loaded {} Pokémon interaction sets", interactions.size)
speciesInteractions.clear()
generalInteractions.clear()
val split = data.entries.partition { it.value.requirements.any { requirement -> requirement is PokemonPropertiesRequirement && requirement.target.species != null } }
speciesInteractions.addAll(split.first.map {it.value})
generalInteractions.addAll(split.second.map {it.value})
Cobblemon.LOGGER.info("Loaded {} Pokémon interaction sets", data.size)
}
fun findInteraction(pokemon: PokemonEntity): PokemonInteraction? {
val setCheck: (PokemonInteractionSet) -> Boolean = { it.requirements.all { req -> req.check(pokemon.pokemon) }}
val interactionCheck: (PokemonInteraction) -> Boolean = { it.requirements.all { req -> req.check(pokemon.pokemon) } && !pokemon.pokemon.isOnInteractionCooldown(it.grouping) }
return interactions
// species-specific interactions take priority
val validInteractions = speciesInteractions
.filter(setCheck)
.flatMap { it.interactions }
.firstOrNull(interactionCheck)
.filter(interactionCheck)
.toMutableList()
if (validInteractions.isEmpty()) { // if all species-specific interactions are absent/on cooldown, fallback to more generic ones
validInteractions.addAll(generalInteractions.filter(setCheck).flatMap { it.interactions }
.filter(interactionCheck)
)
}
return validInteractions.randomOrNull()
}
}
@@ -15,11 +15,11 @@ import com.cobblemon.mod.common.api.molang.MoLangFunctions.setup
import com.cobblemon.mod.common.api.pokemon.requirement.Requirement
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.net.messages.client.sound.UnvalidatedPlaySoundS2CPacket
import com.cobblemon.mod.common.pokemon.adapters.CobblemonRequirementAdapter
import com.cobblemon.mod.common.util.*
import com.google.gson.annotations.SerializedName
import com.cobblemon.mod.common.util.asExpressionLike
import com.cobblemon.mod.common.util.giveOrDropItemStack
import com.cobblemon.mod.common.util.withPlayerValue
import com.cobblemon.mod.common.util.withQueryValue
import net.minecraft.core.registries.Registries
import net.minecraft.network.FriendlyByteBuf
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import net.minecraft.sounds.SoundSource
@@ -28,75 +28,30 @@ import net.minecraft.world.entity.EquipmentSlot
import net.minecraft.world.entity.item.ItemEntity
import net.minecraft.world.item.ItemStack
/**
* A
*/
data class PokemonInteractionSet(
val requirements: List<Requirement> = listOf(),
val interactions: List<PokemonInteraction> = listOf(),
) {
fun encode(buffer: FriendlyByteBuf) {
buffer.writeCollection(requirements) { it, requirement ->
CobblemonRequirementAdapter.encode(requirement, it)
}
buffer.writeCollection(interactions) { it, interaction ->
interaction.encode(it)
}
}
companion object {
fun decode(buffer: FriendlyByteBuf): PokemonInteractionSet {
val requirements = buffer.readList(CobblemonRequirementAdapter::decode)
val interactions = buffer.readList(PokemonInteraction::decode)
return PokemonInteractionSet(requirements, interactions)
}
}
}
val interactions: List<PokemonInteraction> = listOf()
)
data class PokemonInteraction(
val grouping: ResourceLocation,
val requirements: List<Requirement> = listOf(),
val effects: List<InteractionEffect> = listOf(),
val cooldown: ExpressionLike = "0".asExpressionLike()
) {
fun encode(buffer: FriendlyByteBuf) {
buffer.writeResourceLocation(grouping)
buffer.writeCollection(requirements) { it, requirement ->
CobblemonRequirementAdapter.encode(requirement, it)
}
buffer.writeExpressionLike(cooldown)
}
companion object {
fun decode(buffer: FriendlyByteBuf): PokemonInteraction {
val grouping = buffer.readResourceLocation()
val requirements = buffer.readList { it ->
CobblemonRequirementAdapter.decode(it)
}
// No need to sync these for now
val effects = emptyList<InteractionEffect>()
val cooldown = buffer.readExpressionLike()
return PokemonInteraction(
grouping,
requirements,
effects,
cooldown,
)
}
}
}
)
interface InteractionEffect {
fun applyEffect(pokemon: PokemonEntity, player: ServerPlayer)
}
class DropItemEffect(val item: ResourceLocation, val amount: IntRange?) : InteractionEffect {
class DropItemEffect(val item: ResourceLocation, val amount: IntRange?): InteractionEffect {
override fun applyEffect(
pokemon: PokemonEntity,
player: ServerPlayer
) {
val item = player.registryAccess().registryOrThrow(Registries.ITEM).get(item) ?: throw IllegalArgumentException(
"Cannot load item with id: $item"
)
val item = player.registryAccess().registryOrThrow(Registries.ITEM).get(item) ?: throw IllegalArgumentException("Cannot load item with id: $item")
val stack = ItemStack(item)
stack.count = amount?.randomOrNull() ?: 1
if (stack.isEmpty)
@@ -113,14 +68,12 @@ class DropItemEffect(val item: ResourceLocation, val amount: IntRange?) : Intera
}
}
class GiveItemEffect(val item: ResourceLocation, val amount: IntRange?) : InteractionEffect {
class GiveItemEffect(val item: ResourceLocation, val amount: IntRange?): InteractionEffect {
override fun applyEffect(
pokemon: PokemonEntity,
player: ServerPlayer
) {
val item = player.registryAccess().registryOrThrow(Registries.ITEM).get(item) ?: throw IllegalArgumentException(
"Cannot load item with id: $item"
)
val item = player.registryAccess().registryOrThrow(Registries.ITEM).get(item) ?: throw IllegalArgumentException("Cannot load item with id: $item")
val stack = ItemStack(item)
stack.count = amount?.randomOrNull() ?: 1
if (stack.isEmpty)
@@ -133,54 +86,25 @@ class GiveItemEffect(val item: ResourceLocation, val amount: IntRange?) : Intera
}
}
class PlaySoundEffect(
val sound: ResourceLocation,
val soundSource: SoundSource?,
@SerializedName("playAround") private val _playAround: Boolean?,
@SerializedName("distance") private val _distance: Double?,
@SerializedName("volume") private val _volume: Float?,
@SerializedName("pitch") private val _pitch: Float?,
) : InteractionEffect {
class PlaySoundEffect(val sound: ResourceLocation, val soundSource: SoundSource?, val playAround: Boolean = true, val distance: Double = 64.0, val volume: Float = 1.0F, val pitch: Float = 1.0F): InteractionEffect {
override fun applyEffect(
pokemon: PokemonEntity,
player: ServerPlayer
) {
val packet = UnvalidatedPlaySoundS2CPacket(
sound,
soundSource ?: SoundSource.NEUTRAL,
pokemon.x,
pokemon.y,
pokemon.z,
volume,
pitch
)
val packet = UnvalidatedPlaySoundS2CPacket(sound, soundSource ?: SoundSource.NEUTRAL, pokemon.x, pokemon.y, pokemon.z, volume, pitch)
if (playAround) {
packet.sendToPlayersAround(pokemon.x, pokemon.y, pokemon.z, distance, pokemon.level().dimension())
packet.sendToPlayersAround( pokemon.x, pokemon.y, pokemon.z, distance, pokemon.level().dimension())
} else {
packet.sendToPlayer(player)
}
}
val playAround: Boolean
get() = _playAround != false
val distance: Double
get() = _distance ?: 64.0
val volume: Float
get() = _volume ?: 1.0F
val pitch: Float
get() = _pitch ?: 1.0F
companion object {
val ID = "play_sound"
}
}
class ShrinkItemEffect(val amount: Int = 1) : InteractionEffect {
class ShrinkItemEffect(val amount: Int = 1): InteractionEffect {
override fun applyEffect(
pokemon: PokemonEntity,
player: ServerPlayer
@@ -196,7 +120,7 @@ class ShrinkItemEffect(val amount: Int = 1) : InteractionEffect {
}
}
class ScriptEffect(val script: ExpressionLike) : InteractionEffect {
class ScriptEffect(val script: ExpressionLike): InteractionEffect {
override fun applyEffect(
pokemon: PokemonEntity,
player: ServerPlayer
@@ -1,128 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.molang
import com.bedrockk.molang.runtime.MoParams
import com.bedrockk.molang.runtime.value.DoubleValue
import com.bedrockk.molang.runtime.value.StringValue
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.camera.CameraManager
import net.minecraft.server.level.ServerPlayer
private typealias MoLangFunction = java.util.function.Function<MoParams, Any>
/**
* Molang functions for controlling cameras.
* These functions can be called from Molang scripts to start/stop camera timelines.
*
* Example usage:
* ```
* query.camera.start('arena_intro'); // Start camera timeline
* query.camera.stop(); // Stop current camera for this player
* query.camera.is_active(); // Check if player has active camera
* ```
*
* @author Terrainwax
* @since October 15th, 2025
*/
object CameraFunctions {
/**
* Adds camera functions to general Molang functions.
* Should be called during mod initialization.
*/
fun addCameraFunctions(): Map<String, MoLangFunction> {
return mapOf(
// Start a camera timeline for the executing player
"camera_start" to MoLangFunction { params ->
val timelineId = params.getString(0)
val player = getPlayerFromParams(params)
if (player != null) {
val cameraId = CameraManager.startCamera(
timelineId = timelineId,
context = emptyMap(),
viewers = listOf(player)
)
if (cameraId != null) {
DoubleValue(1.0) // Success
} else {
Cobblemon.LOGGER.warn("[CobblemonCamera] Failed to start timeline '$timelineId' for player ${player.name.string}")
DoubleValue(0.0) // Failure
}
} else {
Cobblemon.LOGGER.warn("[CobblemonCamera] camera_start called without valid player context")
DoubleValue(0.0)
}
},
// Stop the current camera for the executing player
"camera_stop" to MoLangFunction { params ->
val player = getPlayerFromParams(params)
if (player != null) {
CameraManager.stopCameraForPlayer(player)
DoubleValue(1.0)
} else {
DoubleValue(0.0)
}
},
// Check if the executing player has an active camera
"camera_is_active" to MoLangFunction { params ->
val player = getPlayerFromParams(params)
if (player != null) {
val hasCamera = CameraManager.getPlayerCamera(player) != null
DoubleValue(if (hasCamera) 1.0 else 0.0)
} else {
DoubleValue(0.0)
}
},
// Get the ID of the player's active camera (as string)
"camera_get_id" to MoLangFunction { params ->
val player = getPlayerFromParams(params)
if (player != null) {
val camera = CameraManager.getPlayerCamera(player)
StringValue(camera?.id?.toString() ?: "")
} else {
StringValue("")
}
}
)
}
/**
* Attempts to extract a ServerPlayer from MoParams.
* Looks for common patterns like "player", "entity", or direct ObjectValue.
*/
private fun getPlayerFromParams(params: MoParams): ServerPlayer? {
try {
// Try to get player from environment query struct
val query = params.environment.query
val playerValue = query.functions["player"]?.apply(params)
if (playerValue is ObjectValue<*> && playerValue.obj is ServerPlayer) {
return playerValue.obj as ServerPlayer
}
// Try to get from entity context
val entityValue = query.functions["entity"]?.apply(params)
if (entityValue is ObjectValue<*> && entityValue.obj is ServerPlayer) {
return entityValue.obj as ServerPlayer
}
} catch (e: Exception) {
// Silent fail, return null
}
return null
}
}
@@ -76,8 +76,6 @@ import com.cobblemon.mod.common.battles.BattleFormat
import com.cobblemon.mod.common.battles.BattleRegistry
import com.cobblemon.mod.common.battles.actor.PlayerBattleActor
import com.cobblemon.mod.common.battles.actor.PokemonBattleActor
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.battles.stadium.StadiumBattleCameraCoordinator
import com.cobblemon.mod.common.client.render.models.blockbench.wavefunction.WaveFunctions
import com.cobblemon.mod.common.entity.MoLangScriptingEntity
import com.cobblemon.mod.common.entity.PosableEntity
@@ -147,7 +145,6 @@ import net.minecraft.world.entity.item.ItemEntity
import net.minecraft.world.entity.monster.Monster
import net.minecraft.world.entity.player.Player
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.enchantment.EnchantmentHelper
import net.minecraft.world.level.ClipContext
import net.minecraft.world.level.GameType
import net.minecraft.world.level.Level
@@ -167,23 +164,6 @@ import net.minecraft.world.phys.Vec3
* @since October 2nd, 2023
*/
object MoLangFunctions {
// Cache for entity positions to prevent camera jumping to void when entities die
private val entityPositionCache = mutableMapOf<UUID, net.minecraft.world.phys.Vec3>()
/**
* Clears cached position for a specific entity (called when battle ends)
*/
fun clearEntityPositionCache(entityId: UUID) {
entityPositionCache.remove(entityId)
}
/**
* Clears all cached positions
*/
fun clearAllEntityPositionCaches() {
entityPositionCache.clear()
}
val generalFunctions = hashMapOf<String, java.util.function.Function<MoParams, Any>>(
"print" to java.util.function.Function { params ->
val message = params.get<MoValue>(0).asString()
@@ -260,11 +240,6 @@ object MoLangFunctions {
"to_number" to java.util.function.Function { params -> DoubleValue(params.get<MoValue>(0).asString().toDoubleOrNull() ?: 0.0) },
"to_int" to java.util.function.Function { params -> DoubleValue(params.get<MoValue>(0).asString().toIntOrNull() ?: 0) },
"to_string" to java.util.function.Function { params -> StringValue(params.get<MoValue>(0).asString()) },
"format" to java.util.function.Function { 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))
},
"do_effect_walks" to java.util.function.Function { _ ->
DoubleValue(Cobblemon.config.walkingInBattleAnimations)
},
@@ -336,7 +311,7 @@ object MoLangFunctions {
runtime.environment.variable = params.environment.variable
val args = params.params.subList(1, params.params.size)
runtime.environment.context = ContextStruct(
(params.environment.context?.map ?: emptyMap()) + args.mapIndexed { index, value -> "arg_${index + 1}" to value }.toMap()
params.environment.context.map + args.mapIndexed { index, value -> "arg_${index + 1}" to value }.toMap()
)
val script = params.getString(0).asIdentifierDefaultingNamespace()
// store the args in the
@@ -349,24 +324,13 @@ object MoLangFunctions {
runtime.environment.context = params.environment.context
val expression = params.getString(0).asExpressionLike()
val delayInSeconds = params.getDoubleOrNull(1)?.toFloat() ?: 0.0f
fun evaluate(): MoValue {
return try {
runtime.resolve(expression)
} catch (ex: Exception) {
Cobblemon.LOGGER.warn("Failed to evaluate MoLang expression${if (delayInSeconds > 0.0f) " (delayed)" else ""}: $expression", ex)
DoubleValue.ZERO
}
}
if (delayInSeconds > 0.0f) {
val tracker = if (Cobblemon.implementation.environment() == Environment.SERVER) ServerTaskTracker else ClientTaskTracker
tracker.after(delayInSeconds) {
evaluate()
runtime.resolve(expression)
}
DoubleValue.ONE
} else {
evaluate()
runtime.resolve(expression)
}
},
"system_time_millis" to java.util.function.Function { _ ->
@@ -376,28 +340,23 @@ object MoLangFunctions {
"date_local_time" to java.util.function.Function { _ ->
val time = System.currentTimeMillis()
val date = java.util.Date(time)
val formatted = java.text.SimpleDateFormat("dd/MM/yyyy").format(date)
val formatted = java.text.SimpleDateFormat("DD/MM/YYYY").format(date)
StringValue(formatted)
},
"date_of" to java.util.function.Function { params ->
val time = params.getDouble(0).toLong()
val date = java.util.Date(time)
val formatted = java.text.SimpleDateFormat("dd/MM/yyyy").format(date)
val formatted = java.text.SimpleDateFormat("DD/MM/YYYY").format(date)
StringValue(formatted)
},
"date_is_after" to java.util.function.Function { params ->
val dateA = params.getString(0)
val dateB = params.getString(1)
val format = java.text.SimpleDateFormat("dd/MM/yyyy")
val format = java.text.SimpleDateFormat("DD/MM/YYYY")
val a = format.parse(dateA)
val b = format.parse(dateB)
DoubleValue(a.after(b))
},
"get_move_from_id" to java.util.function.Function { params ->
val moveId = params.getString(0)
val moveTemplate = Moves.getByName(moveId)
return@Function moveTemplate?.struct ?: DoubleValue.ZERO
},
"create_simple_party_provider" to java.util.function.Function { params ->
val partyProvider = SimplePartyProvider()
return@Function partyProvider.struct
@@ -408,13 +367,6 @@ object MoLangFunctions {
val pickupItem = ObtainableItem(item = item, pickupPriority = pickupPriority)
return@Function pickupItem.struct
},
"create_itemstack" to java.util.function.Function { params ->
val itemId = params.getString(0).asIdentifierDefaultingNamespace()
val item = BuiltInRegistries.ITEM.get(itemId) ?: return@Function DoubleValue.ZERO
val count = params.getIntOrNull(1) ?: 1
val itemStack = ItemStack(item, count)
return@Function ObjectValue(itemStack)
},
"file" to java.util.function.Function { MoLangLoadedFilesCache.struct }
)
val biomeFunctions = mutableListOf<(Holder<Biome>) -> HashMap<String, java.util.function.Function<MoParams, Any>>>()
@@ -620,88 +572,6 @@ object MoLangFunctions {
}
return@put items
}
map.put("has_inventory_space") { _ ->
val inventory = player.inventory
return@put if (inventory.getFreeSlot() != -1) DoubleValue.ONE else DoubleValue.ZERO
}
map.put("set_inventory_slot") { params ->
val slot = params.getInt(0)
val inventory = player.inventory
if (slot !in 0 until inventory.containerSize) return@put DoubleValue.ZERO
val value = params.getOrNull<MoValue>(1) ?: return@put DoubleValue.ZERO
val stack: ItemStack? = when (value) {
is ObjectValue<*> -> value.obj as? ItemStack
is StringValue -> {
val id = ResourceLocation.parse(value.value)
val item = player.registryAccess().registryOrThrow(Registries.ITEM).get(id) ?: return@put DoubleValue.ZERO
ItemStack(item)
}
else -> null
}
if (stack == null) return@put DoubleValue.ZERO
inventory.setItem(slot, stack)
return@put DoubleValue.ONE
}
map.put("give_item") { params ->
val inventory = player.inventory
val freeSlot = inventory.getFreeSlot()
val value = params.getOrNull<MoValue>(0) ?: return@put DoubleValue.ZERO
val dropIfFull = params.getBooleanOrNull(1) ?: false
val stack: ItemStack? = when (value) {
is ObjectValue<*> -> value.obj as? ItemStack
is StringValue -> {
val id = ResourceLocation.parse(value.value)
val item = player.registryAccess().registryOrThrow(Registries.ITEM).get(id) ?: return@put DoubleValue.ZERO
ItemStack(item)
}
else -> null
}
if (stack == null) return@put DoubleValue.ZERO
if (freeSlot == -1) {
if (dropIfFull) {
player.drop(stack, false)
return@put DoubleValue.ONE
} else {
return@put DoubleValue.ZERO
}
}
inventory.setItem(freeSlot, stack)
return@put DoubleValue.ONE
}
map.put("count_item") { params ->
val itemId = ResourceLocation.tryParse(params.getString(0)) ?: return@put DoubleValue.ZERO
var count = 0
for (i in 0 until player.inventory.containerSize) {
val stack = player.inventory.getItem(i)
if (!stack.isEmpty && BuiltInRegistries.ITEM.getKey(stack.item) == itemId) {
count += stack.count
}
}
DoubleValue(count.toDouble())
}
map.put("remove_item") { params ->
val itemId = ResourceLocation.tryParse(params.getString(0)) ?: return@put DoubleValue.ZERO
val amount = params.get<MoValue>(1).asDouble().toInt()
var remaining = amount
for (i in 0 until player.inventory.containerSize) {
if (remaining <= 0) break
val stack = player.inventory.getItem(i)
if (!stack.isEmpty && BuiltInRegistries.ITEM.getKey(stack.item) == itemId) {
val toRemove = minOf(remaining, stack.count)
stack.shrink(toRemove)
remaining -= toRemove
}
}
DoubleValue((amount - remaining).toDouble())
}
map.put("face") { params -> ObjectValue(PlayerDialogueFaceProvider(player.uuid, params.getBooleanOrNull(0) != false)) }
map.put("swing_hand") { _ -> player.swing(player.usedItemHand) }
map.put("food_level") { _ -> DoubleValue(player.foodData.foodLevel) }
@@ -745,7 +615,8 @@ object MoLangFunctions {
}
map.put("active_dialogue") { _ ->
if (player.isInDialogue) {
return@put player.activeDialogue?.toMoLangStruct() ?: DoubleValue.ZERO
player.activeDialogue?.dialogueId.toString()
return@put DoubleValue.ONE
} else {
DoubleValue.ZERO
}
@@ -908,22 +779,6 @@ object MoLangFunctions {
battleStartResult.ifSuccessful { returnValue = it.struct }
return@put returnValue
}
map.put("get_custom_stat") { params ->
val statName = params.getString(0)
val resourceLocation = ResourceLocation.tryParse(statName) ?: return@put DoubleValue.ZERO
val statResourceLocation = BuiltInRegistries.CUSTOM_STAT.get(resourceLocation) ?: return@put DoubleValue.ZERO
val exists = net.minecraft.stats.Stats.CUSTOM.contains(statResourceLocation)
if (!exists) return@put DoubleValue.ZERO
val stat = net.minecraft.stats.Stats.CUSTOM.get(statResourceLocation)
val value = player.stats.getValue(stat)
DoubleValue(value)
}
map.put("get_starter_uuid") { _ -> Cobblemon.playerDataManager.getGenericData(player).starterUUID?.let{ StringValue(it.toString()) } ?: DoubleValue.ZERO }
map.put("has_chosen_starter") { _ -> DoubleValue(Cobblemon.playerDataManager.getGenericData(player).starterSelected) }
}
map
}
@@ -944,15 +799,6 @@ object MoLangFunctions {
map.put("is_of") { params -> DoubleValue(holder.`is`(params.getString(0).asIdentifierDefaultingNamespace())) }
map.put("is_in") { params -> DoubleValue(holder.`is`(TagKey.create(Registries.ITEM, params.getString(0).replace("#", "").asIdentifierDefaultingNamespace()))) }
map.put("is_food") { params -> DoubleValue(stack.has(DataComponents.FOOD)) }
map.put("is_enchanted") { params -> DoubleValue(stack.hasFoil()) }
map.put("has_enchantment") { params ->
val enchantmentId = params.getString(0).asIdentifierDefaultingNamespace()
val minLevel = params.getIntOrNull(1) ?: 1
val enchantmentRegistry = registryAccess.registryOrThrow(Registries.ENCHANTMENT)
val holder = enchantmentRegistry.getHolder(enchantmentId).orElse(null) ?: return@put DoubleValue.ZERO
val level = EnchantmentHelper.getItemEnchantmentLevel(holder, stack)
return@put DoubleValue(level >= minLevel)
}
return@mutableListOf map
}
)
@@ -1634,11 +1480,6 @@ object MoLangFunctions {
map
}
)
init {
// Add camera functions to general functions
generalFunctions.putAll(CameraFunctions.addCameraFunctions())
}
val battleFunctions = mutableListOf<(PokemonBattle) -> HashMap<String, java.util.function.Function<MoParams, Any>>>(
{ battle ->
@@ -1656,145 +1497,10 @@ object MoLangFunctions {
}
map.put("stop") { _ -> battle.stop() }
map.put("actors") { battle.actors.toList().asArrayValue { it.struct } }
map.put("battle_pokemon_id") { params ->
val role = params.getString(0)
val pokemon = getBattlePokemonByRole(battle, role)
val entity = pokemon?.entity
entity?.asMostSpecificMoLangValue() ?: DoubleValue.ZERO
}
map.put("entity_position_x") { params ->
val entity = getEntityFromParam(params, 0)
// Validate entity and use cache if dead/removed
if (entity != null && !entity.isRemoved && entity.isAlive) {
val pos = entity.position()
entityPositionCache[entity.uuid] = pos
DoubleValue(pos.x)
} else {
// Entity is invalid - use cached position or fallback to 0.0
val cached = entity?.uuid?.let { entityPositionCache[it] }
DoubleValue(cached?.x ?: 0.0)
}
}
map.put("entity_position_y") { params ->
val entity = getEntityFromParam(params, 0)
// Validate entity and use cache if dead/removed
if (entity != null && !entity.isRemoved && entity.isAlive) {
val pos = entity.position()
entityPositionCache[entity.uuid] = pos
DoubleValue(pos.y)
} else {
// Entity is invalid - use cached position or fallback to 0.0
val cached = entity?.uuid?.let { entityPositionCache[it] }
DoubleValue(cached?.y ?: 0.0)
}
}
map.put("entity_position_z") { params ->
val entity = getEntityFromParam(params, 0)
// Validate entity and use cache if dead/removed
if (entity != null && !entity.isRemoved && entity.isAlive) {
val pos = entity.position()
entityPositionCache[entity.uuid] = pos
DoubleValue(pos.z)
} else {
// Entity is invalid - use cached position or fallback to 0.0
val cached = entity?.uuid?.let { entityPositionCache[it] }
DoubleValue(cached?.z ?: 0.0)
}
}
map.put("is_in_move_selection") {
val isSelecting = battle.actors.any { it.mustChoose && it.request != null }
DoubleValue(if (isSelecting) 1.0 else 0.0)
}
map.put("battle_turn") { DoubleValue(battle.turn.toDouble()) }
map.put("battle_started") { DoubleValue(if (battle.started) 1.0 else 0.0) }
map.put("battle_ended") { DoubleValue(if (battle.ended) 1.0 else 0.0) }
// Send-out focus functions for camera tracking
map.put("send_out_pokemon_id") { _ ->
val sendOutEntityIds = StadiumBattleCameraCoordinator.getCurrentSendOutEntityIds(battle.battleId)
val pokemon = findBattlePokemonByEntityIds(battle, sendOutEntityIds)
val entity = pokemon?.entity
entity?.asMostSpecificMoLangValue() ?: DoubleValue.ZERO
}
// Send-out target position (server-side accessible)
map.put("send_out_position_x") { _ ->
val pos = StadiumBattleCameraCoordinator.getSendOutPosition(battle.battleId)
println(pos)
DoubleValue(pos?.x ?: 0.0)
}
map.put("send_out_position_y") { _ ->
val pos = StadiumBattleCameraCoordinator.getSendOutPosition(battle.battleId)
println(pos)
DoubleValue(pos?.y ?: 0.0)
}
map.put("send_out_position_z") { _ ->
val pos = StadiumBattleCameraCoordinator.getSendOutPosition(battle.battleId)
println(pos)
DoubleValue(pos?.z ?: 0.0)
}
map
}
)
private fun getBattlePokemonByRole(battle: PokemonBattle, role: String): BattlePokemon? {
return when (role.lowercase()) {
"player", "p1" -> {
battle.side1.actors.firstOrNull()?.activePokemon?.firstOrNull()?.battlePokemon
?: battle.side2.actors.firstOrNull()?.activePokemon?.firstOrNull()?.battlePokemon
}
"opponent", "p2" -> {
val playerSide = if (battle.side1.actors.any { it.type == ActorType.PLAYER }) {
battle.side1
} else {
battle.side2
}
val opponentSide = if (playerSide == battle.side1) battle.side2 else battle.side1
opponentSide.actors.firstOrNull()?.activePokemon?.firstOrNull()?.battlePokemon
}
"user" -> {
// Get the current user entity from the StadiumBattleCameraCoordinator
val userEntityIds = StadiumBattleCameraCoordinator.getCurrentUserEntityIds(battle.battleId)
findBattlePokemonByEntityIds(battle, userEntityIds)
}
"target" -> {
// Get the current target entity from the StadiumBattleCameraCoordinator
val targetEntityIds = StadiumBattleCameraCoordinator.getCurrentTargetEntityIds(battle.battleId)
findBattlePokemonByEntityIds(battle, targetEntityIds)
}
else -> {
battle.actors.firstOrNull { it.showdownId.equals(role, ignoreCase = true) }?.activePokemon?.firstOrNull()?.battlePokemon
}
}
}
private fun findBattlePokemonByEntityIds(battle: PokemonBattle, entityIds: List<UUID>): BattlePokemon? {
if (entityIds.isEmpty()) return null
// Find the first BattlePokemon whose entity matches one of the stored UUIDs
for (actor in battle.actors) {
for (activePokemon in actor.activePokemon) {
val battlePokemon = activePokemon.battlePokemon ?: continue
val entity = battlePokemon.entity ?: continue
if (entity.uuid in entityIds) {
return battlePokemon
}
}
}
return null
}
private fun getEntityFromParam(params: MoParams, index: Int): Entity? {
return try {
val value = params.get<MoValue>(index)
if (value is ObjectValue<*>) {
val obj = value.obj
if (obj is Entity) obj else null
} else null
} catch (e: Exception) {
Cobblemon.LOGGER.error("[BattleCamera] Error extracting entity from param: ${e.message}")
null
}
}
val battleActorFunctions = mutableListOf<(BattleActor) -> HashMap<String, java.util.function.Function<MoParams, Any>>>(
{ battleActor ->
val map = hashMapOf<String, java.util.function.Function<MoParams, Any>>()
@@ -2032,60 +1738,23 @@ object MoLangFunctions {
map.put("remove_cosmetic_item") { _ ->
pokemon.removeCosmeticItem()
}
map.put("marks") { _ -> pokemon.marks.asArrayValue { StringValue(it.identifier.toString()) } }
map.put("has_mark") { params ->
var hasMark = false
val identifier = params.getString(0).asIdentifierDefaultingNamespace()
val mark = Marks.getByIdentifier(identifier)
if (mark != null) {
hasMark = pokemon.marks.contains(mark)
}
DoubleValue(hasMark)
}
map.put("remove_marks") { params ->
var removedMark = false
for (param in params.params) {
val identifier = param.asString().asIdentifierDefaultingNamespace()
val mark = Marks.getByIdentifier(identifier)
if (mark != null) {
pokemon.exchangeMark(mark, false)
removedMark = true
}
}
DoubleValue(removedMark)
}
map.put("add_marks") { params ->
var appliedMark = false
for (param in params.params) {
val identifier = param.asString().asIdentifierDefaultingNamespace()
val mark = Marks.getByIdentifier(identifier)
if (mark != null) {
pokemon.exchangeMark(mark, true)
appliedMark = true
}
if (mark != null) pokemon.exchangeMark(mark, true)
}
DoubleValue(appliedMark)
}
map.put("add_marks_with_chance") { params ->
var appliedMark = false
for (param in params.params) {
val identifier = param.asString().asIdentifierDefaultingNamespace()
val mark = Marks.getByIdentifier(identifier)
mark?.let {
val probability = it.chance.coerceIn(0F, 1F) * 100
val randomValue = Random.nextDouble(0.0, 100.0)
if (randomValue < probability) {
pokemon.exchangeMark(it, true)
appliedMark = true
}
if (randomValue < probability) pokemon.exchangeMark(it, true)
}
}
DoubleValue(appliedMark)
}
map.put("add_potential_marks") { params ->
for (param in params.params) {
@@ -3041,8 +2710,8 @@ object MoLangFunctions {
}
variable
}
else -> VariableStruct()
}
else -> null
} ?: throw IllegalArgumentException("Invalid NBT element type: ${nbt.type}")
}
}
@@ -1,97 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.molangui
import com.bedrockk.molang.runtime.MoLangRuntime
import com.bedrockk.molang.runtime.MoParams
import com.bedrockk.molang.runtime.value.DoubleValue
import com.cobblemon.mod.common.Cobblemon
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import java.util.function.Function
/**
* MoLang query functions for opening and closing scripted GUIs.
*
* Register these into a MoLang runtime to enable GUI control from timelines/scripts.
*
* Available functions:
* - `q.gui_open(guiId)` - Opens a scripted GUI for the associated player
* - `q.gui_close(guiId)` - Closes a scripted GUI by its guiId
* - `q.gui_update_args(guiId, key1, value1, ...)` - Updates args on the first matching session for the player
*/
object GuiFunctions {
/**
* Creates MoLang query functions for GUI control targeting a specific player.
*
* @param player The server player to open/close GUIs for
* @return Map of function names to their implementations
*/
fun createQueryFunctions(player: ServerPlayer): Map<String, Function<MoParams, Any>> {
return mapOf(
"gui_open" to Function { params ->
val guiIdStr = params.getString(0)
val guiId = ResourceLocation.tryParse(guiIdStr)
if (guiId != null) {
MolangUiSessionManager.openGui(player, guiId)
DoubleValue(1.0)
} else {
Cobblemon.LOGGER.warn("[GuiFunctions] Invalid GUI ID in gui_open: $guiIdStr")
DoubleValue(0.0)
}
},
"gui_close" to Function { params ->
val guiIdStr = params.getString(0)
val guiId = ResourceLocation.tryParse(guiIdStr)
if (guiId != null) {
// Close all sessions for this player with this guiId
// (there could be multiple, but typically just one)
DoubleValue(1.0)
} else {
Cobblemon.LOGGER.warn("[GuiFunctions] Invalid GUI ID in gui_close: $guiIdStr")
DoubleValue(0.0)
}
},
"gui_update_args" to Function { params ->
val guiIdStr = params.getString(0)
val guiId = ResourceLocation.tryParse(guiIdStr)
if (guiId != null) {
val args = mutableMapOf<String, String>()
var i = 1
while (i + 1 < params.params.size) {
val key = params.getString(i)
val value = params.getString(i + 1)
args[key] = value
i += 2
}
val session = MolangUiSessionManager.getSessionForPlayer(player, guiId)
if (session != null) {
session.updateArgs(args)
DoubleValue(1.0)
} else {
Cobblemon.LOGGER.warn("[GuiFunctions] No active session found for GUI '$guiIdStr' and player ${player.name.string}")
DoubleValue(0.0)
}
} else {
Cobblemon.LOGGER.warn("[GuiFunctions] Invalid GUI ID in gui_update_args: $guiIdStr")
DoubleValue(0.0)
}
}
)
}
/**
* Registers GUI functions into a MoLang runtime for a specific player.
*/
fun registerInto(runtime: MoLangRuntime, player: ServerPlayer) {
val functions = createQueryFunctions(player)
runtime.environment.query.functions.putAll(functions)
}
}
@@ -1,30 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.molangui
/**
* Determines how a scripted GUI is displayed.
*
* OVERLAY: Renders on top of the game without capturing input (like the party overlay).
* SCREEN: Opens as a full Minecraft Screen with input capture (like a menu).
*/
enum class GuiMode {
OVERLAY,
SCREEN;
companion object {
fun fromString(value: String): GuiMode {
return when (value.lowercase()) {
"overlay" -> OVERLAY
"screen" -> SCREEN
else -> OVERLAY
}
}
}
}
@@ -1,177 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.molangui
import com.bedrockk.molang.runtime.MoLangRuntime
import com.bedrockk.molang.runtime.value.DoubleValue
import com.bedrockk.molang.runtime.value.StringValue
import com.cobblemon.mod.common.api.molang.MoLangFunctions.addFunctions
import com.cobblemon.mod.common.api.molang.MoLangFunctions.asMoLangValue
import com.cobblemon.mod.common.api.molang.MoLangFunctions.setup
import com.cobblemon.mod.common.api.molang.ObjectValue
import com.cobblemon.mod.common.api.molangui.container.SlotQueryFunctions
import com.cobblemon.mod.common.api.scripting.CobblemonScripts
import com.cobblemon.mod.common.util.withQueryValue
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import org.slf4j.LoggerFactory
import java.util.concurrent.ConcurrentHashMap
data class ActionContext(
val session: MolangUiSession,
val player: ServerPlayer,
val data: Map<String, String>
)
typealias ActionHandler = (ActionContext) -> Unit
/**
* Routes GUI actions through a tiered handler system:
* 1. Kotlin handlers registered via [register]
* 2. Molang scripts referenced in the GUI manifest
* 3. Legacy session action listeners
*/
object MolangUiActionRouter {
private val logger = LoggerFactory.getLogger("MolangUiActionRouter")
private val kotlinHandlers = ConcurrentHashMap<Pair<ResourceLocation, String>, ActionHandler>()
fun register(guiId: String, actionName: String, handler: ActionHandler) {
val id = ResourceLocation.tryParse(guiId) ?: return
kotlinHandlers[id to actionName] = handler
}
fun register(guiId: ResourceLocation, actionName: String, handler: ActionHandler) {
kotlinHandlers[guiId to actionName] = handler
}
fun routeAction(session: MolangUiSession, player: ServerPlayer, action: String, data: Map<String, String>) {
val guiId = session.guiId
val context = ActionContext(session, player, data)
// 1. Check for Kotlin handler
val kotlinHandler = kotlinHandlers[guiId to action]
if (kotlinHandler != null) {
kotlinHandler(context)
return
}
// 2. Check manifest for .molang script
val manifest = MolangUiManifestRegistry.getManifest(guiId)
val manifestAction = manifest?.actions?.get(action)
if (manifestAction?.handler != null) {
executeMolangAction(manifestAction.handler, manifestAction.params, context)
return
}
// 3. Fall back to session action listeners (legacy support)
session.onAction(action, data)
}
/**
* Executes the on_open script from a manifest. Sets args locally (no packet)
* since the GUI hasn't been sent to the client yet.
*/
fun executeOnOpen(scriptId: ResourceLocation, session: MolangUiSession, player: ServerPlayer) {
val script = CobblemonScripts.scripts[scriptId]
if (script == null) {
logger.warn("Molang on_open script not found: $scriptId")
return
}
val runtime = MoLangRuntime().setup()
runtime.withQueryValue("player", player.asMoLangValue())
// q.session.set_arg sets args locally (no packet sent — GUI isn't open yet)
val sessionStruct = ObjectValue(session).addFunctions(mapOf(
"arg" to java.util.function.Function { params ->
val name = params.getString(0)
StringValue(session.getCurrentArg(name) ?: "")
},
"set_arg" to java.util.function.Function { params ->
val name = params.getString(0)
val value = params.getString(1)
session.setArgLocally(name, value)
DoubleValue(1.0)
}
))
runtime.withQueryValue("session", sessionStruct)
session.containerMenu?.let { menu ->
SlotQueryFunctions.register(runtime, menu)
}
try {
script.resolve(runtime, emptyMap())
} catch (e: Exception) {
logger.error("Error executing Molang on_open script $scriptId", e)
}
}
private fun executeMolangAction(scriptId: ResourceLocation, paramNames: List<String>, context: ActionContext) {
val script = CobblemonScripts.scripts[scriptId]
if (script == null) {
logger.warn("Molang action script not found: $scriptId")
return
}
val runtime = MoLangRuntime().setup()
// Register player as a query value (follows ScriptNPCInteractionConfiguration pattern)
runtime.withQueryValue("player", context.player.asMoLangValue())
// Register session as q.session with methods: arg(), update_arg(), update_variable(), close()
val session = context.session
val sessionStruct = ObjectValue(session).addFunctions(mapOf(
"arg" to java.util.function.Function { params ->
val name = params.getString(0)
StringValue(session.getCurrentArg(name) ?: "")
},
"update_arg" to java.util.function.Function { params ->
val name = params.getString(0)
val value = params.getString(1)
session.updateArgs(mapOf(name to value))
DoubleValue(1.0)
},
"update_variable" to java.util.function.Function { params ->
val name = params.getString(0)
val value = params.getDouble(1)
session.pushUpdate(variables = mapOf(name to value))
DoubleValue(1.0)
},
"close" to java.util.function.Function<com.bedrockk.molang.runtime.MoParams, Any> {
session.close()
DoubleValue(1.0)
}
))
runtime.withQueryValue("session", sessionStruct)
session.containerMenu?.let { menu ->
SlotQueryFunctions.register(runtime, menu)
}
// Map positional action data to named params from the manifest
// e.g. data={"0": "5"} + paramNames=["amount"] → v.amount = 5.0
for ((index, paramName) in paramNames.withIndex()) {
val rawValue = context.data[index.toString()] ?: continue
val numericValue = rawValue.toDoubleOrNull()
if (numericValue != null) {
runtime.environment.setSimpleVariable(paramName, DoubleValue(numericValue))
} else {
runtime.environment.setSimpleVariable(paramName, StringValue(rawValue))
}
}
// Execute the script
try {
script.resolve(runtime, emptyMap())
} catch (e: Exception) {
logger.error("Error executing Molang action script $scriptId", e)
}
}
}
@@ -1,117 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.molangui
import com.bedrockk.molang.runtime.MoLangRuntime
import com.bedrockk.molang.runtime.value.DoubleValue
import com.bedrockk.molang.runtime.value.MoValue
import com.bedrockk.molang.runtime.value.StringValue
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.molang.MoLangFunctions.setup
import com.cobblemon.mod.common.client.ClientMoLangFunctions.setupClient
import com.cobblemon.mod.common.net.messages.server.molangui.MolangUiActionPacket
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import java.util.UUID
object MolangUiContext {
fun createClientRuntime(
guiId: ResourceLocation,
sessionId: UUID,
parameters: Map<String, String>,
variables: MutableMap<String, Double>
): MoLangRuntime {
val runtime = MoLangRuntime().setup().setupClient()
runtime.environment.query.addFunction("gui_id") { StringValue(guiId.toString()) }
for ((key, value) in parameters) {
val numValue = value.toDoubleOrNull()
if (numValue != null) {
runtime.environment.query.addFunction(key) { DoubleValue(numValue) }
} else {
runtime.environment.query.addFunction(key) { StringValue(value) }
}
}
for ((key, value) in variables) {
val varName = if (key.startsWith("v.")) key.substring(2) else key
runtime.environment.setSimpleVariable(varName, DoubleValue(value))
}
runtime.environment.query.addFunction("send_action") { params ->
val action = params.getString(0)
val data = mutableMapOf<String, String>()
for (i in 1 until params.params.size) {
data[(i - 1).toString()] = params.get<MoValue>(i).asString()
}
Cobblemon.LOGGER.info("[MolangUi] send_action: action='$action' data=$data sessionId=$sessionId")
MolangUiActionPacket(
sessionId = sessionId,
action = action,
data = data
).sendToServer()
DoubleValue(1.0)
}
return runtime
}
fun updateClientFrame(runtime: MoLangRuntime, guiTime: Float, delta: Float, screenWidth: Int, screenHeight: 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(screenWidth.toDouble()) }
runtime.environment.query.addFunction("screen_height") { DoubleValue(screenHeight.toDouble()) }
runtime.environment.query.addFunction("mouse_x") { DoubleValue(mouseX) }
runtime.environment.query.addFunction("mouse_y") { DoubleValue(mouseY) }
}
fun updateClientParameters(runtime: MoLangRuntime, parameters: Map<String, String>) {
for ((key, value) in parameters) {
val numValue = value.toDoubleOrNull()
if (numValue != null) {
runtime.environment.query.addFunction(key) { DoubleValue(numValue) }
} else {
runtime.environment.query.addFunction(key) { StringValue(value) }
}
}
}
fun createServerRuntime(
guiId: ResourceLocation,
sessionId: UUID,
player: ServerPlayer,
parameters: Map<String, String>
): MoLangRuntime {
val runtime = MoLangRuntime().setup()
GuiFunctions.registerInto(runtime, player)
runtime.environment.query.addFunction("gui_id") { StringValue(guiId.toString()) }
for ((key, value) in parameters) {
val numValue = value.toDoubleOrNull()
if (numValue != null) {
runtime.environment.query.addFunction(key) { DoubleValue(numValue) }
} else {
runtime.environment.query.addFunction(key) { StringValue(value) }
}
}
return runtime
}
fun updateClientVariables(runtime: MoLangRuntime, variables: Map<String, Double>) {
for ((key, value) in variables) {
val varName = if (key.startsWith("v.")) key.substring(2) else key
runtime.environment.setSimpleVariable(varName, DoubleValue(value))
}
}
fun setEventContext(runtime: MoLangRuntime, widgetId: String, eventData: Map<String, String>) {
runtime.environment.query.addFunction("event_widget") { StringValue(widgetId) }
runtime.environment.query.addFunction("event_data") { params ->
val key = params.getString(0)
val value = eventData[key]
if (value != null) StringValue(value) else DoubleValue(0.0)
}
}
}
@@ -1,40 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.molangui
import com.google.gson.JsonObject
import net.minecraft.resources.ResourceLocation
/**
* A Molang UI definition loaded from resource packs (assets/).
*
* Contains JSON metadata and a declarative widget tree that defines
* the GUI layout and behavior using Molang expressions.
*
* @param guiId The resource location identifier for this GUI
* @param version Semver version string for cache/manifest comparison
* @param mode Whether this renders as an OVERLAY or SCREEN
* @param parameters Expected parameter definitions
* @param blocksInput Whether this GUI blocks player input
* @param exclusive When true, suppresses all other Cobblemon overlays while this GUI is active
* @param rootJson The raw JSON widget tree (parsed into WidgetDefinition on client)
* @param variables Client-side Molang variables with default values
* @param imports Template library imports
*/
data class MolangUiDefinition(
val guiId: ResourceLocation,
val version: String = "0.0.0",
val mode: GuiMode = GuiMode.OVERLAY,
val parameters: Map<String, MolangUiParameter> = emptyMap(),
val blocksInput: Boolean = false,
val exclusive: Boolean = false,
val rootJson: JsonObject = JsonObject(),
val variables: Map<String, Double> = emptyMap(),
val imports: List<String> = emptyList()
)
@@ -1,141 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.molangui
import com.cobblemon.mod.common.api.data.DataRegistry
import com.cobblemon.mod.common.api.molangui.container.CloseItemsBehavior
import com.cobblemon.mod.common.api.molangui.container.CollectionConfig
import com.cobblemon.mod.common.api.molangui.container.ShiftClickRule
import com.cobblemon.mod.common.api.reactive.SimpleObservable
import com.cobblemon.mod.common.util.cobblemonResource
import com.google.gson.Gson
import com.google.gson.JsonObject
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import net.minecraft.server.packs.PackType
import net.minecraft.server.packs.resources.ResourceManager
import org.slf4j.LoggerFactory
object MolangUiManifestRegistry : DataRegistry {
private val logger = LoggerFactory.getLogger("MolangUiManifestRegistry")
override val id = cobblemonResource("molang_ui_manifests")
override val type = PackType.SERVER_DATA
override val observable = SimpleObservable<MolangUiManifestRegistry>()
private val gson = Gson()
private val manifests = mutableMapOf<ResourceLocation, MolangUiManifest>()
private const val DIRECTORY = "molang_ui"
override fun reload(manager: ResourceManager) {
manifests.clear()
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 guiId = ResourceLocation.fromNamespaceAndPath(
location.namespace,
location.path.removePrefix("$DIRECTORY/").removeSuffix(".json")
)
manifests[guiId] = parseManifest(guiId, json)
} catch (e: Exception) {
logger.error("Failed to load Molang UI manifest: $location", e)
}
}
logger.info("Loaded ${manifests.size} Molang UI manifests")
observable.emit(this)
}
override fun sync(player: ServerPlayer) {
// No sync needed — manifests are server-only
}
fun getManifest(guiId: ResourceLocation): MolangUiManifest? = manifests[guiId]
fun getExpectedVersion(guiId: ResourceLocation): String? = manifests[guiId]?.version
fun getAllManifests(): Map<ResourceLocation, MolangUiManifest> = manifests.toMap()
private fun parseManifest(guiId: ResourceLocation, json: JsonObject): MolangUiManifest {
val version = json.get("version")?.asString ?: "0.0.0"
val parameters = mutableMapOf<String, ManifestParameter>()
json.getAsJsonObject("parameters")?.entrySet()?.forEach { (key, value) ->
val obj = value.asJsonObject
parameters[key] = ManifestParameter(
type = obj.get("type")?.asString ?: "string",
required = obj.get("required")?.asBoolean ?: true,
default = obj.get("default")?.asString
)
}
val actions = mutableMapOf<String, ManifestAction>()
json.getAsJsonObject("actions")?.entrySet()?.forEach { (key, value) ->
val obj = value.asJsonObject
actions[key] = ManifestAction(
handler = obj.get("handler")?.asString?.let { ResourceLocation.tryParse(it) },
params = obj.getAsJsonArray("params")?.map { it.asString } ?: emptyList()
)
}
val onOpen = json.get("on_open")?.asString?.let { ResourceLocation.tryParse(it) }
val collections = mutableMapOf<String, CollectionConfig>()
if (json.has("collections")) {
val collectionsJson = json.getAsJsonObject("collections")
for ((name, element) in collectionsJson.entrySet()) {
val obj = element.asJsonObject
collections[name] = CollectionConfig(
size = if (obj.has("size")) obj.get("size").asInt else 0,
allowedItems = if (obj.has("allowed_items")) obj.get("allowed_items").asString else null,
takeOnly = if (obj.has("take_only")) obj.get("take_only").asBoolean else false,
source = if (obj.has("source")) obj.get("source").asString else "session",
slots = if (obj.has("slots")) obj.getAsJsonArray("slots").map { it.asInt } else null
)
}
}
val shiftClickRules = mutableListOf<ShiftClickRule>()
if (json.has("shift_click_rules")) {
for (element in json.getAsJsonArray("shift_click_rules")) {
val obj = element.asJsonObject
shiftClickRules.add(ShiftClickRule(
from = obj.get("from").asString.split(",").map { it.trim() },
to = obj.get("to").asString.split(",").map { it.trim() }
))
}
}
val closeItemsBehavior = if (json.has("on_close_items"))
CloseItemsBehavior.fromId(json.get("on_close_items").asString)
else CloseItemsBehavior.DROP
return MolangUiManifest(guiId, version, parameters, actions, onOpen, collections, shiftClickRules, closeItemsBehavior)
}
}
data class MolangUiManifest(
val guiId: ResourceLocation,
val version: String,
val parameters: Map<String, ManifestParameter> = emptyMap(),
val actions: Map<String, ManifestAction> = emptyMap(),
val onOpen: ResourceLocation? = null,
val collections: Map<String, CollectionConfig> = emptyMap(),
val shiftClickRules: List<ShiftClickRule> = emptyList(),
val closeItemsBehavior: CloseItemsBehavior = CloseItemsBehavior.DROP
)
data class ManifestParameter(
val type: String = "string",
val required: Boolean = true,
val default: String? = null
)
data class ManifestAction(
val handler: ResourceLocation? = null,
val params: List<String> = emptyList()
)
@@ -1,20 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.molangui
/**
* Defines a parameter expected by a scripted GUI.
*
* @param type The parameter type: "number", "string", or "vec3"
* @param default The default value as a string, or null if required
*/
data class MolangUiParameter(
val type: String = "string",
val default: String? = null
)
@@ -1,121 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.molangui
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.molangui.template.TemplateLibraryRegistry
import com.google.gson.GsonBuilder
import com.google.gson.JsonObject
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.packs.resources.ResourceManager
import java.io.InputStreamReader
/**
* Registry for loading Molang UI definitions from resource packs (assets/).
*
* Loads .json files from `assets/<namespace>/molang_ui/`.
* Each JSON file contains the full GUI definition including metadata,
* variables, imports, and the declarative widget tree.
*
* Reloaded client-side via CobblemonClient.reloadCodedAssets().
*/
object MolangUiRegistry {
private val gson = GsonBuilder().create()
private val definitions = mutableMapOf<ResourceLocation, MolangUiDefinition>()
private const val DIRECTORY = "molang_ui"
fun reload(manager: ResourceManager) {
// Load template libraries first so they are available for resolution
TemplateLibraryRegistry.reload(manager)
definitions.clear()
// Collect all JSON definitions, excluding the templates subdirectory
val jsonResources = manager.listResources(DIRECTORY) { path ->
path.path.endsWith(".json") && !path.path.contains("/templates/")
}
for ((identifier, resource) in jsonResources) {
try {
resource.open().use { stream ->
val json = gson.fromJson(InputStreamReader(stream), JsonObject::class.java)
// Build the GUI resource location (namespace:name without extension)
val path = identifier.path
.removePrefix("$DIRECTORY/")
.removeSuffix(".json")
val guiId = ResourceLocation.fromNamespaceAndPath(identifier.namespace, path)
// Parse version
val version = json.get("version")?.asString ?: "0.0.0"
// Parse mode
val mode = json.get("mode")?.asString?.let { GuiMode.fromString(it) } ?: GuiMode.OVERLAY
// Parse parameters
val parameters = mutableMapOf<String, MolangUiParameter>()
json.getAsJsonObject("parameters")?.entrySet()?.forEach { (name, element) ->
val paramObj = element.asJsonObject
parameters[name] = MolangUiParameter(
type = paramObj.get("type")?.asString ?: "string",
default = paramObj.get("default")?.asString
)
}
// Parse blocksInput
val blocksInput = json.get("blocksInput")?.asBoolean ?: false
// Parse exclusive (suppresses all other overlays when active)
val exclusive = json.get("exclusive")?.asBoolean ?: false
// Parse variables (client-side Molang variables with defaults)
val variables = mutableMapOf<String, Double>()
json.getAsJsonObject("variables")?.entrySet()?.forEach { (name, element) ->
variables[name] = element.asDouble
}
// Parse imports (template library references)
val imports = mutableListOf<String>()
json.getAsJsonArray("imports")?.forEach { element ->
imports.add(element.asString)
}
val definition = MolangUiDefinition(
guiId = guiId,
version = version,
mode = mode,
parameters = parameters,
blocksInput = blocksInput,
exclusive = exclusive,
rootJson = json,
variables = variables,
imports = imports
)
if (definitions.containsKey(guiId)) {
Cobblemon.LOGGER.warn("[MolangUiRegistry] Duplicate GUI '$guiId', overwriting")
}
definitions[guiId] = definition
Cobblemon.LOGGER.debug("[MolangUiRegistry] Loaded GUI '$guiId' (mode=$mode, version=$version, params=${parameters.size})")
}
} catch (e: Exception) {
Cobblemon.LOGGER.error("[MolangUiRegistry] Error loading GUI from $identifier", e)
}
}
Cobblemon.LOGGER.info("[MolangUiRegistry] Loaded ${definitions.size} molang UIs")
}
fun getDefinition(guiId: ResourceLocation): MolangUiDefinition? = definitions[guiId]
fun getAllDefinitions(): Map<ResourceLocation, MolangUiDefinition> = definitions.toMap()
fun getVersionMap(): Map<ResourceLocation, String> = definitions.mapValues { it.value.version }
}
@@ -1,488 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.molangui
import com.bedrockk.molang.runtime.MoLangRuntime
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.molang.MoLangFunctions.asMoLangValue
import com.cobblemon.mod.common.api.molang.MoLangFunctions.setup
import com.cobblemon.mod.common.api.molangui.container.CloseItemsBehavior
import com.cobblemon.mod.common.api.molangui.container.MolangContainerMenu
import com.cobblemon.mod.common.api.molangui.container.ServerCollectionSetup
import com.cobblemon.mod.common.api.molangui.container.SlotLayout
import com.cobblemon.mod.common.api.molangui.container.SlotQueryFunctions
import com.cobblemon.mod.common.api.molangui.widget.WidgetDefinition
import com.cobblemon.mod.common.api.molangui.widget.WidgetTree
import com.cobblemon.mod.common.api.molangui.widget.WidgetType
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 com.cobblemon.mod.common.util.withQueryValue
import net.minecraft.core.BlockPos
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
import java.util.concurrent.ConcurrentHashMap
/**
* Server-side representation of an active scripted GUI session for a specific player.
*/
class MolangUiSession(
val sessionId: UUID,
val guiId: ResourceLocation,
val player: ServerPlayer,
definition: MolangUiDefinition,
val blockPos: BlockPos? = null,
private val actionListeners: MutableList<(String, Map<String, String>) -> Unit> = mutableListOf(),
private val closeListeners: MutableList<() -> Unit> = mutableListOf()
) {
var containerMenu: MolangContainerMenu? = null
internal set
private var closed = false
private val currentArgs = ConcurrentHashMap<String, String>(
definition.parameters.mapValues { (_, param) -> param.default ?: "" }
)
val runtime: MoLangRuntime = MolangUiContext.createServerRuntime(
guiId = guiId,
sessionId = sessionId,
player = player,
parameters = definition.parameters.mapValues { (_, param) -> param.default ?: "" }
)
val rootWidget: WidgetDefinition = WidgetTree.parse(definition.rootJson, isClient = false)
/**
* Returns the current value of a session argument, or null if not set.
*/
fun getCurrentArg(name: String): String? = currentArgs[name]
fun getAllArgs(): Map<String, String> = currentArgs.toMap()
/**
* Sets arguments locally without sending a packet to the client.
* Used by on_open scripts to set initial args before the GUI is opened.
*/
fun setArgLocally(name: String, value: String) {
currentArgs[name] = value
}
/**
* Sends updated arguments to the client and tracks them server-side.
*/
fun updateArgs(args: Map<String, String>) {
if (closed) return
currentArgs.putAll(args)
MolangUiUpdatePacket(sessionId, args).sendToPlayer(player)
}
/**
* Pushes variable and/or parameter updates to the client GUI.
*/
fun pushUpdate(variables: Map<String, Double> = emptyMap(), parameters: Map<String, String> = emptyMap()) {
if (closed) {
Cobblemon.LOGGER.warn("[MolangUiSession] pushUpdate called on closed session $sessionId")
return
}
Cobblemon.LOGGER.info("[MolangUiSession] pushUpdate: sessionId=$sessionId variables=$variables parameters=${parameters.keys}")
MolangUiUpdatePacket(sessionId, parameters, variables).sendToPlayer(player)
}
/**
* Closes this GUI session and notifies the client.
*/
fun close() {
if (closed) return
closed = true
MolangUiClosePacket(sessionId).sendToPlayer(player)
closeListeners.forEach { it() }
MolangUiSessionManager.removeSession(sessionId)
}
/**
* Adds a listener for player actions from the GUI.
*/
fun addActionListener(listener: (action: String, data: Map<String, String>) -> Unit) {
actionListeners.add(listener)
}
/**
* Adds a listener for when the GUI is closed (by server or client).
*/
fun addCloseListener(listener: () -> Unit) {
closeListeners.add(listener)
}
/**
* Handles a widget event by finding the widget definition, setting MoLang event context,
* and evaluating the event handler expression.
*/
fun handleWidgetEvent(widgetId: String, eventType: String, data: Map<String, String>) {
if (closed) return
val widgetDef = findWidget(rootWidget, widgetId)
if (widgetDef == null) {
Cobblemon.LOGGER.debug("[MolangUiSession] Widget '$widgetId' not found in GUI '$guiId'")
return
}
val handler = widgetDef.events[eventType]
if (handler == null) {
Cobblemon.LOGGER.debug("[MolangUiSession] No handler for event '$eventType' on widget '$widgetId' in GUI '$guiId'")
return
}
// "action@smelt" syntax routes directly through the action router (manifest scripts, Kotlin handlers)
if (handler.startsWith("action@")) {
val actionName = handler.substringAfter("action@")
MolangUiActionRouter.routeAction(this, player, actionName, data)
return
}
MolangUiContext.setEventContext(runtime, widgetId, data)
try {
handler.asExpressionLike().resolve(runtime)
} catch (e: Exception) {
Cobblemon.LOGGER.error("[MolangUiSession] Error evaluating event '$eventType' handler on widget '$widgetId': ${e.message}", e)
}
}
private fun findWidget(node: WidgetDefinition, targetId: String): WidgetDefinition? {
if (node.id == targetId) return node
for (child in node.children) {
val result = findWidget(child, targetId)
if (result != null) return result
}
return null
}
/** Called when an action is received from the client. */
internal fun onAction(action: String, data: Map<String, String>) {
if (closed) return
actionListeners.forEach { it(action, data) }
}
/** Called when the client notifies that it closed the GUI. */
internal fun onClientClosed() {
if (closed) return
closed = true
closeListeners.forEach { it() }
MolangUiSessionManager.removeSession(sessionId)
}
fun isClosed(): Boolean = closed
}
/**
* Extracts [SlotLayout] definitions from all SLOT_GRID widgets in this widget tree,
* keyed by the collection_name property.
*/
fun WidgetDefinition.extractSlotLayouts(): Map<String, SlotLayout> {
val layouts = mutableMapOf<String, SlotLayout>()
collectSlotLayouts(this, layouts)
return layouts
}
private fun collectSlotLayouts(node: WidgetDefinition, layouts: MutableMap<String, SlotLayout>) {
if (node.type == WidgetType.SLOT_GRID) {
val collectionName = (node.properties["collection_name"]?.raw as? String) ?: return
val dims = node.properties["grid_dimensions"]?.raw
val offset = node.properties["offset"]?.raw
val slotSize = node.properties["slot_size"]?.raw
val spacing = node.properties["slot_spacing"]?.raw
fun listToInts(raw: Any?, default1: Int, default2: Int): Pair<Int, Int> {
if (raw is List<*> && raw.size >= 2) {
return Pair((raw[0] as? Number)?.toInt() ?: default1, (raw[1] as? Number)?.toInt() ?: default2)
}
return Pair(default1, default2)
}
val (cols, rows) = listToInts(dims, 1, 1)
val (offX, offY) = listToInts(offset, 0, 0)
val (slotW, slotH) = listToInts(slotSize, 18, 18)
val (spX, spY) = listToInts(spacing, 0, 0)
layouts[collectionName] = SlotLayout(
offsetX = offX, offsetY = offY,
columns = cols, rows = rows,
slotWidth = slotW, slotHeight = slotH,
spacingX = spX, spacingY = spY
)
}
for (child in node.children) {
collectSlotLayouts(child, layouts)
}
}
/**
* Returns true if this widget tree contains any SLOT_GRID widgets.
*/
fun WidgetDefinition.hasSlotGrids(): Boolean {
if (type == WidgetType.SLOT_GRID) return true
return children.any { it.hasSlotGrids() }
}
/**
* Server-side manager for active scripted GUI sessions.
*/
object MolangUiSessionManager {
private val sessions = ConcurrentHashMap<UUID, MolangUiSession>()
private val disabledGuis = ConcurrentHashMap<UUID, Set<String>>()
fun setDisabledGuis(playerId: UUID, guiIds: Set<String>) {
if (guiIds.isEmpty()) disabledGuis.remove(playerId)
else disabledGuis[playerId] = guiIds
}
fun isGuiDisabled(playerId: UUID, guiId: ResourceLocation): Boolean {
return disabledGuis[playerId]?.contains(guiId.toString()) == true
}
/**
* Opens a scripted GUI for a player.
*
* @param player The target player
* @param guiId The GUI definition resource location
* @param args Initial arguments to pass to the GUI
* @param actionListener Optional listener for player actions
* @return The session, or null if the GUI definition doesn't exist
*/
fun openGui(
player: ServerPlayer,
guiId: ResourceLocation,
args: Map<String, String> = emptyMap(),
blockPos: BlockPos? = null,
actionListener: ((String, Map<String, String>) -> Unit)? = null
): MolangUiSession? {
if (isGuiDisabled(player.uuid, guiId)) {
Cobblemon.LOGGER.warn("[MolangUiSessionManager] GUI '$guiId' is disabled for player ${player.name.string} due to version mismatch")
return null
}
val definition = MolangUiRegistry.getDefinition(guiId)
if (definition == null) {
Cobblemon.LOGGER.error("[MolangUiSessionManager] GUI definition '$guiId' not found")
return null
}
val sessionId = UUID.randomUUID()
val session = MolangUiSession(sessionId, guiId, player, definition, blockPos)
if (actionListener != null) {
session.addActionListener(actionListener)
}
sessions[sessionId] = session
// Resolve manifest parameter defaults (supports Molang expressions for persistent data)
val manifest = MolangUiManifestRegistry.getManifest(guiId)
if (manifest != null) {
val resolvedDefaults = resolveManifestDefaults(manifest, player)
resolvedDefaults.forEach { (key, value) -> session.setArgLocally(key, value) }
}
// Execute on_open script from manifest if present
if (manifest?.onOpen != null) {
MolangUiActionRouter.executeOnOpen(manifest.onOpen, session, player)
}
// Merge caller args with any args set by defaults/on_open script
val mergedArgs = session.getAllArgs().toMutableMap()
mergedArgs.putAll(args) // caller args take priority
// Container setup: detect slot_grid widgets and create container menu
val hasContainer = session.rootWidget.hasSlotGrids()
// Send our open packet BEFORE player.openMenu() — the client needs pendingContainerData
// set before the vanilla ClientboundOpenScreenPacket triggers the MenuScreens factory.
MolangUiOpenPacket(
sessionId = sessionId,
guiId = guiId,
mode = definition.mode,
args = mergedArgs,
blocksInput = definition.blocksInput,
exclusive = definition.exclusive,
hasContainer = hasContainer,
slotLayouts = if (hasContainer) session.rootWidget.extractSlotLayouts() else emptyMap()
).sendToPlayer(player)
// Now open the container menu via vanilla MenuProvider (sends ClientboundOpenScreenPacket)
// This MUST come after our packet so the client has pendingContainerData ready.
if (hasContainer) {
val slotLayouts = session.rootWidget.extractSlotLayouts()
val configs = manifest?.collections ?: emptyMap()
val collectionSetup = mutableListOf<ServerCollectionSetup>()
val sessionOwned = mutableSetOf<String>()
for ((name, config) in configs) {
val layout = slotLayouts[name] ?: continue
val container: Container = if (config.source == "block_entity" && blockPos != null) {
val be = player.level().getBlockEntity(blockPos)
if (be is Container) be
else SimpleContainer(config.size)
} else {
sessionOwned.add(name)
SimpleContainer(config.size)
}
collectionSetup.add(ServerCollectionSetup(name, config, layout, container))
}
player.openMenu(object : MenuProvider {
override fun getDisplayName() = Component.empty()
override fun createMenu(containerId: Int, inv: Inventory, p: Player): AbstractContainerMenu {
val menu = MolangContainerMenu(containerId, inv)
menu.initializeServer(
collectionSetup = collectionSetup,
shiftClickRules = manifest?.shiftClickRules ?: emptyList(),
sessionOwned = sessionOwned,
closeItemsBehavior = manifest?.closeItemsBehavior ?: CloseItemsBehavior.DROP,
onSlotChange = { collName, slotIdx ->
val slotGridWidget = findSlotGridWidget(session.rootWidget, collName)
val actionName = slotGridWidget?.events?.get("on_slot_change")
if (actionName != null) {
MolangUiActionRouter.routeAction(
session, session.player, actionName,
mapOf("collection" to collName, "index" to slotIdx.toString())
)
}
}
)
session.containerMenu = menu
SlotQueryFunctions.register(session.runtime, menu)
return menu
}
})
}
Cobblemon.LOGGER.debug("[MolangUiSessionManager] Opened GUI '$guiId' for ${player.name.string} (session=$sessionId)")
return session
}
/**
* Closes a GUI session.
*/
fun closeGui(sessionId: UUID) {
sessions[sessionId]?.close()
}
/**
* Handles an action from a client.
*
* Actions in `"widgetId:eventType"` format are routed to [MolangUiSession.handleWidgetEvent]
* for MoLang evaluation. All actions are also forwarded to registered action listeners for
* backward compatibility with code that opens GUIs with action listeners.
*/
fun handleAction(sessionId: UUID, player: ServerPlayer, action: String, data: Map<String, String>) {
val session = sessions[sessionId]
if (session == null) {
Cobblemon.LOGGER.debug("[MolangUiSessionManager] Action for unknown session $sessionId")
return
}
if (session.player.uuid != player.uuid) {
Cobblemon.LOGGER.warn("[MolangUiSessionManager] Action from wrong player for session $sessionId")
return
}
// Route widget events (format: "widgetId:eventType") to MoLang handler
val colonIndex = action.indexOf(':')
if (colonIndex > 0) {
val widgetId = action.substring(0, colonIndex)
val eventType = action.substring(colonIndex + 1)
session.handleWidgetEvent(widgetId, eventType, data)
}
// Always fire action listeners for backward compatibility
session.onAction(action, data)
}
/**
* Handles a client-closed notification.
*/
fun handleClosed(sessionId: UUID, player: ServerPlayer) {
val session = sessions[sessionId]
if (session == null) return
if (session.player.uuid != player.uuid) {
Cobblemon.LOGGER.warn("[MolangUiSessionManager] Close from wrong player for session $sessionId")
return
}
session.onClientClosed()
}
/**
* Closes all GUI sessions for a player. Call on disconnect.
*/
fun closeAllForPlayer(playerId: UUID) {
sessions.values.filter { it.player.uuid == playerId }.forEach {
it.close()
}
disabledGuis.remove(playerId)
}
/** Internal: removes a session from tracking. */
internal fun removeSession(sessionId: UUID) {
sessions.remove(sessionId)
}
/**
* Recursively searches the widget tree for a SLOT_GRID widget with the given collection_name.
*/
private fun findSlotGridWidget(node: WidgetDefinition, collectionName: String): WidgetDefinition? {
if (node.type == WidgetType.SLOT_GRID && (node.properties["collection_name"]?.raw as? String) == collectionName) {
return node
}
for (child in node.children) {
val result = findSlotGridWidget(child, collectionName)
if (result != null) return result
}
return null
}
fun getSession(sessionId: UUID): MolangUiSession? = sessions[sessionId]
/**
* Returns the first active session for the given player and GUI ID, or null if none exists.
*/
fun getSessionForPlayer(player: ServerPlayer, guiId: ResourceLocation): MolangUiSession? {
return sessions.values.firstOrNull { it.player.uuid == player.uuid && it.guiId == guiId && !it.isClosed() }
}
/**
* Resolves manifest parameter defaults. If a default value looks like a Molang expression
* (contains q., v., math., etc.), it's evaluated with the player context.
* This allows persistent data loading: "default": "q.format(q.player.data('my_gui').my_value, 0)"
*/
private fun resolveManifestDefaults(manifest: MolangUiManifest, player: ServerPlayer): Map<String, String> {
val resolved = mutableMapOf<String, String>()
for ((paramName, param) in manifest.parameters) {
val defaultValue = param.default ?: continue
if (com.cobblemon.mod.common.api.molangui.widget.WidgetProperty.isMolangExpression(defaultValue)) {
try {
val runtime = com.bedrockk.molang.runtime.MoLangRuntime().setup()
runtime.withQueryValue("player", player.asMoLangValue())
val expression = defaultValue.asExpressionLike()
val result = expression.resolveString(runtime)
resolved[paramName] = result
} catch (e: Exception) {
Cobblemon.LOGGER.error("[MolangUiSessionManager] Failed to resolve default for param '$paramName': $defaultValue", e)
resolved[paramName] = defaultValue
}
} else {
resolved[paramName] = defaultValue
}
}
return resolved
}
}
@@ -1,112 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.molangui.animation
import com.cobblemon.mod.common.api.molangui.widget.AnimationConfig
import kotlin.math.*
object Easing {
fun resolve(name: String): (Float) -> Float = when (name) {
"linear" -> ::linear
"ease_in" -> ::easeIn
"ease_out" -> ::easeOut
"ease_in_out" -> ::easeInOut
"bounce" -> ::bounce
"elastic" -> ::elastic
else -> ::easeOut
}
fun linear(t: Float): Float = t
fun easeIn(t: Float): Float = t * t
fun easeOut(t: Float): Float = 1f - (1f - t) * (1f - t)
fun easeInOut(t: Float): Float = if (t < 0.5f) 2f * t * t else 1f - (-2f * t + 2f).pow(2) / 2f
fun bounce(t: Float): Float {
val n1 = 7.5625f
val d1 = 2.75f
var x = t
return when {
x < 1f / d1 -> n1 * x * x
x < 2f / d1 -> { x -= 1.5f / d1; n1 * x * x + 0.75f }
x < 2.5f / d1 -> { x -= 2.25f / d1; n1 * x * x + 0.9375f }
else -> { x -= 2.625f / d1; n1 * x * x + 0.984375f }
}
}
fun elastic(t: Float): Float {
if (t == 0f || t == 1f) return t
val c4 = (2f * PI / 3f).toFloat()
return 2f.pow(-10f * t) * sin((t * 10f - 0.75f) * c4) + 1f
}
}
object EnterExitAnimation {
data class AnimationOutput(
val opacity: Float = 1f,
val offsetX: Float = 0f,
val offsetY: Float = 0f,
val scale: Float = 1f
)
fun evaluate(config: AnimationConfig, progress: Float, isExit: Boolean): AnimationOutput {
val easedProgress = Easing.resolve(config.easing)(progress)
val t = if (isExit) 1f - easedProgress else easedProgress
return when (config.type) {
"fade_in", "fade_out" -> AnimationOutput(opacity = t)
"slide_up" -> AnimationOutput(offsetY = (1f - t) * 30f)
"slide_down" -> AnimationOutput(offsetY = (1f - t) * -30f)
"slide_left" -> AnimationOutput(offsetX = (1f - t) * 30f)
"slide_right" -> AnimationOutput(offsetX = (1f - t) * -30f)
"scale_in", "scale_out" -> AnimationOutput(scale = t)
"pop" -> AnimationOutput(scale = if (t < 1f) t * 1.1f else 1f, opacity = t)
else -> AnimationOutput()
}
}
}
class TransitionState(
val duration: Float,
val easingName: String = "ease_out"
) {
private val easingFn = Easing.resolve(easingName)
var startValue: Float = 0f
var targetValue: Float = 0f
var currentValue: Float = 0f
var elapsed: Float = 0f
var active: Boolean = false
fun setTarget(newTarget: Float) {
if (newTarget != targetValue) {
startValue = currentValue
targetValue = newTarget
elapsed = 0f
active = true
}
}
fun update(delta: Float): Float {
if (!active) return currentValue
elapsed += delta
val progress = (elapsed / duration).coerceIn(0f, 1f)
currentValue = startValue + (targetValue - startValue) * easingFn(progress)
if (progress >= 1f) {
currentValue = targetValue
active = false
}
return currentValue
}
fun snapTo(value: Float) {
startValue = value
targetValue = value
currentValue = value
active = false
}
}
@@ -1,76 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.molangui.container
import net.minecraft.core.registries.BuiltInRegistries
import net.minecraft.core.registries.Registries
import net.minecraft.resources.ResourceLocation
import net.minecraft.tags.TagKey
import net.minecraft.world.item.ItemStack
/**
* Configuration for a single slot collection parsed from the manifest JSON.
*/
data class CollectionConfig(
val size: Int = 0,
val allowedItems: String? = null,
val takeOnly: Boolean = false,
val source: String = "session",
val slots: List<Int>? = null
) {
/** Returns true if the given item is allowed in this collection's slots. */
fun isItemAllowed(stack: ItemStack): Boolean {
if (takeOnly) return false
val filter = allowedItems ?: return true
return ItemFilter.matches(stack, filter)
}
}
/**
* A shift-click rule mapping items from one set of collections to another.
*/
data class ShiftClickRule(
val from: List<String>,
val to: List<String>
)
/**
* Utility for matching items against filter strings.
* Supports: item IDs ("minecraft:diamond"), tags ("#minecraft:furnace_fuel"),
* and comma-separated lists ("minecraft:diamond, minecraft:emerald").
*/
object ItemFilter {
fun matches(stack: ItemStack, filter: String): Boolean {
return filter.split(",").map { it.trim() }.any { matchSingle(stack, it) }
}
private fun matchSingle(stack: ItemStack, filter: String): Boolean {
if (filter.startsWith("#")) {
val tagId = ResourceLocation.tryParse(filter.substring(1)) ?: return false
val tagKey = TagKey.create(Registries.ITEM, tagId)
return stack.`is`(tagKey)
}
val itemId = ResourceLocation.tryParse(filter) ?: return false
val item = BuiltInRegistries.ITEM.get(itemId)
return stack.`is`(item)
}
}
/**
* Behavior for items remaining in session-owned collections when the GUI closes.
*/
enum class CloseItemsBehavior(val id: String) {
DROP("drop"),
RETURN("return"),
DESTROY("destroy");
companion object {
fun fromId(id: String): CloseItemsBehavior = entries.firstOrNull { it.id == id } ?: DROP
}
}
@@ -1,336 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.molangui.container
import com.cobblemon.mod.common.CobblemonMenuType
import 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
/**
* Layout data class describing how a grid of slots is arranged in screen space.
*/
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
) {
val slotCount: Int get() = columns * rows
}
/**
* Tracks slot index ranges for a named collection so shift-click rules can resolve them.
*/
private data class CollectionSlotRange(
val name: String,
val startIndex: Int,
val endIndex: Int // exclusive
)
/**
* Container menu for Molang GUIs that declare slot_grid widgets.
*
* Server side: call [initializeServer] with real containers, configs, and rules.
* Client side: call [initializeClient] with layout info so vanilla slot sync works.
*/
class MolangContainerMenu(
containerId: Int,
private val playerInventory: Inventory
) : AbstractContainerMenu(CobblemonMenuType.MOLANG_UI, containerId) {
/** Named containers for custom collections. */
val collections: MutableMap<String, Container> = mutableMapOf()
// Slot range tracking for shift-click resolution
private val collectionRanges: MutableList<CollectionSlotRange> = mutableListOf()
private var playerInventoryStart: Int = 0
private var playerHotbarStart: Int = 0
private var playerHotbarEnd: Int = 0
// Shift-click rules from the manifest (only populated server-side)
private var shiftClickRules: List<ShiftClickRule> = emptyList()
// Collection configs keyed by name (for removed() behavior)
private val collectionConfigs: MutableMap<String, CollectionConfig> = mutableMapOf()
private val sessionOwnedCollections: MutableSet<String> = mutableSetOf()
private var closeItemsBehavior: CloseItemsBehavior = CloseItemsBehavior.DROP
// -------------------------------------------------
// Server-side initialization
// -------------------------------------------------
/**
* Sets up all slots from the given collection configs and containers.
* Also adds player inventory and hotbar slots.
*
* @param collectionSetup List of (name, config, layout, container) tuples
* @param shiftClickRules Shift-click rules from the manifest
* @param sessionOwned Names of collections whose items are session-owned (returned/dropped on close)
* @param closeItemsBehavior How to handle items in session-owned collections on close
*/
fun initializeServer(
collectionSetup: List<ServerCollectionSetup>,
shiftClickRules: List<ShiftClickRule> = emptyList(),
sessionOwned: Set<String> = emptySet(),
closeItemsBehavior: CloseItemsBehavior = CloseItemsBehavior.DROP,
onSlotChange: ((collectionName: String, slotIndex: Int) -> Unit)? = null
) {
this.shiftClickRules = shiftClickRules
this.sessionOwnedCollections.addAll(sessionOwned)
this.closeItemsBehavior = closeItemsBehavior
for (setup in collectionSetup) {
val (name, config, layout, container) = setup
collections[name] = container
collectionConfigs[name] = config
val startIndex = slots.size
addCollectionSlots(name, config, layout, container, onSlotChange)
val endIndex = slots.size
collectionRanges.add(CollectionSlotRange(name, startIndex, endIndex))
}
addPlayerSlots()
}
// -------------------------------------------------
// Client-side initialization
// -------------------------------------------------
/**
* Creates empty [SimpleContainer]s matching expected slot counts so vanilla can sync them.
*
* @param collectionLayouts List of (name, config, layout) — no real containers yet
*/
fun initializeClient(
collectionLayouts: List<ClientCollectionLayout>
) {
for ((name, config, layout) in collectionLayouts) {
val container = SimpleContainer(layout.slotCount)
collections[name] = container
collectionConfigs[name] = config
val startIndex = slots.size
addCollectionSlots(name, config, layout, container)
val endIndex = slots.size
collectionRanges.add(CollectionSlotRange(name, startIndex, endIndex))
}
addPlayerSlots()
}
// -------------------------------------------------
// Shared slot setup helpers
// -------------------------------------------------
private fun addCollectionSlots(
name: String,
config: CollectionConfig,
layout: SlotLayout,
container: Container,
onSlotChange: ((collectionName: String, slotIndex: Int) -> Unit)? = null
) {
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 capturedIndex = containerIndex
addSlot(MolangSlot(
container = container,
index = capturedIndex,
x = x,
y = y,
collectionConfig = config,
onSlotChange = if (onSlotChange != null) { { onSlotChange(name, capturedIndex) } } else null
))
containerIndex++
}
}
}
private fun addPlayerSlots() {
playerInventoryStart = slots.size
// Player inventory rows (indices 9-35 in the Inventory)
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
// Hotbar (indices 0-8 in the Inventory)
for (col in 0 until 9) {
addSlot(Slot(playerInventory, col, 8 + col * 18, 142))
}
playerHotbarEnd = slots.size
}
// -------------------------------------------------
// Public API
// -------------------------------------------------
/** Returns the container for the named collection, or null if not present. */
fun getCollection(name: String): Container? = collections[name]
// -------------------------------------------------
// AbstractContainerMenu overrides
// -------------------------------------------------
override fun quickMoveStack(player: Player, index: Int): ItemStack {
val slot = slots.getOrNull(index) ?: return ItemStack.EMPTY
if (!slot.hasItem()) 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
if (clickedCollection != null) {
// Slot is in a custom collection — try to move to player inventory/hotbar
val rule = findShiftClickRule(clickedCollection.name, playerSideNames())
if (rule != null) {
val moved = tryMoveToCollections(slotItem, rule.to)
if (!moved) return ItemStack.EMPTY
} else {
// Default: move to player inventory then hotbar
if (!moveItemStackTo(slotItem, playerInventoryStart, playerHotbarEnd, false)) {
return ItemStack.EMPTY
}
}
} else if (isPlayerInventory || isHotbar) {
// Slot is in player inventory/hotbar — try to move to custom collections
val rule = findShiftClickRuleFromPlayer()
if (rule != null) {
val moved = tryMoveToCollections(slotItem, rule.to)
if (!moved) return ItemStack.EMPTY
} else {
// Default: move to first custom collection
val firstRange = collectionRanges.firstOrNull()
if (firstRange == null || !moveItemStackTo(slotItem, firstRange.startIndex, firstRange.endIndex, false)) {
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 = true
override fun removed(player: Player) {
super.removed(player)
for (name in sessionOwnedCollections) {
val container = collections[name] ?: continue
when (closeItemsBehavior) {
CloseItemsBehavior.DROP -> clearContainer(player, container)
CloseItemsBehavior.RETURN -> returnItemsToPlayer(player, container)
CloseItemsBehavior.DESTROY -> container.clearContent()
}
}
}
// -------------------------------------------------
// Private helpers
// -------------------------------------------------
private fun playerEndIndex() = playerHotbarEnd
private fun playerInventoryEnd() = playerHotbarStart
private fun playerSideNames(): List<String> = listOf("player_inventory", "hotbar")
private fun findShiftClickRule(fromCollection: String, toNames: List<String>): ShiftClickRule? {
return shiftClickRules.firstOrNull { rule ->
rule.from.contains(fromCollection)
}
}
private fun findShiftClickRuleFromPlayer(): ShiftClickRule? {
return shiftClickRules.firstOrNull { rule ->
rule.from.any { it == "player_inventory" || it == "hotbar" }
}
}
private fun tryMoveToCollections(stack: ItemStack, toNames: List<String>): Boolean {
for (name in toNames) {
if (name == "player_inventory") {
if (moveItemStackTo(stack, playerInventoryStart, playerInventoryEnd(), false)) return true
} else if (name == "hotbar") {
if (moveItemStackTo(stack, playerHotbarStart, playerHotbarEnd, false)) return true
} else {
val range = collectionRanges.firstOrNull { it.name == name } ?: continue
if (moveItemStackTo(stack, range.startIndex, range.endIndex, false)) return true
}
}
return false
}
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)
}
}
}
}
// -------------------------------------------------
// Setup data classes
// -------------------------------------------------
/**
* Server-side collection setup: name, config, layout, and the real container.
*/
data class ServerCollectionSetup(
val name: String,
val config: CollectionConfig,
val layout: SlotLayout,
val container: Container
)
/**
* Client-side collection layout: name, config, and layout (container created internally).
*/
data class ClientCollectionLayout(
val name: String,
val config: CollectionConfig,
val layout: SlotLayout
)
@@ -1,41 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.molangui.container
import net.minecraft.world.Container
import net.minecraft.world.entity.player.Player
import net.minecraft.world.inventory.Slot
import net.minecraft.world.item.ItemStack
/**
* A slot with configurable item filtering and take-only behavior.
* Used by [MolangContainerMenu] to enforce collection rules from the manifest.
*/
class MolangSlot(
container: Container,
index: Int,
x: Int,
y: Int,
private val collectionConfig: CollectionConfig,
private val onSlotChange: (() -> Unit)? = null
) : Slot(container, index, x, y) {
override fun mayPlace(stack: ItemStack): Boolean {
return collectionConfig.isItemAllowed(stack)
}
override fun mayPickup(player: Player): Boolean {
return true
}
override fun setChanged() {
super.setChanged()
onSlotChange?.invoke()
}
}
@@ -1,113 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.molangui.container
import com.bedrockk.molang.runtime.MoLangRuntime
import com.bedrockk.molang.runtime.value.DoubleValue
import com.bedrockk.molang.runtime.value.StringValue
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
/**
* Registers Molang query functions for reading and writing container slot contents
* on a [MoLangRuntime] bound to a [MolangContainerMenu].
*
* Functions registered:
* - `q.slot(collection, index)` — returns the item ID in a slot, or empty string if empty
* - `q.slot_count(collection, index)` — returns the stack count as a double
* - `q.set_slot(collection, index, itemId, count)` — sets slot contents; returns 1.0 on success, 0.0 on failure
* - `q.clear_slot(collection, index)` — empties a slot; returns 1.0
* - `q.slot_tag(collection, index, tag)` — returns 1.0 if the item has the given tag, 0.0 otherwise
*/
object SlotQueryFunctions {
fun register(runtime: MoLangRuntime, menu: MolangContainerMenu) {
// q.slot(collection, index) -> item ID string or empty string
runtime.environment.query.addFunction("slot") { params ->
val collection = params.getString(0)
val index = params.getDouble(1).toInt()
val container = menu.getCollection(collection)
if (container == null || index < 0 || index >= container.containerSize) {
return@addFunction StringValue("")
}
val stack = container.getItem(index)
if (stack.isEmpty) {
StringValue("")
} else {
StringValue(BuiltInRegistries.ITEM.getKey(stack.item).toString())
}
}
// q.slot_count(collection, index) -> stack count as double
runtime.environment.query.addFunction("slot_count") { params ->
val collection = params.getString(0)
val index = params.getDouble(1).toInt()
val container = menu.getCollection(collection)
if (container == null || index < 0 || index >= container.containerSize) {
return@addFunction DoubleValue(0.0)
}
val stack = container.getItem(index)
DoubleValue(stack.count.toDouble())
}
// q.set_slot(collection, index, itemId, count) -> 1.0 on success, 0.0 on failure
runtime.environment.query.addFunction("set_slot") { params ->
val collection = params.getString(0)
val index = params.getDouble(1).toInt()
val itemId = params.getString(2)
val count = params.getDouble(3).toInt()
val container = menu.getCollection(collection)
if (container == null || index < 0 || index >= container.containerSize) {
return@addFunction DoubleValue(0.0)
}
val resourceLocation = ResourceLocation.tryParse(itemId)
?: return@addFunction DoubleValue(0.0)
val item = BuiltInRegistries.ITEM.get(resourceLocation)
val newStack = ItemStack(item, count.coerceAtLeast(1))
container.setItem(index, newStack)
DoubleValue(1.0)
}
// q.clear_slot(collection, index) -> 1.0
runtime.environment.query.addFunction("clear_slot") { params ->
val collection = params.getString(0)
val index = params.getDouble(1).toInt()
val container = menu.getCollection(collection)
if (container != null && index >= 0 && index < container.containerSize) {
container.setItem(index, ItemStack.EMPTY)
}
DoubleValue(1.0)
}
// q.slot_tag(collection, index, tag) -> 1.0 if item has tag, 0.0 otherwise
runtime.environment.query.addFunction("slot_tag") { params ->
val collection = params.getString(0)
val index = params.getDouble(1).toInt()
val tagStr = params.getString(2)
val container = menu.getCollection(collection)
if (container == null || index < 0 || index >= container.containerSize) {
return@addFunction DoubleValue(0.0)
}
val stack = container.getItem(index)
if (stack.isEmpty) {
return@addFunction DoubleValue(0.0)
}
val tagId = ResourceLocation.tryParse(tagStr)
?: return@addFunction DoubleValue(0.0)
val tagKey = TagKey.create(Registries.ITEM, tagId)
if (stack.`is`(tagKey)) DoubleValue(1.0) else DoubleValue(0.0)
}
}
}
@@ -1,81 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.molangui.event
import com.bedrockk.molang.runtime.MoLangRuntime
import com.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
) {
// Run client-side handler first (instant visual feedback)
val clientHandler = widget.clientEvents[event.type.clientId]
if (clientHandler != null) {
try {
val expression = clientHandler.asExpressionLike()
expression.resolve(runtime)
} catch (e: Exception) {
Cobblemon.LOGGER.warn("[ClientEventProcessor] Failed to execute client event '${event.type.clientId}' on widget '${event.widgetId}': ${e.message}")
}
}
// Send server-side handler as action packet
val serverHandler = widget.events[event.type.id]
if (serverHandler != null) {
val packet = MolangUiActionPacket(
sessionId = sessionId,
action = "${event.widgetId}:${event.type.id}",
data = event.data
)
packet.sendToServer()
}
}
fun hitTest(root: WidgetInstance, x: Float, y: Float): WidgetInstance? {
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"),
SUBMIT("on_submit"),
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, String> = emptyMap()
)
@@ -1,149 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.molangui.layout
import com.cobblemon.mod.common.api.molangui.widget.WidgetInstance
import com.cobblemon.mod.common.api.molangui.widget.WidgetProperty
import com.bedrockk.molang.runtime.MoLangRuntime
object LayoutEngine {
fun layout(root: WidgetInstance, screenWidth: Float, screenHeight: Float, runtime: MoLangRuntime) {
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
layoutChildren(root, runtime)
}
private fun layoutChildren(parent: WidgetInstance, runtime: MoLangRuntime) {
val layout = parent.definition.getStringProperty("layout")
val spacing = parent.definition.getProperty("spacing")?.resolveFloat(runtime) ?: 0f
val padding = parsePadding(parent.definition.getProperty("padding"), runtime)
val innerWidth = parent.computedWidth - padding.left - padding.right
val innerHeight = parent.computedHeight - padding.top - padding.bottom
val innerX = parent.absoluteX + padding.left
val innerY = parent.absoluteY + padding.top
for (child in parent.children) {
if (!child.visible) continue
child.computedWidth = child.widthExpr.resolve(innerWidth, runtime)
child.computedHeight = 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, innerHeight, spacing, runtime)
else -> layoutAnchored(parent, innerX, innerY, innerWidth, innerHeight, runtime)
}
for (child in parent.children) {
if (!child.visible) continue
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.offsetXExpr.resolve(innerWidth, runtime)
val offsetY = 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, runtime = runtime)
var currentY = innerY
for (child in visibleChildren) {
child.absoluteX = innerX + child.offsetXExpr.resolve(innerWidth, runtime)
child.absoluteY = currentY + 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, runtime = runtime)
var currentX = innerX
for (child in visibleChildren) {
child.absoluteX = currentX + child.offsetXExpr.resolve(innerWidth, runtime)
child.absoluteY = innerY + child.offsetYExpr.resolve(innerHeight, runtime)
currentX += child.computedWidth + spacing
}
}
private fun layoutGrid(parent: WidgetInstance, innerX: Float, innerY: Float, innerWidth: Float, innerHeight: Float, spacing: Float, runtime: MoLangRuntime) {
val columns = parent.definition.getProperty("columns")?.resolveInt(runtime) ?: 1
val visibleChildren = parent.children.filter { it.visible }
val cellWidth = (innerWidth - spacing * (columns - 1)) / columns
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, runtime: MoLangRuntime) {
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
}
}
}
}
data class Padding(val top: Float, val right: Float, val bottom: Float, val left: Float)
private fun parsePadding(property: WidgetProperty?, runtime: MoLangRuntime): Padding {
if (property == null) return Padding(0f, 0f, 0f, 0f)
val raw = property.raw
if (raw is List<*> && raw.size == 4) {
return Padding(
(raw[0] as? Number)?.toFloat() ?: 0f,
(raw[1] as? Number)?.toFloat() ?: 0f,
(raw[2] as? Number)?.toFloat() ?: 0f,
(raw[3] as? Number)?.toFloat() ?: 0f
)
}
return Padding(0f, 0f, 0f, 0f)
}
}
@@ -1,86 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.molangui.layout
import com.cobblemon.mod.common.api.molang.ExpressionLike
import com.cobblemon.mod.common.util.asExpressionLike
import com.bedrockk.molang.runtime.MoLangRuntime
sealed class SizeExpression {
abstract fun resolve(parentSize: Float, runtime: MoLangRuntime): Float
data class Pixels(val value: Float) : SizeExpression() {
override fun resolve(parentSize: Float, runtime: MoLangRuntime) = value
}
data class Percent(val fraction: Float) : SizeExpression() {
override fun resolve(parentSize: Float, runtime: MoLangRuntime) = parentSize * fraction
}
data class PercentWithOffset(val fraction: Float, val offset: Float) : SizeExpression() {
override fun resolve(parentSize: Float, runtime: MoLangRuntime) = parentSize * fraction + offset
}
data object Auto : SizeExpression() {
override fun resolve(parentSize: Float, runtime: MoLangRuntime) = 0f
}
data object Fill : SizeExpression() {
override fun resolve(parentSize: Float, runtime: MoLangRuntime) = 0f
}
data class MolangSize(val expression: ExpressionLike, val isPercent: Boolean = false) : SizeExpression() {
override fun resolve(parentSize: Float, runtime: MoLangRuntime): Float {
val value = expression.resolveFloat(runtime)
return if (isPercent) parentSize * value / 100f else value
}
}
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)
else -> Pixels(0f)
}
}
private fun parseString(value: String): SizeExpression {
val trimmed = value.trim()
return when {
trimmed == "auto" -> Auto
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 -> {
val isPercent = trimmed.endsWith("%")
val exprStr = if (isPercent) trimmed.dropLast(1).trim() else trimmed
try {
MolangSize(exprStr.asExpressionLike(), isPercent)
} catch (e: Exception) {
Pixels(0f)
}
}
}
}
}
}
@@ -1,92 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.molangui.template
import com.google.gson.Gson
import com.google.gson.JsonObject
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.packs.resources.ResourceManager
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) {
libraries.clear()
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)
libraries[libraryId] = templates
} catch (e: Exception) {
// Log warning and continue
}
}
}
fun getTemplate(qualifiedName: String): TemplateDefinition? {
val dotIndex = qualifiedName.indexOf('.')
if (dotIndex == -1) return null
val libraryName = qualifiedName.substring(0, dotIndex)
val templateName = qualifiedName.substring(dotIndex + 1)
for ((libId, templates) in libraries) {
if (libId.path == libraryName || libId.toString() == libraryName) {
templates[templateName]?.let { return it }
}
}
return null
}
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
inputs[inputName] = TemplateInput(
type = inputObj.get("type")?.asString ?: "string",
default = inputObj.get("default")?.asString
)
}
}
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: String = "string",
val default: String? = null
)
@@ -1,107 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.molangui.template
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import com.google.gson.JsonParser
object TemplateResolver {
fun resolve(root: JsonObject, localTemplates: Map<String, TemplateDefinition>, isClient: Boolean = true): JsonObject {
return resolveNode(root, localTemplates)
}
private fun resolveNode(node: JsonObject, localTemplates: Map<String, TemplateDefinition>): JsonObject {
val result = node.deepCopy()
val controls = result.getAsJsonArray("controls")
if (controls != null) {
val resolvedControls = JsonArray()
for (element in controls) {
val controlObj = element.asJsonObject
for ((key, value) in controlObj.entrySet()) {
val resolvedWidget = resolveWidgetEntry(key, value.asJsonObject, localTemplates)
val wrapper = JsonObject()
val resolvedName = key.split("@")[0]
wrapper.add(resolvedName, resolvedWidget)
resolvedControls.add(wrapper)
}
}
result.add("controls", resolvedControls)
}
result.getAsJsonObject("item_template")?.let { itemTemplate ->
result.add("item_template", resolveNode(itemTemplate, localTemplates))
}
return result
}
private fun resolveWidgetEntry(key: String, overrides: JsonObject, localTemplates: Map<String, TemplateDefinition>): JsonObject {
val atIndex = key.indexOf('@')
if (atIndex == -1) {
return resolveNode(overrides, localTemplates)
}
val templateRef = key.substring(atIndex + 1)
val template = findTemplate(templateRef, localTemplates)
?: return resolveNode(overrides, localTemplates)
val resolved = resolveTemplateChain(template, localTemplates)
val substituted = substituteInputs(resolved, template.inputs, overrides)
val merged = mergeJson(substituted, overrides)
merged.remove("inputs")
merged.remove("extends")
return resolveNode(merged, localTemplates)
}
private fun resolveTemplateChain(template: TemplateDefinition, localTemplates: Map<String, TemplateDefinition>): JsonObject {
val extendsRef = template.extends
if (extendsRef != null) {
val parent = findTemplate(extendsRef, localTemplates)
if (parent != null) {
val parentResolved = resolveTemplateChain(parent, localTemplates)
return mergeJson(parentResolved, template.body)
}
}
return template.body.deepCopy()
}
private fun findTemplate(ref: String, localTemplates: Map<String, TemplateDefinition>): TemplateDefinition? {
localTemplates[ref]?.let { return it }
return TemplateLibraryRegistry.getTemplate(ref)
}
private fun substituteInputs(json: JsonObject, inputs: Map<String, TemplateInput>, values: JsonObject): JsonObject {
val jsonStr = json.toString()
var result = jsonStr
for ((inputName, inputDef) in inputs) {
val value = values.get(inputName)?.asString ?: inputDef.default ?: ""
result = result.replace("\"\$$inputName\"", "\"$value\"")
result = result.replace("\$$inputName", value)
}
return JsonParser.parseString(result).asJsonObject
}
private fun mergeJson(base: JsonObject, overlay: JsonObject): JsonObject {
val result = base.deepCopy()
for ((key, value) in overlay.entrySet()) {
if (key == "inputs") continue
if (value.isJsonObject && result.has(key) && result.get(key).isJsonObject) {
result.add(key, mergeJson(result.getAsJsonObject(key), value.asJsonObject))
} else {
result.add(key, value.deepCopy())
}
}
return result
}
}
@@ -1,86 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.molangui.widget
/**
* A parsed widget definition from JSON. Represents a node in the widget tree.
* Properties are stored as WidgetProperty (static or Molang expression).
* Children are stored as named WidgetDefinitions in the controls list.
*/
data class WidgetDefinition(
val id: String,
val type: WidgetType,
val properties: Map<String, WidgetProperty> = emptyMap(),
val children: List<WidgetDefinition> = emptyList(),
val events: Map<String, String> = emptyMap(),
val clientEvents: Map<String, String> = emptyMap(),
val hoverOverrides: Map<String, WidgetProperty> = emptyMap(),
val transitions: Map<String, TransitionConfig> = emptyMap(),
val enterAnimation: AnimationConfig? = null,
val exitAnimation: AnimationConfig? = null,
val isStatic: Boolean = false
) {
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 TransitionConfig(
val duration: Float,
val easing: String = "ease_out"
)
data class AnimationConfig(
val type: String,
val duration: Float = 0.3f,
val easing: String = "ease_out"
)
enum class WidgetType(val id: String) {
PANEL("panel"),
LABEL("label"),
IMAGE("image"),
FILL("fill"),
BUTTON("button"),
TOGGLE("toggle"),
SLIDER("slider"),
TEXT_INPUT("text_input"),
LIST("list"),
PROGRESS_BAR("progress_bar"),
SCROLL_PANEL("scroll_panel"),
POKEMON_RENDER("pokemon_render"),
CAMERA_FEED("camera_feed"),
COMPOSITOR_OUTPUT("compositor_output"),
MOVE_DISPLAY("move_display"),
NUMBER_INPUT("number_input"),
ITEM_SLOT("item_slot"),
SLOT_GRID("slot_grid");
companion object {
private val BY_ID = entries.associateBy { it.id }
fun fromId(id: String): WidgetType? = BY_ID[id]
}
}
enum class Anchor(val id: String, val xFactor: Float, val yFactor: Float) {
TOP_LEFT("top_left", 0f, 0f),
TOP_CENTER("top_center", 0.5f, 0f),
TOP_RIGHT("top_right", 1f, 0f),
CENTER_LEFT("center_left", 0f, 0.5f),
CENTER("center", 0.5f, 0.5f),
CENTER_RIGHT("center_right", 1f, 0.5f),
BOTTOM_LEFT("bottom_left", 0f, 1f),
BOTTOM_CENTER("bottom_center", 0.5f, 1f),
BOTTOM_RIGHT("bottom_right", 1f, 1f);
companion object {
private val BY_ID = entries.associateBy { it.id }
fun fromId(id: String): Anchor = BY_ID[id] ?: TOP_LEFT
}
}
@@ -1,44 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.molangui.widget
import com.cobblemon.mod.common.api.molangui.animation.TransitionState
import com.cobblemon.mod.common.api.molangui.layout.SizeExpression
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)
val children: MutableList<WidgetInstance> = mutableListOf()
val transitions: MutableMap<String, TransitionState> = mutableMapOf()
var visible: Boolean = true
var hovered: Boolean = false
var pressed: Boolean = false
var focused: Boolean = false
var enterProgress: Float = -1f
var exitProgress: Float = -1f
var scrollOffset: Float = 0f
fun containsPoint(x: Float, y: Float): Boolean {
return x >= absoluteX && x <= absoluteX + computedWidth &&
y >= absoluteY && y <= absoluteY + computedHeight
}
}

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