mirror of
https://gitlab.com/cable-mc/cobblemon.git
synced 2026-10-21 14:46:25 -04:00
Compare commits
24
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9cdf91c97f | ||
|
|
3f5b659616 | ||
|
|
ee20f012e6 | ||
|
|
12dcc98cf9 | ||
|
|
8c92defc90 | ||
|
|
95509df022 | ||
|
|
bb20890b26 | ||
|
|
50d60674bb | ||
|
|
ff994f69f2 | ||
|
|
73448d4737 | ||
|
|
42099d3ae5 | ||
|
|
bc23ab8eb4 | ||
|
|
6c860d8e93 | ||
|
|
de6b713adf | ||
|
|
6f5c2575f3 | ||
|
|
b23afd17c2 | ||
|
|
74541c1bcb | ||
|
|
56c3ff55ca | ||
|
|
c5b82d1ad4 | ||
|
|
1237b51059 | ||
|
|
9529fe66ac | ||
|
|
7058ac21cb | ||
|
|
b5d0cddc82 | ||
|
|
2f918220e9 |
+12
-47
@@ -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:
|
||||
@@ -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]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
-146
@@ -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;
|
||||
}
|
||||
}
|
||||
-86
@@ -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());
|
||||
}
|
||||
}
|
||||
-15
@@ -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);
|
||||
}
|
||||
-26
@@ -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();
|
||||
}
|
||||
}
|
||||
-23
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
-135
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-7
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
-21
@@ -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 } }
|
||||
|
||||
+1
-7
@@ -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()
|
||||
}
|
||||
-45
@@ -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()
|
||||
}
|
||||
}
|
||||
-58
@@ -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
|
||||
}
|
||||
-36
@@ -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?
|
||||
)
|
||||
-129
@@ -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)
|
||||
}
|
||||
}
|
||||
-95
@@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
-184
@@ -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) }
|
||||
}
|
||||
}
|
||||
-38
@@ -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]
|
||||
}
|
||||
}
|
||||
-79
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
-222
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
-117
@@ -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"
|
||||
}
|
||||
-243
@@ -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)
|
||||
}
|
||||
}
|
||||
-22
@@ -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
|
||||
}
|
||||
-27
@@ -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
|
||||
}
|
||||
-33
@@ -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>()
|
||||
}
|
||||
|
||||
+3
-6
@@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
-34
@@ -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 }
|
||||
)
|
||||
}
|
||||
+3
-5
@@ -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
|
||||
+21
-29
@@ -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()
|
||||
}
|
||||
}
|
||||
+19
-95
@@ -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()
|
||||
)
|
||||
-141
@@ -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
|
||||
}
|
||||
}
|
||||
-76
@@ -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
|
||||
}
|
||||
}
|
||||
-336
@@ -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()
|
||||
}
|
||||
}
|
||||
-113
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
-81
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-92
@@ -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
|
||||
)
|
||||
-107
@@ -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
|
||||
}
|
||||
}
|
||||
-86
@@ -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
Reference in New Issue
Block a user