mirror of
https://gitlab.com/cable-mc/cobblemon.git
synced 2026-10-21 06:36:25 -04:00
Compare commits
120
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c38630bd6f | ||
|
|
4dba646de9 | ||
|
|
201da371a5 | ||
|
|
a77a6d21eb | ||
|
|
8dc3b7be42 | ||
|
|
58286d8bb5 | ||
|
|
5e13da3e65 | ||
|
|
03bc8a1b40 | ||
|
|
98eb43d13d | ||
|
|
ca839432aa | ||
|
|
23f98f3a7d | ||
|
|
5aeae2b350 | ||
|
|
b60fc26a97 | ||
|
|
c1a95d0f3f | ||
|
|
3fd04bcdb4 | ||
|
|
a945bad5b5 | ||
|
|
edde3b16bf | ||
|
|
ffaee877a7 | ||
|
|
186f7dbbce | ||
|
|
4ae9b25d35 | ||
|
|
b35fe23831 | ||
|
|
16165a2e68 | ||
|
|
cdec0e5640 | ||
|
|
87db8aadfe | ||
|
|
36921f439c | ||
|
|
f2d2ceb422 | ||
|
|
547ceae9ed | ||
|
|
a921d55812 | ||
|
|
79688ebdad | ||
|
|
16d8c8bb19 | ||
|
|
e5738fecb0 | ||
|
|
a46ac372a8 | ||
|
|
e6d2430405 | ||
|
|
6c425a43c6 | ||
|
|
8b6c8d27b7 | ||
|
|
a505bad554 | ||
|
|
e27e5a1d92 | ||
|
|
5e0357101d | ||
|
|
5420f7419f | ||
|
|
43ed2d3c7d | ||
|
|
93500f59ed | ||
|
|
08912fd53f | ||
|
|
92c8fc0afc | ||
|
|
fae6bdb782 | ||
|
|
bba0100542 | ||
|
|
f9b66de742 | ||
|
|
3d0f1c826a | ||
|
|
2a59add285 | ||
|
|
0b83128aa0 | ||
|
|
e2b04ff770 | ||
|
|
4dabe50d43 | ||
|
|
3dd0795b88 | ||
|
|
70f1a8207f | ||
|
|
3ac18cce6e | ||
|
|
aa0a0672d0 | ||
|
|
ec2958400b | ||
|
|
fcb6bea78b | ||
|
|
5a76a39b77 | ||
|
|
b5a4acfb7a | ||
|
|
1248230e67 | ||
|
|
096db6cdcd | ||
|
|
72e416b9c7 | ||
|
|
9e8e80965b | ||
|
|
2325b436b1 | ||
|
|
57421a2884 | ||
|
|
d6828baeec | ||
|
|
4fdd3eba8e | ||
|
|
e6efe800c0 | ||
|
|
34f1fd2809 | ||
|
|
95aa99e18c | ||
|
|
270484e745 | ||
|
|
09a5fe4c5d | ||
|
|
c42bd7a293 | ||
|
|
33a303a2f5 | ||
|
|
9f7dd84ace | ||
|
|
68e93a13d8 | ||
|
|
611bfd9792 | ||
|
|
0c428526e7 | ||
|
|
b28355b8f2 | ||
|
|
8e163d0e14 | ||
|
|
1660a53867 | ||
|
|
c50ae2d478 | ||
|
|
497de9a8f1 | ||
|
|
f57300dc18 | ||
|
|
0e3c197d9b | ||
|
|
4a12a39287 | ||
|
|
719addf633 | ||
|
|
31e386871f | ||
|
|
bfd7e596d0 | ||
|
|
643545d54c | ||
|
|
fcb017f980 | ||
|
|
dd52da7c42 | ||
|
|
ff45def0e8 | ||
|
|
f078584e56 | ||
|
|
a11382b5a1 | ||
|
|
d1b8094539 | ||
|
|
b8d79af911 | ||
|
|
155b4b2182 | ||
|
|
690ed845b2 | ||
|
|
a7198fd324 | ||
|
|
bc69ce91f3 | ||
|
|
1de1d8cdc3 | ||
|
|
1f8171dd94 | ||
|
|
087bf75500 | ||
|
|
bac435ba7b | ||
|
|
74a6106eb6 | ||
|
|
5ec993ad3c | ||
|
|
73a03d90da | ||
|
|
246d00aa3f | ||
|
|
b3ed47fbca | ||
|
|
3a6ad70b76 | ||
|
|
8120270bf8 | ||
|
|
70fe1682b0 | ||
|
|
9058bde326 | ||
|
|
bf425efd40 | ||
|
|
cebff87656 | ||
|
|
d24fb13877 | ||
|
|
1d82df464a | ||
|
|
2a1d1e4474 | ||
|
|
2da2222540 |
+50
-13
@@ -1,23 +1,60 @@
|
||||
# Changelog
|
||||
## [1.7.3 (January 31st, 2026)](#1-7-3)
|
||||
## [1.7.4 (MONTH Xth, 2026)](#1-7-4)
|
||||
|
||||
### 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
|
||||
- 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.
|
||||
- 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
|
||||
|
||||
### Developer
|
||||
- Poké Ball GUI textures now respect the ball's namespace, enabling proper texture loading for custom balls from other mods.
|
||||
- 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
|
||||
- Added a `chance` requirement type for Pokémon interactions.
|
||||
- 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
|
||||
|
||||
### Localization
|
||||
- Updated translations for:
|
||||
- French
|
||||
- Canadian French
|
||||
- Korean
|
||||
- Portuguese
|
||||
- Simplified Chinese
|
||||
- Spanish
|
||||
@@ -0,0 +1,510 @@
|
||||
{
|
||||
"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]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
# 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
|
||||
@@ -55,7 +55,7 @@ dependencies {
|
||||
// modCompileOnly(libs.flywheelFabric)
|
||||
|
||||
// Showdown
|
||||
modCompileOnly(libs.graal)
|
||||
modCompileOnly(libs.graal.core)
|
||||
|
||||
// Data Storage
|
||||
modCompileOnly(libs.bundles.mongo)
|
||||
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of 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
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of 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
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of 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();
|
||||
}
|
||||
@@ -15,6 +15,8 @@ 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;
|
||||
@@ -59,6 +61,7 @@ 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;
|
||||
|
||||
@@ -169,6 +172,32 @@ 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(
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of 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);
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of 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);
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of 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
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.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,22 +8,32 @@
|
||||
|
||||
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)
|
||||
@@ -32,6 +42,7 @@ 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;
|
||||
@@ -43,6 +54,11 @@ 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;
|
||||
@@ -66,6 +82,28 @@ 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,
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+102
-15
@@ -9,43 +9,122 @@
|
||||
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 class GameRendererMixin {
|
||||
@Unique
|
||||
long lastTime = -1;
|
||||
public abstract class GameRendererMixin {
|
||||
|
||||
@Inject(
|
||||
method = "render",
|
||||
at = @At(value = "TAIL")
|
||||
)
|
||||
public void render(DeltaTracker counter, boolean tick, CallbackInfo ci) {
|
||||
@Shadow @Final @Mutable
|
||||
private Camera mainCamera;
|
||||
|
||||
@Unique
|
||||
private Camera cobblemon$playerCamera;
|
||||
|
||||
@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) {
|
||||
Minecraft client = Minecraft.getInstance();
|
||||
long newTime = System.currentTimeMillis();
|
||||
// Don't play scheduled animations when the game is paused
|
||||
if (client.isPaused()) {
|
||||
lastTime = newTime;
|
||||
if (client.level == null) {
|
||||
cobblemon$restoreCamera();
|
||||
return;
|
||||
}
|
||||
|
||||
if (lastTime != -1) {
|
||||
ClientTaskTracker.INSTANCE.update((newTime - lastTime) / 1000F);
|
||||
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();
|
||||
}
|
||||
lastTime = newTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
|
||||
if (client.isPaused()) {
|
||||
cobblemon$lastTime = newTime;
|
||||
return;
|
||||
}
|
||||
|
||||
if (cobblemon$lastTime != -1) {
|
||||
float delta = (newTime - cobblemon$lastTime) / 1000F;
|
||||
ClientTaskTracker.INSTANCE.update(delta);
|
||||
TweenManager.Companion.getClient().update(delta);
|
||||
}
|
||||
cobblemon$lastTime = newTime;
|
||||
}
|
||||
|
||||
@ModifyArg(
|
||||
@@ -60,4 +139,12 @@ public 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,6 +9,7 @@
|
||||
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;
|
||||
@@ -28,6 +29,12 @@ 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,6 +35,7 @@ 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;
|
||||
@@ -85,11 +86,13 @@ 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -310,7 +313,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(), -90.0F, 90.0F));
|
||||
playerRotater.cobblemon$setRideXRot(Mth.clamp(playerRotater.cobblemon$getRideXRot(), -89.0F, 89.0F));
|
||||
playerRotater.cobblemon$setRideYRot(Mth.clamp(playerRotater.cobblemon$getRideYRot(), -105.0F, 105.0F));
|
||||
if (player.getVehicle() != null) {
|
||||
player.getVehicle().onPassengerTurned(player);
|
||||
|
||||
@@ -8,11 +8,14 @@
|
||||
|
||||
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;
|
||||
@@ -22,6 +25,7 @@ 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)
|
||||
@@ -31,6 +35,27 @@ 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);
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of 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,9 +14,14 @@ 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;
|
||||
@@ -89,4 +94,61 @@ 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
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.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,18 +65,30 @@ 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.*
|
||||
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.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.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.*
|
||||
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.net.messages.client.settings.ServerSettingsPacket
|
||||
import com.cobblemon.mod.common.permission.LaxPermissionValidator
|
||||
import com.cobblemon.mod.common.platform.events.PlatformEvents
|
||||
@@ -90,11 +102,23 @@ 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.*
|
||||
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.stat.CobblemonStatProvider
|
||||
import com.cobblemon.mod.common.starter.CobblemonStarterHandler
|
||||
import com.cobblemon.mod.common.util.*
|
||||
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.world.feature.CobblemonPlacedFeatures
|
||||
import com.cobblemon.mod.common.world.feature.ore.CobblemonOrePlacedFeatures
|
||||
import com.cobblemon.mod.common.world.gamerules.CobblemonGameRules
|
||||
@@ -102,6 +126,17 @@ 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
|
||||
@@ -114,17 +149,6 @@ 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
|
||||
@@ -235,6 +259,7 @@ 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)
|
||||
@@ -277,6 +302,7 @@ object Cobblemon {
|
||||
HeldItemProvider.register(CobblemonHeldItemManager, Priority.LOWEST)
|
||||
}
|
||||
|
||||
|
||||
fun sendServerSettingsPacketToPlayer(player: ServerPlayer) {
|
||||
ServerSettingsPacket(
|
||||
this.config.preventCompletePartyDeposit,
|
||||
@@ -322,6 +348,10 @@ object Cobblemon {
|
||||
CustomPokemonProperty.register(FreezeFrameProperty)
|
||||
CustomPokemonProperty.register(NoAIProperty)
|
||||
|
||||
// Tween & camera system initialization
|
||||
EasingRegistry.registerBuiltinEasings()
|
||||
CameraTrackDeserializers.registerBuiltinTracks()
|
||||
|
||||
CobblemonEvents.POKEMON_PROPERTY_INITIALISED.emit(Unit)
|
||||
|
||||
CallbackHandler.setup()
|
||||
@@ -334,6 +364,7 @@ object Cobblemon {
|
||||
PlatformEvents.SERVER_TICK_POST.subscribe {
|
||||
ServerTaskTracker.update(1/20F)
|
||||
ServerRealTimeTaskTracker.update()
|
||||
TweenManager.server.update(1/20F)
|
||||
}
|
||||
PlatformEvents.SERVER_TICK_PRE.subscribe {
|
||||
ServerRealTimeTaskTracker.update()
|
||||
@@ -451,7 +482,7 @@ object Cobblemon {
|
||||
server()?.getLevel(dimension)
|
||||
} else {
|
||||
val mc = Minecraft.getInstance()
|
||||
return mc.singleplayerServer?.getLevel(dimension) ?: mc.level
|
||||
mc.singleplayerServer?.getLevel(dimension) ?: mc.level
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -106,4 +106,10 @@ 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)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -581,6 +581,9 @@ 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,7 +15,11 @@ 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)
|
||||
@@ -76,5 +80,8 @@ object CobblemonCommands {
|
||||
// with a certain system environment variable set
|
||||
CobblemonInfoCommand.register(dispatcher)
|
||||
CalculateSeatPositionCommand.register(dispatcher)
|
||||
CameraDebugCommand.register(dispatcher)
|
||||
StadiumBattleCommand.register(dispatcher)
|
||||
MolangUiCommand.register(dispatcher)
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,7 @@ 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
|
||||
@@ -75,6 +76,9 @@ 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
|
||||
@@ -460,6 +464,8 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
|
||||
val PASTURE = blockItem("pasture", CobblemonBlocks.PASTURE)
|
||||
@JvmField
|
||||
val DISPLAY_CASE = blockItem("display_case", CobblemonBlocks.DISPLAY_CASE)
|
||||
@JvmField
|
||||
val STADIUM_BLOCK = blockItem("stadium_block", CobblemonBlocks.STADIUM_BLOCK)
|
||||
// @JvmField
|
||||
// val INCENSE_SWEET = blockItem("incense_sweet", CobblemonBlocks.INCENSE_SWEET) // TODO after 1.7
|
||||
|
||||
@@ -819,7 +825,7 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
|
||||
@JvmField val DIRE_HIT = create("dire_hit", DireHitItem(CobblemonBlocks.DIRE_HIT))
|
||||
@JvmField val GUARD_SPEC = create("guard_spec", GuardSpecItem(CobblemonBlocks.GUARD_SPEC))
|
||||
|
||||
@JvmField val FULL_HEAL = create("full_heal", StatusCureItem("item.cobblemon.full_heal", block = CobblemonBlocks.FULL_HEAL))
|
||||
@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))
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
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
|
||||
@@ -20,6 +21,7 @@ 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
|
||||
|
||||
@@ -18,6 +18,14 @@ import com.cobblemon.mod.common.client.net.battle.*
|
||||
import com.cobblemon.mod.common.client.net.callback.move.OpenMoveCallbackHandler
|
||||
import com.cobblemon.mod.common.client.net.callback.party.OpenPartyCallbackHandler
|
||||
import com.cobblemon.mod.common.client.net.callback.partymove.OpenPartyMoveCallbackHandler
|
||||
import com.cobblemon.mod.common.client.net.camera.CameraDebugHandler
|
||||
import com.cobblemon.mod.common.client.net.camera.CameraEndHandler
|
||||
import com.cobblemon.mod.common.client.net.camera.CameraFocusHandler
|
||||
import com.cobblemon.mod.common.client.net.camera.CameraStartHandler
|
||||
import com.cobblemon.mod.common.client.net.camera.CameraStateHandler
|
||||
import com.cobblemon.mod.common.client.net.camera.CameraTransformHandler
|
||||
import com.cobblemon.mod.common.client.net.camera.CompositorHandler
|
||||
import com.cobblemon.mod.common.client.net.camera.SplitScreenHandler
|
||||
import com.cobblemon.mod.common.client.net.cooking.ToggleCookingPotLidHandler
|
||||
import com.cobblemon.mod.common.client.net.data.CobblemonMechanicsSyncHandler
|
||||
import com.cobblemon.mod.common.client.net.data.DataRegistrySyncPacketHandler
|
||||
@@ -33,6 +41,9 @@ import com.cobblemon.mod.common.client.net.effect.SpawnSnowstormParticleHandler
|
||||
import com.cobblemon.mod.common.client.net.gui.ExpGainedDataPacketHandler
|
||||
import com.cobblemon.mod.common.client.net.gui.InteractPokemonUIPacketHandler
|
||||
import com.cobblemon.mod.common.client.net.gui.PokedexUIPacketHandler
|
||||
import com.cobblemon.mod.common.client.net.molangui.MolangUiCloseHandler
|
||||
import com.cobblemon.mod.common.client.net.molangui.MolangUiOpenHandler
|
||||
import com.cobblemon.mod.common.client.net.molangui.MolangUiUpdateHandler
|
||||
import com.cobblemon.mod.common.client.net.gui.SummaryUIPacketHandler
|
||||
import com.cobblemon.mod.common.client.net.npc.CloseNPCEditorHandler
|
||||
import com.cobblemon.mod.common.client.net.npc.OpenNPCEditorHandler
|
||||
@@ -49,6 +60,7 @@ import com.cobblemon.mod.common.client.net.riding.ClientboundUpdateDriverInputHa
|
||||
import com.cobblemon.mod.common.client.net.settings.OpenCobblemonConfigEditorHandler
|
||||
import com.cobblemon.mod.common.client.net.settings.ServerSettingsPacketHandler
|
||||
import com.cobblemon.mod.common.client.net.sound.UnvalidatedPlaySoundS2CPacketHandler
|
||||
import com.cobblemon.mod.common.client.net.stadium.OpenStadiumConfigHandler
|
||||
import com.cobblemon.mod.common.client.net.spawn.SpawnExtraDataEntityHandler
|
||||
import com.cobblemon.mod.common.client.net.starter.StarterUIPacketHandler
|
||||
import com.cobblemon.mod.common.client.net.storage.RemoveClientPokemonHandler
|
||||
@@ -74,6 +86,14 @@ import com.cobblemon.mod.common.net.messages.client.PlayerInteractOptionsPacket
|
||||
import com.cobblemon.mod.common.net.messages.client.SetClientPlayerDataPacket
|
||||
import com.cobblemon.mod.common.net.messages.client.animation.PlayPosableAnimationPacket
|
||||
import com.cobblemon.mod.common.net.messages.client.battle.*
|
||||
import com.cobblemon.mod.common.net.messages.client.camera.CameraDebugPacket
|
||||
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 com.cobblemon.mod.common.net.messages.client.camera.CompositorPacket
|
||||
import com.cobblemon.mod.common.net.messages.client.camera.SplitScreenPacket
|
||||
import com.cobblemon.mod.common.net.messages.client.callback.OpenMoveCallbackPacket
|
||||
import com.cobblemon.mod.common.net.messages.client.callback.OpenPartyCallbackPacket
|
||||
import com.cobblemon.mod.common.net.messages.client.callback.OpenPartyMoveCallbackPacket
|
||||
@@ -81,6 +101,9 @@ import com.cobblemon.mod.common.net.messages.client.cooking.SeasoningRegistrySyn
|
||||
import com.cobblemon.mod.common.net.messages.client.cooking.ToggleCookingPotLidPacket
|
||||
import com.cobblemon.mod.common.net.messages.client.data.*
|
||||
import com.cobblemon.mod.common.net.messages.client.debug.RequestOpenRidingStatsDebugGUIPacket
|
||||
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.net.messages.client.dialogue.DialogueClosedPacket
|
||||
import com.cobblemon.mod.common.net.messages.client.dialogue.DialogueOpenedPacket
|
||||
import com.cobblemon.mod.common.net.messages.client.effect.PokeSnackBlockParticlesPacket
|
||||
@@ -103,6 +126,7 @@ import com.cobblemon.mod.common.net.messages.client.pokemon.update.evolution.Add
|
||||
import com.cobblemon.mod.common.net.messages.client.pokemon.update.evolution.ClearEvolutionsPacket
|
||||
import com.cobblemon.mod.common.net.messages.client.pokemon.update.evolution.RemoveEvolutionPacket
|
||||
import com.cobblemon.mod.common.net.messages.client.settings.OpenCobblemonConfigScreenPacket
|
||||
import com.cobblemon.mod.common.net.messages.client.stadium.OpenStadiumConfigPacket
|
||||
import com.cobblemon.mod.common.net.messages.client.settings.ServerSettingsPacket
|
||||
import com.cobblemon.mod.common.net.messages.client.sound.UnvalidatedPlaySoundS2CPacket
|
||||
import com.cobblemon.mod.common.net.messages.client.spawn.SpawnGenericBedrockPacket
|
||||
@@ -140,13 +164,7 @@ import com.cobblemon.mod.common.net.messages.client.ui.ExpGainedDataPacket
|
||||
import com.cobblemon.mod.common.net.messages.client.ui.InteractPokemonUIPacket
|
||||
import com.cobblemon.mod.common.net.messages.client.ui.PokedexUIPacket
|
||||
import com.cobblemon.mod.common.net.messages.client.ui.SummaryUIPacket
|
||||
import com.cobblemon.mod.common.net.messages.server.BattleChallengePacket
|
||||
import com.cobblemon.mod.common.net.messages.server.BattleChallengeResponsePacket
|
||||
import com.cobblemon.mod.common.net.messages.server.BenchMovePacket
|
||||
import com.cobblemon.mod.common.net.messages.server.RequestMoveSwapPacket
|
||||
import com.cobblemon.mod.common.net.messages.server.RequestPlayerInteractionsPacket
|
||||
import com.cobblemon.mod.common.net.messages.server.SelectStarterPacket
|
||||
import com.cobblemon.mod.common.net.messages.server.SendOutPokemonPacket
|
||||
import com.cobblemon.mod.common.net.messages.server.*
|
||||
import com.cobblemon.mod.common.net.messages.server.battle.BattleSelectActionsPacket
|
||||
import com.cobblemon.mod.common.net.messages.server.battle.BattleTeamLeavePacket
|
||||
import com.cobblemon.mod.common.net.messages.server.battle.BattleTeamRequestPacket
|
||||
@@ -168,7 +186,13 @@ import com.cobblemon.mod.common.net.messages.server.debug.ServerboundUpdateRidin
|
||||
import com.cobblemon.mod.common.net.messages.server.debug.ServerboundUpdateRidingStatsPacket
|
||||
import com.cobblemon.mod.common.net.messages.server.dialogue.EscapeDialoguePacket
|
||||
import com.cobblemon.mod.common.net.messages.server.dialogue.InputToDialoguePacket
|
||||
import com.cobblemon.mod.common.net.messages.server.molangui.MolangUiActionPacket
|
||||
import com.cobblemon.mod.common.net.messages.server.molangui.MolangUiClosedPacket
|
||||
import com.cobblemon.mod.common.net.messages.server.molangui.MolangUiVersionPacket
|
||||
import com.cobblemon.mod.common.net.messages.server.npc.SaveNPCPacket
|
||||
import com.cobblemon.mod.common.net.messages.server.stadium.RequestStadiumPlacementWandPacket
|
||||
import com.cobblemon.mod.common.net.messages.server.stadium.StadiumPlacementSetPacket
|
||||
import com.cobblemon.mod.common.net.messages.server.stadium.UpdateStadiumBlockPacket
|
||||
import com.cobblemon.mod.common.net.messages.server.orientation.ServerboundUpdateOrientationPacket
|
||||
import com.cobblemon.mod.common.net.messages.server.pasture.PasturePokemonPacket
|
||||
import com.cobblemon.mod.common.net.messages.server.pasture.SetPastureConflictPacket
|
||||
@@ -189,6 +213,7 @@ import com.cobblemon.mod.common.net.messages.server.riding.ServerboundUpdateRide
|
||||
import com.cobblemon.mod.common.net.messages.server.starter.RequestStarterScreenPacket
|
||||
import com.cobblemon.mod.common.net.messages.server.storage.SwapPCPartyPokemonPacket
|
||||
import com.cobblemon.mod.common.net.messages.server.storage.party.MovePartyPokemonPacket
|
||||
import com.cobblemon.mod.common.net.messages.server.storage.party.PartyTutorialPacket
|
||||
import com.cobblemon.mod.common.net.messages.server.storage.party.ReleasePartyPokemonPacket
|
||||
import com.cobblemon.mod.common.net.messages.server.storage.party.SwapPartyPokemonPacket
|
||||
import com.cobblemon.mod.common.net.messages.server.storage.pc.*
|
||||
@@ -222,9 +247,16 @@ import com.cobblemon.mod.common.net.serverhandling.debug.ServerboundUpdateRiding
|
||||
import com.cobblemon.mod.common.net.serverhandling.debug.ServerboundUpdateRidingStatsHandler
|
||||
import com.cobblemon.mod.common.net.serverhandling.dialogue.EscapeDialogueHandler
|
||||
import com.cobblemon.mod.common.net.serverhandling.dialogue.InputToDialogueHandler
|
||||
import com.cobblemon.mod.common.net.serverhandling.molangui.MolangUiActionHandler
|
||||
import com.cobblemon.mod.common.net.serverhandling.molangui.MolangUiClosedHandler
|
||||
import com.cobblemon.mod.common.net.serverhandling.molangui.MolangUiVersionHandler
|
||||
import com.cobblemon.mod.common.net.serverhandling.evolution.AcceptEvolutionHandler
|
||||
import com.cobblemon.mod.common.net.serverhandling.npc.SaveNPCHandler
|
||||
import com.cobblemon.mod.common.net.serverhandling.stadium.RequestStadiumPlacementWandHandler
|
||||
import com.cobblemon.mod.common.net.serverhandling.stadium.StadiumPlacementSetHandler
|
||||
import com.cobblemon.mod.common.net.serverhandling.stadium.UpdateStadiumBlockHandler
|
||||
import com.cobblemon.mod.common.net.serverhandling.orientation.ServerboundUpdateOrientationHandler
|
||||
import com.cobblemon.mod.common.net.serverhandling.storage.party.PartyTutorialPacketHandler
|
||||
import com.cobblemon.mod.common.net.serverhandling.pasture.PasturePokemonHandler
|
||||
import com.cobblemon.mod.common.net.serverhandling.pasture.SetPastureConflictHandler
|
||||
import com.cobblemon.mod.common.net.serverhandling.pasture.UnpastureAllPokemonHandler
|
||||
@@ -383,9 +415,85 @@ object CobblemonNetwork {
|
||||
list.add(PacketRegisterInfo(BattlePersistentStatusPacket.ID, BattlePersistentStatusPacket::decode, BattlePersistentStatusHandler))
|
||||
list.add(PacketRegisterInfo(BattleMadeInvalidChoicePacket.ID, BattleMadeInvalidChoicePacket::decode, BattleMadeInvalidChoiceHandler))
|
||||
list.add(PacketRegisterInfo(BattleMusicPacket.ID, BattleMusicPacket::decode, BattleMusicHandler))
|
||||
list.add(PacketRegisterInfo(BattleChallengeExpiredPacket.ID, BattleChallengeExpiredPacket::decode, BattleChallengeExpiredHandler))
|
||||
list.add(PacketRegisterInfo(BattleReplacePokemonPacket.ID, BattleReplacePokemonPacket::decode, BattleReplacePokemonHandler))
|
||||
list.add(PacketRegisterInfo(BattleTransformPokemonPacket.ID, BattleTransformPokemonPacket::decode, BattleTransformPokemonHandler))
|
||||
list.add(
|
||||
PacketRegisterInfo(
|
||||
BattleChallengeExpiredPacket.ID,
|
||||
BattleChallengeExpiredPacket::decode,
|
||||
BattleChallengeExpiredHandler
|
||||
)
|
||||
)
|
||||
list.add(
|
||||
PacketRegisterInfo(
|
||||
BattleReplacePokemonPacket.ID,
|
||||
BattleReplacePokemonPacket::decode,
|
||||
BattleReplacePokemonHandler
|
||||
)
|
||||
)
|
||||
list.add(
|
||||
PacketRegisterInfo(
|
||||
BattleTransformPokemonPacket.ID,
|
||||
BattleTransformPokemonPacket::decode,
|
||||
BattleTransformPokemonHandler
|
||||
)
|
||||
)
|
||||
|
||||
// Camera packets
|
||||
list.add(
|
||||
PacketRegisterInfo(
|
||||
CameraStartPacket.ID,
|
||||
CameraStartPacket::decode,
|
||||
CameraStartHandler
|
||||
)
|
||||
)
|
||||
list.add(
|
||||
PacketRegisterInfo(
|
||||
CameraEndPacket.ID,
|
||||
CameraEndPacket::decode,
|
||||
CameraEndHandler
|
||||
)
|
||||
)
|
||||
list.add(
|
||||
PacketRegisterInfo(
|
||||
CameraDebugPacket.ID,
|
||||
CameraDebugPacket::decode,
|
||||
CameraDebugHandler
|
||||
)
|
||||
)
|
||||
list.add(
|
||||
PacketRegisterInfo(
|
||||
CameraTransformPacket.ID,
|
||||
CameraTransformPacket::decode,
|
||||
CameraTransformHandler
|
||||
)
|
||||
)
|
||||
list.add(
|
||||
PacketRegisterInfo(
|
||||
CameraFocusPacket.ID,
|
||||
CameraFocusPacket::decode,
|
||||
CameraFocusHandler
|
||||
)
|
||||
)
|
||||
list.add(
|
||||
PacketRegisterInfo(
|
||||
CameraStatePacket.ID,
|
||||
CameraStatePacket::decode,
|
||||
CameraStateHandler
|
||||
)
|
||||
)
|
||||
list.add(
|
||||
PacketRegisterInfo(
|
||||
SplitScreenPacket.ID,
|
||||
SplitScreenPacket::decode,
|
||||
SplitScreenHandler
|
||||
)
|
||||
)
|
||||
list.add(
|
||||
PacketRegisterInfo(
|
||||
CompositorPacket.ID,
|
||||
CompositorPacket::decode,
|
||||
CompositorHandler
|
||||
)
|
||||
)
|
||||
|
||||
// MultiBattleTeam Packets
|
||||
list.add(PacketRegisterInfo(TeamRequestNotificationPacket.ID, TeamRequestNotificationPacket::decode, TeamRequestNotificationHandler))
|
||||
@@ -424,6 +532,11 @@ object CobblemonNetwork {
|
||||
list.add(PacketRegisterInfo(MarkRegistrySyncPacket.ID, MarkRegistrySyncPacket::decode, DataRegistrySyncPacketHandler()))
|
||||
list.add(PacketRegisterInfo(RideSettingsSyncPacket.ID, RideSettingsSyncPacket::decode, RideSettingsSyncHandler))
|
||||
|
||||
// Molang UI packets
|
||||
list.add(PacketRegisterInfo(MolangUiOpenPacket.ID, MolangUiOpenPacket::decode, MolangUiOpenHandler))
|
||||
list.add(PacketRegisterInfo(MolangUiClosePacket.ID, MolangUiClosePacket::decode, MolangUiCloseHandler))
|
||||
list.add(PacketRegisterInfo(MolangUiUpdatePacket.ID, MolangUiUpdatePacket::decode, MolangUiUpdateHandler))
|
||||
|
||||
// Effects
|
||||
list.add(PacketRegisterInfo(SpawnSnowstormParticlePacket.ID, SpawnSnowstormParticlePacket::decode, SpawnSnowstormParticleHandler))
|
||||
list.add(PacketRegisterInfo(SpawnSnowstormEntityParticlePacket.ID, SpawnSnowstormEntityParticlePacket::decode, SpawnSnowstormEntityParticleHandler))
|
||||
@@ -453,8 +566,23 @@ object CobblemonNetwork {
|
||||
list.add(PacketRegisterInfo(OpenPasturePacket.ID, OpenPasturePacket::decode, OpenPastureHandler))
|
||||
list.add(PacketRegisterInfo(ClosePasturePacket.ID, ClosePasturePacket::decode, ClosePastureHandler))
|
||||
list.add(PacketRegisterInfo(PokemonPasturedPacket.ID, PokemonPasturedPacket::decode, PokemonPasturedHandler))
|
||||
list.add(PacketRegisterInfo(PokemonUnpasturedPacket.ID, PokemonUnpasturedPacket::decode, PokemonUnpasturedHandler))
|
||||
list.add(PacketRegisterInfo(UpdatePastureConflictFlagPacket.ID, UpdatePastureConflictFlagPacket::decode, UpdatePastureConflictFlagHandler))
|
||||
list.add(
|
||||
PacketRegisterInfo(
|
||||
PokemonUnpasturedPacket.ID,
|
||||
PokemonUnpasturedPacket::decode,
|
||||
PokemonUnpasturedHandler
|
||||
)
|
||||
)
|
||||
list.add(
|
||||
PacketRegisterInfo(
|
||||
UpdatePastureConflictFlagPacket.ID,
|
||||
UpdatePastureConflictFlagPacket::decode,
|
||||
UpdatePastureConflictFlagHandler
|
||||
)
|
||||
)
|
||||
|
||||
// Stadium
|
||||
list.add(PacketRegisterInfo(OpenStadiumConfigPacket.ID, OpenStadiumConfigPacket::decode, OpenStadiumConfigHandler))
|
||||
|
||||
// Orientation
|
||||
list.add(PacketRegisterInfo(ClientboundUpdateOrientationPacket.ID, ClientboundUpdateOrientationPacket::decode, ClientboundUpdateOrientationHandler))
|
||||
@@ -529,7 +657,7 @@ object CobblemonNetwork {
|
||||
list.add(PacketRegisterInfo(ReleasePCPokemonPacket.ID, ReleasePCPokemonPacket::decode, ReleasePCPokemonHandler))
|
||||
list.add(PacketRegisterInfo(UnlinkPlayerFromPCPacket.ID, UnlinkPlayerFromPCPacket::decode, UnlinkPlayerFromPCHandler))
|
||||
|
||||
list.add(PacketRegisterInfo(RequestRenamePCBoxPacket.Companion.ID, RequestRenamePCBoxPacket.Companion::decode, RequestRenamePCBoxHandler))
|
||||
list.add(PacketRegisterInfo(RequestRenamePCBoxPacket.ID, RequestRenamePCBoxPacket::decode, RequestRenamePCBoxHandler))
|
||||
list.add(PacketRegisterInfo(PCBoxWallpapersPacket.ID, PCBoxWallpapersPacket::decode, PCBoxWallpapersHandler))
|
||||
list.add(PacketRegisterInfo(RequestChangePCBoxWallpaperPacket.ID, RequestChangePCBoxWallpaperPacket::decode, RequestChangePCBoxWallpaperHandler))
|
||||
list.add(PacketRegisterInfo(MarkPCBoxWallpapersSeenPacket.ID, MarkPCBoxWallpapersSeenPacket::decode, MarkPCBoxWallpapersSeenHandler))
|
||||
@@ -587,6 +715,9 @@ object CobblemonNetwork {
|
||||
list.add(PacketRegisterInfo(PartyPokemonMoveSelectedPacket.ID, PartyPokemonMoveSelectedPacket::decode, PartyPokemonMoveSelectedHandler))
|
||||
list.add(PacketRegisterInfo(PartyMoveSelectCancelledPacket.ID, PartyMoveSelectCancelledPacket::decode, PartyMoveSelectCancelledHandler))
|
||||
|
||||
//Party move tutorial toasts packets
|
||||
list.add(PacketRegisterInfo(PartyTutorialPacket.ID, PartyTutorialPacket::decode, PartyTutorialPacketHandler))
|
||||
|
||||
// Dialogue packets
|
||||
list.add(PacketRegisterInfo(EscapeDialoguePacket.ID, EscapeDialoguePacket::decode, EscapeDialogueHandler))
|
||||
list.add(PacketRegisterInfo(InputToDialoguePacket.ID, InputToDialoguePacket::decode, InputToDialogueHandler))
|
||||
@@ -608,8 +739,30 @@ object CobblemonNetwork {
|
||||
list.add(PacketRegisterInfo(ToggleCookingPotLidPacket.ID, ToggleCookingPotLidPacket::decode, ToggleCookingPotLidHandler))
|
||||
|
||||
// Behaviour Packets
|
||||
list.add(PacketRegisterInfo(SetEntityBehaviourPacket.ID, SetEntityBehaviourPacket::decode, SetEntityBehaviourHandler))
|
||||
list.add(PacketRegisterInfo(DamageOnCollisionPacket.ID, DamageOnCollisionPacket::decode, DamageOnCollisionPacketHandler))
|
||||
list.add(
|
||||
PacketRegisterInfo(
|
||||
SetEntityBehaviourPacket.ID,
|
||||
SetEntityBehaviourPacket::decode,
|
||||
SetEntityBehaviourHandler
|
||||
)
|
||||
)
|
||||
list.add(
|
||||
PacketRegisterInfo(
|
||||
DamageOnCollisionPacket.ID,
|
||||
DamageOnCollisionPacket::decode,
|
||||
DamageOnCollisionPacketHandler
|
||||
)
|
||||
)
|
||||
|
||||
// Molang UI packets
|
||||
list.add(PacketRegisterInfo(MolangUiActionPacket.ID, MolangUiActionPacket::decode, MolangUiActionHandler))
|
||||
list.add(PacketRegisterInfo(MolangUiClosedPacket.ID, MolangUiClosedPacket::decode, MolangUiClosedHandler))
|
||||
list.add(PacketRegisterInfo(MolangUiVersionPacket.ID, MolangUiVersionPacket::decode, MolangUiVersionHandler))
|
||||
|
||||
// Stadium
|
||||
list.add(PacketRegisterInfo(UpdateStadiumBlockPacket.ID, UpdateStadiumBlockPacket::decode, UpdateStadiumBlockHandler))
|
||||
list.add(PacketRegisterInfo(RequestStadiumPlacementWandPacket.ID, RequestStadiumPlacementWandPacket::decode, RequestStadiumPlacementWandHandler))
|
||||
list.add(PacketRegisterInfo(StadiumPlacementSetPacket.ID, StadiumPlacementSetPacket::decode, StadiumPlacementSetHandler))
|
||||
|
||||
// Debug
|
||||
list.add(PacketRegisterInfo(RequestOpenRidingStatsDebugGUIPacket.ID, RequestOpenRidingStatsDebugGUIPacket::decode, RequestOpenRidingStatsDebugGUIHandler))
|
||||
|
||||
@@ -17,6 +17,7 @@ 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
|
||||
@@ -184,6 +185,12 @@ 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,18 +22,21 @@ 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
|
||||
@@ -48,7 +51,9 @@ 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
|
||||
@@ -80,7 +85,9 @@ 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()
|
||||
|
||||
@@ -137,6 +144,9 @@ 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()
|
||||
|
||||
@@ -244,12 +254,23 @@ 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 opponentNonFaintedPokemons = opponent.pokemonList.filter { it.health > 0 }
|
||||
val opponentPokemons = if (awardToFainted) opponent.pokemonList else opponent.pokemonList.filter { it.health > 0 }
|
||||
faintedPokemons.forEach { faintedPokemon ->
|
||||
for (opponentPokemon in opponentNonFaintedPokemons) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
val facedFainted = opponentPokemon.facedOpponents.contains(faintedPokemon)
|
||||
val pokemon = opponentPokemon.effectedPokemon
|
||||
if (facedFainted) {
|
||||
@@ -270,7 +291,8 @@ open class PokemonBattle(
|
||||
else -> continue
|
||||
}
|
||||
val experience = Cobblemon.experienceCalculator.calculate(opponentPokemon, faintedPokemon, multiplier)
|
||||
if (experience > 0 && actor.pokemonList.all { it.health <= 0 }) {
|
||||
val enemyTeamWiped = actor.pokemonList.all { it.health <= 0 }
|
||||
if (experience > 0 && (enemyTeamWiped || awardOnLoss)) {
|
||||
opponent.awardExperience(opponentPokemon, experience)
|
||||
}
|
||||
Cobblemon.evYieldCalculator.calculate(opponentPokemon, faintedPokemon).forEach { (stat, amount) ->
|
||||
@@ -505,6 +527,7 @@ open class PokemonBattle(
|
||||
|
||||
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() }
|
||||
@@ -512,6 +535,20 @@ 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 } }
|
||||
|
||||
+7
-1
@@ -26,8 +26,9 @@ 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 java.util.UUID
|
||||
import net.minecraft.server.level.ServerPlayer
|
||||
import net.minecraft.world.phys.Vec3
|
||||
import java.util.UUID
|
||||
|
||||
abstract class BattleActor(
|
||||
val uuid: UUID,
|
||||
@@ -54,6 +55,11 @@ 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
|
||||
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of 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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of 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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of 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?)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.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
|
||||
)
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.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)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.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>>)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.api.camera
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
interface ICameraCoordinator {
|
||||
fun bind(cameraId: UUID)
|
||||
fun release()
|
||||
fun getCameraId(): UUID?
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.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
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of 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
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of 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
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of 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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,591 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of 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
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of 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
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.api.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
@@ -0,0 +1,184 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of 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
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of 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) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.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
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.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()
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of 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
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of 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
@@ -0,0 +1,222 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of 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
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of 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
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of 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
@@ -0,0 +1,243 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of 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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.api.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" }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of 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" }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of 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>
|
||||
)
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.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" }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.api.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)
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ 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
|
||||
@@ -21,6 +22,7 @@ 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
|
||||
@@ -110,6 +112,9 @@ object CobblemonEvents {
|
||||
@JvmField
|
||||
val BATTLE_STARTED_POST = EventObservable<BattleStartedEvent.Post>()
|
||||
|
||||
@JvmField
|
||||
val BATTLE_FLEE_ATTEMPT = EventObservable<BattleFleeAttemptEvent>()
|
||||
|
||||
@JvmField
|
||||
val BATTLE_FLED = EventObservable<BattleFledEvent>()
|
||||
|
||||
@@ -361,4 +366,33 @@ 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>()
|
||||
}
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.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 }
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of 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
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of 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,6 +76,8 @@ 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
|
||||
@@ -145,6 +147,7 @@ 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
|
||||
@@ -164,6 +167,23 @@ 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()
|
||||
@@ -240,6 +260,11 @@ 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)
|
||||
},
|
||||
@@ -651,6 +676,32 @@ object MoLangFunctions {
|
||||
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) }
|
||||
@@ -871,6 +922,8 @@ object MoLangFunctions {
|
||||
|
||||
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
|
||||
}
|
||||
@@ -891,6 +944,15 @@ 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
|
||||
}
|
||||
)
|
||||
@@ -1572,6 +1634,11 @@ 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 ->
|
||||
@@ -1589,10 +1656,145 @@ 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>>()
|
||||
@@ -1830,6 +2032,30 @@ 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) {
|
||||
@@ -2815,8 +3041,8 @@ object MoLangFunctions {
|
||||
}
|
||||
variable
|
||||
}
|
||||
else -> null
|
||||
} ?: throw IllegalArgumentException("Invalid NBT element type: ${nbt.type}")
|
||||
else -> VariableStruct()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.api.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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of 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
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of 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()
|
||||
)
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.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
|
||||
)
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of 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 }
|
||||
}
|
||||
@@ -0,0 +1,488 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of 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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of 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
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.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
@@ -0,0 +1,336 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of 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
|
||||
)
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of 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
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of 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
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of 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()
|
||||
)
|
||||
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of 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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of 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
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.api.molangui.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
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of 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
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of 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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.api.molangui.widget
|
||||
|
||||
import com.cobblemon.mod.common.api.molang.ExpressionLike
|
||||
import com.cobblemon.mod.common.util.asExpressionLike
|
||||
import com.bedrockk.molang.runtime.MoLangRuntime
|
||||
|
||||
/**
|
||||
* A widget property that is either a static value or a Molang expression.
|
||||
* String values that look like Molang (contain operators, function calls, or variable refs)
|
||||
* are parsed as expressions. Plain literals are kept static.
|
||||
*/
|
||||
class WidgetProperty(val raw: Any) {
|
||||
val expression: ExpressionLike? = when (raw) {
|
||||
is String -> {
|
||||
if (isMolangExpression(raw)) raw.asExpressionLike() else null
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
|
||||
val isStatic: Boolean get() = expression == null
|
||||
|
||||
fun resolveDouble(runtime: MoLangRuntime): Double {
|
||||
return expression?.resolveDouble(runtime) ?: when (raw) {
|
||||
is Number -> raw.toDouble()
|
||||
is String -> parseNumericString(raw)
|
||||
is Boolean -> if (raw) 1.0 else 0.0
|
||||
else -> 0.0
|
||||
}
|
||||
}
|
||||
|
||||
fun resolveString(runtime: MoLangRuntime): String {
|
||||
return expression?.resolveString(runtime) ?: raw.toString()
|
||||
}
|
||||
|
||||
fun resolveBoolean(runtime: MoLangRuntime): Boolean {
|
||||
return expression?.resolveBoolean(runtime) ?: when (raw) {
|
||||
is Boolean -> raw
|
||||
is Number -> raw.toDouble() != 0.0
|
||||
is String -> raw.toBooleanStrictOrNull() ?: (raw.isNotEmpty())
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
fun resolveInt(runtime: MoLangRuntime): Int {
|
||||
if (expression == null && raw is String && raw.startsWith("0x", ignoreCase = true)) {
|
||||
return raw.substring(2).toLongOrNull(16)?.toInt() ?: 0
|
||||
}
|
||||
return resolveDouble(runtime).toInt()
|
||||
}
|
||||
|
||||
fun resolveFloat(runtime: MoLangRuntime): Float = resolveDouble(runtime).toFloat()
|
||||
|
||||
companion object {
|
||||
private val MOLANG_INDICATORS = listOf("q.", "v.", "t.", "c.", "math.", "temp.", "query.", "variable.", "+", "-", "*", "/", "?", "!", "==", "!=", "<", ">", "(", "&&", "||")
|
||||
|
||||
fun isMolangExpression(value: String): Boolean {
|
||||
return MOLANG_INDICATORS.any { value.contains(it) }
|
||||
}
|
||||
|
||||
fun parseNumericString(value: String): Double {
|
||||
value.toDoubleOrNull()?.let { return it }
|
||||
if (value.startsWith("0x", ignoreCase = true)) {
|
||||
value.substring(2).toLongOrNull(16)?.let { return it.toDouble() }
|
||||
}
|
||||
return 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of 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.template.TemplateDefinition
|
||||
import com.cobblemon.mod.common.api.molangui.template.TemplateInput
|
||||
import com.cobblemon.mod.common.api.molangui.template.TemplateResolver
|
||||
import com.google.gson.JsonArray
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonObject
|
||||
|
||||
object WidgetTree {
|
||||
|
||||
fun parse(guiJson: JsonObject, isClient: Boolean): WidgetDefinition {
|
||||
val localTemplates = parseLocalTemplates(guiJson)
|
||||
val rootJson = guiJson.getAsJsonObject("root")
|
||||
?: throw IllegalArgumentException("GUI definition missing 'root'")
|
||||
val resolved = TemplateResolver.resolve(rootJson, localTemplates, isClient)
|
||||
return parseWidget("root", resolved)
|
||||
}
|
||||
|
||||
private fun parseLocalTemplates(guiJson: JsonObject): Map<String, TemplateDefinition> {
|
||||
val result = mutableMapOf<String, TemplateDefinition>()
|
||||
guiJson.getAsJsonObject("templates")?.let { templatesObj ->
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
result[name] = TemplateDefinition(
|
||||
name = name,
|
||||
namespace = "",
|
||||
inputs = inputs,
|
||||
extends = templateJson.get("extends")?.asString,
|
||||
body = templateJson
|
||||
)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fun parseWidget(id: String, json: JsonObject): WidgetDefinition {
|
||||
val typeName = json.get("type")?.asString ?: "panel"
|
||||
val type = WidgetType.fromId(typeName) ?: WidgetType.PANEL
|
||||
|
||||
val properties = mutableMapOf<String, WidgetProperty>()
|
||||
val events = mutableMapOf<String, String>()
|
||||
val clientEvents = mutableMapOf<String, String>()
|
||||
var hoverOverrides = emptyMap<String, WidgetProperty>()
|
||||
var transitions = emptyMap<String, TransitionConfig>()
|
||||
var enterAnimation: AnimationConfig? = null
|
||||
var exitAnimation: AnimationConfig? = null
|
||||
val isStatic = json.get("static")?.asBoolean ?: false
|
||||
|
||||
val reservedKeys = setOf("type", "controls", "item_template", "static", "hover", "transition", "enter", "exit", "inputs", "extends")
|
||||
for ((key, value) in json.entrySet()) {
|
||||
when {
|
||||
key.startsWith("on_") && key.endsWith("_client") -> clientEvents[key] = value.asString
|
||||
key.startsWith("on_") -> events[key] = value.asString
|
||||
key == "hover" && value.isJsonObject -> hoverOverrides = parsePropertyMap(value.asJsonObject)
|
||||
key == "transition" && value.isJsonObject -> transitions = parseTransitions(value.asJsonObject)
|
||||
key == "enter" -> enterAnimation = parseAnimationConfig(value)
|
||||
key == "exit" -> exitAnimation = parseAnimationConfig(value)
|
||||
key !in reservedKeys -> properties[key] = WidgetProperty(jsonToRaw(value))
|
||||
}
|
||||
}
|
||||
|
||||
val children = mutableListOf<WidgetDefinition>()
|
||||
json.getAsJsonArray("controls")?.let { controls ->
|
||||
for (element in controls) {
|
||||
val controlObj = element.asJsonObject
|
||||
for ((key, value) in controlObj.entrySet()) {
|
||||
children.add(parseWidget(key, value.asJsonObject))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
json.getAsJsonObject("item_template")?.let { itemTemplate ->
|
||||
properties["_item_template"] = WidgetProperty(itemTemplate)
|
||||
}
|
||||
|
||||
return WidgetDefinition(
|
||||
id = id, type = type, properties = properties, children = children,
|
||||
events = events, clientEvents = clientEvents, hoverOverrides = hoverOverrides,
|
||||
transitions = transitions, enterAnimation = enterAnimation, exitAnimation = exitAnimation,
|
||||
isStatic = isStatic
|
||||
)
|
||||
}
|
||||
|
||||
private fun parsePropertyMap(json: JsonObject): Map<String, WidgetProperty> {
|
||||
return json.entrySet().associate { (key, value) -> key to WidgetProperty(jsonToRaw(value)) }
|
||||
}
|
||||
|
||||
private fun parseTransitions(json: JsonObject): Map<String, TransitionConfig> {
|
||||
val result = mutableMapOf<String, TransitionConfig>()
|
||||
for ((key, value) in json.entrySet()) {
|
||||
result[key] = when {
|
||||
value.isJsonPrimitive && value.asJsonPrimitive.isNumber -> TransitionConfig(duration = value.asFloat)
|
||||
value.isJsonObject -> {
|
||||
val obj = value.asJsonObject
|
||||
TransitionConfig(
|
||||
duration = obj.get("duration")?.asFloat ?: 0.3f,
|
||||
easing = obj.get("easing")?.asString ?: "ease_out"
|
||||
)
|
||||
}
|
||||
else -> TransitionConfig(0.3f)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun parseAnimationConfig(value: JsonElement): AnimationConfig {
|
||||
return when {
|
||||
value.isJsonPrimitive && value.asJsonPrimitive.isString -> AnimationConfig(type = value.asString)
|
||||
value.isJsonObject -> {
|
||||
val obj = value.asJsonObject
|
||||
AnimationConfig(
|
||||
type = obj.get("type")?.asString ?: "fade_in",
|
||||
duration = obj.get("duration")?.asFloat ?: 0.3f,
|
||||
easing = obj.get("easing")?.asString ?: "ease_out"
|
||||
)
|
||||
}
|
||||
else -> AnimationConfig("fade_in")
|
||||
}
|
||||
}
|
||||
|
||||
private fun jsonToRaw(element: JsonElement): Any {
|
||||
return when {
|
||||
element.isJsonPrimitive -> {
|
||||
val prim = element.asJsonPrimitive
|
||||
when {
|
||||
prim.isNumber -> prim.asNumber
|
||||
prim.isBoolean -> prim.asBoolean
|
||||
else -> prim.asString
|
||||
}
|
||||
}
|
||||
element.isJsonArray -> element.asJsonArray.map { jsonToRaw(it) }
|
||||
element.isJsonObject -> element.asJsonObject
|
||||
else -> element.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,7 @@ object ActionEffects : JsonDataRegistry<ActionEffectTimeline> {
|
||||
ActionEffectKeyframe.register<RunActionEffectKeyframe>("run_action_effect")
|
||||
ActionEffectKeyframe.register<EntityParticlesActionEffectKeyframe>("entity_particles")
|
||||
ActionEffectKeyframe.register<EntitySoundActionEffectKeyframe>("entity_sound")
|
||||
ActionEffectKeyframe.register<CameraFocusActionEffectKeyframe>("camera_focus")
|
||||
}
|
||||
|
||||
override val gson = GsonBuilder()
|
||||
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of 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.moves.animations
|
||||
|
||||
import com.bedrockk.molang.Expression
|
||||
import com.cobblemon.mod.common.api.data.JsonDataRegistry
|
||||
import com.cobblemon.mod.common.api.molang.ExpressionLike
|
||||
import com.cobblemon.mod.common.api.moves.animations.keyframes.ActionEffectKeyframe
|
||||
import com.cobblemon.mod.common.api.pokemon.stats.Stat
|
||||
import com.cobblemon.mod.common.api.reactive.SimpleObservable
|
||||
import com.cobblemon.mod.common.pokemon.adapters.CobblemonStatTypeAdapter
|
||||
import com.cobblemon.mod.common.util.adapters.*
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import net.minecraft.advancements.critereon.MinMaxBounds
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.server.level.ServerPlayer
|
||||
import net.minecraft.server.packs.PackType
|
||||
import net.minecraft.world.phys.AABB
|
||||
import net.minecraft.world.phys.Vec3
|
||||
import java.awt.Color
|
||||
|
||||
/**
|
||||
* Data registry containing camera action effect timelines for battle moves.
|
||||
*
|
||||
* These camera timelines run in parallel with the main action effects timeline,
|
||||
* allowing camera work to be independent from move timing and execution.
|
||||
*
|
||||
* Camera timeline files are stored at: `data/<namespace>/action_effects/moves/camera/<movename>_camera.json`
|
||||
*
|
||||
* Example structure:
|
||||
* ```json
|
||||
* {
|
||||
* "timeline": [
|
||||
* {
|
||||
* "type": "camera_focus",
|
||||
* "source": "USERS",
|
||||
* "duration": 2
|
||||
* },
|
||||
* {
|
||||
* "type": "pause",
|
||||
* "pause": 0.5
|
||||
* },
|
||||
* {
|
||||
* "type": "camera_focus",
|
||||
* "source": "TARGETS",
|
||||
* "duration": 2
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @author Terrainwax
|
||||
* @since January 2025
|
||||
*/
|
||||
object MoveCameraTimelines : JsonDataRegistry<ActionEffectTimeline> {
|
||||
override val id: ResourceLocation = cobblemonResource("move_camera_timelines")
|
||||
override val type: PackType = PackType.SERVER_DATA
|
||||
override val observable = SimpleObservable<MoveCameraTimelines>()
|
||||
|
||||
override val gson = GsonBuilder()
|
||||
.disableHtmlEscaping()
|
||||
.setPrettyPrinting()
|
||||
.registerTypeAdapter(ActionEffectKeyframe::class.java, ActionEffectKeyframeAdapter)
|
||||
.registerTypeAdapter(MinMaxBounds.Doubles::class.java, FloatNumberRangeAdapter)
|
||||
.registerTypeAdapter(TypeToken.getParameterized(Collection::class.java, AABB::class.java).type, BoxCollectionAdapter)
|
||||
.registerTypeAdapter(AABB::class.java, BoxAdapter)
|
||||
.registerTypeAdapter(Vec3::class.java, VerboseVec3dAdapter)
|
||||
.registerTypeAdapter(ResourceLocation::class.java, IdentifierAdapter)
|
||||
.registerTypeAdapter(IntRange::class.java, VerboseIntRangeAdapter)
|
||||
.registerTypeAdapter(Color::class.java, LiteralHexColorAdapter)
|
||||
.registerTypeAdapter(Stat::class.java, CobblemonStatTypeAdapter)
|
||||
.registerTypeAdapter(Expression::class.java, ExpressionAdapter)
|
||||
.registerTypeAdapter(ExpressionLike::class.java, ExpressionLikeAdapter)
|
||||
.registerTypeAdapter(
|
||||
TypeToken.getParameterized(
|
||||
TypeToken.get(List::class.java).type,
|
||||
TypeToken.get(ActionEffectKeyframe::class.java).type
|
||||
).type,
|
||||
SingleToPluralAdapter(ActionEffectKeyframe::class.java) { it }
|
||||
)
|
||||
.create()
|
||||
|
||||
override val typeToken: TypeToken<ActionEffectTimeline> = TypeToken.get(ActionEffectTimeline::class.java)
|
||||
override val resourcePath = "action_effects/moves/camera"
|
||||
|
||||
private val cameraTimelines = mutableMapOf<ResourceLocation, ActionEffectTimeline>()
|
||||
|
||||
override fun reload(data: Map<ResourceLocation, ActionEffectTimeline>) {
|
||||
cameraTimelines.clear()
|
||||
cameraTimelines.putAll(data)
|
||||
observable.emit(this)
|
||||
}
|
||||
|
||||
override fun sync(player: ServerPlayer) {
|
||||
// Camera timelines are server-side data, no sync needed
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a camera timeline for a specific move.
|
||||
*
|
||||
* @param moveId The move identifier (e.g., "cobblemon:flameburst")
|
||||
* @return The camera action effect timeline, or null if not found
|
||||
*/
|
||||
fun getCameraTimeline(moveId: ResourceLocation): ActionEffectTimeline? {
|
||||
// Try direct match first (e.g., "cobblemon:flameburst_camera")
|
||||
val directId = ResourceLocation(moveId.namespace, "${moveId.path}_camera")
|
||||
if (cameraTimelines.containsKey(directId)) {
|
||||
return cameraTimelines[directId]
|
||||
}
|
||||
|
||||
// Try without namespace (e.g., just "flameburst_camera")
|
||||
val simpleId = cobblemonResource("${moveId.path}_camera")
|
||||
if (cameraTimelines.containsKey(simpleId)) {
|
||||
return cameraTimelines[simpleId]
|
||||
}
|
||||
|
||||
// Fallback to generic_move_camera if move-specific camera not found
|
||||
val genericId = cobblemonResource("generic_move_camera")
|
||||
return cameraTimelines[genericId]
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a camera timeline exists for a move.
|
||||
*
|
||||
* @param moveId The move identifier
|
||||
* @return True if a camera timeline exists for this move
|
||||
*/
|
||||
fun hasCameraTimeline(moveId: ResourceLocation): Boolean {
|
||||
return getCameraTimeline(moveId) != null
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all registered camera timeline IDs.
|
||||
*/
|
||||
fun getTimelineIds(): Set<ResourceLocation> = cameraTimelines.keys.toSet()
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of 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.moves.animations.keyframes
|
||||
|
||||
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
|
||||
import com.cobblemon.mod.common.api.molang.ExpressionLike
|
||||
import com.cobblemon.mod.common.api.moves.animations.ActionEffectContext
|
||||
import com.cobblemon.mod.common.api.moves.animations.EntityProvider
|
||||
import com.cobblemon.mod.common.api.moves.animations.TargetsProvider
|
||||
import com.cobblemon.mod.common.api.moves.animations.UsersProvider
|
||||
import com.cobblemon.mod.common.api.scheduling.delayedFuture
|
||||
import com.cobblemon.mod.common.battles.stadium.StadiumBattleCameraCoordinator
|
||||
import com.cobblemon.mod.common.battles.stadium.StadiumBattleCameraCoordinator.FocusRole
|
||||
import com.cobblemon.mod.common.util.asExpressionLike
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
class CameraFocusActionEffectKeyframe : ActionEffectKeyframe {
|
||||
enum class FocusSource {
|
||||
USERS,
|
||||
TARGETS,
|
||||
ENTITIES
|
||||
}
|
||||
|
||||
var source: FocusSource = FocusSource.USERS
|
||||
var release: Boolean = false
|
||||
var duration: ExpressionLike = "0".asExpressionLike()
|
||||
|
||||
/**
|
||||
* Blend duration for smooth camera transitions.
|
||||
*
|
||||
* When set to a value > 0, the camera will smoothly transition to the new focus
|
||||
* over this duration (in seconds) instead of snapping instantly.
|
||||
*
|
||||
* This helps prevent motion sickness by avoiding abrupt camera movements during battles.
|
||||
*
|
||||
* Recommended values:
|
||||
* - 0.0: Instant snap (use for quick reactions)
|
||||
* - 0.3-0.5: Fast but smooth transition (good for most moves)
|
||||
* - 0.8-1.0: Slow, cinematic transition (for dramatic moments)
|
||||
*
|
||||
* Note: The actual transition smoothness is also limited by MAX_ROTATION_SPEED_DEG_PER_SEC
|
||||
* in ClientCameraController to prevent motion sickness.
|
||||
*/
|
||||
var blendDuration: ExpressionLike = "0.5".asExpressionLike()
|
||||
|
||||
override fun play(context: ActionEffectContext): CompletableFuture<Unit> {
|
||||
val battle = context.providers.filterIsInstance<PokemonBattle>().firstOrNull() ?: return skip()
|
||||
|
||||
if (release) {
|
||||
StadiumBattleCameraCoordinator.forceRelease(battle.battleId)
|
||||
return skip()
|
||||
}
|
||||
|
||||
val entities = when (source) {
|
||||
FocusSource.USERS -> context.providers.filterIsInstance<UsersProvider>().flatMap { it.entities }
|
||||
FocusSource.TARGETS -> context.providers.filterIsInstance<TargetsProvider>().flatMap { it.entities }
|
||||
FocusSource.ENTITIES -> context.providers.filterIsInstance<EntityProvider>().flatMap { it.entities }
|
||||
}
|
||||
|
||||
if (entities.isEmpty()) {
|
||||
return skip()
|
||||
}
|
||||
|
||||
val focusRole = if (source == FocusSource.TARGETS) FocusRole.TARGET else FocusRole.USER
|
||||
val releaseHandle = StadiumBattleCameraCoordinator.startFocus(battle, entities, focusRole)
|
||||
val seconds = duration.resolveFloat(context.runtime)
|
||||
|
||||
if (seconds > 0f && releaseHandle != null) {
|
||||
delayedFuture(seconds).whenComplete { _, _ -> releaseHandle.invoke() }
|
||||
}
|
||||
|
||||
return CompletableFuture.completedFuture(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
@@ -12,6 +12,7 @@ import com.cobblemon.mod.common.api.npc.configuration.interaction.CustomScriptNP
|
||||
import com.cobblemon.mod.common.api.npc.configuration.interaction.DialogueNPCInteractionConfiguration
|
||||
import com.cobblemon.mod.common.api.npc.configuration.interaction.NoneNPCInteractionConfiguration
|
||||
import com.cobblemon.mod.common.api.npc.configuration.interaction.ScriptNPCInteractionConfiguration
|
||||
import com.cobblemon.mod.common.api.npc.configuration.interaction.ShopNPCInteractionConfiguration
|
||||
import com.cobblemon.mod.common.api.text.text
|
||||
import com.cobblemon.mod.common.entity.npc.NPCEntity
|
||||
import net.minecraft.nbt.CompoundTag
|
||||
@@ -43,6 +44,7 @@ interface NPCInteractConfiguration {
|
||||
register(type = "custom_script", displayName = "Custom Script".text(), clazz = CustomScriptNPCInteractionConfiguration::class.java)
|
||||
register(type = "dialogue", displayName = "Dialogue".text(), clazz = DialogueNPCInteractionConfiguration::class.java)
|
||||
register(type = "none", displayName = "None".text(), clazz = NoneNPCInteractionConfiguration::class.java)
|
||||
register(type = "shop", displayName = "Shop".text(), clazz = ShopNPCInteractionConfiguration::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+336
@@ -0,0 +1,336 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of 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.npc.configuration.interaction
|
||||
|
||||
import com.cobblemon.mod.common.Cobblemon
|
||||
import com.cobblemon.mod.common.CobblemonMemories
|
||||
import com.cobblemon.mod.common.api.dialogue.ActiveDialogue
|
||||
import com.cobblemon.mod.common.api.dialogue.Dialogue
|
||||
import com.cobblemon.mod.common.api.dialogue.DialogueManager
|
||||
import com.cobblemon.mod.common.api.dialogue.DialoguePage
|
||||
import com.cobblemon.mod.common.api.dialogue.FunctionDialogueAction
|
||||
import com.cobblemon.mod.common.api.dialogue.WrappedDialogueText
|
||||
import com.cobblemon.mod.common.api.dialogue.input.DialogueOption
|
||||
import com.cobblemon.mod.common.api.dialogue.input.DialogueOptionSetInput
|
||||
import com.cobblemon.mod.common.api.molangui.MolangUiSessionManager
|
||||
import com.cobblemon.mod.common.api.npc.configuration.NPCInteractConfiguration
|
||||
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
|
||||
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
|
||||
import com.cobblemon.mod.common.api.text.text
|
||||
import com.cobblemon.mod.common.api.toast.Toast
|
||||
import com.cobblemon.mod.common.entity.npc.NPCEntity
|
||||
import com.cobblemon.mod.common.util.DataKeys
|
||||
import com.cobblemon.mod.common.util.cobblemonResource
|
||||
import com.cobblemon.mod.common.util.party
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.JsonArray
|
||||
import com.google.gson.JsonObject
|
||||
import net.minecraft.core.registries.BuiltInRegistries
|
||||
import net.minecraft.nbt.CompoundTag
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.server.level.ServerPlayer
|
||||
import net.minecraft.world.item.ItemStack
|
||||
import net.minecraft.world.item.Items
|
||||
|
||||
/**
|
||||
* An [NPCInteractConfiguration] which opens a shop dialogue when the player interacts with the NPC.
|
||||
* Presents "See Articles" / "Leave" options, then opens a scripted GUI shop with purchase handling.
|
||||
*
|
||||
* @author Cobblemon Contributors
|
||||
* @since April 2026
|
||||
*/
|
||||
class ShopNPCInteractionConfiguration : NPCInteractConfiguration {
|
||||
override val type: String = "shop"
|
||||
var shopConfig: ResourceLocation = cobblemonResource("example_shop")
|
||||
|
||||
/**
|
||||
* A shop entry that can be either a Minecraft item or a Pokémon.
|
||||
* @param type "item" or "pokemon"
|
||||
* @param id Item registry ID (for items) or species name (for Pokémon)
|
||||
* @param price Cost in emeralds
|
||||
* @param level Pokémon level (only for pokemon type, default 5)
|
||||
*/
|
||||
data class ShopEntry(
|
||||
val type: String,
|
||||
val id: String,
|
||||
val price: Int,
|
||||
val level: Int = 5
|
||||
) {
|
||||
val isItem get() = type == "item"
|
||||
val isPokemon get() = type == "pokemon"
|
||||
|
||||
fun getDisplayName(): String {
|
||||
return if (isItem) {
|
||||
val itemId = ResourceLocation.tryParse(id) ?: return id
|
||||
BuiltInRegistries.ITEM.get(itemId).defaultInstance.hoverName.string
|
||||
} else {
|
||||
PokemonSpecies.getByName(id)?.translatedName?.string ?: id.replaceFirstChar { it.uppercase() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun loadShopItems(): List<ShopEntry> {
|
||||
return listOf(
|
||||
ShopEntry("item", "cobblemon:poke_ball", 3),
|
||||
ShopEntry("item", "cobblemon:great_ball", 6),
|
||||
ShopEntry("item", "cobblemon:ultra_ball", 12),
|
||||
ShopEntry("item", "minecraft:potion", 5),
|
||||
ShopEntry("pokemon", "bulbasaur", 20, level = 5),
|
||||
ShopEntry("pokemon", "charmander", 20, level = 5),
|
||||
ShopEntry("pokemon", "squirtle", 20, level = 5),
|
||||
ShopEntry("pokemon", "pikachu", 25, level = 10),
|
||||
ShopEntry("pokemon", "eevee", 30, level = 5),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun interact(npc: NPCEntity, player: ServerPlayer): Boolean {
|
||||
val shopItems = loadShopItems()
|
||||
|
||||
val seeArticlesOption = DialogueOption(
|
||||
text = WrappedDialogueText("See Articles".text()),
|
||||
value = "see_articles",
|
||||
action = FunctionDialogueAction { activeDialogue, _ ->
|
||||
activeDialogue.close()
|
||||
openShopGui(player, npc, shopItems)
|
||||
}
|
||||
)
|
||||
|
||||
val leaveOption = DialogueOption(
|
||||
text = WrappedDialogueText("Leave".text()),
|
||||
value = "leave",
|
||||
action = FunctionDialogueAction { activeDialogue, _ ->
|
||||
activeDialogue.close()
|
||||
}
|
||||
)
|
||||
|
||||
val page = DialoguePage.of(
|
||||
lines = listOf("Welcome to the shop! What would you like to do?".text()),
|
||||
input = DialogueOptionSetInput(
|
||||
options = mutableListOf(seeArticlesOption, leaveOption),
|
||||
vertical = true
|
||||
)
|
||||
)
|
||||
|
||||
val dialogue = Dialogue(
|
||||
pages = listOf(page),
|
||||
escapeAction = FunctionDialogueAction { activeDialogue, _ -> activeDialogue.close() }
|
||||
)
|
||||
|
||||
val currentDialogues = npc.brain.getMemory(CobblemonMemories.DIALOGUES).orElse(mutableListOf())
|
||||
val activeDialogue = DialogueManager.startDialogue(player, npc, dialogue)
|
||||
val newDialogues = currentDialogues + activeDialogue
|
||||
npc.brain.setMemory(CobblemonMemories.DIALOGUES, newDialogues)
|
||||
activeDialogue.completion.thenRun { onDialogueStopped(npc, activeDialogue) }
|
||||
return true
|
||||
}
|
||||
|
||||
private fun openShopGui(player: ServerPlayer, npc: NPCEntity, shopItems: List<ShopEntry>) {
|
||||
val guiId = cobblemonResource("npc_shop")
|
||||
|
||||
// Serialize shop items as a JSON array for the data-driven LIST widget
|
||||
val itemsJson = JsonArray()
|
||||
for (entry in shopItems) {
|
||||
val obj = JsonObject()
|
||||
obj.addProperty("name", entry.getDisplayName())
|
||||
obj.addProperty("price", entry.price.toString())
|
||||
obj.addProperty("entry_type", entry.type)
|
||||
if (entry.isItem) {
|
||||
obj.addProperty("item_id", entry.id)
|
||||
} else {
|
||||
obj.addProperty("species", entry.id)
|
||||
obj.addProperty("level", entry.level.toString())
|
||||
}
|
||||
itemsJson.add(obj)
|
||||
}
|
||||
val args = mapOf(
|
||||
"shop_items" to Gson().toJson(itemsJson),
|
||||
"emerald_count" to countEmeralds(player).toString()
|
||||
)
|
||||
|
||||
val session = MolangUiSessionManager.openGui(
|
||||
player = player,
|
||||
guiId = guiId,
|
||||
args = args
|
||||
)
|
||||
|
||||
if (session == null) {
|
||||
Cobblemon.LOGGER.error("[ShopNPCInteraction] Failed to open shop GUI '$guiId' for ${player.name.string}")
|
||||
return
|
||||
}
|
||||
|
||||
// Set the emerald count variable immediately so the list page shows it
|
||||
session.pushUpdate(variables = mapOf("v.emerald_count" to countEmeralds(player).toDouble()))
|
||||
|
||||
session.addActionListener { action, data ->
|
||||
Cobblemon.LOGGER.info("[ShopNPC] Action received: action='$action' data=$data")
|
||||
when (action) {
|
||||
"select_item" -> {
|
||||
val index = data["0"]?.toDoubleOrNull()?.toInt() ?: return@addActionListener
|
||||
if (index !in shopItems.indices) return@addActionListener
|
||||
|
||||
val entry = shopItems[index]
|
||||
|
||||
val params = mutableMapOf(
|
||||
"selected_item_name" to entry.getDisplayName(),
|
||||
"selected_entry_type" to entry.type
|
||||
)
|
||||
if (entry.isItem) {
|
||||
params["selected_item_id"] = entry.id
|
||||
} else {
|
||||
params["selected_species"] = entry.id
|
||||
}
|
||||
|
||||
session.pushUpdate(
|
||||
variables = mapOf(
|
||||
"v.page" to 1.0,
|
||||
"v.selected_index" to index.toDouble(),
|
||||
"v.unit_price" to entry.price.toDouble(),
|
||||
"v.quantity" to 1.0,
|
||||
"v.emerald_count" to countEmeralds(player).toDouble()
|
||||
),
|
||||
parameters = params
|
||||
)
|
||||
}
|
||||
"back" -> {
|
||||
session.pushUpdate(
|
||||
variables = mapOf(
|
||||
"v.page" to 0.0,
|
||||
"v.emerald_count" to countEmeralds(player).toDouble()
|
||||
)
|
||||
)
|
||||
}
|
||||
"purchase" -> {
|
||||
val index = data["0"]?.toDoubleOrNull()?.toInt() ?: return@addActionListener
|
||||
val quantity = data["1"]?.toDoubleOrNull()?.toInt() ?: return@addActionListener
|
||||
if (index !in shopItems.indices || quantity <= 0) return@addActionListener
|
||||
|
||||
val entry = shopItems[index]
|
||||
val totalCost = entry.price * quantity
|
||||
val emeralds = countEmeralds(player)
|
||||
|
||||
if (emeralds < totalCost) {
|
||||
showFeedbackToast(player, "Not enough emeralds", isError = true)
|
||||
return@addActionListener
|
||||
}
|
||||
|
||||
if (entry.isItem) {
|
||||
if (!hasInventorySpace(player)) {
|
||||
showFeedbackToast(player, "Inventory full", isError = true)
|
||||
return@addActionListener
|
||||
}
|
||||
removeEmeralds(player, totalCost)
|
||||
val itemId = ResourceLocation.tryParse(entry.id) ?: return@addActionListener
|
||||
giveItems(player, itemId, quantity)
|
||||
} else {
|
||||
val party = player.party()
|
||||
if (party.occupied() + quantity > 6) {
|
||||
showFeedbackToast(player, "Party is full", isError = true)
|
||||
return@addActionListener
|
||||
}
|
||||
removeEmeralds(player, totalCost)
|
||||
repeat(quantity) {
|
||||
val pokemon = PokemonProperties.parse("${entry.id} level=${entry.level}").create()
|
||||
party.add(pokemon)
|
||||
}
|
||||
}
|
||||
|
||||
session.pushUpdate(
|
||||
variables = mapOf(
|
||||
"v.emerald_count" to countEmeralds(player).toDouble(),
|
||||
"v.quantity" to 1.0
|
||||
)
|
||||
)
|
||||
showFeedbackToast(player, "Purchased ${quantity}x ${entry.getDisplayName()}", isError = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showFeedbackToast(player: ServerPlayer, message: String, isError: Boolean) {
|
||||
val toast = Toast(
|
||||
title = "Shop".text(),
|
||||
description = message.text(),
|
||||
icon = ItemStack(Items.EMERALD)
|
||||
)
|
||||
toast.addListeners(player)
|
||||
toast.expireAfter(3f)
|
||||
}
|
||||
|
||||
private fun countEmeralds(player: ServerPlayer): Int {
|
||||
var count = 0
|
||||
for (i in 0 until player.inventory.containerSize) {
|
||||
val stack = player.inventory.getItem(i)
|
||||
if (stack.`is`(Items.EMERALD)) {
|
||||
count += stack.count
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
private fun removeEmeralds(player: ServerPlayer, amount: Int) {
|
||||
var remaining = amount
|
||||
for (i in 0 until player.inventory.containerSize) {
|
||||
if (remaining <= 0) break
|
||||
val stack = player.inventory.getItem(i)
|
||||
if (stack.`is`(Items.EMERALD)) {
|
||||
val toRemove = minOf(remaining, stack.count)
|
||||
stack.shrink(toRemove)
|
||||
remaining -= toRemove
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasInventorySpace(player: ServerPlayer): Boolean {
|
||||
return player.inventory.freeSlot != -1
|
||||
}
|
||||
|
||||
private fun giveItems(player: ServerPlayer, itemId: ResourceLocation, quantity: Int) {
|
||||
val item = BuiltInRegistries.ITEM.get(itemId)
|
||||
var remaining = quantity
|
||||
while (remaining > 0) {
|
||||
val stackSize = minOf(remaining, item.defaultMaxStackSize)
|
||||
val stack = ItemStack(item, stackSize)
|
||||
player.inventory.add(stack)
|
||||
remaining -= stackSize
|
||||
}
|
||||
}
|
||||
|
||||
private fun onDialogueStopped(npc: NPCEntity, activeDialogue: ActiveDialogue) {
|
||||
val currentDialogues = npc.brain.getMemory(CobblemonMemories.DIALOGUES).orElse(mutableListOf())
|
||||
val newDialogues = currentDialogues - activeDialogue
|
||||
if (newDialogues.isEmpty()) {
|
||||
npc.brain.eraseMemory(CobblemonMemories.DIALOGUES)
|
||||
} else {
|
||||
npc.brain.setMemory(CobblemonMemories.DIALOGUES, newDialogues)
|
||||
}
|
||||
}
|
||||
|
||||
override fun encode(buffer: RegistryFriendlyByteBuf) {
|
||||
buffer.writeResourceLocation(shopConfig)
|
||||
}
|
||||
|
||||
override fun decode(buffer: RegistryFriendlyByteBuf) {
|
||||
shopConfig = buffer.readResourceLocation()
|
||||
}
|
||||
|
||||
override fun writeToNBT(compoundTag: CompoundTag) {
|
||||
compoundTag.putString(DataKeys.NPC_INTERACT_SHOP, shopConfig.toString())
|
||||
}
|
||||
|
||||
override fun readFromNBT(compoundTag: CompoundTag) {
|
||||
shopConfig = ResourceLocation.parse(compoundTag.getString(DataKeys.NPC_INTERACT_SHOP))
|
||||
}
|
||||
|
||||
override fun isDifferentTo(other: NPCInteractConfiguration): Boolean {
|
||||
return other !is ShopNPCInteractionConfiguration || other.shopConfig != shopConfig
|
||||
}
|
||||
}
|
||||
@@ -71,6 +71,8 @@ object CobblemonPermissions {
|
||||
|
||||
@JvmStatic
|
||||
val STOP_BATTLE = this.create("${COMMAND_PREFIX}stopbattle", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
|
||||
@JvmStatic
|
||||
val STADIUM_BATTLE = this.create("${COMMAND_PREFIX}stadiumbattle", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
|
||||
|
||||
|
||||
@JvmStatic
|
||||
|
||||
@@ -289,11 +289,11 @@ object PokeBalls : JsonDataRegistry<PokeBall> {
|
||||
createDefault("ancient_great_ball", MultiplierModifier(1.5F), ancient = true)
|
||||
createDefault("ancient_ultra_ball", MultiplierModifier(2F), ancient = true)
|
||||
createDefault("ancient_heavy_ball", throwPower = 0.75f, ancient = true)
|
||||
createDefault("ancient_leaden_ball", throwPower = 0.75f, ancient = true)
|
||||
createDefault("ancient_gigaton_ball", throwPower = 0.75f, ancient = true)
|
||||
createDefault("ancient_leaden_ball", MultiplierModifier(1.5F), throwPower = 0.75f, ancient = true)
|
||||
createDefault("ancient_gigaton_ball", MultiplierModifier(2F), throwPower = 0.75f, ancient = true)
|
||||
createDefault("ancient_feather_ball", throwPower = 2.5f, ancient = true)
|
||||
createDefault("ancient_wing_ball", throwPower = 2.5f, ancient = true)
|
||||
createDefault("ancient_jet_ball", throwPower = 2.5f, ancient = true)
|
||||
createDefault("ancient_wing_ball", MultiplierModifier(1.5F), throwPower = 2.5f, ancient = true)
|
||||
createDefault("ancient_jet_ball", MultiplierModifier(2F), throwPower = 2.5f, ancient = true)
|
||||
createDefault("ancient_origin_ball", GuaranteedModifier(), ancient = true)
|
||||
// Luxury ball effect, low priority as it must be triggered before soothe bell as of gen 4
|
||||
CobblemonEvents.FRIENDSHIP_UPDATED.subscribe(priority = Priority.LOW) { event ->
|
||||
|
||||
@@ -314,11 +314,11 @@ object PokemonSpecies : JsonDataRegistry<Species> {
|
||||
val result = mutableMapOf<String, String>()
|
||||
this.species.forEach {species ->
|
||||
val baseSpecies = ShowdownSpecies(species, null)
|
||||
result[ShowdownIdentifiable.REGEX.replace(baseSpecies.name, "")] = this.gson.toJson(baseSpecies)
|
||||
result[ShowdownIdentifiable.REGEX.replace(baseSpecies.name.lowercase(), "")] = this.gson.toJson(baseSpecies)
|
||||
species.forms.forEach { form ->
|
||||
if (form != species.standardForm) {
|
||||
val formSpecies = ShowdownSpecies(species, form)
|
||||
result[ShowdownIdentifiable.REGEX.replace(formSpecies.name, "")] = this.gson.toJson(formSpecies)
|
||||
result[ShowdownIdentifiable.REGEX.replace(formSpecies.name.lowercase(), "")] = this.gson.toJson(formSpecies)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,15 @@ open class EventObservable<T> : SimpleObservable<T>() {
|
||||
emit(*events)
|
||||
events.forEach(then)
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun postEvents(
|
||||
vararg events: T,
|
||||
then: ((T) -> Unit)? = null
|
||||
): Unit {
|
||||
post(*events) { then?.invoke(it) }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
open class CancelableObservable<T : Cancelable> : EventObservable<T>() {
|
||||
|
||||
+4
-1
@@ -119,7 +119,10 @@ class RidingController(
|
||||
private fun canTransitionToLand(): Boolean {
|
||||
if ((entity.isUnderWater)) return false
|
||||
if ((entity.isEyeInFluid(FluidTags.WATER) || entity.isEyeInFluid(FluidTags.LAVA))) return false
|
||||
return entity.onGround()
|
||||
// If the currently in the air but has no land style then don't transition and render the ride useless.
|
||||
// Hover is a good example for this.
|
||||
val shouldStayInAir = entity.pokemon.riding.behaviours?.get(RidingStyle.LAND) == null && context?.style == RidingStyle.AIR
|
||||
return entity.onGround() && !shouldStayInAir
|
||||
}
|
||||
|
||||
private fun canTransitionToLiquid(): Boolean {
|
||||
|
||||
+15
-2
@@ -213,10 +213,23 @@ class HoverBehaviour : RidingBehaviour<HoverSettings, HoverState> {
|
||||
val accel = topSpeed / (vehicle.runtime.resolveDouble(settings.accelerationExpr ?: globalHover.accelerationExpr!!) * 20.0)
|
||||
|
||||
var newVelocity = state.rideVelocity.get()
|
||||
val unrotateVel = vehicle.deltaMovement.yRot(vehicle.yRot)
|
||||
|
||||
val vehicleFrontVec = Vec3(0.0, 0.0, 1.0)
|
||||
val faceVel = Vec3(vehicle.deltaMovement.x, 0.0, vehicle.deltaMovement.z)
|
||||
|
||||
val d = vehicle.deltaMovement.horizontalDistance()
|
||||
val v2 = Vec3(faceVel.dot(Vec3(1.0, 0.0, 0.0)) * d, newVelocity.y, faceVel.dot(Vec3(0.0, 0.0, 1.0)) * d)
|
||||
|
||||
// Check for collisions and if found, update the speed to reflect it
|
||||
if (vehicle.verticalCollision || vehicle.horizontalCollision) {
|
||||
state.speed.set(vehicle.deltaMovement.length())
|
||||
if (vehicle.horizontalCollision) {
|
||||
val smackingIntoWallSpeed = max( vehicle.deltaMovement.horizontalDistance(), 0.2)
|
||||
newVelocity = Vec3(newVelocity.x, 0.0, newVelocity.z).normalize().scale(smackingIntoWallSpeed).add(0.0, newVelocity.y, 0.0)
|
||||
}
|
||||
|
||||
|
||||
if (vehicle.verticalCollision) {
|
||||
newVelocity = Vec3(newVelocity.x, 0.0, newVelocity.z)
|
||||
}
|
||||
|
||||
// align the velocity vector to be in local vehicle space
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user