Compare commits

...
Author SHA1 Message Date
Landon Wills 5cb1cdceb6 Use opponents model and skin for remote showdown battles (needs cleanup) 2025-03-27 20:03:39 -04:00
Landon Wills 603db456b0 Fail gracefully if errors occur on connect or disconnect from remote showdown services 2025-03-26 22:52:20 -04:00
Landon Wills 6fb19ccd0b Revise logic to fetch showdown logs to be more consistent 2025-03-26 22:31:00 -04:00
Landon Wills aebe2bb2d9 Prototype of wifi battles 2025-03-25 22:44:13 -04:00
Landon Wills c11f2b15d9 Support battle processing on other showdown services 2025-03-25 19:41:31 -04:00
Landon Wills 50eefd5307 Implement base of remote showdown service 2025-03-25 00:06:37 -04:00
Landon Wills 9fa3cd86af Make action buttons work in debug ui 2025-03-24 22:55:51 -04:00
Landon Wills 27494bb654 Merge branch 'refs/heads/main' into showdown-fun 2025-03-24 00:17:56 -04:00
Hiroku 2413981f6f Merge branch 'charizardmarketing' into 'main'
Updated Charizard with the latest model and animations to be used for marketing on the main branch.

See merge request cable-mc/cobblemon!1572
2025-03-24 01:23:40 +00:00
Tyzillion 4ac244003a -6+ ?????? 2025-03-23 21:22:47 -04:00
Tyzillion 9ac92a084f Updated Charizard with new locators. 2025-03-23 21:18:31 -04:00
Apion aacbcb1f28 Merge branch 'jackowes/concrete_bird' into 'main'
Overhaul of bird and fix of jet velocity bug

See merge request cable-mc/cobblemon!1563
2025-03-23 23:37:47 +00:00
Jackson BowesandApion 39ff7e418c Overhaul of bird and fix of jet velocity bug 2025-03-23 23:37:47 +00:00
Hiroku 58498a091d Merge branch 'josh/npc-item-rendering' into 'main'
Molang for rendering items during animations.

See merge request cable-mc/cobblemon!1570
2025-03-23 23:12:06 +00:00
Hiroku a806f77aff Merge branch 'entity-scale' into 'main'
Pokemon hitboxes scale with attribute generic.scale

Closes #1410

See merge request cable-mc/cobblemon!1529
2025-03-23 22:05:38 +00:00
JordanandHiroku 60ae9e3a55 Pokemon hitboxes scale with attribute generic.scale 2025-03-23 22:05:37 +00:00
Hiroku 03ddc35d36 Merge branch 'Fix-pokemon-loadFrom' into 'main'
Fixed Pokemon.loadFrom[JSON/NBT]

Closes #1507

See merge request cable-mc/cobblemon!1571
2025-03-23 03:22:17 +00:00
ShiroFenixandHiroku 1b8e9b9b36 Fixed Pokemon.loadFrom[JSON/NBT] 2025-03-23 03:22:17 +00:00
Paul e52ac956f0 Added q.entity.is_holding_item for pokemon posers to use 2025-03-23 12:40:59 +10:00
Josh 4bfe147730 Merge branch 'main' into josh/npc-item-rendering 2025-03-22 12:41:31 -04:00
Landon Wills 3aa7e72839 Add ability to cycle between showdown services to debug UI 2025-03-22 02:21:31 -04:00
Landon Wills 4be4da58c0 Add support for multiple showdown connections, more work on showdown debugging UI 2025-03-22 01:20:16 -04:00
Landon Wills 5b60a822ef Remove broken socket service 2025-03-21 22:31:46 -04:00
Landon Wills 7457827f27 Start refactoring showdown architecture to support several services 2025-03-21 22:30:48 -04:00
Hiroku 493664bef2 Merge branch 'mealam/pokeballlocation' into 'main'
fix: Adjust `NPCEntity` pokeball throw positioning to account for `baseScale`

See merge request cable-mc/cobblemon!1568
2025-03-21 23:50:33 +00:00
AramandHiroku 981e3e5efc fix: Adjust NPCEntity pokeball throw positioning to account for baseScale 2025-03-21 23:50:33 +00:00
Josh 963614f5fa Updated CHANGELOG.md 2025-03-21 11:51:18 -04:00
Josh 7fdb70e3c3 Updated trainer npcs for testing 2025-03-21 11:20:24 -04:00
Landon Wills 39030fa18d Create base of showdown debugging GUI 2025-03-21 02:09:07 -04:00
Landon Wills 01f720e274 Add utility to allow us to get and post http requests 2025-03-20 22:41:54 -04:00
Josh 37e8f0eab6 multiple items can render on a model per locator and the render_item() molang function now supports items with components 2025-03-20 16:14:16 -04:00
Josh 05edafa5c4 NPC Trainers send out/return animation are able to show the correct ball. 2025-03-19 22:35:00 -04:00
Hiroku c15350e647 Merge branch 'mealam/1499' into 'main'
fix: Adjust `NPCEntity` beam positioning to account for `baseScale`

Closes #1499

See merge request cable-mc/cobblemon!1567
2025-03-20 00:36:12 +00:00
AramandHiroku f9e5784b10 fix: Adjust NPCEntity beam positioning to account for baseScale 2025-03-20 00:36:12 +00:00
Josh cea600617e Added render_item() molang function for PosableState. Allows rendering dummy items for animations. 2025-03-19 18:47:11 -04:00
Hiroku 2f4b861f98 Merge branch 'implement_pokemon' into 'main'
Dragonite and Gyarados models with locators for testing on main

See merge request cable-mc/cobblemon!1564
2025-03-16 00:20:05 +00:00
Frank The FarmerandHiroku a7022268c1 Dragonite and Gyarados models with locators for testing on main 2025-03-16 00:20:05 +00:00
Hiroku 3082600e11 Merge branch 'meowscarada-fix' into 'main'
Fixed Meowscarada's resolver having the wrong name

See merge request cable-mc/cobblemon!1562
2025-03-15 08:48:41 +00:00
gruxor a348cfd338 Fixed Meowscarada's resolver having the wrong name 2025-03-15 04:19:38 -04:00
Paul 35bfc7bf6c Merge remote-tracking branch 'origin/main' 2025-03-15 13:22:28 +10:00
Paul ae87c3dd84 Fixed downward correction of spawning, closes #1498 2025-03-15 13:22:04 +10:00
Hiroku d944803571 Merge branch 'jackowes/concrete_jet/01' into 'main'
Full Base Jet Controller

See merge request cable-mc/cobblemon!1560
2025-03-15 01:14:00 +00:00
Jackson BowesandHiroku 2c3b474c01 Full Base Jet Controller 2025-03-15 01:13:59 +00:00
Paul c2538560e1 Added check for activate_ action effects, implemented protect blocking effect 2025-03-14 10:23:50 +10:00
Paul d10c058c74 Added properties map to posers and look for item display contexts from there as a first try for item rendering 2025-03-14 09:55:31 +10:00
Paul d1b6d95d57 Fixed interpolation being applied to full model transformed parts 2025-03-14 09:17:34 +10:00
Paul 977387a184 Merge remote-tracking branch 'origin/main' 2025-03-13 20:26:35 +10:00
Paul 69bbc09d6a Updated fabric version, fixed our is_overworld including end biomes 2025-03-13 20:26:23 +10:00
Hiroku 46b04ad462 Merge branch 'async-player-data-saving' into 'main'
Moved player data saving to do the heavier work on a worker thread,...

See merge request cable-mc/cobblemon!1559
2025-03-13 05:03:28 +00:00
Paul a1b6fd3d1a Fixed silent pokemon still playing shiny sounds and particles, closes #1316 2025-03-13 11:19:10 +10:00
Paul fb9ed28b74 Moved player data saving to do the heavier work on a worker thread, potentially fixing storages not saving in race conditions 2025-03-13 11:01:51 +10:00
Paul 4bfc2a97dd Fixed base entity functions not being added to pokemon structs, messing with a lot of things 2025-03-13 09:47:12 +10:00
Nick a2cf66fbc1 Merge branch 'seat-generator' into 'main'
Seat generator

See merge request cable-mc/cobblemon!1558
2025-03-12 16:10:03 -07:00
HirokuandNick 5bda81a5c4 Seat generator 2025-03-12 16:10:03 -07:00
Test 32ccd15121 biome test bg 2025-03-12 03:56:25 +00:00
Hiroku a51cda998b Merge branch 'Adainish-main-patch-50806' into 'main'
Fixes a deserialisation issue due to IVS/EVS being required

See merge request cable-mc/cobblemon!1557
2025-03-12 02:25:20 +00:00
Adenydd Stuut || Winglet 9dc14eff7c Edit PokemonProperties.kt 2025-03-12 01:48:15 +00:00
Hiroku 4b1690c62e Merge branch 'observe-less' into 'main'
Removed heaps of observables from Pokemon because they weren't needed and it added heaps to the RAM

See merge request cable-mc/cobblemon!1555
2025-03-11 22:57:49 +00:00
Hiroku d02251b67d Edit CHANGELOG.md 2025-03-11 11:57:07 +00:00
Hiroku 54c8cbfbe8 Merge branch 'unlockable-wallpapers' into 'main'
Added unlockable PC box wallpapers

See merge request cable-mc/cobblemon!1525
2025-03-11 09:49:11 +00:00
Paul 2d8ce63dcd Changelog 2025-03-11 15:53:02 +10:00
Paul 1a92af2cd6 Removed heaps of observables from Pokemon because they weren't needed and it added heaps to the RAM 2025-03-11 15:51:45 +10:00
Paul 7e088bdeec @JvmStatic on all the permissions so the interop isn't whack 2025-03-11 13:30:01 +10:00
Paul 6142049d51 Added uuid function to pokemonStore molang 2025-03-11 12:57:38 +10:00
Hiroku d031f339c6 Merge branch 'spawning-improvements' into 'main'
Spawning performance improvements

See merge request cable-mc/cobblemon!1553
2025-03-10 11:33:39 +00:00
Paul 3ddbc9b313 Unmark wallpapers as new once you try them 2025-03-10 20:59:42 +10:00
Paul dd2c1ddbc6 Regenerate the validBiomes lists when data is reloaded (yknow, for when that works) 2025-03-10 20:34:31 +10:00
Paul badd7e8c2e Merge remote-tracking branch 'origin/main' into spawning-improvements 2025-03-10 20:27:48 +10:00
Paul d7c4380cc2 oop 2025-03-09 20:09:00 +10:00
Paul 7fc26ab939 Fixed held items being given with "Air" as the name oops 2025-03-09 14:50:08 +10:00
Paul 37f430ddf7 Fixed issues with unseen wallpapers not communicating properly to the client 2025-03-09 14:42:29 +10:00
Hiroku f0a2182149 Merge branch 'riding-settings' into 'main'
Added riding config options and implemented the invert mouse options on current controllers.

See merge request cable-mc/cobblemon!1550
2025-03-09 04:14:47 +00:00
Paul 9605775f1e Merge remote-tracking branch 'origin/main' into spawning-improvements 2025-03-09 11:00:56 +10:00
Paul f251bc20c1 Fixed species feature lookups not targeting code features first because of a funnybug 2025-03-09 11:00:29 +10:00
Paul a02b3d74b8 Fixed species additions not being able to add implemented, closes #779 2025-03-09 10:58:18 +10:00
Paul 7f3a4c20b3 Added biome precalculation to the spawning system, improved RegistryLikeCondition performance 2025-03-09 10:39:10 +10:00
Hiroku e76999c611 Merge branch 'Fix-NPCs-using-pokemon-outside-Pool' into 'main'
Fixing NPCs using pokemon outside of their Pool

Closes #1482

See merge request cable-mc/cobblemon!1552
2025-03-07 23:30:04 +00:00
WhiteFenix19 c2771bcb95 Fixed Formatting 2025-03-07 19:27:51 -04:00
WhiteFenix19 5b858b5982 Merge remote-tracking branch 'origin/Fix-NPCs-using-pokemon-outside-Pool' into Fix-NPCs-using-pokemon-outside-Pool 2025-03-07 17:09:29 -04:00
WhiteFenix19 7082220e29 Fixed NPCs using Pokémon outside of their pool when a Pokémon name had a typo. (Fixed indentations) 2025-03-07 17:08:52 -04:00
WhiteFenix19 1a87830a52 Fixed NPCs using Pokémon outside of their pool when a Pokémon name had a typo. 2025-03-07 17:02:54 -04:00
WhiteFenix19 9eff6dcced Fixed NPCs using Pokémon outside of their pool when a Pokémon name had a typo. 2025-03-07 15:05:48 -04:00
WhiteFenix19 52690c3aa6 Added check to avoid random Pokémon on Pool, added warning if species wrong or party empty 2025-03-07 13:59:40 -04:00
Paul 8f0780c5f6 I'm autistic 2025-03-07 12:34:59 +10:00
Test 465a7fbe73 update changelog, alt icons for descending sort 2025-03-05 17:47:42 -05:00
Hiroku 6b5abfa319 Merge branch 'fix-interpolation' into 'main'
Fixed interpolation breaking with non emissive textures

See merge request cable-mc/cobblemon!1551
2025-03-05 11:31:37 +00:00
Paul 39197adebb Why'd I format that like that 2025-03-05 18:45:00 +10:00
BuildTools 4b1e192705 Fixed interpolation breaking with non emmisive textures 2025-03-05 10:32:54 +02:00
Test a3fa3d4b82 Merge branch 'unlockable-wallpapers' of https://gitlab.com/cable-mc/cobblemon into unlockable-wallpapers 2025-03-05 02:44:32 -05:00
Test c5ad7a0fb7 refactor pc updates 2025-03-05 02:44:11 -05:00
Paul d17f94c0d6 Reworked way pokemon are blocked from spawning on rails to not be as intensive 2025-03-05 14:27:02 +10:00
JPAK 398bff4c15 Fix some formatting. 2025-03-03 20:48:45 -06:00
JPAK e0655df5f0 Merge remote-tracking branch 'origin/riding-settings' into riding-settings
# Conflicts:
#	common/src/main/kotlin/com/cobblemon/mod/common/api/riding/controller/RideController.kt
#	common/src/main/kotlin/com/cobblemon/mod/common/pokemon/riding/controllers/BirdAirController.kt
#	common/src/main/kotlin/com/cobblemon/mod/common/pokemon/riding/controllers/HelicopterAirController.kt
#	common/src/main/kotlin/com/cobblemon/mod/common/pokemon/riding/controllers/JetAirController.kt
2025-03-03 20:44:58 -06:00
JPAK 9174aac64c Added Config options and implemented the invert mouse options on current controllers. 2025-03-03 20:43:35 -06:00
JPAK 3cdc4c7d97 Added Config options and implemented the invert mouse options on current controllers. 2025-03-03 17:35:32 -06:00
Polymeta 9ce6acc44d Merge branch 'fix-hide-additional-tooltip' into 'main'
hide_additional_tooltip working for Pokerod and Bait

See merge request cable-mc/cobblemon!1547
2025-03-03 08:44:49 +00:00
Polymeta f170ed726b Merge branch 'jackowes/riding/02' into 'main'
Fix: Whole json object being passed to deserialize rather than the single JsonElement.

See merge request cable-mc/cobblemon!1549
2025-03-02 18:11:14 +00:00
JacksonB 979d6f02a6 Fix: Whole json object being passed to deserialize rather than the single JsonElement. 2025-03-02 11:32:38 -06:00
BuildTools 3fbe80cb1a Changelog 2025-03-02 17:02:07 +02:00
Polymeta 8fdb32273d Merge branch 'nuggets-of-nuggets' into 'main'
Fling

See merge request cable-mc/cobblemon!1548
2025-03-02 13:11:56 +00:00
dhi_holo d5c9d8d5b1 Fling
Blocks of Gold now can be flung for 150 BP
2025-03-02 20:57:37 +10:00
BuildTools 752c9c234e hide_additional_tooltip working for Pokerod and Bait 2025-03-02 12:10:28 +02:00
Hiroku b7b6185ada Merge branch 'jackowes/ride_cleanup' into 'main'
WIP jet, bird, and helicopter controller

See merge request cable-mc/cobblemon!1543
2025-03-02 09:31:46 +00:00
Jackson BowesandHiroku 211b886bf3 WIP jet, bird, and helicopter controller 2025-03-02 09:31:45 +00:00
Paul 3859eb574e Another tweak for Polymeta 2025-03-02 15:25:12 +10:00
Paul b4a29214a7 Minor changes to unlockable wallpapers code for Poly's happiness 2025-03-02 15:24:05 +10:00
Test ed0e0fc65a Merge branch 'main' of https://gitlab.com/cable-mc/cobblemon into unlockable-wallpapers 2025-02-23 11:04:13 -05:00
Paul c5dcfc57ea Added toast for unlocking PC wallpapers, fixed some molang issues for living entities 2025-02-21 10:26:13 +10:00
Paul 9997a5c4b4 Touch-ups to wallpaper unlocked flow and the right click platform event flows 2025-02-17 20:29:43 +10:00
Paul 2b70e5614c Merge remote-tracking branch 'origin/main' into unlockable-wallpapers
# Conflicts:
#	common/src/main/kotlin/com/cobblemon/mod/common/CobblemonFlows.kt
#	common/src/main/kotlin/com/cobblemon/mod/common/api/molang/MoLangFunctions.kt
#	common/src/main/kotlin/com/cobblemon/mod/common/events/FlowHandler.kt
2025-02-17 20:18:29 +10:00
Test 589c5381e2 Merge branch 'main' of https://gitlab.com/cable-mc/cobblemon into unlockable-wallpapers 2025-02-15 15:14:40 -05:00
Test 4539703497 prevent filenotfoundexception; update colors 2025-02-15 15:14:23 -05:00
Paul 0152bc2e32 Added event for unlocking wallpapers, added flows for advancement_earned, right_clicked_entity, right_clicked_block, player_died, wallpaper_unlocked 2025-02-10 18:13:27 +10:00
Paul e175c44118 Added unlockable PC box wallpapers 2025-02-10 15:36:46 +10:00
399 changed files with 16167 additions and 2047 deletions
+17 -12
View File
@@ -1,16 +1,21 @@
### Additions
- Added ability to rename PC Boxes by clicking on the name of a box
- Added ability to set wallpapers per PC Box by clicking the button right to the box name
- Added sorting by Name, Level and type by clicking the button left of the box name (shift reverses order)
- Added filtering/search functionality in PC UI, which also supports pokemon properties (e.g. shiny=yes shows all shinies)
- Added /changewallpaper <player> <boxNumber> <wallpaper> command to change a wallpaper through commands
- Added /renamebox <player> <boxNumber> <name> command to rename a PC box through commands
- Added ability to rename PC Boxes by clicking on the name of a box.
- Added box options buttons for boxes, toggleable by clicking the right icon button in the bottom bar.
- The button on the right allows for changing the box wallpaper.
- The option buttons on the left side allow for sorting the box by name, level, Pokédex number, gender, and type. Shift clicking allows for sorting in reverse order.
- Added filter functionality in PC UI, which supports `PokemonProperties` (e.g. `shiny=yes` shows all shiny Pokémon).
- Added `/changewallpaper <player> <boxNumber> <wallpaper>` command to change a box wallpaper through commands.
- Added `/renamebox <player> <boxNumber> <name>` command to rename a PC box through commands.
### Developer
- Added `RenamePCBoxEvent.Pre` and `RenamePCBoxEvent.Post` events to prevent players from renaming a box or changing their input.
- Added `ChangePCBoxWallpaperEvent.Pre` and `ChangePCBoxWallpaperEvent.Post` to prevent players from changing wallpapers or changing their selection.
- Added `WallpaperCollectionEvent` which gets called when clients connect to a server, allowing the server to handle which of the client-found wallpapers it's allowed to move (collected wallpapers can be removed for example to make it "vanish" client-side).
- Added `WallpaperUnlockedEvent`.
- Renamed `SetPCBoxPokemonPacket` and the respective handler to `SetPCBoxPacket`.
- Added RenamePCBoxEvent.Pre/Post events to prevent players from renaming a box or change their input
- Added ChangePCBoxWallpaperEvent.Pre/Post to prevent players from changing to a wallpaper or change their selection
- Added WallpaperCollectionEvent which gets called when clients connect to a server, allowing the server which of the clients found wallpapers it's allowed to move (collected wallpapers can be removed for example to make it "vanish" client-side)
- Wallpapers are loaded from `assets/<namespace>/textures/gui/pc/wallpaper/` and all wallpapers in this folder are available to the client to choose by default
- Renamed `SetPCBoxPokemonPacket` and the respective handler to `SetPCBoxPacket`
### Addons
- Added Flows for `player_tick_pre`, `player_tick_post`, `advancement_earned`, `right_clicked_entity`, `right_clicked_block`, `player_died`, `wallpaper_unlocked`.
- Flows now have `q.` for any context properties in addition to the `c.` properties, making it a bit more convenient when calling other scripts from Flows.
- Added `unlockable_pc_box_wallpapers` datapack folder for wallpapers that need to be unlocked before they're usable.
- Wallpapers are loaded from `assets/<namespace>/textures/gui/pc/wallpaper/` and all wallpapers in this folder are available to the client to choose by default.
+2 -1
View File
@@ -1,3 +1,4 @@
### Additions
- Added the ability to ride Pokémon by shift-right clicking and selecting the ride option.
- Added the ability to ride Pokémon by shift-right clicking and selecting the ride option.
- Added /calculateseatpositions command for approximating hitbox locations for riding configurations.
+21 -3
View File
@@ -13,6 +13,11 @@
### Changes
- Renamed `chargeGainedPerTick` config to `secondsToChargeHealingMachine`.
- Made Blocks of Gold count as Big Nuggets when held by a Pokémon (for Fling functionality)
- Substantially optimised spawning checks mainly by front-loading biome filtering.
- When using the `cobblemon` or `generation_9` capture calculators a critical capture with a single shake will always play for successful captures when you've already registered the Pokémon as caught in your Pokédex.
- Improved the performance of saving Pokédex and player data.
- Pokémon hitbox now scales with entity attribute `generic.scale`.
### Fixes
- Fixed Particles sometimes facing the wrong direction (looking at you, Swords Dance)
@@ -34,6 +39,12 @@
- Fixed owned Pokémon sometimes being un-interactable after the player relogs fast
- Fixed field name in evolution requirements for Spewpa Pokeball.
- Fixed LevelUpCriterion logic to correctly check that the Pokémon is a preEvo.
- Fixed `hide_additional_tooltip` vanilla flag not properly hiding tooltips on pokerod and bait items
- Fixed NPCs using Pokémon outside of their pool when a Pokémon name had a typo.
- Fixed an issue with datapacked species features not being applied properly when relogging.
- Fixed Pokémon marked as silent still playing shiny sounds and effects.
- Fixed an issue with newer versions of Fabric API where underground Pokémon were spawning in The End.
- Fixed spawning not working well when you're at high points surrounded by lower altitude spawning areas, such as flying.
### Developer
- A finished battle now has winners and losers set inside of `PokemonBattle` instead of them always being empty.
@@ -44,9 +55,13 @@
- Removed the NbtItemPredicate class, all the mod usages now use the vanilla item predicate solution, this causes breaking changes on Fossil, HeldItemRequirement & ItemInteractionEvolution
- Renamed Cobblemon's creative tabs to start with "Cobblemon: " to distinguish Cobblemon's tabs from tabs for other mods.
- Various items now have a rarity value.
### Changes
- When using the `cobblemon` or `generation_9` capture calculators a critical capture with a single shake will always play for successful captures when you've already registered the Pokémon as caught in your Pokédex.
- Reworked observable handling in `Pokemon.kt` to cut down on RAM usage and clarify the file.
- Note: This will break mods that used our observable functionality there or in MoveSet, IVs, EVs, or BenchedMoves.
- Using `Pokemon#onChange()` is now the way to mark a Pokémon as needing a save.
- Using `[Pokemon].changeObservable` is now the way to get an `Observable` for any save-worthy changes.
- Updated NPCEntity beam positioning to properly account for the baseScale property.
- Updated NPCEntity pokeball throw positioning to properly account for the baseScale property.
- Fixed `[Pokemon].copyFrom` error causing forms, IVs, and EVs to not be applied properly when using `[Pokemon].loadFromJSON` or `[Pokemon].loadFromNBT`
### MoLang & Datapacks
- The following usages for item predicates can now use item conditions like advancements do, you can learn about them in the [Minecraft wiki](https://minecraft.wiki/w/Advancement_definition#minecraft:filled_bucket)
@@ -55,6 +70,9 @@
- The `fossils` for a fossil entry
- Added MoLang flows for `poke_ball_capture_calculated`, `evolution_tested`, `evolution_accepted`, `evolution_completed`
- Added `interpolate` boolean property to animated textures to allow gradual colour changes between frames.
- Fixed species additions not being capable of changing implemented status.
- Added support for action effects that are triggered by `|-activate|` Showdown instructions. `activate_{effect_id}` is the syntax.
- Added Molang functions for rendering items `render_item(item_id, locator_name)` and `clear_items()`.
## [1.6.1 (January 26th, 2025)](#1-6-1)
@@ -9,6 +9,9 @@
package com.cobblemon.mod.common;
import net.minecraft.util.Mth;
import net.minecraft.util.SmoothDouble;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.phys.Vec3;
import org.jetbrains.annotations.Nullable;
import org.joml.Matrix3f;
import org.joml.Vector3f;
@@ -117,8 +120,24 @@ public interface Rollable {
return (float) Mth.wrapDegrees(-Math.toDegrees(getUpVector().angleSigned(UP, getForwardVector())));
}
/**
* Get the angular velocity in degrees per second as a Vec3
*/
Vec3 angRollVel( double deltaTime );
public Vec3 rotationOnMouseXY(
double yMouse,
double xMouse,
SmoothDouble yMouseSmoother,
SmoothDouble xMouseSmoother,
double sensitivity,
double deltaTime
);
boolean shouldRoll();
boolean useAngVelSmoothing();
void clearRotation();
}
@@ -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.mixin;
import net.minecraft.client.player.LocalPlayer;
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity;
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.CallbackInfoReturnable;
@Mixin(LocalPlayer.class)
public abstract class LocalPlayerMixin {
@Shadow
private float jumpRidingScale;
@Inject(method = "getJumpRidingScale", at = @At("HEAD"), cancellable = true)
public void modifyJumpRidingScale(CallbackInfoReturnable<Float> cir) {
LocalPlayer player = (LocalPlayer) (Object) this;
if (player.isPassenger() && player.getVehicle() instanceof PokemonEntity) {
PokemonEntity ride = (PokemonEntity) player.getVehicle();
//Use custom jump bar logic if the current ride does not jump using it.
if( !ride.getRiding().canJump(ride, player))
{
cir.setReturnValue(ride.setRideBar());
}
}
}
}
@@ -0,0 +1,30 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.mixin;
import com.cobblemon.mod.common.platform.events.PlatformEvents;
import com.cobblemon.mod.common.platform.events.ServerPlayerEvent;
import net.minecraft.advancements.AdvancementHolder;
import net.minecraft.server.PlayerAdvancements;
import net.minecraft.server.level.ServerPlayer;
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.CallbackInfoReturnable;
@Mixin(PlayerAdvancements.class)
public class PlayerAdvancementsMixin {
@Shadow private ServerPlayer player;
@Inject(method = "award", at = @At(value = "INVOKE", target = "Lnet/minecraft/advancements/AdvancementRewards;grant(Lnet/minecraft/server/level/ServerPlayer;)V"))
private void cobblemon$award(AdvancementHolder advancement, String criterionKey, CallbackInfoReturnable<Boolean> cir) {
PlatformEvents.SERVER_PLAYER_ADVANCEMENT_EARNED.emit(new ServerPlayerEvent.AdvancementEarned(player, advancement));
}
}
@@ -38,6 +38,7 @@ import net.minecraft.nbt.Tag;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.util.SmoothDouble;
import net.minecraft.world.damagesource.DamageSource;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.LivingEntity;
@@ -47,6 +48,7 @@ import net.minecraft.world.food.FoodProperties;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.Vec3;
import org.jetbrains.annotations.Nullable;
import org.joml.Matrix3f;
import org.objectweb.asm.Opcodes;
@@ -55,6 +57,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.ModifyArg;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@@ -271,6 +274,7 @@ public abstract class PlayerMixin extends LivingEntity implements Rollable, Scan
@Override
public Rollable updateOrientation(Function<Matrix3f, Matrix3f> update) {
//Disabled to allow non mouse inputs to modify roll
if (!shouldRoll()) return this;
if (this.orientation == null) {
@@ -296,6 +300,60 @@ public abstract class PlayerMixin extends LivingEntity implements Rollable, Scan
return pokemonEntity.getRiding().shouldRoll(pokemonEntity);
}
@Override
public Vec3 rotationOnMouseXY(
double yMouse,
double xMouse,
SmoothDouble yMouseSmoother,
SmoothDouble xMouseSmoother,
double sensitivity,
double deltaTime
) {
LocalPlayer player = Minecraft.getInstance().player;
if (!this.equals(player)) return Vec3.ZERO;
var playerVehicle = player.getVehicle();
if (playerVehicle == null) return Vec3.ZERO;
if (!(playerVehicle instanceof PokemonEntity pokemonEntity)) return Vec3.ZERO;
return pokemonEntity.getRiding().rotationOnMouseXY(
pokemonEntity,
player,
yMouse,
xMouse,
yMouseSmoother,
xMouseSmoother,
sensitivity,
deltaTime
);
}
@Override
public boolean useAngVelSmoothing() {
LocalPlayer player = Minecraft.getInstance().player;
if (!this.equals(player)) return true;
var playerVehicle = player.getVehicle();
if (playerVehicle == null) return true;
if (!(playerVehicle instanceof PokemonEntity pokemonEntity)) return true;
return pokemonEntity.getRiding().useAngVelSmoothing(pokemonEntity);
}
@Override
public Vec3 angRollVel( double deltaTime ) {
var ret = new Vec3(0.0, 0.0, 0.0);
LocalPlayer player = Minecraft.getInstance().player;
if (!this.equals(player)) return ret;
var playerVehicle = player.getVehicle();
if (playerVehicle == null) return ret;
if (!(playerVehicle instanceof PokemonEntity pokemonEntity)) return ret;
return pokemonEntity.getRiding().angRollVel( pokemonEntity, player, deltaTime );
}
@Override
public void clearRotation() {
this.orientation = null;
@@ -9,8 +9,10 @@
package com.cobblemon.mod.common.mixin.client;
import com.cobblemon.mod.common.client.CobblemonClient;
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity;
import com.cobblemon.mod.common.pokedex.scanner.PokedexUsageContext;
import net.minecraft.client.player.AbstractClientPlayer;
import net.minecraft.client.player.LocalPlayer;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
@@ -30,5 +32,13 @@ public class AbstractClientPlayerMixin {
if (usageContext.getScanningGuiOpen()) {
cir.setReturnValue(usageContext.getFovMultiplier());
}
LocalPlayer player = (LocalPlayer) (Object) this;
if (player != null && player.isPassenger() && player.getVehicle() instanceof PokemonEntity) {
PokemonEntity ride = (PokemonEntity) player.getVehicle();
//Return custom fov mult
cir.setReturnValue(ride.rideFovMult());
}
}
}
@@ -14,11 +14,13 @@ import com.cobblemon.mod.common.client.keybind.keybinds.PartySendBinding;
import com.cobblemon.mod.common.pokedex.scanner.PokedexUsageContext;
import com.llamalad7.mixinextras.injector.v2.WrapWithCondition;
import com.llamalad7.mixinextras.sugar.Local;
import com.mojang.blaze3d.Blaze3D;
import net.minecraft.client.Minecraft;
import net.minecraft.client.MouseHandler;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.util.Mth;
import net.minecraft.util.SmoothDouble;
import net.minecraft.world.phys.Vec3;
import org.objectweb.asm.Opcodes;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
@@ -33,8 +35,11 @@ public class MouseHandlerMixin {
@Shadow private double accumulatedScrollY;
@Shadow @Final private Minecraft minecraft;
@Unique SmoothDouble xMouseSmoother = new SmoothDouble();
@Unique SmoothDouble yMouseSmoother = new SmoothDouble();
@Unique SmoothDouble pitchSmoother = new SmoothDouble();
@Unique SmoothDouble rollSmoother = new SmoothDouble();
@Unique SmoothDouble yawSmoother = new SmoothDouble();
@Shadow @Final private SmoothDouble smoothTurnY;
@@ -44,6 +49,8 @@ public class MouseHandlerMixin {
@Shadow private double accumulatedDY;
@Shadow private double accumulatedScrollX;
@Inject(
method = "onScroll",
at = @At(
@@ -95,7 +102,13 @@ public class MouseHandlerMixin {
target = "Lnet/minecraft/client/player/LocalPlayer;turn(DD)V"
)
)
public boolean cobblemon$modifyRotation(LocalPlayer player, double cursorDeltaX, double cursorDeltaY, @Local(argsOnly = true) double d) {
public boolean cobblemon$modifyRotation(
LocalPlayer player,
double cursorDeltaX,
double cursorDeltaY,
@Local(argsOnly = true) double d,
@Local(argsOnly = true) double movementTime
) {
PokedexUsageContext usageContext = CobblemonClient.INSTANCE.getPokedexUsageContext();
if (usageContext.getScanningGuiOpen()) {
this.smoothTurnY.reset();
@@ -109,16 +122,66 @@ public class MouseHandlerMixin {
}
if (!(player instanceof Rollable rollable)) return true;
if (!rollable.shouldRoll()) {
xMouseSmoother.reset();
yMouseSmoother.reset();
pitchSmoother.reset();
rollSmoother.reset();
yawSmoother.reset();
return true;
}
var pitch = pitchSmoother.getNewDeltaValue(cursorDeltaY * 0.15f, d);
var roll = rollSmoother.getNewDeltaValue(cursorDeltaX * 0.15f, d);
rollable.rotate(0.0F, (float)pitch, (float)roll);
var defaultSensitivity = this.minecraft.options.sensitivity().get() * 0.6000000238418579 + 0.20000000298023224;
var ridingSensitivity = Math.pow(defaultSensitivity, 3);
//Send mouse input to be interpreted into rotation
//deltas by the ride controller
Vec3 angVecMouse = rollable.rotationOnMouseXY(
cursorDeltaY,
cursorDeltaX ,
yMouseSmoother,
xMouseSmoother,
ridingSensitivity,
movementTime
);
//Perform Rotation using mouse influenced rotation deltas.
rollable.rotate(
(float) angVecMouse.x,
(float) angVecMouse.y,
(float) angVecMouse.z
);
//Gather and apply the current rotation deltas
var angRot = rollable.angRollVel(movementTime);
//Apply smoothing if requested by the controller.
//This Might be best if done by the controller itself?
if( rollable.useAngVelSmoothing() )
{
if( angRot != null ) {
var yaw = yawSmoother.getNewDeltaValue(angRot.x * 0.5f, d);
var pitch = pitchSmoother.getNewDeltaValue(angRot.y * 0.5f, d);
var roll = rollSmoother.getNewDeltaValue(angRot.z * 0.5f, d);
rollable.rotate((float) yaw, (float) pitch, (float) roll);
}
}
//Otherwise simply apply the smoothing
else
{
if( angRot != null ) {
rollable.rotate((float) (
angRot.x * 10 * d),
(float) (angRot.y * 10 * d),
(float) (angRot.z * 10 * d)
);
}
}
return false;
}
@Inject(method = "handleAccumulatedMovement", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/MouseHandler;isMouseGrabbed()Z", ordinal = 0))
@@ -19,6 +19,7 @@ import com.cobblemon.mod.common.api.drop.ItemDropEntry
import com.cobblemon.mod.common.api.events.CobblemonEvents
import com.cobblemon.mod.common.api.events.CobblemonEvents.DATA_SYNCHRONIZED
import com.cobblemon.mod.common.api.interaction.RequestManager
import com.cobblemon.mod.common.api.molang.ObjectValue
import com.cobblemon.mod.common.api.permission.PermissionValidator
import com.cobblemon.mod.common.api.pokeball.catching.calculators.CaptureCalculator
import com.cobblemon.mod.common.api.pokeball.catching.calculators.CaptureCalculators
@@ -62,7 +63,11 @@ 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.client.CobblemonPack
@@ -86,11 +91,23 @@ import com.cobblemon.mod.common.pokemon.aspects.SHINY_ASPECT
import com.cobblemon.mod.common.pokemon.evolution.variants.BlockClickEvolution
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.NoAIProperty
import com.cobblemon.mod.common.pokemon.properties.UnaspectPropertyType
import com.cobblemon.mod.common.pokemon.properties.UncatchableProperty
import com.cobblemon.mod.common.pokemon.properties.tags.PokemonFlagProperty
import com.cobblemon.mod.common.pokemon.stat.CobblemonStatProvider
import com.cobblemon.mod.common.starter.CobblemonStarterHandler
import com.cobblemon.mod.common.util.*
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
@@ -98,27 +115,29 @@ 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
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import net.minecraft.server.packs.PackType
import net.minecraft.world.entity.player.Player
import net.minecraft.world.item.NameTagItem
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 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
@@ -152,7 +171,9 @@ object Cobblemon {
var permissionValidator: PermissionValidator by Delegates.observable(LaxPermissionValidator().also { it.initialize() }) { _, _, newValue -> newValue.initialize() }
var statProvider: StatProvider = CobblemonStatProvider
var seasonResolver: SeasonResolver = TagSeasonResolver
var wallpapers = mapOf<UUID, List<ResourceLocation>>()
var wallpapers = mutableMapOf<UUID, Set<ResourceLocation>>()
val serverPlayerStructs = mutableMapOf<UUID, ObjectValue<Player>>()
@JvmStatic
val builtinPacks = listOf<CobblemonPack>(
@@ -218,6 +239,7 @@ object Cobblemon {
storage.onPlayerDisconnect(it.player)
playerDataManager.onPlayerDisconnect(it.player)
RequestManager.onLogoff(it.player)
serverPlayerStructs.remove(it.player.uuid)
}
PlatformEvents.PLAYER_DEATH.subscribe {
PCLinkManager.removeLink(it.player.uuid)
@@ -387,9 +409,11 @@ object Cobblemon {
PlatformEvents.SERVER_STOPPED.subscribe {
storage.unregisterAll(it.server.registryAccess())
playerDataManager.saveAllStores()
playerDataManager.saveExecutor.shutdown()
playerDataManager.saveExecutor.awaitTermination(30L, TimeUnit.SECONDS)
}
PlatformEvents.SERVER_STARTED.subscribe {
bestSpawner.onServerStarted()
PlatformEvents.SERVER_STARTED.subscribe { event ->
bestSpawner.onServerStarted(event.server)
battleRegistry.onServerStarted()
}
PlatformEvents.SERVER_TICK_POST.subscribe { ServerTickHandler.onTick(it.server) }
@@ -537,6 +561,7 @@ object Cobblemon {
this.implementation.registerCommandArgument(cobblemonResource("form"), FormArgumentType::class, SingletonArgumentInfo.contextFree(FormArgumentType::form))
this.implementation.registerCommandArgument(cobblemonResource("dex"), DexArgumentType::class, SingletonArgumentInfo.contextFree (DexArgumentType::dex))
this.implementation.registerCommandArgument(cobblemonResource("npc_class"), NPCClassArgumentType::class, SingletonArgumentInfo.contextFree(NPCClassArgumentType::npcClass))
this.implementation.registerCommandArgument(cobblemonResource("wallpaper"), UnlockablePCBoxWallpaperArgumentType::class, SingletonArgumentInfo.contextFree(UnlockablePCBoxWallpaperArgumentType::wallpaper))
}
}
@@ -25,7 +25,6 @@ object CobblemonCommands {
ChangeWalkSpeed.register(dispatcher)
ChangeJointScaleCommand.register(dispatcher)
TestCommand.register(dispatcher)
ReloadShowdownCommand.register(dispatcher)
ClickTextCommand.register(dispatcher)
PokemonEditCommand.register(dispatcher)
TeachCommand.register(dispatcher)
@@ -61,9 +60,11 @@ object CobblemonCommands {
ChangeBoxWallpaperCommand.register(dispatcher)
RunMolangScriptCommand.register(dispatcher)
CobblemonConfigCommand.register(dispatcher)
UnlockPCBoxWallpaperCommand.register(dispatcher)
// Possibly lock down registration if and only if under dev environment or running in an environment
// with a certain system environment variable set
CobblemonInfoCommand.register(dispatcher)
CalculateSeatPositionCommand.register(dispatcher)
}
}
}
@@ -99,6 +99,9 @@ object CobblemonFlows : DataRegistry {
runtime.environment.query.addFunction("cancel") { cancelable }
}
// Most places in the mod use query structs so it's probably nicer to present the things as query values.
context.forEach { (key, value) -> runtime.environment.query.addFunction(key) { return@addFunction value } }
functions.forEach { (name, function) -> runtime.environment.query.addFunction(name, function) }
flows[eventResourceLocation]?.forEach {
@@ -1278,6 +1278,7 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
CobblemonHeldItemManager.registerRemap(it, remappedName)
CobblemonHeldItemManager.registerRemap(Items.BONE, "thickclub")
CobblemonHeldItemManager.registerRemap(Items.SNOWBALL, "snowball")
CobblemonHeldItemManager.registerRemap(Items.GOLD_BLOCK, "bignugget")
}
}
)
@@ -9,6 +9,7 @@
package com.cobblemon.mod.common
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.client.net.CalculateSeatPositionsHandler
import com.cobblemon.mod.common.client.net.PlayerInteractOptionsHandler
import com.cobblemon.mod.common.client.net.SetClientPlayerDataHandler
import com.cobblemon.mod.common.client.net.animation.PlayPosableAnimationHandler
@@ -18,6 +19,7 @@ import com.cobblemon.mod.common.client.net.callback.party.OpenPartyCallbackHandl
import com.cobblemon.mod.common.client.net.callback.partymove.OpenPartyMoveCallbackHandler
import com.cobblemon.mod.common.client.net.data.DataRegistrySyncPacketHandler
import com.cobblemon.mod.common.client.net.data.UnlockReloadPacketHandler
import com.cobblemon.mod.common.client.net.debug.showdown.ShowdownRefreshInfoHandler
import com.cobblemon.mod.common.client.net.dialogue.DialogueClosedHandler
import com.cobblemon.mod.common.client.net.dialogue.DialogueOpenedHandler
import com.cobblemon.mod.common.client.net.effect.RunPosableMoLangHandler
@@ -47,15 +49,9 @@ import com.cobblemon.mod.common.client.net.storage.party.SetPartyPokemonHandler
import com.cobblemon.mod.common.client.net.storage.party.SetPartyReferenceHandler
import com.cobblemon.mod.common.client.net.storage.pc.*
import com.cobblemon.mod.common.client.net.toast.ToastPacketHandler
import com.cobblemon.mod.common.client.net.trade.TradeAcceptanceChangedHandler
import com.cobblemon.mod.common.client.net.trade.TradeCancelledHandler
import com.cobblemon.mod.common.client.net.trade.TradeCompletedHandler
import com.cobblemon.mod.common.client.net.trade.TradeOfferExpiredHandler
import com.cobblemon.mod.common.client.net.trade.TradeOfferNotificationHandler
import com.cobblemon.mod.common.client.net.trade.TradeProcessStartedHandler
import com.cobblemon.mod.common.client.net.trade.TradeStartedHandler
import com.cobblemon.mod.common.client.net.trade.TradeUpdatedHandler
import com.cobblemon.mod.common.client.net.trade.*
import com.cobblemon.mod.common.net.PacketRegisterInfo
import com.cobblemon.mod.common.net.messages.client.CalculateSeatPositionsPacket
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
@@ -64,6 +60,7 @@ import com.cobblemon.mod.common.net.messages.client.callback.OpenMoveCallbackPac
import com.cobblemon.mod.common.net.messages.client.callback.OpenPartyCallbackPacket
import com.cobblemon.mod.common.net.messages.client.callback.OpenPartyMoveCallbackPacket
import com.cobblemon.mod.common.net.messages.client.data.*
import com.cobblemon.mod.common.net.messages.client.debug.showdown.ShowdownRefreshInfoPacket
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.RunPosableMoLangPacket
@@ -94,41 +91,18 @@ import com.cobblemon.mod.common.net.messages.client.storage.party.InitializePart
import com.cobblemon.mod.common.net.messages.client.storage.party.MoveClientPartyPokemonPacket
import com.cobblemon.mod.common.net.messages.client.storage.party.SetPartyPokemonPacket
import com.cobblemon.mod.common.net.messages.client.storage.party.SetPartyReferencePacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.ClosePCPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.InitializePCPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.MoveClientPCPokemonPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.OpenPCPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.RenamePCBoxPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.SetPCBoxPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.SetPCPokemonPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.*
import com.cobblemon.mod.common.net.messages.client.storage.pc.wallpaper.ChangePCBoxWallpaperPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.wallpaper.RequestPCBoxWallpapersPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.wallpaper.SetPCBoxWallpapersPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.wallpaper.UnlockPCBoxWallpaperPacket
import com.cobblemon.mod.common.net.messages.client.toast.ToastPacket
import com.cobblemon.mod.common.net.messages.client.trade.TradeAcceptanceChangedPacket
import com.cobblemon.mod.common.net.messages.client.trade.TradeCancelledPacket
import com.cobblemon.mod.common.net.messages.client.trade.TradeCompletedPacket
import com.cobblemon.mod.common.net.messages.client.trade.TradeOfferExpiredPacket
import com.cobblemon.mod.common.net.messages.client.trade.TradeOfferNotificationPacket
import com.cobblemon.mod.common.net.messages.client.trade.TradeProcessStartedPacket
import com.cobblemon.mod.common.net.messages.client.trade.TradeStartedPacket
import com.cobblemon.mod.common.net.messages.client.trade.TradeUpdatedPacket
import com.cobblemon.mod.common.net.messages.client.trade.*
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.battle.BattleSelectActionsPacket
import com.cobblemon.mod.common.net.messages.server.battle.BattleTeamLeavePacket
import com.cobblemon.mod.common.net.messages.server.battle.BattleTeamRequestPacket
import com.cobblemon.mod.common.net.messages.server.battle.BattleTeamResponsePacket
import com.cobblemon.mod.common.net.messages.server.battle.RemoveSpectatorPacket
import com.cobblemon.mod.common.net.messages.server.battle.SpectateBattlePacket
import com.cobblemon.mod.common.net.messages.server.*
import com.cobblemon.mod.common.net.messages.server.battle.*
import com.cobblemon.mod.common.net.messages.server.block.AdjustBlockEntityViewerCountPacket
import com.cobblemon.mod.common.net.messages.server.callback.move.MoveSelectCancelledPacket
import com.cobblemon.mod.common.net.messages.server.callback.move.MoveSelectedPacket
@@ -136,6 +110,9 @@ import com.cobblemon.mod.common.net.messages.server.callback.party.PartyPokemonS
import com.cobblemon.mod.common.net.messages.server.callback.party.PartySelectCancelledPacket
import com.cobblemon.mod.common.net.messages.server.callback.partymove.PartyMoveSelectCancelledPacket
import com.cobblemon.mod.common.net.messages.server.callback.partymove.PartyPokemonMoveSelectedPacket
import com.cobblemon.mod.common.net.messages.server.debug.showdown.RequestShowdownInfoPacket
import com.cobblemon.mod.common.net.messages.server.debug.showdown.ShowdownActionPacket
import com.cobblemon.mod.common.net.messages.server.debug.showdown.ShowdownInputPacket
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.npc.SaveNPCPacket
@@ -155,21 +132,11 @@ import com.cobblemon.mod.common.net.messages.server.storage.party.MovePartyPokem
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.*
import com.cobblemon.mod.common.net.messages.server.trade.AcceptTradeRequestPacket
import com.cobblemon.mod.common.net.messages.server.trade.CancelTradePacket
import com.cobblemon.mod.common.net.messages.server.trade.ChangeTradeAcceptancePacket
import com.cobblemon.mod.common.net.messages.server.trade.OfferTradePacket
import com.cobblemon.mod.common.net.messages.server.trade.PerformTradePacket
import com.cobblemon.mod.common.net.messages.server.trade.UpdateTradeOfferPacket
import com.cobblemon.mod.common.net.messages.server.trade.*
import com.cobblemon.mod.common.net.serverhandling.ChallengeHandler
import com.cobblemon.mod.common.net.serverhandling.ChallengeResponseHandler
import com.cobblemon.mod.common.net.serverhandling.RequestInteractionsHandler
import com.cobblemon.mod.common.net.serverhandling.battle.BattleSelectActionsHandler
import com.cobblemon.mod.common.net.serverhandling.battle.RemoveSpectatorHandler
import com.cobblemon.mod.common.net.serverhandling.battle.SpectateBattleHandler
import com.cobblemon.mod.common.net.serverhandling.battle.TeamLeaveHandler
import com.cobblemon.mod.common.net.serverhandling.battle.TeamRequestHandler
import com.cobblemon.mod.common.net.serverhandling.battle.TeamRequestResponseHandler
import com.cobblemon.mod.common.net.serverhandling.battle.*
import com.cobblemon.mod.common.net.serverhandling.block.AdjustBlockEntityViewerCountHandler
import com.cobblemon.mod.common.net.serverhandling.callback.move.MoveSelectCancelledHandler
import com.cobblemon.mod.common.net.serverhandling.callback.move.MoveSelectedHandler
@@ -177,6 +144,9 @@ import com.cobblemon.mod.common.net.serverhandling.callback.party.PartyPokemonSe
import com.cobblemon.mod.common.net.serverhandling.callback.party.PartySelectCancelledHandler
import com.cobblemon.mod.common.net.serverhandling.callback.partymove.PartyMoveSelectCancelledHandler
import com.cobblemon.mod.common.net.serverhandling.callback.partymove.PartyPokemonMoveSelectedHandler
import com.cobblemon.mod.common.net.serverhandling.debug.showdown.RequestShowdownInfoHandler
import com.cobblemon.mod.common.net.serverhandling.debug.showdown.ShowdownActionHandler
import com.cobblemon.mod.common.net.serverhandling.debug.showdown.ShowdownInputHandler
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.evolution.AcceptEvolutionHandler
@@ -200,12 +170,7 @@ import com.cobblemon.mod.common.net.serverhandling.storage.party.MovePartyPokemo
import com.cobblemon.mod.common.net.serverhandling.storage.party.ReleasePCPokemonHandler
import com.cobblemon.mod.common.net.serverhandling.storage.party.SwapPartyPokemonHandler
import com.cobblemon.mod.common.net.serverhandling.storage.pc.*
import com.cobblemon.mod.common.net.serverhandling.trade.AcceptTradeRequestHandler
import com.cobblemon.mod.common.net.serverhandling.trade.CancelTradeHandler
import com.cobblemon.mod.common.net.serverhandling.trade.ChangeTradeAcceptanceHandler
import com.cobblemon.mod.common.net.serverhandling.trade.OfferTradeHandler
import com.cobblemon.mod.common.net.serverhandling.trade.PerformTradeHandler
import com.cobblemon.mod.common.net.serverhandling.trade.UpdateTradeOfferHandler
import com.cobblemon.mod.common.net.serverhandling.trade.*
import com.cobblemon.mod.common.util.server
import net.minecraft.server.level.ServerPlayer
@@ -287,6 +252,7 @@ object CobblemonNetwork {
list.add(PacketRegisterInfo(RequestPCBoxWallpapersPacket.ID, RequestPCBoxWallpapersPacket::decode, RequestPCBoxWallpapersHandler))
list.add(PacketRegisterInfo(SetPCBoxWallpapersPacket.ID, SetPCBoxWallpapersPacket::decode, SetPCBoxWallpapersHandler))
list.add(PacketRegisterInfo(ChangePCBoxWallpaperPacket.ID, ChangePCBoxWallpaperPacket::decode, ChangePCBoxWallpaperHandler))
list.add(PacketRegisterInfo(UnlockPCBoxWallpaperPacket.ID, UnlockPCBoxWallpaperPacket::decode, UnlockPCBoxWallpaperHandler))
// UI Packets
list.add(PacketRegisterInfo(SummaryUIPacket.ID, SummaryUIPacket::decode, SummaryUIPacketHandler))
@@ -405,6 +371,11 @@ object CobblemonNetwork {
// Pokédex scanning
list.add(PacketRegisterInfo(ServerConfirmedRegisterPacket.ID, ServerConfirmedRegisterPacket::decode, ServerConfirmedRegisterHandler))
// Debug / cheats
list.add(PacketRegisterInfo(CalculateSeatPositionsPacket.ID, CalculateSeatPositionsPacket::decode, CalculateSeatPositionsHandler))
list.add(PacketRegisterInfo(ShowdownRefreshInfoPacket.ID, ShowdownRefreshInfoPacket::decode, ShowdownRefreshInfoHandler))
return list
}
@@ -440,6 +411,7 @@ object CobblemonNetwork {
list.add(PacketRegisterInfo(RequestRenamePCBoxPacket.Companion.ID, RequestRenamePCBoxPacket.Companion::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))
// Starter packets
list.add(PacketRegisterInfo(SelectStarterPacket.ID, SelectStarterPacket::decode, SelectStarterPacketHandler))
@@ -502,6 +474,11 @@ object CobblemonNetwork {
// Orientation packet(s)
list.add(PacketRegisterInfo(C2SUpdateOrientationPacket.ID, C2SUpdateOrientationPacket::decode, OrientationPacketHandler))
// Debug / cheats
list.add(PacketRegisterInfo(RequestShowdownInfoPacket.ID, RequestShowdownInfoPacket::decode, RequestShowdownInfoHandler))
list.add(PacketRegisterInfo(ShowdownInputPacket.ID, ShowdownInputPacket::decode, ShowdownInputHandler))
list.add(PacketRegisterInfo(ShowdownActionPacket.ID, ShowdownActionPacket::decode, ShowdownActionHandler))
return list
}
@@ -40,6 +40,8 @@ object CobblemonSounds : PlatformRegistry<Registry<SoundEvent>, ResourceKey<Regi
val PC_RELEASE = this.create("pc.release")
@JvmField
val PC_CLICK = this.create("pc.click")
@JvmField
val PC_WALLPAPER_UNLOCK = this.create("pc.wallpaper.unlock")
@JvmField
val VILLAGER_WORK_NURSE = this.create("entity.villager.work_nurse")
@@ -0,0 +1,63 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common
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.reactive.SimpleObservable
import com.cobblemon.mod.common.api.storage.pc.UnlockablePCWallpaper
import com.cobblemon.mod.common.util.adapters.ExpressionAdapter
import com.cobblemon.mod.common.util.adapters.ExpressionLikeAdapter
import com.cobblemon.mod.common.util.adapters.IdentifierAdapter
import com.cobblemon.mod.common.util.adapters.TextAdapter
import com.cobblemon.mod.common.util.cobblemonResource
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.reflect.TypeToken
import net.minecraft.network.chat.Component
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import net.minecraft.server.packs.PackType
/**
* A data registry for unlockable wallpapers that can be applied to a PC box. This is different to a resource packed
* wallpaper, as those are purely client-side and don't require any unlocking.
*
* See docs/pc_wallpapers.md
*
* @author Hiroku
* @since February 9th, 2025
*/
object CobblemonUnlockableWallpapers : JsonDataRegistry<UnlockablePCWallpaper> {
override val gson: Gson = GsonBuilder()
.setPrettyPrinting()
.registerTypeAdapter(Expression::class.java, ExpressionAdapter)
.registerTypeAdapter(ExpressionLike::class.java, ExpressionLikeAdapter)
.registerTypeAdapter(ResourceLocation::class.java, IdentifierAdapter)
.registerTypeAdapter(Component::class.java, TextAdapter)
.create()
override val typeToken = TypeToken.get(UnlockablePCWallpaper::class.java)
override val resourcePath = "unlockable_pc_box_wallpapers"
override val id: ResourceLocation = cobblemonResource("unlockable_pc_box_wallpapers")
override val type = PackType.SERVER_DATA
override val observable = SimpleObservable<CobblemonUnlockableWallpapers>()
val unlockableWallpapers = mutableMapOf<ResourceLocation, UnlockablePCWallpaper>()
override fun sync(player: ServerPlayer) { /* These don't sync, they're applied to a PC instance from which they're hooked into RequestChangePCBoxWallpaperHandler. */ }
override fun reload(data: Map<ResourceLocation, UnlockablePCWallpaper>) {
unlockableWallpapers.clear()
data.forEach { (id, value) -> value.id = id }
unlockableWallpapers.putAll(data)
observable.emit(this)
}
}
@@ -28,7 +28,7 @@ class BattleCountableCriterion(
val CODEC: Codec<BattleCountableCriterion> = RecordCodecBuilder.create { it.group(
ContextAwarePredicate.CODEC.optionalFieldOf("player").forGetter(BattleCountableCriterion::playerCtx),
Codec.STRING.listOf().optionalFieldOf("battle_types", listOf("any")).forGetter(BattleCountableCriterion::battleTypes),
Codec.INT.optionalFieldOf( "count", 0).forGetter(BattleCountableCriterion::count)
Codec.INT.optionalFieldOf("count", 0).forGetter(BattleCountableCriterion::count)
).apply(it, ::BattleCountableCriterion) }
}
@@ -37,7 +37,7 @@ object Abilities : DataRegistry {
PotentialAbility.types.add(CommonAbilityType)
PotentialAbility.types.add(HiddenAbilityType)
this.abilityMap.clear()
val abilitiesJson = ShowdownService.service.getAbilityIds()
val abilitiesJson = ShowdownService.get().getAbilityIds()
for (i in 0 until abilitiesJson.size()) {
val id = abilitiesJson[i].asString
val ability = AbilityTemplate(id)
@@ -67,4 +67,4 @@ object Abilities : DataRegistry {
abilities.forEach { ability -> this.register(ability) }
}
}
}
@@ -15,10 +15,7 @@ import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.Cobblemon.LOGGER
import com.cobblemon.mod.common.CobblemonNetwork
import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.actor.ActorType
import com.cobblemon.mod.common.api.battles.model.actor.BattleActor
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.battles.model.actor.*
import com.cobblemon.mod.common.api.events.CobblemonEvents
import com.cobblemon.mod.common.api.events.battles.BattleFledEvent
import com.cobblemon.mod.common.api.molang.MoLangFunctions.asMoLangValue
@@ -74,7 +71,9 @@ import net.minecraft.world.item.ItemStack
open class PokemonBattle(
val format: BattleFormat,
val side1: BattleSide,
val side2: BattleSide
val side2: BattleSide,
val service: ShowdownService = ShowdownService.get(),
val battleId: UUID = UUID.randomUUID()
) {
/** Whether logging will be silenced for this battle. */
var mute = true
@@ -109,8 +108,6 @@ open class PokemonBattle(
val players = playerUUIDs.mapNotNull { it.getPlayer() }
val spectators = mutableSetOf<UUID>()
val battleId = UUID.randomUUID()
val showdownMessages = mutableListOf<String>()
val battleLog = mutableListOf<String>()
val chatLog = mutableListOf<Component>()
@@ -225,7 +222,7 @@ open class PokemonBattle(
fun writeShowdownAction(vararg messages: String) {
log(messages.joinToString("\n"))
ShowdownService.service.send(battleId, messages.toList().toTypedArray())
service.send(battleId, messages.toList().toTypedArray())
}
fun turn(newTurnNumber: Int) {
@@ -378,7 +375,6 @@ open class PokemonBattle(
fun dispatch(dispatcher: () -> DispatchResult) {
dispatches.add(BattleDispatch { dispatcher() })
}
fun dispatchToFront(dispatcher: () -> DispatchResult) {
@@ -502,6 +498,7 @@ open class PokemonBattle(
}
fun checkForInputDispatch() {
actors.filter { it is UnionBattleActor }.forEach { it.mustChoose = false }
if (checkForfeit()) return // ignore actors that are still choosing, their choices don't matter anymore
val readyToInput = (actors.any { !it.mustChoose && it.responses.isNotEmpty() } && actors.none { it.mustChoose })
if (readyToInput && captureActions.isEmpty()) {
@@ -558,4 +555,4 @@ open class PokemonBattle(
queryStruct.addFunction("has_rule") { params -> DoubleValue(params.getString(0) in format.ruleSet) }
return queryStruct
}
}
}
@@ -42,4 +42,4 @@ abstract class AIBattleActor(
setActionResponses(request!!.iterate(this.activePokemon) { _, _, _ -> PassActionResponse })
}
}
}
}
@@ -73,7 +73,7 @@ abstract class BattleActor(
open fun isForPlayer(serverPlayerEntity: ServerPlayer) = serverPlayerEntity.uuid in getPlayerUUIDs()
open fun isForPokemon(pokemonEntity: PokemonEntity) = activePokemon.any { it.battlePokemon?.effectedPokemon?.entity == pokemonEntity }
fun turn() {
open fun turn() {
val request = request ?: return
responses.clear()
if(activePokemon.any { it.isAlive() }) {
@@ -90,7 +90,7 @@ abstract class BattleActor(
}
}
fun upkeep() {
open fun upkeep() {
val request = request ?: return
val forceSwitchPokemon = request.forceSwitch.mapIndexedNotNull { index, b -> if (b) activePokemon[index] else null }
if (forceSwitchPokemon.isEmpty()) {
@@ -125,7 +125,7 @@ abstract class BattleActor(
battle.checkForInputDispatch()
}
fun writeShowdownResponse() {
open fun writeShowdownResponse() {
val showdownMessages = mutableListOf<String>()
var index = 0
request!!.iterate(activePokemon) { activeBattlePokemon, showdownMoveSet, forceSwitch ->
@@ -159,4 +159,4 @@ abstract class BattleActor(
open fun win(otherWinners: List<BattleActor>, losers: List<BattleActor>) {}
open fun lose(winners: List<BattleActor>, otherLosers: List<BattleActor>) {}
}
}
@@ -0,0 +1,25 @@
package com.cobblemon.mod.common.api.battles.model.actor
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.entity.npc.NPCBattleActor
import com.cobblemon.mod.common.entity.npc.NPCEntity
class UnionBattleActor(npc: NPCEntity, pokemonList: List<BattlePokemon>) : NPCBattleActor(npc, pokemonList.toMutableList(), 1) {
override fun writeShowdownResponse() {}
override fun onChoiceRequested() {
this.mustChoose = false
}
override fun turn() {
super.turn()
this.mustChoose = false
}
override fun upkeep() {
super.upkeep()
this.mustChoose = false
}
}
@@ -8,6 +8,7 @@
package com.cobblemon.mod.common.api.conditional
import net.minecraft.core.Holder
import net.minecraft.core.Registry
/**
@@ -17,5 +18,10 @@ import net.minecraft.core.Registry
* @since July 16th, 2022
*/
interface RegistryLikeCondition<T> {
fun fits(t: T, registry: Registry<T>): Boolean
fun fits(t: T, registry: Registry<T>): Boolean {
val holder = registry.wrapAsHolder(t)
return holder != null && fits(holder)
}
fun fits(holder: Holder<T>): Boolean
}
@@ -9,7 +9,7 @@
package com.cobblemon.mod.common.api.conditional
import com.google.gson.JsonElement
import net.minecraft.core.Registry
import net.minecraft.core.Holder
import net.minecraft.resources.ResourceLocation
/**
@@ -24,5 +24,6 @@ open class RegistryLikeIdentifierCondition<T : Any>(val identifier: ResourceLoca
constructor: (ResourceLocation) -> RegistryLikeIdentifierCondition<T>
): (JsonElement) -> RegistryLikeIdentifierCondition<T>? = { constructor(ResourceLocation.parse(it.asString)) }
}
override fun fits(t: T, registry: Registry<T>) = registry.getKey(t) == identifier
override fun fits(t: Holder<T>) = t.`is`(identifier)
}
@@ -9,6 +9,7 @@
package com.cobblemon.mod.common.api.conditional
import com.google.gson.JsonElement
import net.minecraft.core.Holder
import net.minecraft.core.Registry
import net.minecraft.resources.ResourceKey
import net.minecraft.resources.ResourceLocation
@@ -39,16 +40,5 @@ open class RegistryLikeTagCondition<T : Any>(val tag: TagKey<T>) : RegistryLikeC
}
}
override fun fits(t: T, registry: Registry<T>): Boolean {
// val registryHasTag = registry.containsTag(tag)
// if (!registryHasTag) {
//// LOGGER.warn("No tag in registry: ${tag.id}")
// return false
// }
return registry.getResourceKey(t)
.flatMap(registry::getHolder)
.map { entry -> entry.`is`(tag) }
.orElse(false)
}
override fun fits(t: Holder<T>) = t.`is`(tag)
}
@@ -46,6 +46,7 @@ import com.cobblemon.mod.common.api.events.storage.ChangePCBoxWallpaperEvent
import com.cobblemon.mod.common.api.events.storage.ReleasePokemonEvent
import com.cobblemon.mod.common.api.events.storage.RenamePCBoxEvent
import com.cobblemon.mod.common.api.events.storage.WallpaperCollectionEvent
import com.cobblemon.mod.common.api.events.storage.WallpaperUnlockedEvent
import com.cobblemon.mod.common.api.events.world.BigRootPropagatedEvent
import com.cobblemon.mod.common.api.reactive.CancelableObservable
import com.cobblemon.mod.common.api.reactive.EventObservable
@@ -179,6 +180,8 @@ object CobblemonEvents {
@JvmField
val WALLPAPER_COLLECTION_EVENT = EventObservable<WallpaperCollectionEvent>()
@JvmField
val WALLPAPER_UNLOCKED_EVENT = CancelableObservable<WallpaperUnlockedEvent>()
@JvmField
val CHANGE_PC_BOX_WALLPAPER_EVENT_PRE = CancelableObservable<ChangePCBoxWallpaperEvent.Pre>()
@@ -31,7 +31,7 @@ data class PokemonInteractionGUICreationEvent(
private fun getNextFreeOrientation(): Orientation {
var largest = Orientation.TOP_LEFT
for(orientation in Orientation.values()) {
for (orientation in Orientation.entries) {
if(!options.containsKey(orientation)) {
return orientation
} else {
@@ -22,5 +22,5 @@ import net.minecraft.server.level.ServerPlayer
*/
data class WallpaperCollectionEvent(
val player: ServerPlayer,
val wallpapers: MutableList<ResourceLocation>
val wallpapers: MutableSet<ResourceLocation>
)
@@ -0,0 +1,57 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of 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.storage
import com.bedrockk.molang.runtime.value.DoubleValue
import com.bedrockk.molang.runtime.value.MoValue
import com.cobblemon.mod.common.api.events.Cancelable
import com.cobblemon.mod.common.api.molang.MoLangFunctions.asMoLangValue
import com.cobblemon.mod.common.api.molang.MoLangFunctions.moLangFunctionMap
import com.cobblemon.mod.common.api.storage.pc.PCStore
import com.cobblemon.mod.common.api.storage.pc.UnlockablePCWallpaper
import com.cobblemon.mod.common.util.asArrayValue
import com.cobblemon.mod.common.util.getBoolean
import net.minecraft.server.level.ServerPlayer
/**
* Event fired when a wallpaper is unlocked for a PC. Typically, this will have a single player, but it is not
* technically guaranteed to due to shared PCs and offline support.
*
* @author Hiroku
* @since February 10th, 2025
*/
data class WallpaperUnlockedEvent(
val pc: PCStore,
val wallpaper: UnlockablePCWallpaper,
var shouldNotify: Boolean
): Cancelable() {
/** For shared PCs with multiple players online, and for offline PC access, the player will be null. */
val player: ServerPlayer?
get() = pc.getObservingPlayers().takeIf { it.size == 1 }?.first()
/** All online, observing players for the PC. This would typically have a single player in it. */
val players = pc.getObservingPlayers()
val context: MutableMap<String, MoValue>
get() = mutableMapOf(
"pc" to pc.struct,
"wallpaper" to wallpaper.struct,
"player" to (player?.asMoLangValue() ?: DoubleValue.ZERO),
"players" to players.asArrayValue { it.asMoLangValue() },
"should_notify" to DoubleValue(shouldNotify)
)
val functions = moLangFunctionMap(
cancelFunc,
"set_should_notify" to { params ->
shouldNotify = params.getBoolean(0)
DoubleValue.ONE
}
)
}
@@ -37,6 +37,8 @@ import net.minecraft.resources.ResourceLocation
import net.minecraft.util.FormattedCharSequence
import org.joml.Matrix4f
import org.joml.Vector3f
import org.lwjgl.opengl.GL30
import java.nio.ByteBuffer
@JvmOverloads
fun blitk(
@@ -364,4 +366,26 @@ fun renderSprite(matrixStack: PoseStack, sprite: ResourceLocation) {
buffer.addVertex(matrix, 2f, 2f, 0.0f).setUv(1f, 1f)
BufferUploader.drawWithShader(buffer.buildOrThrow())
}
}
fun getPixelRGB(x: Int, y: Int): Triple<Int, Int, Int> {
val window = Minecraft.getInstance().window
val scale = window.guiScale
val buffer = ByteBuffer.allocateDirect(4)
RenderSystem.readPixels(
(x * scale).toInt(),
(window.height - y * scale - scale).toInt(),
1,
1,
GL30.GL_RGBA,
GL30.GL_UNSIGNED_BYTE,
buffer
)
return Triple(
buffer[0].toInt() and 0xFF,
buffer[1].toInt() and 0xFF,
buffer[2].toInt() and 0xFF
)
}
@@ -21,6 +21,7 @@ import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.CobblemonActivities
import com.cobblemon.mod.common.CobblemonBlockEntities
import com.cobblemon.mod.common.CobblemonMemories
import com.cobblemon.mod.common.CobblemonUnlockableWallpapers
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.battles.model.actor.ActorType
import com.cobblemon.mod.common.api.battles.model.actor.BattleActor
@@ -39,6 +40,8 @@ import com.cobblemon.mod.common.api.pokedex.SeenPercent
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.api.pokemon.evolution.Evolution
import com.cobblemon.mod.common.api.pokemon.stats.Stats
import com.cobblemon.mod.common.api.riding.RidingStyle
import com.cobblemon.mod.common.api.riding.stats.RidingStat
import com.cobblemon.mod.common.api.scripting.CobblemonScripts
import com.cobblemon.mod.common.api.spawning.context.SpawningContext
import com.cobblemon.mod.common.api.storage.PokemonStore
@@ -68,6 +71,8 @@ import com.cobblemon.mod.common.pokemon.Species
import com.cobblemon.mod.common.pokemon.evolution.variants.ItemInteractionEvolution
import com.cobblemon.mod.common.pokemon.evolution.variants.LevelUpEvolution
import com.cobblemon.mod.common.pokemon.evolution.variants.TradeEvolution
import com.cobblemon.mod.common.pokemon.riding.controllers.BirdAirController
import com.cobblemon.mod.common.pokemon.riding.states.BirdAirState
import com.cobblemon.mod.common.util.*
import com.mojang.datafixers.util.Either
import java.util.UUID
@@ -77,6 +82,7 @@ import net.minecraft.commands.arguments.EntityAnchorArgument
import net.minecraft.core.BlockPos
import net.minecraft.core.Holder
import net.minecraft.core.Registry
import net.minecraft.core.RegistryAccess
import net.minecraft.core.registries.BuiltInRegistries
import net.minecraft.core.registries.Registries
import net.minecraft.nbt.CompoundTag
@@ -92,6 +98,7 @@ import net.minecraft.server.level.ServerPlayer
import net.minecraft.tags.TagKey
import net.minecraft.world.damagesource.DamageSource
import net.minecraft.world.damagesource.DamageTypes
import net.minecraft.world.entity.Entity
import net.minecraft.world.entity.EntityType
import net.minecraft.world.entity.LightningBolt
import net.minecraft.world.entity.LivingEntity
@@ -101,6 +108,7 @@ import net.minecraft.world.entity.ai.memory.MemoryModuleType
import net.minecraft.world.entity.ai.memory.MemoryStatus
import net.minecraft.world.entity.ai.memory.WalkTarget
import net.minecraft.world.entity.player.Player
import net.minecraft.world.item.ItemStack
import net.minecraft.world.level.Level
import net.minecraft.world.level.Level.ExplosionInteraction
import net.minecraft.world.level.biome.Biome
@@ -306,6 +314,11 @@ object MoLangFunctions {
.map { it.asMostSpecificMoLangValue() }
.asArrayValue()
}
map.put("is_healer_in_use") { params ->
val pos = params.get<ArrayStruct>(0).asBlockPos()
val healer = world.getBlockEntity(pos, CobblemonBlockEntities.HEALING_MACHINE).orElse(null) ?: return@put DoubleValue.ONE
return@put DoubleValue(healer.isInUse)
}
return@mutableListOf map
}
@@ -317,8 +330,8 @@ object MoLangFunctions {
val map = hashMapOf<String, java.util.function.Function<MoParams, Any>>()
map.put("username") { _ -> StringValue(player.gameProfile.name) }
map.put("uuid") { _ -> StringValue(player.gameProfile.id.toString()) }
map.put("main_held_item") { _ -> player.level().itemRegistry.wrapAsHolder(player.mainHandItem.item).asMoLangValue(Registries.ITEM) }
map.put("off_held_item") { _ -> player.level().itemRegistry.wrapAsHolder(player.offhandItem.item).asMoLangValue(Registries.ITEM) }
map.put("main_held_item") { _ -> player.mainHandItem.asMoLangValue(player.registryAccess()) }
map.put("off_held_item") { _ -> player.offhandItem.asMoLangValue(player.registryAccess()) }
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) }
@@ -412,11 +425,39 @@ object MoLangFunctions {
return@put DoubleValue.ONE
}
map.put("pokedex") { player.pokedex().struct }
map.put("has_advancement") { params ->
val requiredAdvancement = ResourceLocation.parse(params.getString(0))
for (entry in player.advancements.progress) {
if (entry.key.id == requiredAdvancement && entry.value.isDone) {
return@put DoubleValue.ONE
}
}
return@put DoubleValue.ZERO
}
}
map
}
)
val entityFunctions: MutableList<(LivingEntity) -> HashMap<String, java.util.function.Function<MoParams, Any>>> = mutableListOf<(LivingEntity) -> HashMap<String, java.util.function.Function<MoParams, Any>>>(
val itemStackFunctions = mutableListOf<(ItemStack, RegistryAccess) -> HashMap<String, java.util.function.Function<MoParams, Any>>>(
{ stack, registryAccess ->
val itemRegistry = registryAccess.registryOrThrow(Registries.ITEM)
val holder = itemRegistry.wrapAsHolder(stack.item)
val map = hashMapOf<String, java.util.function.Function<MoParams, Any>>()
map.put("item") { _ -> holder.asMoLangValue(Registries.ITEM) }
map.put("count") { _ -> DoubleValue(stack.count.toDouble()) }
map.put("damage_value") { _ -> DoubleValue(stack.damageValue) }
map.put("max_damage") { _ -> DoubleValue(stack.maxDamage) }
map.put("is_empty") { _ -> DoubleValue(stack.isEmpty) }
map.put("shrink") { params -> stack.shrink(params.getInt(0)) }
map.put("grow") { params -> stack.grow(params.getInt(0)) }
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()))) }
return@mutableListOf map
}
)
val entityFunctions: MutableList<(Entity) -> HashMap<String, java.util.function.Function<MoParams, Any>>> = mutableListOf(
{ entity ->
val map = hashMapOf<String, java.util.function.Function<MoParams, Any>>()
map.put("uuid") { _ -> StringValue(entity.uuid.toString()) }
@@ -425,31 +466,8 @@ object MoLangFunctions {
val source = DamageSource(entity.level().registryAccess().registryOrThrow(Registries.DAMAGE_TYPE).getHolder(DamageTypes.GENERIC).get())
entity.hurt(source, amount.toFloat())
}
map.put("walk_to") { params ->
val x = params.getDouble(0)
val y = params.getDouble(1)
val z = params.getDouble(2)
val speedMultiplier = params.getDoubleOrNull(3) ?: 0.35
if (entity is PathfinderMob) {
if (entity.brain.checkMemory(MemoryModuleType.WALK_TARGET, MemoryStatus.REGISTERED)) {
entity.brain.setMemory(MemoryModuleType.WALK_TARGET, WalkTarget(Vec3(x, y, z), speedMultiplier.toFloat(), 1))
entity.brain.setMemory(MemoryModuleType.LOOK_TARGET, BlockPosTracker(Vec3(x, y + entity.eyeHeight, z)))
} else {
entity.navigation.moveTo(x, y, z, speedMultiplier)
entity.lookControl.setLookAt(Vec3(x, y + entity.eyeHeight, z))
}
}
}
map.put("has_walk_target") { _ ->
if (entity is PathfinderMob) {
DoubleValue(entity.brain.getMemory(MemoryModuleType.WALK_TARGET).isPresent || entity.isPathFinding)
} else {
DoubleValue.ZERO
}
}
map.put("is_sneaking") { _ -> DoubleValue(entity.isShiftKeyDown) }
map.put("is_sprinting") { _ -> DoubleValue(entity.isSprinting) }
map.put("is_flying") { _ -> DoubleValue(entity.isFallFlying) }
map.put("is_in_water") { _ -> DoubleValue(entity.isUnderWater) }
map.put("is_touching_water_or_rain") { _ -> DoubleValue(entity.isInWaterRainOrBubble) }
map.put("is_touching_water") { _ -> DoubleValue(entity.isInWater) }
@@ -457,16 +475,13 @@ object MoLangFunctions {
map.put("is_in_lava") { _ -> DoubleValue(entity.isInLava) }
map.put("is_on_fire") { _ -> DoubleValue(entity.isOnFire) }
map.put("is_invisible") { _ -> DoubleValue(entity.isInvisible) }
map.put("is_sleeping") { _ -> DoubleValue(entity.isSleeping) }
map.put("is_riding") { _ -> DoubleValue(entity.isPassenger) }
map.put("health") { _ -> DoubleValue(entity.health) }
map.put("distance_to_pos") { params ->
val x = params.getDouble(0)
val y = params.getDouble(1)
val z = params.getDouble(2)
return@put DoubleValue(sqrt(entity.distanceToSqr(Vec3(x, y, z))))
}
map.put("max_health") { _ -> DoubleValue(entity.maxHealth) }
map.put("name") { _ -> StringValue(entity.effectiveName().string) }
map.put("type") { _ ->
entity.registryAccess().registry(Registries.ENTITY_TYPE).get().getKey(entity.type)?.toString()?.let {
@@ -493,11 +508,6 @@ object MoLangFunctions {
map.put("world") { _ -> entity.level().worldRegistry.wrapAsHolder(entity.level()).asWorldMoLangValue() }
map.put("biome") { _ -> entity.level().getBiome(entity.blockPosition()).asBiomeMoLangValue() }
map.put("is_passenger") { DoubleValue(entity.isPassenger) }
map.put("is_healer_in_use") { params ->
val pos = params.get<ArrayStruct>(0).asBlockPos()
val healer = entity.level().getBlockEntity(pos, CobblemonBlockEntities.HEALING_MACHINE).orElse(null) ?: return@put DoubleValue.ONE
return@put DoubleValue(healer.isInUse)
}
map.put("find_nearby_block") { params ->
val type = params.getString(0).asIdentifierDefaultingNamespace(namespace = "minecraft")
val isTag = type.path.startsWith("#")
@@ -516,7 +526,7 @@ object MoLangFunctions {
val distance = params.getDouble(0)
val entities = entity.level().getEntities(entity, AABB.ofSize(entity.position(), distance, distance, distance))
return@put entities
.filterIsInstance<LivingEntity>()
.filterIsInstance<Entity>()
.map { it.asMostSpecificMoLangValue() }
.asArrayValue()
}
@@ -573,6 +583,45 @@ object MoLangFunctions {
packet.sendToPlayer(player)
}
}
return@mutableListOf map
}
)
val livingEntityFunctions: MutableList<(LivingEntity) -> HashMap<String, java.util.function.Function<MoParams, Any>>> = mutableListOf<(LivingEntity) -> HashMap<String, java.util.function.Function<MoParams, Any>>>(
{ entity ->
val map = hashMapOf<String, java.util.function.Function<MoParams, Any>>()
map.put("is_living_entity") { DoubleValue.ONE }
map.put("is_flying") { _ -> DoubleValue(entity.isFallFlying) }
map.put("is_sleeping") { _ -> DoubleValue(entity.isSleeping) }
map.put("health") { _ -> DoubleValue(entity.health) }
map.put("max_health") { _ -> DoubleValue(entity.maxHealth) }
if (entity is PathfinderMob) {
map.put("walk_to") { params ->
val x = params.getDouble(0)
val y = params.getDouble(1)
val z = params.getDouble(2)
val speedMultiplier = params.getDoubleOrNull(3) ?: 0.35
if (entity.brain.checkMemory(MemoryModuleType.WALK_TARGET, MemoryStatus.REGISTERED)) {
entity.brain.setMemory(
MemoryModuleType.WALK_TARGET,
WalkTarget(Vec3(x, y, z), speedMultiplier.toFloat(), 1)
)
entity.brain.setMemory(
MemoryModuleType.LOOK_TARGET,
BlockPosTracker(Vec3(x, y + entity.eyeHeight, z))
)
} else {
entity.navigation.moveTo(x, y, z, speedMultiplier)
entity.lookControl.setLookAt(Vec3(x, y + entity.eyeHeight, z))
}
}
map.put("has_walk_target") { _ ->
DoubleValue(entity.brain.getMemory(MemoryModuleType.WALK_TARGET).isPresent || entity.isPathFinding)
}
}
map
}
)
@@ -739,8 +788,10 @@ object MoLangFunctions {
map.put("in_battle") { DoubleValue(pokemonEntity.isBattling) }
map.put("is_moving") { DoubleValue((pokemonEntity.moveControl as? PokemonMoveControl)?.hasWanted() == true) }
map.put("is_flying") { DoubleValue(pokemonEntity.getBehaviourFlag(PokemonBehaviourFlag.FLYING)) }
map.put("is_gliding") { DoubleValue(pokemonEntity.useRidingAltPose()) }
map.put("has_aspect") { DoubleValue(it.getString(0) in pokemonEntity.aspects) }
map.put("is_pokemon") { DoubleValue.ONE }
map.put("is_holding_item") { DoubleValue(!pokemonEntity.entityData.get(PokemonEntity.SHOWN_HELD_ITEM).isEmpty) }
map
}
)
@@ -748,6 +799,7 @@ object MoLangFunctions {
val pokemonStoreFunctions = mutableListOf<(PokemonStore<*>) -> HashMap<String, java.util.function.Function<MoParams, Any>>>(
{ store ->
val map = hashMapOf<String, java.util.function.Function<MoParams, Any>>()
map.put("uuid") { StringValue(store.uuid.toString()) }
map.put("add") { params ->
val pokemon = params.get<ObjectValue<Pokemon>>(0)
return@put DoubleValue(store.add(pokemon.obj))
@@ -848,6 +900,17 @@ object MoLangFunctions {
return@put DoubleValue.ONE
}
map.put("get_box_count") { _ -> DoubleValue(pc.boxes.size.toDouble()) }
map.put("has_unlocked_wallpaper") { params ->
val wallpaper = params.getString(0).asIdentifierDefaultingNamespace()
return@put DoubleValue(pc.unlockedWallpapers.contains(wallpaper))
}
map.put("get_unlocked_wallpapers") { pc.unlockedWallpapers.asArrayValue { StringValue(it.toString()) } }
map.put("unlock_wallpaper") {
val wallpaper = it.getString(0).asIdentifierDefaultingNamespace()
val playSound = it.getBooleanOrNull(1) != false
CobblemonUnlockableWallpapers.unlockableWallpapers[wallpaper] ?: return@put DoubleValue.ZERO
return@put DoubleValue(pc.unlockWallpaper(wallpaper, playSound))
}
return@mutableListOf map
}
)
@@ -856,7 +919,7 @@ object MoLangFunctions {
{ spawningContext ->
val map = hashMapOf<String, java.util.function.Function<MoParams, Any>>()
val worldValue = spawningContext.world.registryAccess().registryOrThrow(Registries.DIMENSION).wrapAsHolder(spawningContext.world).asWorldMoLangValue()
val biomeValue = spawningContext.biomeRegistry.wrapAsHolder(spawningContext.biome).asBiomeMoLangValue()
val biomeValue = spawningContext.biomeHolder.asBiomeMoLangValue()
map.put("biome") { _ -> biomeValue }
map.put("world") { _ -> worldValue }
map.put("light") { _ -> DoubleValue(spawningContext.light.toDouble()) }
@@ -1084,18 +1147,35 @@ object MoLangFunctions {
}
)
fun ItemStack.asMoLangValue(registryAccess: RegistryAccess) = ObjectValue(this).addStandardFunctions().addFunctions(itemStackFunctions.flatMap { it(this, registryAccess).entries.map { it.key to it.value } }.toMap())
fun Holder<Biome>.asBiomeMoLangValue() = asMoLangValue(Registries.BIOME).addFunctions(biomeFunctions.flatMap { it(this).entries.map { it.key to it.value } }.toMap())
fun Holder<Level>.asWorldMoLangValue() = asMoLangValue(Registries.DIMENSION).addFunctions(worldFunctions.flatMap { it(this).entries.map { it.key to it.value } }.toMap())
fun Holder<Block>.asBlockMoLangValue() = asMoLangValue(Registries.BLOCK).addFunctions(blockFunctions.flatMap { it(this).entries.map { it.key to it.value } }.toMap())
fun MinecraftServer.asMoLangValue() = ObjectValue(this).addStandardFunctions().addFunctions(serverFunctions.flatMap { it(this).entries.map { it.key to it.value } }.toMap())
fun Holder<DimensionType>.asDimensionTypeMoLangValue() = asMoLangValue(Registries.DIMENSION_TYPE).addFunctions(dimensionTypeFunctions.flatMap { it(this).entries.map { it.key to it.value } }.toMap())
fun Player.asMoLangValue(): ObjectValue<Player> {
if (this is ServerPlayer && uuid in Cobblemon.serverPlayerStructs) {
val existing = Cobblemon.serverPlayerStructs[uuid]!!
if (existing.obj == this) {
return existing
} else {
Cobblemon.serverPlayerStructs.remove(uuid)
}
}
val value = ObjectValue(
obj = this,
stringify = { it.effectiveName().string }
)
value.addFunctions(entityFunctions.flatMap { it(this).entries.map { it.key to it.value } }.toMap())
value.addFunctions(livingEntityFunctions.flatMap { it(this).entries.map { it.key to it.value } }.toMap())
value.addFunctions(playerFunctions.flatMap { it(this).entries.map { it.key to it.value } }.toMap())
if (this is ServerPlayer) {
Cobblemon.serverPlayerStructs[uuid] = value
}
return value
}
@@ -1150,6 +1230,7 @@ object MoLangFunctions {
stringify = { it.name.string }
)
value.addFunctions(entityFunctions.flatMap { it(this).entries.map { it.key to it.value } }.toMap())
value.addFunctions(livingEntityFunctions.flatMap { it(this).entries.map { it.key to it.value } }.toMap())
value.addFunctions(npcFunctions.flatMap { it(this).entries.map { it.key to it.value } }.toMap())
return value
}
@@ -1160,6 +1241,7 @@ object MoLangFunctions {
stringify = { it.pokemon.uuid.toString() }
)
value.addFunctions(entityFunctions.flatMap { it(this).entries.map { it.key to it.value } }.toMap())
value.addFunctions(livingEntityFunctions.flatMap { it(this).entries.map { it.key to it.value } }.toMap())
value.addFunctions(pokemonFunctions.flatMap { it(this.pokemon).entries.map { it.key to it.value } }.toMap()) // Convenience
value.addFunctions(pokemonEntityFunctions.flatMap { it(this).entries.map { it.key to it.value } }.toMap())
return value
@@ -1195,12 +1277,18 @@ object MoLangFunctions {
/**
* Different functions exist depending on
*/
fun LivingEntity.asMostSpecificMoLangValue(): ObjectValue<out LivingEntity> {
fun Entity.asMostSpecificMoLangValue(): ObjectValue<out Entity> {
return when (this) {
is Player -> asMoLangValue()
is PokemonEntity -> struct
is NPCEntity -> struct
else -> ObjectValue(this).also { it.addStandardFunctions().addFunctions(entityFunctions.flatMap { it(this).entries.map { it.key to it.value } }.toMap()) }
else -> ObjectValue(this).also {
it.addStandardFunctions()
it.addFunctions(entityFunctions.flatMap { it(this).entries.map { it.key to it.value } }.toMap())
if (this is LivingEntity) {
it.addFunctions(livingEntityFunctions.flatMap { it(this).entries.map { it.key to it.value } }.toMap())
}
}
}
}
@@ -1225,7 +1313,7 @@ object MoLangFunctions {
return this
}
fun QueryStruct.addEntityFunctions(entity: LivingEntity): QueryStruct {
fun QueryStruct.addEntityFunctions(entity: Entity): QueryStruct {
val addedFunctions = entityFunctions
.flatMap { it.invoke(entity).entries }
.associate { it.key to it.value }
@@ -1233,6 +1321,14 @@ object MoLangFunctions {
return this
}
fun QueryStruct.addLivingEntityFunctions(entity: LivingEntity): QueryStruct {
val addedFunctions = livingEntityFunctions
.flatMap { it.invoke(entity).entries }
.associate { it.key to it.value }
functions.putAll(addedFunctions)
return this
}
fun QueryStruct.addPokedexFunctions(pokedexManager: AbstractPokedexManager): QueryStruct {
val addedFunctions = pokedexFunctions
.flatMap { it.invoke(pokedexManager).entries }
@@ -8,7 +8,6 @@
package com.cobblemon.mod.common.api.moves
import com.cobblemon.mod.common.api.reactive.SimpleObservable
import com.cobblemon.mod.common.net.IntSize
import com.cobblemon.mod.common.util.DataKeys
import com.cobblemon.mod.common.util.readSizedInt
@@ -24,7 +23,7 @@ import net.minecraft.nbt.ListTag
import net.minecraft.network.RegistryFriendlyByteBuf
class BenchedMoves : Iterable<BenchedMove> {
val observable = SimpleObservable<BenchedMoves>()
var changeFunction: ((BenchedMoves) -> Unit) = {}
private var emit = true
private val benchedMoves = mutableListOf<BenchedMove>()
@@ -50,7 +49,7 @@ class BenchedMoves : Iterable<BenchedMove> {
fun update() {
if (emit) {
observable.emit(this)
changeFunction(this)
}
}
@@ -10,7 +10,6 @@ package com.cobblemon.mod.common.api.moves
import com.bedrockk.molang.runtime.struct.QueryStruct
import com.bedrockk.molang.runtime.value.DoubleValue
import com.cobblemon.mod.common.api.reactive.SimpleObservable
import com.cobblemon.mod.common.net.IntSize
import com.cobblemon.mod.common.util.DataKeys
import com.cobblemon.mod.common.util.readSizedInt
@@ -24,7 +23,7 @@ import com.mojang.serialization.Codec
import kotlin.math.min
class MoveSet : Iterable<Move> {
val observable = SimpleObservable<MoveSet>()
var changeFunction: (MoveSet) -> Unit = {}
private var emit = true
private val moves = arrayOfNulls<Move>(MOVE_COUNT)
@@ -153,7 +152,7 @@ class MoveSet : Iterable<Move> {
fun update() {
if (emit) {
observable.emit(this)
changeFunction(this)
}
}
@@ -166,7 +166,7 @@ open class MoveTemplate(
// TODO: Handle Liquid Voice: need to know what moves have the sound flag
// TODO: Handle weatherball, naturalgift, judgement, technoblast, terrainpulse, and terrablast
if (this.elementalType == ElementalTypes.NORMAL) {
if( this.damageCategory != DamageCategories.STATUS) {
if (this.damageCategory != DamageCategories.STATUS) {
return when (pokemon.ability.name) {
"pixilate" -> ElementalTypes.FAIRY
"aerilate" -> ElementalTypes.FLYING
@@ -40,7 +40,7 @@ object Moves : DataRegistry {
override fun reload(manager: ResourceManager) {
this.allMoves.clear()
this.idMapping.clear()
val movesJson = ShowdownService.service.getMoves()
val movesJson = ShowdownService.get().getMoves()
for (i in 0 until movesJson.size()) {
val jsMove = movesJson[i].asJsonObject
val id = jsMove.get("id").asString
@@ -116,4 +116,4 @@ object Moves : DataRegistry {
this.idMapping[move.num] = move
}
}
}
@@ -7,27 +7,31 @@
*/
package com.cobblemon.mod.common.api.npc.partyproviders
import com.bedrockk.molang.Expression
import com.bedrockk.molang.runtime.MoLangRuntime
import com.bedrockk.molang.runtime.value.DoubleValue
import com.cobblemon.mod.common.Cobblemon.LOGGER
import com.cobblemon.mod.common.api.molang.MoLangFunctions.asMoLangValue
import com.cobblemon.mod.common.api.molang.MoLangFunctions.setup
import com.cobblemon.mod.common.api.npc.NPCPartyProvider
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.api.storage.party.NPCPartyStore
import com.cobblemon.mod.common.entity.npc.NPCEntity
import com.cobblemon.mod.common.util.asArrayValue
import com.cobblemon.mod.common.util.asExpression
import com.cobblemon.mod.common.util.resolveFloat
import com.cobblemon.mod.common.util.resolveInt
import com.cobblemon.mod.common.util.weightedSelection
import com.cobblemon.mod.common.util.withQueryValue
import com.cobblemon.mod.common.util.*
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive
import kotlin.random.Random
import net.minecraft.server.level.ServerPlayer
import kotlin.collections.List
import kotlin.collections.any
import kotlin.collections.filter
import kotlin.collections.first
import kotlin.collections.mutableListOf
import kotlin.collections.mutableMapOf
import kotlin.collections.randomOrNull
import kotlin.collections.set
import kotlin.collections.toMutableList
import kotlin.random.Random
/**
* A provider of a party for battling the NPC. It generates a party using a more complex process
@@ -60,9 +64,9 @@ class PoolPartyProvider : NPCPartyProvider {
json as JsonObject
this.minPokemon = json.getAsJsonPrimitive("minPokemon").asString?.asExpression() ?: "1".asExpression()
this.maxPokemon = json.getAsJsonPrimitive("maxPokemon").asString?.asExpression() ?: "6".asExpression()
this.pool = json.getAsJsonArray("pool").map {
this.pool = json.getAsJsonArray("pool").mapNotNull {
if (it is JsonPrimitive) {
return@map DynamicPokemon(
return@mapNotNull DynamicPokemon(
PokemonProperties.parse(it.asString),
"0".asExpression(),
null,
@@ -73,17 +77,26 @@ class PoolPartyProvider : NPCPartyProvider {
}
it as JsonObject
DynamicPokemon(
PokemonProperties.parse(it.get("pokemon").asString),
it.get("levelVariation")?.asString?.asExpression() ?: "0".asExpression(),
it.get("level")?.asString?.asExpression(),
it.get("npcLevels")?.asString?.split("-")?.let { it[0].toInt()..it[1].toInt() } ?: 1..100,
it.get("selectableTimes")?.asString?.asExpression() ?: "1".asExpression(),
it.get("weight")?.asString?.asExpression() ?: "1".asExpression()
)
val properties = PokemonProperties.parse(it.get("pokemon").asString)
// Checks for errors on Pokémon name, if not a valid species ignores it to avoid random Pokémon in the Pool.
if (properties.species == null) {
LOGGER.warn("Error making NPC pokemon pool. ${it.get("pokemon").asString.substringBefore(" ")} is not a valid species, ignoring")
null
}
else {
DynamicPokemon(
properties,
it.get("levelVariation")?.asString?.asExpression() ?: "0".asExpression(),
it.get("level")?.asString?.asExpression(),
it.get("npcLevels")?.asString?.split("-")?.let { it[0].toInt()..it[1].toInt() } ?: 1..100,
it.get("selectableTimes")?.asString?.asExpression() ?: "1".asExpression(),
it.get("weight")?.asString?.asExpression() ?: "1".asExpression()
)
}
}.toMutableList()
isStatic = json.get("isStatic").asBoolean
json.get("useFixedRandom")?.asBoolean?.let { useFixedRandom = it }
}
class DynamicPokemon(
@@ -137,6 +150,10 @@ class PoolPartyProvider : NPCPartyProvider {
val instance = selected.pokemon.copy().also { it.level = it.level ?: dictatedLevel ?: randomLevel }.create()
party.add(instance)
}
if (party.none()) {
LOGGER.error("${npc} has no Pokemon on Party")
}
}
override fun provide(npc: NPCEntity, level: Int, players: List<ServerPlayer>): NPCPartyStore {
@@ -13,81 +13,124 @@ object CobblemonPermissions {
private const val COMMAND_PREFIX = "command."
private val permissions = arrayListOf<Permission>()
@JvmStatic
val CHANGE_EYE_HEIGHT = this.create("${COMMAND_PREFIX}changeeyeheight", PermissionLevel.ALL_COMMANDS)
@JvmStatic
val CHANGE_SCALE_AND_SIZE = this.create("${COMMAND_PREFIX}changescaleandsize", PermissionLevel.ALL_COMMANDS)
@JvmStatic
val CHANGE_WALK_SPEED = this.create("${COMMAND_PREFIX}changewalkspeed", PermissionLevel.ALL_COMMANDS)
@JvmStatic
val CHANGE_JOINT_SCALE = this.create("${COMMAND_PREFIX}changejointscale", PermissionLevel.ALL_COMMANDS)
@JvmStatic
val CHECKSPAWNS = this.create("${COMMAND_PREFIX}checkspawns", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
@JvmStatic
val GET_NBT = this.create("${COMMAND_PREFIX}getnbt", PermissionLevel.ALL_COMMANDS)
private const val GIVE_POKEMON_BASE = "${COMMAND_PREFIX}givepokemon"
@JvmStatic
val GIVE_POKEMON_SELF = this.create("${GIVE_POKEMON_BASE}.self", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
@JvmStatic
val GIVE_POKEMON_OTHER = this.create("${GIVE_POKEMON_BASE}.other", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
private const val HEAL_POKEMON_BASE = "${COMMAND_PREFIX}healpokemon"
@JvmStatic
val HEAL_POKEMON_SELF = this.create("$HEAL_POKEMON_BASE.self", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
@JvmStatic
val HEAL_POKEMON_OTHER = this.create("$HEAL_POKEMON_BASE.other", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
private const val LEVEL_UP_BASE = "${COMMAND_PREFIX}levelup"
@JvmStatic
val LEVEL_UP_SELF = this.create("$LEVEL_UP_BASE.self", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
@JvmStatic
val LEVEL_UP_OTHER = this.create("$LEVEL_UP_BASE.other", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
@JvmStatic
val OPEN_STARTER_SCREEN = this.create("${COMMAND_PREFIX}openstarterscreen", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
@JvmStatic
val BEDROCK_PARTICLE = this.create("${COMMAND_PREFIX}bedrockparticle", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
@JvmStatic
val OPEN_DIALOGUE = this.create("${COMMAND_PREFIX}opendialogue", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
@JvmStatic
val UNLOCK_PC_BOX_WALLPAPER = this.create("${COMMAND_PREFIX}unlockpcboxwallpaper", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
private const val POKEMON_EDIT_BASE = "${COMMAND_PREFIX}pokemonedit"
@JvmStatic
val POKEMON_EDIT_SELF = this.create("$POKEMON_EDIT_BASE.self", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
@JvmStatic
val POKEMON_EDIT_OTHER = this.create("$POKEMON_EDIT_BASE.other", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
@JvmStatic
val SPAWN_ALL_POKEMON = this.create("${COMMAND_PREFIX}spawnallpokemon", PermissionLevel.ALL_COMMANDS)
@JvmStatic
val GIVE_ALL_POKEMON = this.create("${COMMAND_PREFIX}giveallpokemon", PermissionLevel.ALL_COMMANDS)
@JvmStatic
val SPAWN_POKEMON = this.create("${COMMAND_PREFIX}spawnpokemon", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
@JvmStatic
val SPAWN_NPC = this.create("${COMMAND_PREFIX}spawnnpc", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
@JvmStatic
val STOP_BATTLE = this.create("${COMMAND_PREFIX}stopbattle", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
@JvmStatic
val TAKE_POKEMON = this.create("${COMMAND_PREFIX}takepokemon", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
@JvmStatic
val TEACH = this.create("${COMMAND_PREFIX}teach.base", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
@JvmStatic
val TEACH_BYPASS_LEARNSET = this.create("${COMMAND_PREFIX}teach.bypass", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
@JvmStatic
val FRIENDSHIP = this.create("${COMMAND_PREFIX}friendship", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
@JvmStatic
val HELD_ITEM = this.create("${COMMAND_PREFIX}helditem", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
@JvmStatic
val PC = this.create("${COMMAND_PREFIX}pc", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
@JvmStatic
val POKEBOX = this.create("${COMMAND_PREFIX}pokebox", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
@JvmStatic
val RENAMEBOX = this.create("${COMMAND_PREFIX}renamebox", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
@JvmStatic
val CHANGE_WALLPAPER = this.create("${COMMAND_PREFIX}changewallpaper", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
@JvmStatic
val TEST_STORE = this.create("${COMMAND_PREFIX}teststore", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
@JvmStatic
val QUERY_LEARNSET = this.create("${COMMAND_PREFIX}querylearnset", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
@JvmStatic
val TEST_PC_SLOT = this.create("${COMMAND_PREFIX}testpcslot", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
@JvmStatic
val TEST_PARTY_SLOT = this.create("${COMMAND_PREFIX}testpartyslot", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
@JvmStatic
val CLEAR_PARTY = this.create("${COMMAND_PREFIX}clearparty", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
@JvmStatic
val CLEAR_PC = this.create("${COMMAND_PREFIX}clearpc", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
@JvmStatic
val POKEDEX = this.create("${COMMAND_PREFIX}pokedex", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
@JvmStatic
val NPC_EDIT = this.create("${COMMAND_PREFIX}npcedit", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
@JvmStatic
val NPC_DELETE = this.create("${COMMAND_PREFIX}npcdelete", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
@JvmStatic
val FREEZE_POKEMON = this.create("${COMMAND_PREFIX}freezepokemon", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
@JvmStatic
val APPLY_PLAYER_TEXTURE = this.create("${COMMAND_PREFIX}applyplayertexture", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
@JvmStatic
val ABANDON_MULTITEAM = this.create("${COMMAND_PREFIX}abandonmultiteam", PermissionLevel.NONE)
@JvmStatic
val RUN_MOLANG_SCRIPT = this.create("${COMMAND_PREFIX}runmolangscript", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
@JvmStatic
val COBBLEMON_CONFIG_RELOAD = this.create("${COMMAND_PREFIX}cobblemonconfig.reload", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
val CALCULATE_SEAT_POSITIONS = this.create("${COMMAND_PREFIX}calculateseatpositions", PermissionLevel.CHEAT_COMMANDS_AND_COMMAND_BLOCKS)
fun all(): Iterable<Permission> = this.permissions
private fun create(node: String, level: PermissionLevel): Permission {
@@ -722,8 +722,8 @@ open class PokemonProperties {
nature = json.get(DataKeys.POKEMON_NATURE)?.asString
ability = json.get(DataKeys.POKEMON_ABILITY)?.asString
status = json.get(DataKeys.POKEMON_STATUS_NAME)?.asString
ivs = json.getAsJsonObject(DataKeys.POKEMON_IVS)?.let { IVs().loadFromJSON(it) } as IVs
evs = json.getAsJsonObject(DataKeys.POKEMON_EVS)?.let { EVs().loadFromJSON(it) } as EVs
ivs = json.getAsJsonObject(DataKeys.POKEMON_IVS)?.let { IVs().loadFromJSON(it) } as? IVs
evs = json.getAsJsonObject(DataKeys.POKEMON_EVS)?.let { EVs().loadFromJSON(it) } as? EVs
type = json.get(DataKeys.ELEMENTAL_TYPE)?.asString
teraType = json.get(DataKeys.POKEMON_TERA_TYPE)?.asString
dmaxLevel = json.get(DataKeys.POKEMON_DMAX_LEVEL)?.asInt
@@ -11,9 +11,11 @@ package com.cobblemon.mod.common.api.pokemon
import com.cobblemon.mod.common.pokemon.Pokemon
enum class PokemonSortMode(val comparator: Comparator<Pokemon?>, val reverseComparator: Comparator<Pokemon?>) {
NAME( { it?.getDisplayName()?.string } ),
LEVEL( { it?.level } ),
TYPE( { it?.primaryType?.name } );
NAME({ it?.getDisplayName()?.string }),
LEVEL({ it?.level }),
TYPE({ it?.primaryType?.name }),
POKEDEX_NUMBER({ it?.species?.nationalPokedexNumber }),
GENDER({ it?.gender });
constructor(mapper: (Pokemon?) -> Comparable<*>?) : this(compareBy({ it == null }, mapper),
compareBy<Pokemon?> { it == null }.thenComparing(compareBy(mapper).reversed()))
@@ -128,6 +128,11 @@ object PokemonSpecies : JsonDataRegistry<Species> {
init {
SpeciesAdditions.observable.subscribe {
this.species.forEach(Species::initialize)
this.species.forEach {
if (it.implemented) {
this.implemented.add(it)
}
}
this.species.forEach(Species::resolveEvolutionMoves)
Cobblemon.showdownThread.queue {
it.registerSpecies()
@@ -206,9 +211,6 @@ object PokemonSpecies : JsonDataRegistry<Species> {
this.speciesByDex.remove(old.resourceIdentifier.namespace, old.nationalPokedexNumber)
}
this.speciesByDex.put(species.resourceIdentifier.namespace, species.nationalPokedexNumber, species)
if (species.implemented) {
this.implemented.add(species)
}
}
}
@@ -176,7 +176,7 @@ interface Evolution : EvolutionLike {
pokemonEntity.after(11.2F) {
evolutionMethod(pokemon)
}
pokemonEntity.after( seconds = 12F ) {
pokemonEntity.after(seconds = 12F) {
cryAnimation(pokemonEntity)
pokemonEntity.entityData.set(PokemonEntity.EVOLUTION_STARTED, false)
pokemon.getOwnerPlayer()?.sendSystemMessage(lang("ui.evolve.into", preEvoName, pokemon.species.translatedName))
@@ -68,8 +68,8 @@ object SpeciesFeatures : JsonDataRegistry<SpeciesFeatureProvider<*>> {
data.forEach(this::registerFromAssets)
}
fun getCodeFeature(name: String) = resourceFeatures[name]
fun getResourceFeature(name: String) = codeFeatures[name]
fun getCodeFeature(name: String) = codeFeatures[name]
fun getResourceFeature(name: String) = resourceFeatures[name]
fun getFeature(name: String) = getCodeFeature(name) ?: getResourceFeature(name)
fun loadOnClient(entries: Collection<Map.Entry<String, SpeciesFeatureProvider<*>>>) {
codeFeatures.putAll(entries.map { it.toPair() })
@@ -10,15 +10,23 @@ package com.cobblemon.mod.common.api.riding
import com.bedrockk.molang.runtime.MoLangRuntime
import com.bedrockk.molang.runtime.value.DoubleValue
import com.cobblemon.mod.common.Rollable
import com.cobblemon.mod.common.api.molang.MoLangFunctions.setup
import com.cobblemon.mod.common.api.riding.controller.RideController
import com.cobblemon.mod.common.api.riding.stats.RidingStat
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.util.toProperties
import com.cobblemon.mod.common.util.withQueryValue
import net.minecraft.ChatFormatting
import net.minecraft.network.chat.Component
import net.minecraft.resources.ResourceLocation
import net.minecraft.util.SmoothDouble
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.player.Player
import net.minecraft.world.phys.Vec2
import net.minecraft.world.phys.Vec3
import kotlin.math.sin
import kotlin.math.sin
data class RidingManager(val entity: PokemonEntity) {
var lastSpeed = 0F
@@ -31,6 +39,17 @@ data class RidingManager(val entity: PokemonEntity) {
.withQueryValue("entity", entity.struct)
.also {
it.environment.query.addFunction("passenger_count") { DoubleValue(entity.passengers.size.toDouble()) }
it.environment.query.addFunction("get_ride_stats") { params ->
val rideStat = RidingStat.valueOf(params.getString(0).uppercase())
val rideStyle = RidingStyle.valueOf(params.getString(1).uppercase())
val maxVal = params.getDouble(2)
val minVal = params.getDouble(3)
//TODO: Use the mons actual boost once implemented
val normalizedStat = entity.rideProp.calculate(rideStat, rideStyle, 0) / 100.0f
val trueStatVal = (normalizedStat * (maxVal - minVal)) + minVal
DoubleValue(trueStatVal)
}
}
}
@@ -58,8 +77,12 @@ data class RidingManager(val entity: PokemonEntity) {
val pose = controller.pose(entity)
entity.entityData.set(PokemonEntity.POSE_TYPE, pose)
// driver.displayClientMessage(Component.literal("Speed: ").withStyle { it.withColor(ChatFormatting.GREEN) }.append(Component.literal("$lastSpeed b/t")), true)
//val speedPlayer = driver.deltaMovement.horizontalDistance()
//driver.displayClientMessage(Component.literal("Speed player: ").withStyle { it.withColor(ChatFormatting.GREEN) }.append(Component.literal("$speedPlayer b/t")), true)
//val speedEntity = entity.deltaMovement.length() //* 20 * 60 * 60) / ( 1000 )
//println(speedEntity)
//driver.displayClientMessage(Component.literal("Speed: ").withStyle { it.withColor(ChatFormatting.GREEN) }.append(Component.literal("${String.format("%.2f", speedEntity)} km/h")), true)
}
fun speed(entity: PokemonEntity, driver: Player): Float {
@@ -91,6 +114,16 @@ data class RidingManager(val entity: PokemonEntity) {
return controller.velocity(entity, driver, input)
}
fun setRideBar(entity: PokemonEntity, driver: Player): Float {
val controller = getController(entity) ?: return 0.0f
return controller.setRideBar(entity, driver)
}
fun rideFovMult(entity: PokemonEntity, driver: Player): Float {
val controller = getController(entity) ?: return 1.0f
return controller.rideFovMult(entity, driver)
}
fun canJump(entity: PokemonEntity, driver: Player): Boolean {
val controller = getController(entity) ?: return false
return controller.canJump(entity, driver)
@@ -101,11 +134,28 @@ data class RidingManager(val entity: PokemonEntity) {
return controller.jumpForce(entity, driver, jumpStrength)
}
fun useRidingAltPose(entity: PokemonEntity, driver: Player): Boolean {
val controller = getController(entity) ?: return false
return controller.useRidingAltPose(entity, driver)
}
/**
* Used to supply the delta for the lerp() between velocity
* vectors. Called inertia for now as the way it feels when
* affecting the ride is somewhat akin to inertia.
*/
fun inertia(entity: PokemonEntity): Double {
val controller = getController(entity) ?: return 0.5
return controller.inertia(entity)
}
fun gravity(entity: PokemonEntity, regularGravity: Double): Double? {
val controller = getController(entity) ?: return null
return controller.gravity(entity, regularGravity)
}
//Maybe should be changed to be a function that conveys:
// "turn on use our camera system function"
fun shouldRoll(entity: PokemonEntity): Boolean {
val controller = getController(entity) ?: return false
return controller.shouldRoll(entity)
@@ -121,8 +171,50 @@ data class RidingManager(val entity: PokemonEntity) {
return controller.shouldRotatePokemonHead()
}
//FIXME: Make this not just be false for all controllers lol
fun rotationOnMouseXY(
entity: PokemonEntity,
driver: Player,
yMouse: Double,
xMouse: Double,
yMouseSmoother: SmoothDouble,
xMouseSmoother: SmoothDouble,
sensitivity: Double,
deltaTime: Double
): Vec3 {
val controller = getController(entity) ?: return Vec3.ZERO
return controller.rotationOnMouseXY(
entity,
driver,
yMouse,
xMouse,
yMouseSmoother,
xMouseSmoother,
sensitivity,
deltaTime
) }
/**
* Determines whether mouse input will affect the rotation of
* the rollable object. Need to remove this as it's not used anymore
*/
//I think this might better serve as a "use smoothing on angular velocity"
fun useAngVelSmoothing(entity: PokemonEntity): Boolean {
val controller = getController(entity) ?: return true
return controller.useAngVelSmoothing(entity)
}
fun turnOffOnGround(entity: PokemonEntity): Boolean {
val controller = getController(entity) ?: return false
return controller.turnOffOnGround(entity)
}
fun angRollVel(entity: PokemonEntity, driver: Player, deltaTime: Double): Vec3 {
val controller = getController(entity) ?: return Vec3(0.0, 0.0, 0.0)
return controller.angRollVel(entity, driver, deltaTime)
}
fun dismountOnShift(): Boolean {
return false
val controller = getController(entity) ?: return false
return controller.dismountOnShift(entity)
}
}
@@ -8,6 +8,8 @@
package com.cobblemon.mod.common.api.riding.controller
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.Rollable
import com.cobblemon.mod.common.api.net.Decodable
import com.cobblemon.mod.common.api.net.Encodable
import com.cobblemon.mod.common.api.riding.RidingState
@@ -17,6 +19,7 @@ import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.util.writeIdentifier
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.resources.ResourceLocation
import net.minecraft.util.SmoothDouble
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.player.Player
import net.minecraft.world.phys.Vec2
@@ -56,20 +59,28 @@ interface RideController : Encodable, Decodable {
* the entity as a means of determining the conditions the entity is currently under. This allows for
* dynamic pose updates.
*/
fun pose(entity: PokemonEntity) : PoseType {
fun pose(entity: PokemonEntity): PoseType {
return this.poseProvider.select(entity)
}
/**
* Calculates the current speed of the mount.
*/
fun speed(entity: PokemonEntity, driver: Player) : Float
fun speed(entity: PokemonEntity, driver: Player): Float
/**
* Sets the rotation of the mount. This is typically based on the controlling driver and is manipulated as
* necessary.
*/
fun rotation(entity: PokemonEntity, driver: LivingEntity) : Vec2
fun rotation(entity: PokemonEntity, driver: LivingEntity): Vec2
/**
* Sets the change current change in rotation for that tick in three
* dimensions
*/
fun angRollVel(entity: PokemonEntity, driver: Player, deltaTime: Double): Vec3 {
return Vec3(0.0, 0.0, 0.0)
}
fun clampPassengerRotation(entity: PokemonEntity, driver: LivingEntity) {}
@@ -82,15 +93,51 @@ interface RideController : Encodable, Decodable {
* to the types of movements. For instance, we can apply limits to any sort of sideways movement input, so that
* it would otherwise be slower than normal forward movement.
*/
fun velocity(entity: PokemonEntity, driver: Player, input: Vec3) : Vec3
fun velocity(entity: PokemonEntity, driver: Player, input: Vec3): Vec3
fun canJump(entity: PokemonEntity, driver: Player) : Boolean
fun jumpForce(entity: PokemonEntity, driver: Player, jumpStrength: Int) : Vec3
fun setRideBar(entity: PokemonEntity, driver: Player): Float = 0.0f
fun gravity(entity: PokemonEntity, regularGravity: Double) : Double? = null
fun rideFovMult(entity: PokemonEntity, driver: Player): Float = 1.0f
fun canJump(entity: PokemonEntity, driver: Player): Boolean
fun jumpForce(entity: PokemonEntity, driver: Player, jumpStrength: Int): Vec3
fun gravity(entity: PokemonEntity, regularGravity: Double): Double? = null
fun inertia(entity: PokemonEntity): Double = 0.5
fun shouldRoll(entity: PokemonEntity): Boolean = false
fun useAngVelSmoothing(entity: PokemonEntity): Boolean = false
fun useRidingAltPose(entity: PokemonEntity, driver: Player): Boolean = false
//If function is not overwritten by controllers then just perform the defualt
//rolling function which is roll on mousex and pitch on mousey
fun rotationOnMouseXY(
entity: PokemonEntity,
driver: Player,
yMouse: Double,
xMouse: Double,
yMouseSmoother: SmoothDouble,
xMouseSmoother: SmoothDouble,
sensitivity: Double,
deltaTime: Double
): Vec3 {
if (driver !is Rollable) return Vec3.ZERO
//Might need to add the smoothing here for default.
val rollable = driver as Rollable
val invertRoll = if (Cobblemon.config.invertRoll) -1 else 1
val invertPitch = if (Cobblemon.config.invertPitch) -1 else 1
return Vec3(0.0, yMouse * invertPitch, xMouse * invertRoll)
}
fun turnOffOnGround(entity: PokemonEntity): Boolean = false
fun dismountOnShift(entity: PokemonEntity): Boolean = false
fun getRuntime(entity: PokemonEntity) = entity.riding.runtime
fun shouldRotatePokemonHead(): Boolean = false
@@ -10,7 +10,14 @@ package com.cobblemon.mod.common.api.spawning
import com.cobblemon.mod.common.Cobblemon.LOGGER
import com.cobblemon.mod.common.api.entity.Despawner
import com.cobblemon.mod.common.api.spawning.condition.*
import com.cobblemon.mod.common.api.spawning.condition.AreaSpawningCondition
import com.cobblemon.mod.common.api.spawning.condition.BasicSpawningCondition
import com.cobblemon.mod.common.api.spawning.condition.FishingSpawningCondition
import com.cobblemon.mod.common.api.spawning.condition.GroundedSpawningCondition
import com.cobblemon.mod.common.api.spawning.condition.SeafloorSpawningCondition
import com.cobblemon.mod.common.api.spawning.condition.SpawningCondition
import com.cobblemon.mod.common.api.spawning.condition.SubmergedSpawningCondition
import com.cobblemon.mod.common.api.spawning.condition.SurfaceSpawningCondition
import com.cobblemon.mod.common.api.spawning.context.AreaContextResolver
import com.cobblemon.mod.common.api.spawning.context.FishingSpawningContext
import com.cobblemon.mod.common.api.spawning.context.GroundedSpawningContext
@@ -44,6 +51,7 @@ import com.cobblemon.mod.common.api.spawning.spawner.Spawner
import com.cobblemon.mod.common.api.spawning.spawner.TickingSpawner
import com.cobblemon.mod.common.entity.pokemon.CobblemonAgingDespawner
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import net.minecraft.server.MinecraftServer
/**
* A grouping of all the overarching behaviours of the Best Spawner system. This is a convenient accessor to
@@ -130,7 +138,8 @@ object BestSpawner {
spawnerManagers.forEach(SpawnerManager::onConfigReload)
}
fun onServerStarted() {
fun onServerStarted(server: MinecraftServer) {
CobblemonSpawnPools.onServerLoad(server)
spawnerManagers.forEach(SpawnerManager::onServerStarted)
fishingSpawner = FishingSpawner()
}
@@ -8,10 +8,13 @@
package com.cobblemon.mod.common.api.spawning
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.spawning.condition.BiomePrecalculation
import com.cobblemon.mod.common.api.spawning.condition.BucketPrecalculation
import com.cobblemon.mod.common.api.spawning.condition.ContextPrecalculation
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
import com.cobblemon.mod.common.data.CobblemonDataProvider
import net.minecraft.server.MinecraftServer
/**
* A collection of all of Cobblemon's general-purpose [SpawnPool]s. These
@@ -26,6 +29,18 @@ object CobblemonSpawnPools {
lateinit var WORLD_SPAWN_POOL: SpawnPool
fun load() {
WORLD_SPAWN_POOL = CobblemonDataProvider.register(SpawnPool("world").addPrecalculators(ContextPrecalculation, BucketPrecalculation))
WORLD_SPAWN_POOL = CobblemonDataProvider.register(SpawnPool("world")
.addPrecalculators(
ContextPrecalculation,
BucketPrecalculation,
BiomePrecalculation
)
)
}
fun onServerLoad(server: MinecraftServer) {
Cobblemon.LOGGER.info("Optimizing spawn pools...")
WORLD_SPAWN_POOL.forEach { it.onServerLoad(server) }
WORLD_SPAWN_POOL.precalculate()
}
}
@@ -32,7 +32,7 @@ abstract class AreaTypeSpawningCondition<T : AreaSpawningContext> : SpawningCond
return false
} else if (maxHeight != null && ctx.height > maxHeight!!) {
return false
} else if (neededNearbyBlocks != null && neededNearbyBlocks!!.none { cond -> ctx.nearbyBlockTypes.any { cond.fits(it, ctx.blockRegistry) } }) {
} else if (neededNearbyBlocks != null && neededNearbyBlocks!!.none { cond -> ctx.nearbyBlockHolders.any { cond.fits(it) } }) {
return false
} else {
return true
@@ -10,6 +10,8 @@ package com.cobblemon.mod.common.api.spawning.condition
import com.cobblemon.mod.common.api.spawning.context.SpawningContext
import com.cobblemon.mod.common.api.spawning.detail.SpawnDetail
import net.minecraft.core.Holder
import net.minecraft.world.level.biome.Biome
/**
* A spawning condition that is composed of a list of conditions and anticonditions.
@@ -30,6 +32,15 @@ class CompositeSpawningCondition {
var conditions = mutableListOf<SpawningCondition<*>>()
var anticonditions = mutableListOf<SpawningCondition<*>>()
fun isBiomeValid(holder: Holder<Biome>): Boolean {
if (conditions.isEmpty() || conditions.any { it.biomes == null || it.biomes!!.isEmpty() || it.biomes!!.any { it.fits(holder) } }) {
if (anticonditions.isEmpty() || anticonditions.none { it.biomes != null && it.biomes!!.any { it.fits(holder) } }) {
return true
}
}
return false
}
fun satisfiedBy(ctx: SpawningContext): Boolean {
return if (conditions.isNotEmpty() && conditions.none { it.isSatisfiedBy(ctx) }) {
false
@@ -30,7 +30,11 @@ abstract class GroundedTypeSpawningCondition<T : GroundedSpawningContext> : Area
return false
} else if (maxHeight != null && ctx.height > maxHeight!!) {
return false
} else !(neededBaseBlocks != null && neededBaseBlocks!!.none { it.fits(ctx.baseBlock.block, ctx.blockRegistry) })
} else if (neededBaseBlocks != null && neededBaseBlocks!!.none { it.fits(ctx.baseBlockHolder) }) {
return false
} else {
return true
}
}
override fun copyFrom(other: SpawningCondition<*>, merger: Merger) {
@@ -30,10 +30,14 @@ abstract class SeafloorTypeSpawningCondition<T : SeafloorSpawningContext> : Area
return if (!super.fits(ctx)) {
false
} else if (minHeight != null && ctx.height < minHeight!!) {
return false
false
} else if (maxHeight != null && ctx.height > maxHeight!!) {
return false
} else !(neededBaseBlocks != null && neededBaseBlocks!!.none { it.fits(ctx.baseBlock.block, ctx.blockRegistry) })
false
} else if (neededBaseBlocks != null && neededBaseBlocks!!.none { it.fits(ctx.baseBlockHolder) }) {
false
} else {
true
}
}
override fun copyFrom(other: SpawningCondition<*>, merger: Merger) {
@@ -44,7 +48,7 @@ abstract class SeafloorTypeSpawningCondition<T : SeafloorSpawningContext> : Area
}
override fun isValid(): Boolean {
val containsNullValues = neededBaseBlocks != null && neededBaseBlocks!!.any {it == null}
val containsNullValues = neededBaseBlocks != null && neededBaseBlocks!!.any { it == null }
return super.isValid() && !containsNullValues
}
}
@@ -42,6 +42,7 @@ abstract class SpawningCondition<T : SpawningContext> {
}
var dimensions: MutableList<ResourceLocation>? = null
/** This gets checked in a precalculation but still needs to be checked for things like rarity multipliers. */
var biomes: MutableSet<RegistryLikeCondition<Biome>>? = null
var moonPhase: MoonPhaseRange? = null
var canSeeSky: Boolean? = null
@@ -98,7 +99,7 @@ abstract class SpawningCondition<T : SpawningContext> {
return false
} else if (dimensions != null && dimensions!!.isNotEmpty() && ctx.world.dimensionType().effectsLocation !in dimensions!!) {
return false
} else if (biomes != null && biomes!!.isNotEmpty() && biomes!!.none { condition -> condition.fits(ctx.biome, ctx.biomeRegistry) }) {
} else if (biomes != null && biomes!!.isNotEmpty() && biomes!!.none { condition -> condition.fits(ctx.biomeHolder) }) {
return false
} else if (appendages.any { !it.fits(ctx) }) {
return false
@@ -153,11 +154,11 @@ abstract class SpawningCondition<T : SpawningContext> {
}
open fun isValid(): Boolean {
if(biomes != null && biomes!!.any {it == null})
if (biomes != null && biomes!!.any { it == null })
return false
if(dimensions != null && dimensions!!.any {it == null})
if (dimensions != null && dimensions!!.any { it == null })
return false
if(structures != null && structures!!.any {it == null})
if (structures != null && structures!!.any { it == null })
return false
return true
}
@@ -12,6 +12,7 @@ import com.cobblemon.mod.common.api.spawning.SpawnBucket
import com.cobblemon.mod.common.api.spawning.context.SpawningContext
import com.cobblemon.mod.common.api.spawning.detail.SpawnDetail
import com.cobblemon.mod.common.api.spawning.spawner.Spawner
import net.minecraft.resources.ResourceLocation
/**
* A type of precalculation that can occur on a list of [SpawnDetail] to accelerate
@@ -31,7 +32,7 @@ import com.cobblemon.mod.common.api.spawning.spawner.Spawner
*/
interface SpawningPrecalculation<T : Any> {
/** Retrieves the precalculation keys for the given spawn detail. */
fun select(detail: SpawnDetail): List<T>
fun select(detail: SpawnDetail): Collection<T>
/** Retrieves the precalculation key for the given context, if that key exists for this context. */
fun select(ctx: SpawningContext): T?
@@ -71,7 +72,8 @@ interface SpawningPrecalculation<T : Any> {
* @since January 31st, 2022
*/
object RootPrecalculation : SpawningPrecalculation<Any> {
override fun select(detail: SpawnDetail): List<Any> = listOf(Unit)
val list = listOf(Unit)
override fun select(detail: SpawnDetail): List<Any> = list
override fun select(ctx: SpawningContext): Any = Unit
}
@@ -89,7 +91,15 @@ object ContextPrecalculation : SpawningPrecalculation<Class<out SpawningContext>
object BucketPrecalculation : SpawningPrecalculation<SpawnBucket> {
override fun select(detail: SpawnDetail) = listOf(detail.bucket)
override fun select(ctx: SpawningContext) = ctx.cause.bucket
}
/**
* This isn't so much of a precalculation as it is a yoinking of another precalculation.
* Uses pre-established [SpawnDetail.validBiomes] to shortcut knowing which spawns are valid for a biome.
*/
object BiomePrecalculation : SpawningPrecalculation<ResourceLocation> {
override fun select(detail: SpawnDetail) = detail.validBiomes
override fun select(ctx: SpawningContext) = ctx.biomeHolder.unwrapKey().map { it.location() }.orElse(null)
}
/**
@@ -18,6 +18,7 @@ import net.minecraft.world.level.block.Block
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.server.level.ServerLevel
import net.minecraft.core.BlockPos
import net.minecraft.core.Holder
/**
* A [SpawningContext] that is for a particular area, and therefore has spatial properties.
@@ -39,6 +40,7 @@ open class AreaSpawningContext(
val slice: WorldSlice
) : SpawningContext() {
val nearbyBlockTypes: List<Block> by lazy { nearbyBlocks.mapNotNull { it.block }.distinct() }
val nearbyBlockHolders: List<Holder<Block>> by lazy { nearbyBlockTypes.mapNotNull(blockRegistry::wrapAsHolder) }
override fun getStructureCache(pos: BlockPos): StructureChunkCache {
return slice.getStructureCache(pos)
@@ -11,9 +11,12 @@ package com.cobblemon.mod.common.api.spawning.context
import com.cobblemon.mod.common.api.spawning.SpawnCause
import com.cobblemon.mod.common.api.spawning.WorldSlice
import com.cobblemon.mod.common.api.spawning.influence.SpawningInfluence
import com.cobblemon.mod.common.util.blockRegistry
import net.minecraft.core.BlockPos
import net.minecraft.core.Holder
import net.minecraft.server.level.ServerLevel
import net.minecraft.tags.FluidTags
import net.minecraft.world.level.block.Block
import net.minecraft.world.level.block.state.BlockState
/**
@@ -36,6 +39,7 @@ abstract class FlooredSpawningContext(
) : AreaSpawningContext(cause, world, position, light, skyLight, canSeeSky, influences, height, nearbyBlocks, slice) {
/** The block that the spawning is occurring on. */
val baseBlock = slice.getBlockState(position.x, position.y, position.z)
val baseBlockHolder: Holder<Block> by lazy { world.blockRegistry.wrapAsHolder(baseBlock.block) }
}
/**
@@ -82,7 +82,9 @@ abstract class SpawningContext {
/** The current phase of the moon at this location. */
val moonPhase: Int by lazy { world.moonPhase }
/** The biome of this location. */
val biome: Biome by lazy { world.getBiome(position).value() }
val biome: Biome by lazy { biomeHolder.value() }
/** The registry holder for the biome this context is in. */
val biomeHolder: Holder<Biome> by lazy { world.getBiome(position) }
val biomeRegistry: Registry<Biome> by lazy { world.registryAccess().registryOrThrow(Registries.BIOME) }
val blockRegistry: Registry<Block> by lazy { world.registryAccess().registryOrThrow(Registries.BLOCK) }
@@ -13,6 +13,7 @@ import com.cobblemon.mod.common.api.Priority
import com.cobblemon.mod.common.api.spawning.SpawnCause
import com.cobblemon.mod.common.api.spawning.context.SpawningContext
import net.minecraft.server.level.ServerLevel
import net.minecraft.tags.BlockTags
import net.minecraft.tags.FluidTags
import net.minecraft.world.level.block.state.BlockState
@@ -29,7 +30,7 @@ import net.minecraft.world.level.block.state.BlockState
*/
interface SpawningContextCalculator<I : SpawningContextInput, O : SpawningContext> {
companion object {
val isAirCondition: (BlockState) -> Boolean = { it.isAir || (!it.isSolid && !it.fluidState.`is`(FluidTags.WATER)) }
val isAirCondition: (BlockState) -> Boolean = { it.isAir || (!it.isSolid && !it.fluidState.`is`(FluidTags.WATER) && !it.`is`(BlockTags.RAILS)) }
val isSolidCondition: (BlockState) -> Boolean = { it.isSolid }
val isWaterCondition: (BlockState) -> Boolean = { it.fluidState.`is`(FluidTags.WATER) && it.fluidState.isSource }
val isLavaCondition: (BlockState) -> Boolean = { it.fluidState.`is`(FluidTags.LAVA) && it.fluidState.isSource }
@@ -23,6 +23,9 @@ import com.cobblemon.mod.common.api.spawning.multiplier.WeightMultiplier
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.util.asArrayValue
import com.cobblemon.mod.common.util.asTranslated
import net.minecraft.core.registries.Registries
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.MinecraftServer
/**
* A spawnable unit in the Best Spawner API. This is extended for any kind of entity
@@ -56,6 +59,14 @@ abstract class SpawnDetail : ModDependant {
var labels = mutableListOf<String>()
/**
* This is calculated when the server starts. It is a set of all biome identifiers in which this spawn
* is possible. It is used as part of the [com.cobblemon.mod.common.api.spawning.condition.BiomePrecalculation].
*/
@Transient
val validBiomes = mutableSetOf<ResourceLocation>()
@Transient
val struct: QueryStruct = queryStructOf(
"weight" to { DoubleValue(weight) },
"percentage" to { DoubleValue(percentage) },
@@ -74,6 +85,23 @@ abstract class SpawnDetail : ModDependant {
open fun getName() = displayName?.asTranslated() ?: id.text()
open fun onServerLoad(server: MinecraftServer) {
val biomeRegistry = server.registryAccess().registryOrThrow(Registries.BIOME)
validBiomes.clear()
// Calculate in advance what biomes of this world the spawn detail is valid for.
biomeRegistry.holders().forEach { holder ->
val key = holder.unwrapKey().orElse(null) ?: return@forEach
if (conditions.isEmpty() || conditions.any { it.biomes == null || it.biomes!!.isEmpty() || it.biomes!!.any { it.fits(holder) } }) {
if (anticonditions.isEmpty() || anticonditions.none { it.biomes != null && it.biomes!!.any { it.fits(holder) } }) {
if (compositeCondition?.isBiomeValid(holder) != false) {
validBiomes.add(key.location())
}
}
}
}
}
open fun isSatisfiedBy(ctx: SpawningContext): Boolean {
if (!ctx.preFilter(this)) {
return false
@@ -10,6 +10,7 @@ package com.cobblemon.mod.common.api.spawning.detail
import com.cobblemon.mod.common.api.data.JsonDataRegistry
import com.cobblemon.mod.common.api.reactive.SimpleObservable
import com.cobblemon.mod.common.api.spawning.CobblemonSpawnPools
import com.cobblemon.mod.common.api.spawning.SpawnLoader
import com.cobblemon.mod.common.api.spawning.SpawnSet
import com.cobblemon.mod.common.api.spawning.condition.PrecalculationResult
@@ -18,6 +19,7 @@ import com.cobblemon.mod.common.api.spawning.condition.SpawningPrecalculation
import com.cobblemon.mod.common.api.spawning.context.SpawningContext
import com.cobblemon.mod.common.api.spawning.spawner.Spawner
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.server
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import net.minecraft.resources.ResourceLocation
@@ -48,6 +50,10 @@ class SpawnPool(val name: String) : JsonDataRegistry<SpawnSet>, Iterable<SpawnDe
for (set in data.values) {
details.addAll(set.filter { it.isValid() })
}
val server = server()
if (server != null && server.isRunning) {
CobblemonSpawnPools.onServerLoad(server)
}
precalculate()
}
@@ -1,51 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.spawning.influence
import com.cobblemon.mod.common.api.spawning.context.SpawningContext
import com.cobblemon.mod.common.api.spawning.detail.SpawnDetail
import net.minecraft.tags.BlockTags
/**
* Implements a [SpawningInfluence] that prevents Pokémon spawning on certain blocks.
* This ensures that Pokémon don't spawn in areas that could disrupt player-built structures or mechanisms.
* The influence checks the block directly above the potential spawn location; if the block is in the restricted list,
* the spawn is disallowed.
*
* @author Aethen
* @since February 20th, 2024
*/
open class RestrictedSpawnBlocksInfluence
: SpawningInfluence {
// The list of blocks that Pokémon are not allowed to spawn on.
private val restrictedBlocks = BlockTags.RAILS
/**
* Affects the spawnable status of a Pokémon spawn detail.
* If the block directly above the spawn location is in the restricted list, the spawn is disallowed.
*
* @param detail The Pokémon spawn detail to be affected.
* @param ctx The context of the spawning event.
* @return `true` if the spawn is allowed, `false` if it is disallowed.
*/
override fun affectSpawnable(detail: SpawnDetail, ctx: SpawningContext): Boolean {
// If the spawn detail is not a Pokémon, it is not affected by this influence.
if (detail.type != "pokemon") return true
// Get the position of the potential spawn location.
val pokemonSpawnPos = ctx.position
// Examine the block immediately above the spawn block to check for a rail
// Potentially need a refactor if we ever want to check the spawn block itself.
return ctx.world.getBlockState(pokemonSpawnPos.atY(pokemonSpawnPos.y + 1)).`is`(restrictedBlocks).not()
}
}
@@ -66,9 +66,12 @@ abstract class AreaSpawner(
if (constrainedArea != null) {
val areaBox = AABB.ofSize(
Vec3(
constrainedArea.getCenter().toVec3f()
), CHUNK_REACH * 16.0 * 2, 1000.0, CHUNK_REACH * 16.0 * 2)
Vec3(constrainedArea.getCenter().toVec3f()),
CHUNK_REACH * 16.0 * 2,
1000.0,
CHUNK_REACH * 16.0 * 2
)
if (!constrainedArea.world.isBoxLoaded(areaBox)) {
return null
}
@@ -142,7 +145,7 @@ abstract class AreaSpawner(
break
} else if (isValidStartPoint(area.world, chunk, basePos.setY(originalY - offset))) {
valid = true
basePos.y = originalY + offset
basePos.y = originalY - offset
break
}
offset++
@@ -14,7 +14,6 @@ import com.cobblemon.mod.common.api.spawning.SpawnerManager
import com.cobblemon.mod.common.api.spawning.detail.SpawnPool
import com.cobblemon.mod.common.api.spawning.influence.PlayerLevelRangeInfluence
import com.cobblemon.mod.common.api.spawning.influence.PlayerLevelRangeInfluence.Companion.TYPICAL_VARIATION
import com.cobblemon.mod.common.api.spawning.influence.RestrictedSpawnBlocksInfluence
import com.cobblemon.mod.common.api.spawning.influence.SpawningInfluence
import com.cobblemon.mod.common.api.spawning.rules.SpawnRule
import com.cobblemon.mod.common.util.mutableLazy
@@ -45,7 +44,7 @@ object PlayerSpawnerFactory {
it,
variation = TYPICAL_VARIATION
)
}, { RestrictedSpawnBlocksInfluence() })
})
fun create(spawnerManager: SpawnerManager, player: ServerPlayer): PlayerSpawner {
val influences = influenceBuilders.mapNotNull { it(player) }
@@ -45,8 +45,8 @@ open class BottomlessStore(override val uuid: UUID) : PokemonStore<BottomlessPos
override fun initialize() {
pokemon.forEachIndexed { index, pokemon ->
pokemon.storeCoordinates.set(StoreCoordinates(this, BottomlessPosition(index)))
pokemon.getChangeObservable().pipe(
stopAfter { it.storeCoordinates.get()?.store != this }
pokemon.changeObservable.pipe(
stopAfter { pokemon.storeCoordinates.get()?.store != this }
).subscribe { storeChangeObservable.emit(Unit) }
}
}
@@ -110,7 +110,7 @@ open class BottomlessStore(override val uuid: UUID) : PokemonStore<BottomlessPos
} else {
this.pokemon.removeAt(position.currentIndex)
}
for(i in startIndex until this.pokemon.size) {
for (i in startIndex until this.pokemon.size) {
this.pokemon[i].storeCoordinates.set(StoreCoordinates(this, BottomlessPosition(i)))
}
storeChangeObservable.emit(Unit)
@@ -18,10 +18,10 @@ import com.cobblemon.mod.common.api.storage.adapter.flatfile.FileStoreAdapter
import com.cobblemon.mod.common.api.storage.party.PlayerPartyStore
import com.cobblemon.mod.common.api.storage.pc.PCStore
import com.cobblemon.mod.common.platform.events.PlatformEvents
import com.cobblemon.mod.common.platform.events.ServerPlayerEvent
import com.cobblemon.mod.common.util.subscribeOnServer
import java.util.UUID
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import net.minecraft.core.RegistryAccess
import net.minecraft.server.level.ServerPlayer
@@ -97,7 +97,7 @@ open class FileBackedPokemonStoreFactory<S>(
fun save(store: PokemonStore<*>, registryAccess: RegistryAccess) {
val serialized = SerializedStore(store::class.java, store.uuid, adapter.serialize(store, registryAccess))
dirtyStores.remove(store)
saveExecutor.submit { adapter.save(serialized.storeClass, serialized.uuid, serialized.serializedForm) }
saveExecutor.execute { adapter.save(serialized.storeClass, serialized.uuid, serialized.serializedForm) }
}
fun saveAll(registryAccess: RegistryAccess) {
@@ -105,7 +105,7 @@ open class FileBackedPokemonStoreFactory<S>(
val serializedStores = dirtyStores.map { SerializedStore(it::class.java, it.uuid, adapter.serialize(it, registryAccess)) }
dirtyStores.clear()
LOGGER.debug("Queueing save.")
saveExecutor.submit {
saveExecutor.execute {
serializedStores.forEach { adapter.save(it.storeClass, it.uuid, it.serializedForm) }
LOGGER.debug("Saved ${serializedStores.size} Pokémon stores.")
}
@@ -123,6 +123,7 @@ open class FileBackedPokemonStoreFactory<S>(
saveSubscription.unsubscribe()
saveAll(registryAccess)
saveExecutor.shutdown()
saveExecutor.awaitTermination(30L, TimeUnit.SECONDS)
}
override fun onPlayerDisconnect(player: ServerPlayer) {
@@ -73,7 +73,7 @@ open class PartyStore(override val uuid: UUID) : PokemonStore<PartyPosition>() {
}
fun trackPokemon(pokemon: Pokemon) {
pokemon.getChangeObservable()
pokemon.changeObservable
.pipe(stopAfter { pokemon.storeCoordinates.get()?.store != this })
.subscribe { anyChangeObservable.emit(Unit) }
}
@@ -111,7 +111,7 @@ open class PCBox(val pc: PCStore) : Iterable<Pokemon> {
}
fun trackPokemon(pokemon: Pokemon) {
pokemon.getChangeObservable()
pokemon.changeObservable
.pipe(
stopAfter {
val coordinates = it.storeCoordinates.get() ?: return@stopAfter true
@@ -9,26 +9,43 @@
package com.cobblemon.mod.common.api.storage.pc
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.CobblemonItems
import com.cobblemon.mod.common.CobblemonNetwork.sendPacket
import com.cobblemon.mod.common.CobblemonSounds
import com.cobblemon.mod.common.CobblemonUnlockableWallpapers
import com.cobblemon.mod.common.api.events.CobblemonEvents
import com.cobblemon.mod.common.api.events.storage.WallpaperUnlockedEvent
import com.cobblemon.mod.common.api.molang.MoLangFunctions.asMoLangValue
import com.cobblemon.mod.common.api.reactive.SimpleObservable
import com.cobblemon.mod.common.api.storage.BottomlessStore
import com.cobblemon.mod.common.api.storage.PokemonStore
import com.cobblemon.mod.common.api.storage.StoreCoordinates
import com.cobblemon.mod.common.api.text.add
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.api.toast.Toast
import com.cobblemon.mod.common.net.messages.client.storage.RemoveClientPokemonPacket
import com.cobblemon.mod.common.net.messages.client.storage.SwapClientPokemonPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.InitializePCPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.MoveClientPCPokemonPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.SetPCPokemonPacket
import com.cobblemon.mod.common.net.messages.client.storage.pc.wallpaper.UnlockPCBoxWallpaperPacket
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.util.DataKeys
import com.cobblemon.mod.common.util.getPlayer
import com.cobblemon.mod.common.util.lang
import com.cobblemon.mod.common.util.toJsonArray
import com.google.gson.JsonObject
import java.util.UUID
import net.minecraft.core.RegistryAccess
import net.minecraft.nbt.CompoundTag
import net.minecraft.nbt.ListTag
import net.minecraft.nbt.StringTag
import net.minecraft.nbt.Tag
import net.minecraft.network.chat.MutableComponent
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import net.minecraft.sounds.SoundSource
import net.minecraft.world.item.ItemStack
/**
* The store used for PCs. It is divided into some number of [PCBox]es, and can
@@ -52,6 +69,8 @@ open class PCStore(
protected var lockedSize = false
val backupStore = BottomlessStore(UUID(0L, 0L))
val observingUUIDs = mutableSetOf(uuid)
val unlockedWallpapers = mutableSetOf<ResourceLocation>()
val unseenWallpapers = mutableSetOf<ResourceLocation>()
override fun iterator() = boxes.flatMap { it.toList() }.iterator()
override fun getObservingPlayers() = observingUUIDs.mapNotNull { it.getPlayer() }
@@ -135,6 +154,8 @@ open class PCStore(
nbt.put(DataKeys.STORE_BOX + index, box.saveToNBT(CompoundTag(), registryAccess))
}
nbt.put(DataKeys.STORE_BACKUP, backupStore.saveToNBT(CompoundTag(), registryAccess))
nbt.put(DataKeys.STORE_UNLOCKED_WALLPAPERS, ListTag().also { it.addAll(unlockedWallpapers.map { StringTag.valueOf(it.toString()) }) })
nbt.put(DataKeys.STORE_UNSEEN_WALLPAPERS, ListTag().also { it.addAll(unseenWallpapers.map { StringTag.valueOf(it.toString()) }) })
return nbt
}
@@ -152,6 +173,8 @@ open class PCStore(
removeDuplicates()
unlockedWallpapers.addAll(nbt.getList(DataKeys.STORE_UNLOCKED_WALLPAPERS, Tag.TAG_STRING.toInt()).map { ResourceLocation.parse(it.asString) })
unseenWallpapers.addAll(nbt.getList(DataKeys.STORE_UNSEEN_WALLPAPERS, Tag.TAG_STRING.toInt()).map { ResourceLocation.parse(it.asString) })
return this
}
@@ -177,6 +200,8 @@ open class PCStore(
json.add(DataKeys.STORE_BOX + index, box.saveToJSON(JsonObject(), registryAccess))
}
json.add(DataKeys.STORE_BACKUP, backupStore.saveToJSON(JsonObject(), registryAccess))
json.add(DataKeys.STORE_UNLOCKED_WALLPAPERS, unlockedWallpapers.map(ResourceLocation::toString).toJsonArray())
json.add(DataKeys.STORE_UNSEEN_WALLPAPERS, unseenWallpapers.map(ResourceLocation::toString).toJsonArray())
return json
}
@@ -194,6 +219,9 @@ open class PCStore(
removeDuplicates()
unlockedWallpapers.addAll(json.getAsJsonArray(DataKeys.STORE_UNLOCKED_WALLPAPERS).map { ResourceLocation.parse(it.asString) })
unseenWallpapers.addAll(json.getAsJsonArray(DataKeys.STORE_UNSEEN_WALLPAPERS).map { ResourceLocation.parse(it.asString) })
return this
}
@@ -257,4 +285,47 @@ open class PCStore(
}
}
}
fun markWallpapersSeen(textures: Set<ResourceLocation>) {
// This first step cleans out any that were unseen but were removed from the datapack side before they were seen.
val currentlyUnseen = unseenWallpapers.mapNotNull { CobblemonUnlockableWallpapers.unlockableWallpapers[it] }
val remainingUnseen = currentlyUnseen.filter { it.texture !in textures }
this.unseenWallpapers.clear()
this.unseenWallpapers.addAll(remainingUnseen.map { it.id })
pcChangeObservable.emit(Unit)
}
fun unlockWallpaper(wallpaper: ResourceLocation, playSound: Boolean = true): Boolean {
val unlockableWallpaper = CobblemonUnlockableWallpapers.unlockableWallpapers[wallpaper]
var succeeded = false
if (unlockableWallpaper != null && unlockableWallpaper.enabled) {
val event = WallpaperUnlockedEvent(pc = this, wallpaper = unlockableWallpaper, shouldNotify = playSound)
CobblemonEvents.WALLPAPER_UNLOCKED_EVENT.postThen(
event = event,
ifSucceeded = {
if (unlockedWallpapers.add(wallpaper)) {
unseenWallpapers.add(unlockableWallpaper.texture)
pcChangeObservable.emit(Unit)
getObservingPlayers().forEach { player ->
player.sendPacket(UnlockPCBoxWallpaperPacket(unlockableWallpaper.texture))
if (event.shouldNotify) {
val toast = Toast(
title = lang("wallpaper_unlocked"),
description = unlockableWallpaper.displayName?.let { "\"".text().add(it).add("\"") } ?: lang("unknown_wallpaper") ,
icon = ItemStack(CobblemonItems.PC)
)
toast.addListeners(player)
toast.expireAfter(5F)
player.playNotifySound(CobblemonSounds.PC_WALLPAPER_UNLOCK, SoundSource.MASTER, 1F, 1F)
}
}
succeeded = true
}
}
)
}
return succeeded
}
}
@@ -0,0 +1,42 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.api.storage.pc
import com.bedrockk.molang.runtime.value.DoubleValue
import com.bedrockk.molang.runtime.value.StringValue
import com.cobblemon.mod.common.api.molang.ObjectValue
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.network.chat.Component
import net.minecraft.resources.ResourceLocation
/**
* A server-defined wallpaper for PC boxes that are not immediately usable. Typically a command
* or a code action is required to add the wallpaper to the unlock list.
*
* @author Hiroku
* @since February 9th, 2025
*/
class UnlockablePCWallpaper {
@Transient
lateinit var id: ResourceLocation
/** This is for when we provide a wallpaper but some server wants to remove it from the unlockables. */
var enabled = true
/** If unlocked with a notification toast, this is the name displayed in the toast. If blank, ??? will be used. */
var displayName: Component? = null
var texture: ResourceLocation = cobblemonResource("dummy.png")
// I feel like there's more that could be added to this. Things like whether it displays when still locked, and if so what kind of hint to provide.
// That requires client side changes which means it requires The Expert which is why I'm not committing to it right this second.
@Transient
val struct = ObjectValue(this).also {
it.addFunction("texture") { StringValue(texture.toString()) }
it.addFunction("id") { StringValue(id.toString()) }
it.addFunction("enabled") { DoubleValue(enabled) }
}
}
@@ -37,9 +37,9 @@ class Search(
if (inverted) {
filter = filter.substring(1)
}
val pokemonFilter: PokemonFilter = when (filter) {
"holding" -> PokemonFilter { pokemon -> !pokemon.heldItem().isEmpty }
"holding", "helditem", "held_item" -> PokemonFilter { pokemon -> !pokemon.heldItem().isEmpty }
"fainted" -> PokemonFilter { pokemon -> pokemon.isFainted() }
"legendary" -> PokemonFilter { pokemon -> pokemon.isLegendary() }
"mythical" -> PokemonFilter { pokemon -> pokemon.isMythical() }
@@ -8,10 +8,14 @@
package com.cobblemon.mod.common.api.storage.player
import com.cobblemon.mod.common.Cobblemon.MODID
import com.cobblemon.mod.common.api.pokedex.PokedexManager
import com.cobblemon.mod.common.api.scheduling.ScheduledTask
import com.cobblemon.mod.common.api.scheduling.ServerTaskTracker
import com.google.common.util.concurrent.ThreadFactoryBuilder
import java.util.UUID
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import net.minecraft.server.MinecraftServer
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.entity.player.Player
@@ -28,6 +32,13 @@ import net.minecraft.world.entity.player.Player
*/
open class PlayerInstancedDataStoreManager {
val saveExecutor = Executors.newSingleThreadExecutor(
ThreadFactoryBuilder()
.setNameFormat("$MODID Player Data Save Executor")
.setDaemon(true)
.setPriority(1)
.build()
)
val factories = mutableMapOf<PlayerInstancedDataStoreType, PlayerInstancedDataFactory<*>>()
val saveTasks = mutableMapOf<PlayerInstancedDataStoreType, ScheduledTask>()
open fun setFactory(factory: PlayerInstancedDataFactory<*>, dataType: PlayerInstancedDataStoreType) {
@@ -37,10 +37,10 @@ abstract class FileBasedPlayerDataStoreBackend<T : InstancedPlayerData>(
savePath = server.getWorldPath(LevelResource.PLAYER_DATA_DIR).parent
}
protected fun postSaveFileMoving(playerData: T) {
val tempFile = filePath(playerData.uuid, TEMPORARY_FILE_EXTENSION)
val oldFile = filePath(playerData.uuid, OLD_FILE_EXTENSION)
val file = filePath(playerData.uuid)
protected fun postSaveFileMoving(uuid: UUID) {
val tempFile = filePath(uuid, TEMPORARY_FILE_EXTENSION)
val oldFile = filePath(uuid, OLD_FILE_EXTENSION)
val file = filePath(uuid)
if (file.exists()) {
file.copyTo(oldFile, overwrite = true)
}
@@ -8,13 +8,11 @@
package com.cobblemon.mod.common.api.storage.player.adapter
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.storage.player.InstancedPlayerData
import com.cobblemon.mod.common.api.storage.player.PlayerInstancedDataStoreType
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import java.io.BufferedReader
import java.io.FileReader
import java.io.PrintWriter
import java.util.UUID
import kotlin.reflect.KMutableProperty
import kotlin.reflect.full.memberProperties
@@ -30,10 +28,11 @@ abstract class JsonBackedPlayerDataStoreBackend<T : InstancedPlayerData>(
override fun save(playerData: T) {
val fileTmp = filePath(playerData.uuid, TEMPORARY_FILE_EXTENSION)
fileTmp.parentFile.mkdirs()
fileTmp.printWriter().use { pw ->
pw.write(gson.toJson(playerData))
val json = gson.toJson(playerData)
Cobblemon.playerDataManager.saveExecutor.execute {
fileTmp.printWriter().use { pw -> pw.write(json) }
postSaveFileMoving(playerData.uuid)
}
postSaveFileMoving(playerData)
}
override fun load(uuid: UUID): T {
@@ -8,19 +8,17 @@
package com.cobblemon.mod.common.api.storage.player.adapter
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.storage.player.InstancedPlayerData
import com.cobblemon.mod.common.api.storage.player.PlayerInstancedDataStoreType
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.mongodb.client.MongoClient
import com.mongodb.client.model.ReplaceOptions
import org.bson.Document
import java.io.BufferedReader
import java.io.FileReader
import java.io.PrintWriter
import java.util.UUID
import kotlin.reflect.KMutableProperty
import kotlin.reflect.full.memberProperties
import org.bson.Document
abstract class MongoBackedPlayerDataStoreBackend<T : InstancedPlayerData>(
mongoClient: MongoClient, databaseName: String, collectionName: String, type: PlayerInstancedDataStoreType
@@ -31,11 +29,14 @@ abstract class MongoBackedPlayerDataStoreBackend<T : InstancedPlayerData>(
abstract val classToken: TypeToken<T>
override fun save(playerData: T) {
collection.replaceOne(
Document("uuid", playerData.uuid.toString()),
Document.parse(gson.toJson(playerData)),
ReplaceOptions().upsert(true)
)
val json = gson.toJson(playerData)
Cobblemon.playerDataManager.saveExecutor.execute {
collection.replaceOne(
Document("uuid", playerData.uuid.toString()),
Document.parse(json),
ReplaceOptions().upsert(true)
)
}
}
override fun load(uuid: UUID): T {
@@ -29,8 +29,11 @@ abstract class NbtBackedPlayerData<T : InstancedPlayerData>(
val fileTmp = filePath(playerData.uuid, TEMPORARY_FILE_EXTENSION)
fileTmp.parentFile.mkdirs()
val encodeResult = codec.encodeStart(NbtOps.INSTANCE, playerData)
NbtIo.write(encodeResult.result().get() as CompoundTag, fileTmp.toPath())
postSaveFileMoving(playerData)
val tag = encodeResult.result().get() as CompoundTag
Cobblemon.playerDataManager.saveExecutor.submit {
NbtIo.write(tag, fileTmp.toPath())
postSaveFileMoving(playerData.uuid)
}
}
override fun load(uuid: UUID): T {
@@ -10,17 +10,17 @@ package com.cobblemon.mod.common.api.toast
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.scheduling.afterOnServer
import com.cobblemon.mod.common.api.toast.Toast.Companion.VANILLA_FRAME
import com.cobblemon.mod.common.api.toast.Toast.Companion.VANILLA_PROGRESS_COLOR
import com.cobblemon.mod.common.net.messages.client.toast.ToastPacket
import com.cobblemon.mod.common.platform.events.PlatformEvents
import java.util.UUID
import kotlin.properties.Delegates
import net.minecraft.client.gui.components.toasts.AdvancementToast
import net.minecraft.network.chat.Component
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import net.minecraft.util.Mth
import net.minecraft.world.item.ItemStack
import java.util.*
import kotlin.properties.Delegates
/**
* Represents a toast that can be shown to multiple clients.
@@ -40,7 +40,7 @@ class Toast(
title: Component,
description: Component,
icon: ItemStack,
frameTexture: ResourceLocation = VANILLA_FRAME,
frameTexture: ResourceLocation = AdvancementToast.BACKGROUND_SPRITE,
progress: Float = -1F,
progressColor: Int = VANILLA_PROGRESS_COLOR
) {
@@ -195,12 +195,6 @@ class Toast(
)
companion object {
/**
* The default Minecraft toast frame texture.
*/
val VANILLA_FRAME = ResourceLocation.fromNamespaceAndPath("minecraft", "textures/gui/toasts.png")
const val VANILLA_PROGRESS_COLOR = -1675545
}
@@ -17,6 +17,7 @@ import com.cobblemon.mod.common.api.storage.party.PlayerPartyStore
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.runner.ShowdownService
import com.cobblemon.mod.common.entity.npc.NPCBattleActor
import com.cobblemon.mod.common.entity.npc.NPCEntity
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
@@ -59,7 +60,9 @@ object BattleBuilder {
battleFormat: BattleFormat = BattleFormat.GEN_9_SINGLES,
cloneParties: Boolean = false,
healFirst: Boolean = false,
partyAccessor: (ServerPlayer) -> PartyStore = { it.party() }
partyAccessor: (ServerPlayer) -> PartyStore = { it.party() },
service: ShowdownService = ShowdownService.get(),
battleId: UUID = UUID.randomUUID()
): BattleStartResult {
val adjustLevel = battleFormat.adjustLevel
@@ -116,7 +119,9 @@ object BattleBuilder {
BattleRegistry.startBattle(
battleFormat = battleFormat,
side1 = BattleSide(player1Actor),
side2 = BattleSide(player2Actor)
side2 = BattleSide(player2Actor),
service = service,
battleId = battleId
).ifSuccessful {
it.battlePartyStores.addAll(battlePartyStores)
}
@@ -132,7 +137,9 @@ object BattleBuilder {
battleFormat: BattleFormat = BattleFormat.GEN_9_MULTI,
cloneParties: Boolean = false,
healFirst: Boolean = false,
partyAccessor: (ServerPlayer) -> PartyStore = { it.party() }
partyAccessor: (ServerPlayer) -> PartyStore = { it.party() },
service: ShowdownService = ShowdownService.get(),
battleId: UUID = UUID.randomUUID()
): BattleStartResult {
val adjustLevel = battleFormat.adjustLevel
val teams = players.mapIndexed { index, it ->
@@ -213,7 +220,9 @@ object BattleBuilder {
BattleRegistry.startBattle(
battleFormat = battleFormat,
side1 = BattleSide(playerActors[0], playerActors[1]),
side2 = BattleSide(playerActors[2], playerActors[3])
side2 = BattleSide(playerActors[2], playerActors[3]),
service = service,
battleId = battleId
).ifSuccessful {
it.battlePartyStores.addAll(battlePartyStores)
}
@@ -245,7 +254,9 @@ object BattleBuilder {
cloneParties: Boolean = false,
healFirst: Boolean = false,
fleeDistance: Float = Cobblemon.config.defaultFleeDistance,
party: PartyStore = player.party()
party: PartyStore = player.party(),
service: ShowdownService = ShowdownService.get(),
battleId: UUID = UUID.randomUUID()
): BattleStartResult {
val playerTeam = party.toBattleTeam(clone = cloneParties, healPokemon = healFirst, leadingPokemon = leadingPokemon).sortedBy { it.health <= 0 }
val playerActor = PlayerBattleActor(player.uuid, playerTeam)
@@ -286,7 +297,9 @@ object BattleBuilder {
BattleRegistry.startBattle(
battleFormat = battleFormat,
side1 = BattleSide(playerActor),
side2 = BattleSide(wildActor)
side2 = BattleSide(wildActor),
service = service,
battleId = battleId
).ifSuccessful {
if (!cloneParties) {
pokemonEntity.battleId = it.battleId
@@ -316,7 +329,9 @@ object BattleBuilder {
battleFormat: BattleFormat = BattleFormat.GEN_9_SINGLES,
cloneParties: Boolean = false,
healFirst: Boolean = false,
party: PartyStore = player.party()
party: PartyStore = player.party(),
service: ShowdownService = ShowdownService.get(),
battleId: UUID = UUID.randomUUID()
): BattleStartResult {
val playerTeam = party.toBattleTeam(clone = cloneParties, healPokemon = healFirst, leadingPokemon = leadingPokemon)
val playerActor = PlayerBattleActor(player.uuid, playerTeam)
@@ -363,7 +378,9 @@ object BattleBuilder {
BattleRegistry.startBattle(
battleFormat = battleFormat,
side1 = BattleSide(playerActor),
side2 = BattleSide(npcActor)
side2 = BattleSide(npcActor),
service = service,
battleId = battleId
).ifSuccessful { battle ->
npcEntity.entityData.update(NPCEntity.BATTLE_IDS) { it + battle.battleId }
}
@@ -537,4 +554,4 @@ open class ErroredBattleStart(
val errors: Iterable<BattleStartError>
get() = generalErrors + participantErrors.flatMap { it.value }
}
}
@@ -125,7 +125,7 @@ object BattleRegistry {
return team.joinToString("]")
}
private fun startShowdown(battle: PokemonBattle) {
private fun startShowdown(battle: PokemonBattle, sendMessages: Boolean = true) {
// Build request message
val messages = mutableListOf<String>()
@@ -176,7 +176,9 @@ object BattleRegistry {
}
// Compiles the request and sends it off
ShowdownService.service.startBattle(battle, messages.toTypedArray())
if (sendMessages) {
battle.service.startBattle(battle, messages.toTypedArray())
}
}
/**
@@ -186,6 +188,7 @@ object BattleRegistry {
* @param side1 The first side for the battle.
* @param side2 The second side for the battle.
* @param canPreempt Whether the battle can be blocked by subscribers to [CobblemonEvents.BATTLE_STARTED_PRE].
* @param service The showdown service to use for the battle.
*
* NOTE: this does NOT validate the [battleFormat] or sides before launching Showdown.
* This creates the battle and hands it off to Showdown - YOU WILL GET ISSUES IF THE BATTLE IS INVALID!
@@ -195,12 +198,15 @@ object BattleRegistry {
battleFormat: BattleFormat,
side1: BattleSide,
side2: BattleSide,
canPreempt: Boolean = true
canPreempt: Boolean = true,
service: ShowdownService = ShowdownService.get(),
battleId: UUID = UUID.randomUUID(),
sendMessages: Boolean = true
): BattleStartResult {
val battle = PokemonBattle(battleFormat, side1, side2)
val battle = PokemonBattle(battleFormat, side1, side2, service, battleId)
val start: () -> Unit = {
battleMap[battle.battleId] = battle
startShowdown(battle)
startShowdown(battle, sendMessages)
}
if (!canPreempt) start().also { return SuccessfulBattleStart(battle) }
@@ -234,4 +240,4 @@ object BattleRegistry {
fun tick() {
battleMap.forEachValue(Long.MAX_VALUE) { it.tick() }
}
}
}
@@ -23,13 +23,13 @@ class ShowdownThread : Thread("Cobblemon Showdown") {
this.start()
this.latch.await()
for (action in whenReady) {
action(ShowdownService.service)
action(ShowdownService.get())
}
}
fun queue(action: (ShowdownService) -> Unit) {
if (this.latch.count == 0L) {
action(ShowdownService.service)
action(ShowdownService.get())
} else {
this.whenReady.add(action)
}
@@ -37,8 +37,8 @@ class ShowdownThread : Thread("Cobblemon Showdown") {
override fun run() {
LOGGER.info("Starting showdown service...")
ShowdownService.service.openConnection()
ShowdownService.get().openConnection()
LOGGER.info("Showdown has been started!")
this.latch.countDown()
}
}
}
@@ -785,7 +785,7 @@ class StrongBattleAI(skill: Int) : BattleAI {
/*availableSwitches.forEach {
estimateMatchup(request, battle, it.effectedPokemon)
}*/
val bestEstimation = if ( 1 == 1 /* todo if player is in bad matchup and switches out a lot and has a better matchup in revealed party */) {
val bestEstimation = if (1 == 1 /* todo if player is in bad matchup and switches out a lot and has a better matchup in revealed party */) {
// todo make it so that bestEstimation is actually in comparison to that potential pokemon instead and be sure to switch to that instead
availableSwitches.maxOfOrNull { estimateMatchup(activeBattlePokemon, request, battle, it.effectedPokemon) }
@@ -13,6 +13,7 @@ import com.cobblemon.mod.common.api.battles.interpreter.BattleMessage
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.moves.Moves
import com.cobblemon.mod.common.api.moves.animations.ActionEffectContext
import com.cobblemon.mod.common.api.moves.animations.ActionEffects
import com.cobblemon.mod.common.api.moves.animations.UsersProvider
import com.cobblemon.mod.common.api.pokemon.status.Statuses
import com.cobblemon.mod.common.battles.ShowdownInterpreter
@@ -56,7 +57,9 @@ class ActivateInstruction(val instructionSet: InstructionSet, val message: Battl
battle.dispatch {
val effect = message.effectAt(1) ?: return@dispatch GO
val status = Statuses.getStatus(effect.id)
val actionEffect = status?.getActionEffect() ?: return@dispatch GO
val actionEffect = status?.getActionEffect() ?: let {
ActionEffects.actionEffects[cobblemonResource("activate_${effect.id}")] ?: return@dispatch GO
}
val providers = mutableListOf<Any>(battle)
val pokemon = message.battlePokemon(0, battle) ?: return@dispatch GO
pokemon.effectedPokemon.entity?.let { UsersProvider(it) }?.let(providers::add)
@@ -90,7 +90,7 @@ class MoveInstruction(
val providers = mutableListOf<Any>(battle)
userPokemon.effectedPokemon.entity?.let { UsersProvider(it) }?.let(providers::add)
if (spreadTargetPokemon.isNotEmpty()) {
providers.add(TargetsProvider( spreadTargetPokemon.filter { it?.effectedPokemon?.entity != null}.mapNotNull { spreadTarget -> spreadTarget?.effectedPokemon?.entity } ))
providers.add(TargetsProvider(spreadTargetPokemon.filter { it?.effectedPokemon?.entity != null}.mapNotNull { spreadTarget -> spreadTarget?.effectedPokemon?.entity }))
} else {
targetPokemon?.effectedPokemon?.entity?.let { TargetsProvider(it) }?.let(providers::add)
}
@@ -49,4 +49,4 @@ class RequestInstruction(val battleActor: BattleActor, val message: BattleMessag
}
}
}
}
@@ -26,7 +26,7 @@ import com.cobblemon.mod.common.util.asTranslated
* @author Hiroku
* @since October 3rd, 2022
*/
class StatusInstruction( val message: BattleMessage): InterpreterInstruction {
class StatusInstruction(val message: BattleMessage): InterpreterInstruction {
override fun invoke(battle: PokemonBattle) {
val (pnx, _) = message.pnxAndUuid(0) ?: return
@@ -0,0 +1,64 @@
package com.cobblemon.mod.common.battles.runner
import com.cobblemon.mod.common.Cobblemon
import java.text.SimpleDateFormat
import java.util.Date
/**
* Class responsible for logging debug messages for the showdown service.
*
* @author landonjw
*/
class ShowdownDebugLog {
val log = mutableListOf<LogMessage>()
/**
* Adds a log message to the debug log.
*
* @param source The source of the log message.
* @param message The log message.
*/
fun add(source: String, message: String) {
if (!Cobblemon.config.enableDebugKeys) return
val now = Date()
val timestamp = SimpleDateFormat("HH:mm:ss").format(now)
log.add(LogMessage("[$source][${timestamp}]> $message", now))
}
/**
* Clears all log messages from the debug log.
*/
fun clear() {
log.clear()
}
/**
* Retrieves log messages after a specified date.
*
* @param after The date after which to retrieve log messages. If null, retrieves the last 250 messages.
* @return A list of log messages.
*/
fun get(after: Date?): List<String> {
if (after == null) return log.take(250).map { it.message }
return log.filter { it.time.after(after) }.map { it.message }
}
/**
* Checks if the provided messages are the start of the log.
*
* @param messages The messages to check.
* @return True if the messages are the start of the log, false otherwise.
*/
fun isStartOfLog(messages: List<String>): Boolean {
if (messages.isEmpty() && log.isEmpty()) return true
return messages.size == log.size && messages.first() == log.first().message
}
/**
* Data class representing a log message.
*
* @property message The log message.
* @property time The time the log message was created.
*/
data class LogMessage(val message: String, val time: Date)
}
@@ -10,32 +10,154 @@ package com.cobblemon.mod.common.battles.runner
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.battles.runner.graal.GraalShowdownService
import com.cobblemon.mod.common.battles.runner.remote.UnionShowdownService
import com.cobblemon.mod.common.util.cobblemonResource
import com.google.gson.JsonArray
import net.minecraft.resources.ResourceLocation
import java.util.UUID
/**
* Mediator service for communicating between the Cobblemon Minecraft mod and Cobblemon showdown service.
*
* All outgoing calls to showdown should be done through this layer to decouple the mod from a specific implementation.
* To get an instance of the service, use the singleton provided via `ShowdownService.get()`
* To get an instance of the service, use the singleton provided via `ShowdownService.get()`.
*
* @since February 27th, 2023
* @author landonjw
*/
interface ShowdownService {
/**
* The unique identifier for this showdown service.
*/
val id: ResourceLocation
/**
* The current status of the showdown service.
*/
val status: ShowdownStatus
/**
* The log of messages from the showdown service.
* This is accessed for use in [ShowdownDebugGUI]. It is not intended for general use
* and will be empty in production environments.
*/
val log: ShowdownDebugLog
/**
* Opens a connection to the showdown service.
*/
fun openConnection()
/**
* Closes the connection to the showdown service.
*/
fun closeConnection()
/**
* Restarts the connection to the showdown service.
*/
fun restartConnection()
/**
* Starts a battle in the showdown service.
*
* @param battle The battle to start.
* @param messages The initial messages for the battle.
*/
fun startBattle(battle: PokemonBattle, messages: Array<String>)
/**
* Sends messages to a specific battle in the showdown service.
*
* @param battleId The unique identifier of the battle.
* @param messages The messages to send.
*/
fun send(battleId: UUID, messages: Array<String>)
/**
* Sends a message to the showdown service.
*
* @param message The message to send.
*/
fun send(message: String)
/**
* Retrieves the ability IDs from the showdown service.
*
* @return A `JsonArray` of ability IDs.
*/
fun getAbilityIds(): JsonArray
/**
* Retrieves the moves from the showdown service.
*
* @return A `JsonArray` of moves.
*/
fun getMoves(): JsonArray
/**
* Retrieves the item IDs from the showdown service.
*
* @return A `JsonArray` of item IDs.
*/
fun getItemIds(): JsonArray
/**
* Registers species in the showdown service.
*/
fun registerSpecies()
/**
* Registers bag items in the showdown service.
*/
fun registerBagItems()
/**
* Indicates that species have been initialized in the showdown service.
*/
fun indicateSpeciesInitialized() {}
companion object {
val service: ShowdownService by lazy { GraalShowdownService() }
private val services = mutableMapOf<ResourceLocation, ShowdownService>(
cobblemonResource("embedded") to GraalShowdownService(cobblemonResource("embedded")),
cobblemonResource("union") to UnionShowdownService(cobblemonResource("union"))
)
/**
* Retrieves the showdown service for the specified path.
*
* @param path The resource location of the showdown service.
* @return The showdown service.
* @throws IllegalStateException if no showdown service is found for the specified path.
*/
fun get(path: ResourceLocation = cobblemonResource("embedded")): ShowdownService {
return services[path] ?: error("No showdown service found for path $path")
}
/**
* Checks if a showdown service exists for the specified path.
*
* @param path The resource location of the showdown service.
* @return `true` if the showdown service exists, `false` otherwise.
*/
fun contains(path: ResourceLocation): Boolean {
return services.containsKey(path)
}
/**
* Retrieves all available showdown services.
*
* @return A list of all showdown services.
*/
fun all(): List<ShowdownService> {
return services.values.toList()
}
}
}
}
/**
* Enum representing the status of the showdown service.
*/
enum class ShowdownStatus {
CONNECTED, DISCONNECTED, RESTARTING
}
@@ -13,9 +13,12 @@ import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.battles.BagItems
import com.cobblemon.mod.common.battles.ShowdownInterpreter
import com.cobblemon.mod.common.battles.runner.ShowdownDebugLog
import com.cobblemon.mod.common.battles.runner.ShowdownService
import com.cobblemon.mod.common.battles.runner.ShowdownStatus
import com.google.gson.Gson
import com.google.gson.JsonArray
import net.minecraft.resources.ResourceLocation
import java.io.File
import java.io.IOException
import java.net.URI
@@ -38,12 +41,20 @@ import org.graalvm.polyglot.io.FileSystem
* Mediator service for communicating between the Cobblemon Minecraft mod and Cobblemon showdown service via
* GraalVM. This directly invokes JavaScript functions provided within the showdown service.
*
* @property id The unique identifier for this showdown service.
* @property context The GraalVM context for executing JavaScript.
* @property sendBattleMessageFunction The JavaScript function for sending battle messages.
* @property unbundler The unbundler for extracting necessary showdown files.
* @property gson The Gson instance for JSON operations.
* @property log The debug log for the showdown service.
* @property status The current status of the showdown service.
*
* @see {@code cobbled-exports/cobbled-index.js} within cobbledmon-showdown repository
* @see <a href="https://www.graalvm.org/">
* @since February 27, 2023
* @author Hiroku, landonjw
*/
class GraalShowdownService : ShowdownService {
class GraalShowdownService(override val id: ResourceLocation) : ShowdownService {
@Transient
lateinit var context: Context
@@ -54,12 +65,35 @@ class GraalShowdownService : ShowdownService {
@Transient
val gson = Gson()
override fun openConnection() {
override val log = ShowdownDebugLog()
override var status: ShowdownStatus = ShowdownStatus.DISCONNECTED
private set
/**
* Opens a connection to the showdown service.
*/
override fun openConnection() = openConnection(false)
/**
* Opens a connection to the showdown service.
*
* @param silent If true, suppresses changing the status of the connection process.
*/
private fun openConnection(silent: Boolean) {
log.clear()
log.add(source = "Minecraft", message = "Opening showdown connection")
unbundler.attemptUnbundle()
createContext()
boot()
if (!silent) {
status = ShowdownStatus.CONNECTED
}
}
/**
* Creates the GraalVM context for executing JavaScript.
*/
private fun createContext() {
val wd = Paths.get("./showdown")
val access = HostAccess.newBuilder(HostAccess.EXPLICIT)
@@ -116,42 +150,126 @@ class GraalShowdownService : ShowdownService {
""".trimIndent())
}
override fun closeConnection() {
/**
* Closes the connection to the showdown service.
*/
override fun closeConnection() = closeConnection(false)
/**
* Closes the connection to the showdown service.
*
* @param silent If true, suppresses changing the status of the connection process.
*/
private fun closeConnection(silent: Boolean) {
log.add(source = "Minecraft", message = "Closing showdown connection")
context.close()
if (!silent) {
this.status = ShowdownStatus.DISCONNECTED
}
}
/**
* Restarts the connection to the showdown service.
*/
override fun restartConnection() {
this.status = ShowdownStatus.RESTARTING
try {
closeConnection(true)
openConnection(true)
this.status = ShowdownStatus.CONNECTED
} catch (e: Exception) {
Cobblemon.LOGGER.error("Failed to restart showdown connection", e)
this.status = ShowdownStatus.DISCONNECTED
}
}
/**
* Boots the showdown service by evaluating the JavaScript entry point.
*/
private fun boot() {
log.add(source = "Minecraft", message = "Booting showdown")
context.eval("js", File("showdown/index.js").readText())
sendBattleMessageFunction = context.getBindings("js").getMember("sendBattleMessage")
}
/**
* Starts a battle in the showdown service.
*
* @param battle The battle to start.
* @param messages The initial messages for the battle.
*/
override fun startBattle(battle: PokemonBattle, messages: Array<String>) {
val startBattleFunction = context.getBindings("js").getMember("startBattle")
log.add(source = "Minecraft", message = "Starting battle ${battle.battleId}")
messages.forEach { message ->
log.add(source = "Minecraft", message = "(${battle.battleId}) $message")
}
startBattleFunction.execute(this, battle.battleId.toString(), messages)
}
/**
* Sends messages to a specific battle in the showdown service.
*
* @param battleId The unique identifier of the battle.
* @param messages The messages to send.
*/
override fun send(battleId: UUID, messages: Array<String>) {
sendToShowdown(battleId, messages)
}
/**
* Sends a message to the showdown service.
*
* @param message The message to send.
*/
override fun send(message: String) {
log.add("Minecraft", message)
val regex = Regex("^\\(([^()]*)\\)")
val match = regex.find(message)
val battleId = match?.groups?.get(1)?.value ?: return
val endIndex = match.groups[1]!!.range.last + 1
sendToShowdown(UUID.fromString(battleId), arrayOf(message.substring(endIndex)))
}
/**
* Retrieves the ability IDs from the showdown service.
*
* @return A `JsonArray` of ability IDs.
*/
override fun getAbilityIds(): JsonArray {
val getCobbledAbilityIdsFn = context.getBindings("js").getMember("getCobbledAbilityIds")
val arrayResult = getCobbledAbilityIdsFn.execute().asString()
log.add(source = "Minecraft", message = "Getting ability ids")
return gson.fromJson(arrayResult, JsonArray::class.java)
}
/**
* Retrieves the moves from the showdown service.
*
* @return A `JsonArray` of moves.
*/
override fun getMoves(): JsonArray {
val getCobbledMovesFn = context.getBindings("js").getMember("getCobbledMoves")
log.add(source = "Minecraft", message = "Getting moves")
val arrayResult = getCobbledMovesFn.execute().asString()
return gson.fromJson(arrayResult, JsonArray::class.java)
}
/**
* Retrieves the item IDs from the showdown service.
*
* @return A `JsonArray` of item IDs.
*/
override fun getItemIds(): JsonArray {
val getCobbledItemIdsFn = context.getBindings("js").getMember("getCobbledItemIds")
log.add(source = "Minecraft", message = "Getting item ids")
val arrayResult = getCobbledItemIdsFn.execute().asString()
return gson.fromJson(arrayResult, JsonArray::class.java)
}
/**
* Registers species in the showdown service.
*/
override fun registerSpecies() {
val receiveSpeciesDataFn = this.context.getBindings("js").getMember("receiveSpeciesData")
val jsArray = this.context.eval("js", "new Array();")
@@ -164,32 +282,62 @@ class GraalShowdownService : ShowdownService {
}
}
}
log.add(source = "Minecraft", message = "Registering species")
receiveSpeciesDataFn.execute(jsArray)
}
/**
* Indicates that species have been initialized to the showdown service.
*/
override fun indicateSpeciesInitialized() {
val afterCobbledSpeciesInitFn = this.context.getBindings("js").getMember("afterCobbledSpeciesInit")
log.add(source = "Minecraft", message = "Indicating species initialized")
afterCobbledSpeciesInitFn.execute()
}
/**
* Registers bag items in the showdown service.
*/
override fun registerBagItems() {
val receiveBagItemDataFn = this.context.getBindings("js").getMember("receiveBagItemData")
for ((itemId, js) in BagItems.bagItemsScripts) {
log.add(source = "Minecraft", message = "Registering bag item $itemId")
receiveBagItemDataFn.execute(itemId, js.replace("\n", " "))
}
}
/**
* Sends messages to the showdown service.
*
* @param battleId The unique identifier of the battle.
* @param messages The messages to send.
*/
private fun sendToShowdown(battleId: UUID, messages: Array<String>) {
messages.forEach { message ->
log.add(source = "Minecraft", message = "($battleId) $message")
}
sendBattleMessageFunction.execute(battleId.toString(), messages)
}
/**
* Receives messages from the showdown service.
*
* @param battleId The unique identifier of the battle.
* @param message The message received.
*/
@Export
fun sendFromShowdown(battleId: String, message: String) {
log.add(source = "Showdown", message = "($battleId) $message")
ShowdownInterpreter.interpretMessage(UUID.fromString(battleId), message)
}
/**
* Logs messages from the showdown service.
*
* @param message The message to log.
*/
@Export
fun log(message: String) {
Cobblemon.LOGGER.info(message)
}
}
}
@@ -0,0 +1,381 @@
package com.cobblemon.mod.common.battles.runner.remote
import com.bedrockk.molang.runtime.value.DoubleValue
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.battles.model.actor.UnionBattleActor
import com.cobblemon.mod.common.api.npc.NPCClasses
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.api.storage.party.PartyStore
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.battles.*
import com.cobblemon.mod.common.battles.actor.PlayerBattleActor
import com.cobblemon.mod.common.battles.pokemon.BattlePokemon
import com.cobblemon.mod.common.battles.runner.ShowdownDebugLog
import com.cobblemon.mod.common.battles.runner.ShowdownService
import com.cobblemon.mod.common.battles.runner.ShowdownStatus
import com.cobblemon.mod.common.entity.npc.NPCEntity
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.util.HttpService
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.server
import com.google.gson.JsonArray
import com.google.gson.JsonParser
import net.minecraft.resources.ResourceLocation
import net.minecraft.world.entity.schedule.Activity
import java.net.URI
import java.net.http.HttpClient
import java.net.http.WebSocket
import java.util.*
import java.util.concurrent.CompletionStage
/**
* Implementation of the ShowdownService interface for use with the Union Block.
*
* This wraps around a WebSocket connection to a remote showdown service.
*
* @property id The unique identifier for this showdown service.
* @property host The host address of the showdown service.
* @property httpPort The port for HTTP communication with the showdown service.
* @property socketPort The port for WebSocket communication with the showdown service.
*
* @author landonjw
*/
class UnionShowdownService(
override val id: ResourceLocation,
val host: String = "localhost",
val httpPort: Int = 8003,
val socketPort: Int = 8002
) : ShowdownService {
override var status: ShowdownStatus = ShowdownStatus.DISCONNECTED
private set
override val log = ShowdownDebugLog()
private lateinit var webSocket: WebSocket
/**
* Opens a connection to the showdown service.
*/
override fun openConnection() = openConnection(false)
/**
* Opens a connection to the showdown service.
*
* @param silent If true, suppresses changing the status of the connection process.
*/
private fun openConnection(silent: Boolean) {
log.clear()
log.add(source = "Minecraft", message = "Opening showdown connection")
try {
webSocket = HttpClient.newHttpClient()
.newWebSocketBuilder()
.buildAsync(URI.create("ws://$host:$socketPort"), WebSocketListener(this))
.join()
// TODO: See if this is needed
registerSpecies()
indicateSpeciesInitialized()
registerBagItems()
if (!silent) {
this.status = ShowdownStatus.CONNECTED
}
} catch (e: Exception) {
Cobblemon.LOGGER.warn("Failed to open showdown connection to $host:$socketPort", e)
log.add(source = "Minecraft", message = "Failed to open showdown connection with error: ${e.message}")
if (!silent) {
this.status = ShowdownStatus.DISCONNECTED
}
}
}
/**
* Closes the connection to the showdown service.
*/
override fun closeConnection() = closeConnection(false)
/**
* Closes the connection to the showdown service.
*
* @param silent If true, suppresses changing the status of the disconnection process.
*/
private fun closeConnection(silent: Boolean) {
log.add(source = "Minecraft", message = "Closing showdown connection")
try {
webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "Goodbye!").join()
} catch (e: Exception) {
Cobblemon.LOGGER.warn("Failed to close showdown connection to $host:$socketPort", e)
log.add(source = "Minecraft", message = "Failed to close showdown connection to $host:$socketPort with error: ${e.message}")
}
finally {
if (!silent) {
this.status = ShowdownStatus.DISCONNECTED
}
}
}
/**
* Restarts the connection to the showdown service.
*/
override fun restartConnection() {
this.status = ShowdownStatus.RESTARTING
closeConnection(true)
openConnection(true)
}
/**
* Starts a battle in the showdown service.
*
* @param battle The battle to start.
* @param messages The initial messages for the battle.
*/
override fun startBattle(battle: PokemonBattle, messages: Array<String>) {
send("[${battle.battleId}]>start-battle")
send(battle.battleId, messages)
}
/**
* Sends messages to a specific battle in the showdown service.
*
* @param battleId The unique identifier of the battle.
* @param messages The messages to send.
*/
override fun send(battleId: UUID, messages: Array<String>) {
messages.forEach { send("[$battleId]$it") }
}
/**
* Sends a message to the showdown service.
*
* @param message The message to send.
*/
override fun send(message: String) {
webSocket.sendText(message, true)
}
/**
* Retrieves the ability IDs from the showdown service.
*
* @return A `JsonArray` of ability IDs.
*/
override fun getAbilityIds(): JsonArray {
TODO("Not yet implemented")
}
/**
* Retrieves the moves from the showdown service.
*
* @return A `JsonArray` of moves.
*/
override fun getMoves(): JsonArray {
TODO("Not yet implemented")
}
/**
* Retrieves the item IDs from the showdown service.
*
* @return A `JsonArray` of item IDs.
*/
override fun getItemIds(): JsonArray {
TODO("Not yet implemented")
}
/**
* Registers species in the showdown service.
*/
override fun registerSpecies() {
HttpService.post<Unit>(
url = "http://$host:$httpPort/Species",
body = PokemonSpecies.species.map { PokemonSpecies.ShowdownSpecies(it, null) }
)
}
/**
* Registers bag items in the showdown service.
*/
override fun registerBagItems() {
HttpService.post<Unit>(
url = "http://$host:$httpPort/BagItems",
body = BagItems.bagItemsScripts.map { BagItem(it.key, it.value) }.toList()
)
}
override fun indicateSpeciesInitialized() {
HttpService.post<Unit>(url = "http://$host:$httpPort/InitializeDex")
}
/**
* WebSocket listener for handling events from the showdown service.
*
* @property service The UnionShowdownService instance.
*/
class WebSocketListener(val service: UnionShowdownService) : WebSocket.Listener {
val builder = StringBuilder()
override fun onOpen(webSocket: WebSocket) {
service.log.add(source = "Showdown", message = "Connected to server at ${service.host}:${service.socketPort}")
super.onOpen(webSocket)
}
/**
* Starts a battle based on the received instruction.
*
* @param instruction The instruction to start the battle.
*/
fun startBattle(instruction: String) {
val arguments = instruction.replace("|start-battle|", "").split("|")
if (arguments.size != 7) return
val battleId = UUID.fromString(arguments[0])
val playerIdA = UUID.fromString(arguments[1])
val playerNameA = arguments[2]
val playerIdB = UUID.fromString(arguments[3])
val playerNameB = arguments[4]
val teamA = JsonParser.parseString(arguments[5]).asJsonObject
val teamB = JsonParser.parseString(arguments[6]).asJsonObject
val playerA = server()?.playerList?.getPlayer(playerIdA)
val playerB = server()?.playerList?.getPlayer(playerIdB)
println(arguments[5])
println(arguments[6])
if (playerA == null && playerB == null) {
Cobblemon.LOGGER.warn("Tried to start battle with no players present on server")
return
}
val registryAccess = server()?.registryAccess() ?: return
val playerAParty = PartyStore(UUID.randomUUID()).loadFromJSON(teamA, registryAccess)
val playerBParty = PartyStore(UUID.randomUUID()).loadFromJSON(teamB, registryAccess)
val playerATeam = toShowdownBattleTeam(playerAParty.toList()).onEach {
it.effectedPokemon.level = 100
it.effectedPokemon.heal()
}
val playerBTeam = toShowdownBattleTeam(playerBParty.toList()).onEach {
it.effectedPokemon.level = 100
it.effectedPokemon.heal()
}
if (playerA != null && playerB != null) {
val playerActorA = PlayerBattleActor(playerIdA, playerATeam)
val playerActorB = PlayerBattleActor(playerIdB, playerBTeam)
BattleRegistry.startBattle(
battleId = battleId,
battleFormat = BattleFormat.GEN_9_SINGLES,
side1 = BattleSide(playerActorA),
side2 = BattleSide(playerActorB),
sendMessages = false,
service = service
)
}
else if (playerA != null && playerB == null) {
val playerActorA = PlayerBattleActor(playerIdA, playerATeam)
val npc = NPCEntity(playerA.level())
npc.moveTo(playerA.x + 5, playerA.y, playerA.z, playerA.yRot, (playerA.xRot + 180f) % 360f)
npc.npc = NPCClasses.getByIdentifier(cobblemonResource("standard"))!!
npc.loadTextureFromGameProfileName(playerNameB)
npc.initialize(1)
npc.config.setDirectly("yaw", DoubleValue((playerA.xRot + 90f) % 360f))
npc.uuid = playerIdB
npc.customName = playerNameB.text()
npc.isInvulnerable = true
npc.isMovable = false
playerA.level().addFreshEntity(npc)
val playerActorB = UnionBattleActor(npc, playerBTeam)
BattleRegistry.startBattle(
battleId = battleId,
battleFormat = BattleFormat.GEN_9_SINGLES,
side1 = BattleSide(playerActorA),
side2 = BattleSide(playerActorB),
sendMessages = false,
service = service
)
}
else {
val npc = NPCEntity(playerB!!.level())
npc.moveTo(playerB.x + 5, playerB.y, playerB.z, playerB.yRot, (playerB.xRot + 180f) % 360f)
npc.npc = NPCClasses.getByIdentifier(cobblemonResource("standard"))!!
npc.loadTextureFromGameProfileName(playerNameA)
npc.initialize(1)
npc.uuid = playerIdA
npc.customName = playerNameA.text()
npc.isInvulnerable = true
npc.isMovable = false
npc.config.setDirectly("yaw", DoubleValue((playerB.xRot + 90f) % 360f))
playerB.level().addFreshEntity(npc)
val playerActorA = UnionBattleActor(npc, playerATeam)
val playerActorB = PlayerBattleActor(playerIdB, playerBTeam)
BattleRegistry.startBattle(
battleId = battleId,
battleFormat = BattleFormat.GEN_9_SINGLES,
side1 = BattleSide(playerActorA),
side2 = BattleSide(playerActorB),
sendMessages = false,
service = service
)
}
}
/**
* Converts a list of Pokemon to a list of BattlePokemon for the showdown service.
*
* @param team The list of Pokemon to convert.
* @return A list of BattlePokemon.
*/
fun toShowdownBattleTeam(team: List<Pokemon>): List<BattlePokemon> {
return team.map { pokemon ->
BattlePokemon(
originalPokemon = pokemon,
effectedPokemon = pokemon.clone(newUUID = false),
postBattleEntityOperation = { entity -> entity.discard() }
)
}
}
override fun onText(webSocket: WebSocket, data: CharSequence, last: Boolean): CompletionStage<*>? {
if (last) {
builder.append(data)
val message = builder.toString()
service.log.add(source = "Showdown", message = message)
if (data.startsWith("|start-battle|")) {
startBattle(data.toString())
}
else {
val battleIdRegex = Regex("^\\[([^]]+)]")
val match = battleIdRegex.find(message) ?: return null
val battleId = match.groupValues[1]
val instruction = message.replace("[$battleId]", "")
ShowdownInterpreter.interpretMessage(UUID.fromString(battleId), instruction)
}
builder.clear()
}
else {
builder.append(data)
}
return super.onText(webSocket, data, last)
}
override fun onError(webSocket: WebSocket, error: Throwable) {
service.log.add(source = "Showdown", message = "Error: ${error.message}")
Cobblemon.LOGGER.warn("Error in showdown service ${service.id} websocket", error)
super.onError(webSocket, error)
}
override fun onClose(webSocket: WebSocket, statusCode: Int, reason: String): CompletionStage<*>? {
service.log.add(source = "Showdown", message = "Closed connection with status $statusCode and reason $reason")
return super.onClose(webSocket, statusCode, reason)
}
}
/**
* Data class representing a bag item. We use this for serialization to the showdown service.
*
* @property id The unique identifier of the bag item.
* @property script The script associated with the bag item.
*/
private data class BagItem(val id: String, val script: String)
}
@@ -1,169 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.battles.runner.socket
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.battles.model.PokemonBattle
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.battles.BagItems
import com.cobblemon.mod.common.battles.ShowdownInterpreter
import com.cobblemon.mod.common.battles.runner.ShowdownService
import com.cobblemon.mod.common.pokemon.FormData
import com.cobblemon.mod.common.pokemon.Species
import com.google.gson.Gson
import com.google.gson.JsonArray
import java.io.BufferedReader
import java.io.InputStreamReader
import java.io.OutputStreamWriter
import java.net.InetAddress
import java.net.Socket
import java.nio.charset.Charset
import java.util.UUID
/**
* Mediator service for communicating between the Cobblemon Minecraft mod and Cobblemon showdown service via
* a socket client.
*
* This is primarily used for debugging purposes, but could be extended in the future to provide
* a means of connecting to a remote Showdown server. This does not provide any fault handling in the
* event that the server goes down.
*
* When messages are sent to showdown, this will await a response from showdown.
* The protocol for messages sent from showdown is <length of characters in payload><payload>,
* and a payload length of 0 indicates that there is no response.
*
* @see {@code sim/cobbled-debug-server.ts} within cobblemon-showdown repository
* @since February 27, 2023
* @author landonjw
*/
class SocketShowdownService(val host: String = "localhost", val port: Int = 18468, val localPort: Int = 0) : ShowdownService {
private lateinit var socket: Socket
private lateinit var writer: OutputStreamWriter
private lateinit var reader: BufferedReader
val gson = Gson()
override fun openConnection() {
socket = Socket(InetAddress.getLocalHost(), port, InetAddress.getLocalHost(), localPort)
writer = socket.getOutputStream().writer(charset = Charset.forName("ascii"))
reader = BufferedReader(InputStreamReader(socket.getInputStream()))
}
override fun closeConnection() {
socket.close()
}
override fun startBattle(battle: PokemonBattle, messages: Array<String>) {
writer.write(">startbattle ${battle.battleId}\n")
acknowledge { Cobblemon.LOGGER.error("Failed to start battle!") }
send(battle.battleId, messages)
}
override fun send(battleId: UUID, messages: Array<String>) {
for (message in messages) {
writer.write("$battleId~$message\n")
writer.flush()
readBattleInput().forEach { interpretMessage(battleId, it) }
}
}
private fun read(reader: BufferedReader, size: Int): String {
val buffer = CharArray(size)
while (true) {
if(reader.read(buffer) == 0) continue
return String(buffer)
}
}
private fun readMessage(): String {
val payloadSize = read(reader, 8).toInt()
val payload = read(reader, payloadSize)
return payload
}
private fun readBattleInput(): List<String> {
val lines = mutableListOf<String>()
val numLines = read(reader, 8).toInt()
if (numLines != 0) {
for (i in 0 until numLines) {
lines.add(readMessage())
}
}
return lines
}
private fun interpretMessage(battleId: UUID, message: String) {
ShowdownInterpreter.interpretMessage(battleId, message)
}
override fun getAbilityIds(): JsonArray {
writer.write(">getCobbledAbilityIds")
writer.flush()
val response = readMessage()
return gson.fromJson(response, JsonArray::class.java)
}
override fun getMoves(): JsonArray {
writer.write(">getCobbledMoves\n")
writer.flush()
val response = readMessage()
return gson.fromJson(response, JsonArray::class.java)
}
override fun getItemIds(): JsonArray {
writer.write(">getCobbledItemIds")
writer.flush()
val response = readMessage()
return gson.fromJson(response, JsonArray::class.java)
}
private fun sendSpeciesData(species: Species, form: FormData?) {
writer.write(">receiveSpeciesData ${gson.toJson(PokemonSpecies.ShowdownSpecies(species, form))}\n")
acknowledge()
}
private fun sendBagItem(itemId: String, js: String) {
writer.write(">receiveBagItemData $itemId $js")
acknowledge { Cobblemon.LOGGER.error("Failed to send bag item to Showdown: $itemId") }
}
override fun registerSpecies() {
writer.write(">resetSpeciesData\n")
acknowledge()
PokemonSpecies.species.forEach { species ->
sendSpeciesData(species, null)
species.forms.forEach { form ->
if (form != species.standardForm) {
sendSpeciesData(species, form)
}
}
}
}
fun acknowledge(ifFails: () -> Unit = {}) {
writer.flush()
val ack = CharArray(3)
reader.read(ack)
if (String(ack) != "ACK") {
ifFails()
}
}
override fun registerBagItems() {
for ((itemId, js) in BagItems.bagItemsScripts) {
sendBagItem(itemId, js.replace("\n", " "))
}
}
override fun indicateSpeciesInitialized() {
writer.write(">afterCobbledSpeciesInit")
acknowledge()
}
}
@@ -285,7 +285,7 @@ class PCBlock(properties: Properties): BaseEntityBlock(properties), SimpleWaterl
val pc = Cobblemon.storage.getPCForPlayer(player, baseEntity) ?: return InteractionResult.SUCCESS
// TODO add event to check if they can open this PC? (answer: the getPCForPlayer should be where we do that)
PCLinkManager.addLink(ProximityPCLink(pc, player.uuid, baseEntity))
OpenPCPacket(pc.uuid).sendToPlayer(player)
OpenPCPacket(pc).sendToPlayer(player)
world.playSoundServer(
position = blockPos.toVec3d(),
sound = CobblemonSounds.PC_ON,
@@ -114,8 +114,8 @@ class RevivalHerbBlock(settings: Properties) : CropBlock(settings), Mulchable {
var growthSpeed = 1.0F
var ground = pos.below()
for(x in -1..1) {
for(z in -1..1) {
for (x in -1..1) {
for (z in -1..1) {
var nearbyFarmLandBoost = 0.0F
var blockState = level.getBlockState(ground.offset(x, 0, z))
if (blockState.`is`(Blocks.FARMLAND)) {
@@ -94,7 +94,7 @@ class FossilAnalyzerBlockEntity(
return stack?.let { Fossils.isFossilIngredient(it) } == true
&& structure.fossilInventory.size < Cobblemon.config.maxInsertedFossilItems
&& structure.isRunning() == false && structure.resultingFossil == null
&& Fossils.getSubFossilByItemStacks( structure.fossilInventory + mutableListOf(stack) ) != null
&& Fossils.getSubFossilByItemStacks(structure.fossilInventory + mutableListOf(stack)) != null
}
return false
}
@@ -15,6 +15,7 @@ import com.cobblemon.mod.common.CobblemonClientImplementation
import com.cobblemon.mod.common.CobblemonEntities
import com.cobblemon.mod.common.CobblemonItems
import com.cobblemon.mod.common.api.berry.Berries
import com.cobblemon.mod.common.api.molang.ObjectValue
import com.cobblemon.mod.common.api.scheduling.ClientTaskTracker
import com.cobblemon.mod.common.api.storage.player.client.ClientGeneralPlayerData
import com.cobblemon.mod.common.api.storage.player.client.ClientPokedexManager
@@ -78,6 +79,8 @@ import net.minecraft.world.InteractionHand
import net.minecraft.world.entity.player.Player
import net.minecraft.world.phys.AABB
import java.util.UUID
import net.minecraft.client.player.AbstractClientPlayer
import net.minecraft.server.level.ServerPlayer
object CobblemonClient {
@@ -144,7 +147,7 @@ object CobblemonClient {
PlatformEvents.CLIENT_ENTITY_UNLOAD.subscribe { event -> EntitySoundTracker.clear(event.entity.id) }
PlatformEvents.CLIENT_TICK_POST.subscribe { event ->
val player = event.client.player
if (player !== null) {
if (player != null) {
var selectedItem = player.inventory.getItem(player.inventory.selected)
if (pokedexUsageContext.scanningGuiOpen &&
!(selectedItem.`is`(CobblemonItemTags.POKEDEX)) &&
@@ -156,13 +159,14 @@ object CobblemonClient {
// Stop using Pokédex in main hand if player switches to a different slot in hotbar
pokedexUsageContext.stopUsing(PokedexUsageContext.OPEN_SCANNER_BUFFER_TICKS + 1)
}
if(event.client.isPaused) {
if (event.client.isPaused) {
return@subscribe
}
val nearbyShinies = player?.level()?.getEntities(player, AABB.ofSize(player.position(), 16.0, 16.0, 16.0)) { (it is PokemonEntity) && it.pokemon.shiny }
val nearbyShinies = player.level().getEntities(player, AABB.ofSize(player.position(), 16.0, 16.0, 16.0)) { it is PokemonEntity && it.pokemon.shiny && !it.isSilent }
nearbyShinies?.firstOrNull { player.isLookingAt(it) && !player.isSpectator }.let {
if(it is PokemonEntity)
it.delegate.spawnShinyParticle(player!!)
if (it is PokemonEntity) {
it.delegate.spawnShinyParticle(player)
}
}
}
ClientPlayerIcon.onTick()
@@ -382,7 +382,7 @@ class PokemonClientDelegate : PosableState(), PokemonSideDelegate {
val player = Minecraft.getInstance().player ?: return
val isWithinRange = player.position().distanceTo(currentEntity.position()) <= Cobblemon.config.shinyNoticeParticlesDistance
if (currentEntity.pokemon.shiny && currentEntity.ownerUUID == null) {
if (currentEntity.pokemon.shiny && currentEntity.ownerUUID == null && !currentEntity.isSilent) {
if (isWithinRange) {
if (secondsSinceLastShinyParticle > SHINY_PARTICLE_COOLDOWN && !currentEntity.isBattling) {
playShinyEffect("cobblemon:shiny_sparkle_ambient_wild")
@@ -420,7 +420,7 @@ class PokemonClientDelegate : PosableState(), PokemonSideDelegate {
}
override fun spawnShinyParticle(player: Player) {
if(secondsSinceLastShinyParticle > SHINY_PARTICLE_COOLDOWN) {
if (secondsSinceLastShinyParticle > SHINY_PARTICLE_COOLDOWN) {
playShinyEffect("cobblemon:ambient_shiny_sparkle")
lastShinyParticle = System.currentTimeMillis()
}
@@ -0,0 +1,330 @@
package com.cobblemon.mod.common.client.gui.debug
import com.cobblemon.mod.common.CobblemonNetwork
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.battles.runner.ShowdownStatus
import com.cobblemon.mod.common.client.gui.CobblemonRenderable
import com.cobblemon.mod.common.client.gui.debug.ShowdownDebugGUI.Companion.activeShowdownService
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.net.messages.client.debug.showdown.ShowdownRefreshInfoPacket
import com.cobblemon.mod.common.net.messages.server.debug.showdown.RequestShowdownInfoPacket
import com.cobblemon.mod.common.net.messages.server.debug.showdown.ShowdownActionPacket
import com.cobblemon.mod.common.net.messages.server.debug.showdown.ShowdownInputPacket
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.lang
import com.mojang.blaze3d.platform.ClipboardManager
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.components.Button
import net.minecraft.client.gui.components.EditBox
import net.minecraft.client.gui.components.ObjectSelectionList
import net.minecraft.client.gui.components.ObjectSelectionList.Entry
import net.minecraft.client.gui.screens.Screen
import net.minecraft.locale.Language
import net.minecraft.network.chat.Component
import net.minecraft.util.FormattedCharSequence
import java.awt.Color
import java.util.*
/**
* GUI screen for debugging the Showdown service in the Cobblemon mod.
* This screen allows users to view and interact with the Showdown service logs and status.
*
* @property status The current status of the showdown service.
* @property service The resource location of the showdown service.
* @property messages A list of messages from the active showdown service currently being refreshed.
* @property availableServices A list of available showdown services. This allows the client to switch which server they are viewing.
* @property requestProcessedAt The date and time when the request was processed.
* @property includesStartOfLog Indicates if the messages include the start of the log.
*
* @author landonjw
*/
class ShowdownDebugGUI : Screen(lang("ui.debug.showdown")), CobblemonRenderable {
companion object {
var activeShowdownService = cobblemonResource("embedded")
private set
}
private var showdownStatus: ShowdownStatus? = null
val log = mutableListOf<String>()
var availableServices = listOf(cobblemonResource("embedded"))
var lastProcessedLogTime: Date? = null
val outputWindow = addRenderableWidget(ShowdownOutputList())
val serviceButton = addRenderableWidget(
Button.builder(Component.literal("Service: $activeShowdownService")) {
cycleActiveService()
}.bounds(10, 10, 200, 20).build()
)
val startButton = addRenderableWidget(
Button.builder(Component.literal("Start")) { button ->
CobblemonNetwork.sendToServer(ShowdownActionPacket(
service = activeShowdownService,
action = ShowdownActionPacket.Action.START
))
}.bounds(10, 40, 50, 20).build()
)
val stopButton = addRenderableWidget(
Button.builder(Component.literal("Stop")) { button ->
CobblemonNetwork.sendToServer(ShowdownActionPacket(
service = activeShowdownService,
action = ShowdownActionPacket.Action.STOP
))
}.bounds(70, 40, 50, 20).build()
)
val restartButton = addRenderableWidget(
Button.builder(Component.literal("Restart")) { button ->
this.showdownStatus = ShowdownStatus.RESTARTING
CobblemonNetwork.sendToServer(ShowdownActionPacket(
service = activeShowdownService,
action = ShowdownActionPacket.Action.RESTART
))
}.bounds(130, 40, 50, 20).build()
)
val startNPCBattleButton = addRenderableWidget(
Button.builder(Component.literal("Queue")) { button ->
CobblemonNetwork.sendToServer(ShowdownActionPacket(
service = activeShowdownService,
action = ShowdownActionPacket.Action.QUEUE
))
}.bounds(10, 70, 100, 20).build()
)
val clipboardManager = ClipboardManager()
var timer = Timer()
init {
addRenderableWidget(
Button.builder(Component.literal("Copy to Clipboard")) Clipboard@{
if (log.isEmpty()) return@Clipboard
clipboardManager.setClipboard(Minecraft.getInstance().window.window, log.joinToString("\n"))
}.bounds(170, Minecraft.getInstance().window.guiScaledHeight - 40, 100, 30).build()
)
addRenderableWidget(ShowdownInputWidget(this))
startTimer()
}
/**
* Cycles through the available showdown services.
*/
fun cycleActiveService() {
if (availableServices.size == 1) return
val currentIndex = availableServices.indexOf(activeShowdownService)
activeShowdownService = availableServices[(currentIndex + 1) % availableServices.size]
serviceButton.message = Component.literal("Service: $activeShowdownService")
outputWindow.reset()
log.clear()
lastProcessedLogTime = null
startTimer()
}
/**
* Starts the timer for periodically refreshing the showdown info.
*/
fun startTimer() {
timer.cancel()
timer = Timer()
timer.schedule(object : TimerTask() {
override fun run() {
CobblemonNetwork.sendToServer(RequestShowdownInfoPacket(activeShowdownService, lastProcessedLogTime))
}
}, 0, 500)
}
override fun onClose() {
timer.cancel()
super.onClose()
}
override fun render(guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) {
guiGraphics.fill(0, 0, width, height, Color(0, 0, 0, 100).rgb)
super.render(guiGraphics, mouseX, mouseY, partialTick)
if (showdownStatus != null) {
guiGraphics.fill(
190,
45,
200,
55,
when (showdownStatus!!) {
ShowdownStatus.DISCONNECTED -> Color(255, 0, 0, 255).rgb
ShowdownStatus.RESTARTING -> Color(255, 255, 0, 255).rgb
ShowdownStatus.CONNECTED -> Color(0, 255, 0, 255).rgb
}
)
drawScaledText(
context = guiGraphics,
text = Component.literal(showdownStatus!!.name.replaceFirstChar {
if (it.isLowerCase()) it.titlecase(
Locale.getDefault()
) else it.toString()
}),
x = 205,
y = 47
)
}
}
/**
* Refreshes the showdown info with the data from the given packet.
*
* @param packet The packet containing the refreshed showdown info.
*/
fun refreshShowdownInfo(packet: ShowdownRefreshInfoPacket) {
// Prevent false positives from old packets
if (activeShowdownService != packet.service) return
if (packet.includesStartOfLog) {
log.clear()
outputWindow.reset()
}
packet.messages.forEach {
log.add(it)
outputWindow.addNewEntry(it.text())
}
lastProcessedLogTime = packet.requestProcessedAt
showdownStatus = packet.status
when (showdownStatus) {
ShowdownStatus.CONNECTED -> {
startButton.active = false
stopButton.active = true
restartButton.active = true
startNPCBattleButton.active = true
}
ShowdownStatus.DISCONNECTED -> {
startButton.active = true
stopButton.active = false
restartButton.active = false
startNPCBattleButton.active = false
}
ShowdownStatus.RESTARTING -> {
startButton.active = false
stopButton.active = false
restartButton.active = false
startNPCBattleButton.active = false
}
else -> {
startButton.active = false
stopButton.active = false
restartButton.active = false
startNPCBattleButton.active = false
}
}
this.availableServices = packet.availableServices
}
}
/**
* List widget for displaying the output log of the Showdown service.
*/
class ShowdownOutputList : ObjectSelectionList<OutputEntry>(
Minecraft.getInstance(),
Minecraft.getInstance().window.guiScaledWidth - 280 - 10,
Minecraft.getInstance().window.guiScaledHeight - 10 - 40,
10,
14
) {
/**
* Adds a new entry to the output list.
*
* @param text The text to add as a new entry.
*/
fun addNewEntry(text: Component) {
val line = text.copy()
val textRenderer = Minecraft.getInstance().font
val lines = Language.getInstance().getVisualOrder(textRenderer.splitter.splitLines(line, rowWidth, line.style))
val fullyScrolledDown = maxScroll - scrollAmount < 10
lines.forEach { addEntry(OutputEntry(it)) }
if (fullyScrolledDown) {
scrollAmount = maxScroll.toDouble()
}
}
override fun getSelected(): OutputEntry? = null
override fun getRowWidth(): Int {
return Minecraft.getInstance().window.guiScaledWidth - 280 - 10 - 10
}
override fun getScrollbarPosition(): Int {
return this.x + rowWidth + 5
}
override fun renderWidget(guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) {
setRectangle(
Minecraft.getInstance().window.guiScaledWidth - 280 - 10,
Minecraft.getInstance().window.guiScaledHeight - 10 - 10 - 40,
280,
10
)
this.x = 280
super.renderWidget(guiGraphics, mouseX, mouseY, partialTick)
}
/**
* Resets the output list by clearing all entries.
*/
fun reset() {
super.clearEntries()
}
}
/**
* Entry for the output list, representing a single line of text.
*
* @param line The formatted line of text to display.
*/
class OutputEntry(val line: FormattedCharSequence) : Entry<OutputEntry>() {
override fun getNarration() = "".text()
override fun render(
context: GuiGraphics,
index: Int,
rowTop: Int,
rowLeft: Int,
rowWidth: Int,
rowHeight: Int,
mouseX: Int,
mouseY: Int,
isHovered: Boolean,
partialTicks: Float
) {
drawScaledText(
context,
line,
rowLeft,
rowTop - 2
)
}
override fun isFocused() = false
}
/**
* Input widget for entering commands to the Showdown service.
*/
class ShowdownInputWidget(val gui: ShowdownDebugGUI) : EditBox(
Minecraft.getInstance().font,
280,
Minecraft.getInstance().window.guiScaledHeight - 40,
Minecraft.getInstance().window.guiScaledWidth - 280 - 10,
30,
Component.literal("Enter command")
) {
override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
val result = super.keyPressed(keyCode, scanCode, modifiers)
if (keyCode == 257 && this.value.isNotEmpty()) {
CobblemonNetwork.sendToServer(ShowdownInputPacket(
service = activeShowdownService,
input = this.value
))
deleteCharsToPos(0)
CobblemonNetwork.sendToServer(RequestShowdownInfoPacket(activeShowdownService, gui.lastProcessedLogTime))
}
return result
}
}
@@ -115,7 +115,7 @@ class PasturePokemonScrollList(
font = CobblemonResources.DEFAULT_LARGE,
text = "${children().count { it.isOwned() }}/${config.permissions.maxPokemon.takeIf { it >= 0 } ?: config.limit }".text().bold(),
x = x + (WIDTH / 2),
y = y - 9,
y = y - 7,
centered = true
)
}
@@ -15,67 +15,60 @@ import com.cobblemon.mod.common.client.CobblemonResources
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.client.storage.ClientBox
import com.cobblemon.mod.common.net.messages.server.storage.pc.RequestRenamePCBoxPacket
import com.mojang.blaze3d.platform.InputConstants
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.components.EditBox
import net.minecraft.network.chat.Component
import net.minecraft.network.chat.MutableComponent
class BoxNameWidget(
private val pX: Int,
pX: Int,
pY: Int,
text: Component = "BoxNameWidget".text(),
private val pcGui: PCGUI,
private val storageWidget: StorageWidget,
): EditBox(Minecraft.getInstance().font, pX, pY, 0, HEIGHT, text) {
companion object {
const val HEIGHT = 14
}
init {
setMaxLength(21)
update()
setResponder { update() }
}
private val storageWidget: StorageWidget
): TextWidget(pX, pY, text = text, update = {}) {
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
if (cursorPosition != value.length) moveCursorToEnd(false)
val label = getBox().name?: defaultBoxLabel()
val input = applyTextCursor(value, label).bold()
val textWidth = Minecraft.getInstance().font.width((if (isFocused) value.text() else label).bold().font(CobblemonResources.DEFAULT_LARGE))
val centerX = x + (width / 2) - (textWidth / 2)
if (startPosX != centerX) startPosX = centerX
drawScaledText(
context = context,
text = text(),
x = x,
y = y
font = CobblemonResources.DEFAULT_LARGE,
x = startPosX,
y = y + 3,
text = input,
)
renderCursor(context, label)
}
override fun setFocused(focused: Boolean) {
super.setFocused(focused)
update()
if (!focused) {
RequestRenamePCBoxPacket(pcGui.pc.uuid, storageWidget.box, value).sendToServer()
pcGui.pc.boxes[storageWidget.box].name = if (value.isBlank()) null else Component.literal(value).bold()
value = ""
// Set text value to box's saved name if applicable on focus
if (focused && value.isEmpty() && getBox().name != null) {
value = getBox().name?.string ?: ""
}
}
private fun text(): MutableComponent {
return (if (isFocused) {
"${value}_".text()
} else if (value.isEmpty()) {
getBox().name?: Component.translatable("cobblemon.ui.pc.box.title", storageWidget.box + 1)
} else {
value.text()
}).bold().font(CobblemonResources.DEFAULT_LARGE)
override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
// Reset box name
if (isFocused && keyCode == InputConstants.KEY_ESCAPE) value = getBox().name?.string ?: ""
return super.keyPressed(keyCode, scanCode, modifiers)
}
fun getBox(): ClientBox {
return pcGui.pc.boxes[storageWidget.box]
override fun unfocused() {
super.unfocused()
RequestRenamePCBoxPacket(pcGui.pc.uuid, storageWidget.box, value).sendToServer()
getBox().name = if (value.isBlank()) null else Component.literal(value).bold()
value = ""
}
fun update() {
width = Minecraft.getInstance().font.width(text())
x = pX - width / 2
}
}
fun getBox(): ClientBox = pcGui.pc.boxes[storageWidget.box]
fun defaultBoxLabel(): MutableComponent = Component.translatable("cobblemon.ui.pc.box.title", storageWidget.box + 1)
}
@@ -0,0 +1,84 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.pc
import com.cobblemon.mod.common.CobblemonSounds
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.text.bold
import com.cobblemon.mod.common.api.text.font
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.client.CobblemonResources
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.util.cobblemonResource
import com.cobblemon.mod.common.util.lang
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.resources.sounds.SimpleSoundInstance
import net.minecraft.network.chat.Component
class FilterWidget(
private var pX: Int,
private var pY: Int,
text: Component = "FilterWidget".text(),
update: () -> (Unit)
): TextWidget(pX, pY, text = text, update = update) {
companion object {
const val ICON_SIZE = 16
const val SCALE = 0.5F
private val iconFilterResource = cobblemonResource("textures/gui/pc/pc_icon_filter.png")
private val labelFilter = Component.translatable("cobblemon.ui.pc.filter")
}
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
blitk(
matrixStack = context.pose(),
x = (x - 9) / SCALE,
y = (y + 3) / SCALE,
texture = iconFilterResource,
width = ICON_SIZE,
height = ICON_SIZE,
vOffset = if (isFocused || !value.isEmpty() || iconHovered(mouseX, mouseY)) ICON_SIZE else 0,
textureHeight = ICON_SIZE * 2,
scale = SCALE
)
val input = applyTextCursor(value, labelFilter)
val textWidth = Minecraft.getInstance().font.width((if (!isFocused && value.isEmpty()) labelFilter else value.text()).bold().font(CobblemonResources.DEFAULT_LARGE))
val centerX = x + (width / 2) - (textWidth / 2)
if (startPosX != centerX) startPosX = centerX
drawScaledText(
context = context,
font = CobblemonResources.DEFAULT_LARGE,
text = input.bold(),
x = startPosX,
y = pY + 2
)
renderCursor(context, input)
if (iconHovered(mouseX, mouseY)) {
context.renderTooltip(Minecraft.getInstance().font, lang("ui.pc.filter.tooltip"), mouseX, mouseY)
}
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (iconHovered(mouseX.toInt(), mouseY.toInt())) {
Minecraft.getInstance().soundManager.play(SimpleSoundInstance.forUI(CobblemonSounds.PC_CLICK, 1.0F, 0.25F))
// Simulate clicking on text area if icon is clicked
if (!isFocused) return super.mouseClicked(x + (width / 2.0), y + (height / 2.0), button)
}
return super.mouseClicked(mouseX, mouseY, button)
}
fun iconHovered(mouseX: Int, mouseY: Int): Boolean = mouseX >= (x - 9) && mouseY >= (y + 3)
&& mouseX <= ((x - 9) + (ICON_SIZE * SCALE)) && mouseY <= ((y + 3) + (ICON_SIZE * SCALE))
}
@@ -0,0 +1,65 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.pc
import com.cobblemon.mod.common.CobblemonSounds
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.client.gui.CobblemonRenderable
import com.cobblemon.mod.common.util.lang
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.components.Button
import net.minecraft.client.resources.sounds.SimpleSoundInstance
import net.minecraft.client.sounds.SoundManager
import net.minecraft.network.chat.Component
import net.minecraft.resources.ResourceLocation
class IconButton(
pX: Int, pY: Int,
val buttonWidth: Int,
val buttonHeight: Int,
val resource: ResourceLocation,
val altResource: ResourceLocation? = null,
val tooltipKey: String? = null,
label: String,
onPress: OnPress,
): Button(pX, pY, (buttonWidth * SCALE).toInt(), (buttonHeight * SCALE).toInt(), Component.literal(label), onPress, DEFAULT_NARRATION), CobblemonRenderable {
companion object {
private const val SCALE = 0.5F
}
var highlighted = false
var showAlt = false
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, partialTicks: Float) {
blitk(
matrixStack = context.pose(),
x = x / SCALE,
y = y / SCALE,
texture = if (altResource != null && showAlt) altResource else resource,
width = buttonWidth,
height = buttonHeight,
vOffset = if (isHovered() || highlighted) buttonHeight else 0,
textureHeight = buttonHeight * 2,
scale = SCALE
)
if (isHovered()) {
tooltipKey?.let {
context.renderTooltip(Minecraft.getInstance().font, lang(it), mouseX, mouseY)
}
}
}
override fun playDownSound(soundManager: SoundManager) {
soundManager.play(SimpleSoundInstance.forUI(CobblemonSounds.PC_CLICK, 1.0F))
}
}
@@ -22,11 +22,10 @@ class NavigationButton(
pX: Int, pY: Int,
private val forward: Boolean,
onPress: OnPress
): Button(pX, pY, (WIDTH * SCALE).toInt(), (HEIGHT * SCALE).toInt(), Component.literal("Navigation"), onPress, DEFAULT_NARRATION), CobblemonRenderable {
): Button(pX, pY, (SIZE * SCALE).toInt(), (SIZE * SCALE).toInt(), Component.literal("Navigation"), onPress, DEFAULT_NARRATION), CobblemonRenderable {
companion object {
private const val WIDTH = 8F
private const val HEIGHT = 10F
private const val SIZE = 14
private const val SCALE = 0.5F
private val forwardButtonResource = cobblemonResource("textures/gui/pc/pc_arrow_next.png")
private val backwardsButtonResource = cobblemonResource("textures/gui/pc/pc_arrow_previous.png")
@@ -38,10 +37,10 @@ class NavigationButton(
x = x / SCALE,
y = y / SCALE,
texture = if (forward) forwardButtonResource else backwardsButtonResource,
width = WIDTH,
height = HEIGHT,
vOffset = if (isHovered()) HEIGHT else 0,
textureHeight = HEIGHT * 2,
width = SIZE,
height = SIZE,
vOffset = if (isHovered()) SIZE else 0,
textureHeight = SIZE * 2,
scale = SCALE
)
}
@@ -10,6 +10,7 @@ package com.cobblemon.mod.common.client.gui.pc
import com.cobblemon.mod.common.CobblemonSounds
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.pokemon.PokemonSortMode
import com.cobblemon.mod.common.api.storage.pc.search.Search
import com.cobblemon.mod.common.api.text.bold
import com.cobblemon.mod.common.api.text.text
@@ -23,6 +24,7 @@ import com.cobblemon.mod.common.client.gui.summary.widgets.common.reformatNature
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.client.storage.ClientPC
import com.cobblemon.mod.common.client.storage.ClientParty
import com.cobblemon.mod.common.net.messages.server.storage.pc.MarkPCBoxWallpapersSeenPacket
import com.cobblemon.mod.common.net.messages.server.storage.pc.SortPCBoxPacket
import com.cobblemon.mod.common.net.messages.server.storage.pc.UnlinkPlayerFromPCPacket
import com.cobblemon.mod.common.pokemon.Gender
@@ -39,12 +41,14 @@ import net.minecraft.client.resources.sounds.SimpleSoundInstance
import net.minecraft.network.chat.Component
import net.minecraft.sounds.SoundEvent
import kotlin.isInitialized
import net.minecraft.resources.ResourceLocation
class PCGUI(
val pc: ClientPC,
val party: ClientParty,
val configuration: PCGUIConfiguration,
val openOnBox: Int = 0
val openOnBox: Int = 0,
val unseenWallpapers: MutableSet<ResourceLocation> = mutableSetOf()
) : Screen(Component.translatable("cobblemon.ui.pc.title")), CobblemonRenderable {
companion object {
@@ -61,6 +65,10 @@ class PCGUI(
private val baseResource = cobblemonResource("textures/gui/pc/pc_base.png")
private val portraitBackgroundResource = cobblemonResource("textures/gui/pc/portrait_background.png")
private val buttonOptionsResource = cobblemonResource("textures/gui/pc/pc_icon_options.png")
private val buttonWallpaperResource = cobblemonResource("textures/gui/pc/pc_button_set_wallpaper.png")
private val topSpacerResource = cobblemonResource("textures/gui/pc/pc_spacer_top.png")
private val bottomSpacerResource = cobblemonResource("textures/gui/pc/pc_spacer_bottom.png")
private val rightSpacerResource = cobblemonResource("textures/gui/pc/pc_spacer_right.png")
@@ -71,17 +79,21 @@ class PCGUI(
private lateinit var storageWidget: StorageWidget
private lateinit var boxNameWidget: BoxNameWidget
private lateinit var searchWidget: SearchWidget
private lateinit var filterWidget: FilterWidget
private lateinit var wallpaperWidget: WallpapersScrollingWidget
private var modelWidget: ModelWidget? = null
internal var previewPokemon: Pokemon? = null
private val optionButtons: MutableList<IconButton> = mutableListOf()
var search: Search = Search.DEFAULT
var ticksElapsed = 0
var selectPointerOffsetY = 0
var selectPointerOffsetIncrement = false
var displayOptions = false
override fun renderBlurredBackground(delta: Float) { }
override fun renderBlurredBackground(delta: Float) {}
override fun renderMenuBackground(context: GuiGraphics) {}
@@ -90,40 +102,44 @@ class PCGUI(
val y = (height - BASE_HEIGHT) / 2
// Add Exit Button
this.addRenderableWidget(ExitButton(pX = x + 320, pY = y + 186) { configuration.exitFunction(this) })
this.addRenderableWidget(
ExitButton(pX = x + 320, pY = y + 186) {
if (::wallpaperWidget.isInitialized && wallpaperWidget.visible) {
wallpaperWidget.visible = false
configuration.showParty = true
for (button in optionButtons) button.highlighted = false
playSound(CobblemonSounds.PC_CLICK)
} else {
configuration.exitFunction(this)
}
}
)
// Add Forward Button
this.addRenderableWidget(
NavigationButton(
pX = x + 221,
pY = y + 17,
pX = x + 220,
pY = y + 16,
forward = true
) {
storageWidget.box += 1
}
) { storageWidget.box += 1 }
)
// Add Backwards Button
this.addRenderableWidget(
NavigationButton(
pX = x + 119,
pY = y + 17,
pX = x + 117,
pY = y + 16,
forward = false
) {
storageWidget.box -= 1
}
) { storageWidget.box -= 1 }
)
// Add Sort Button
addRenderableWidget(
SortButton(
pX = x + 85,
pY = y + 12,
onPress = { it as SortButton
SortPCBoxPacket(pc.uuid, storageWidget.box, it.sortMode, hasShiftDown()).sendToServer()
}
)
// Add Filter Widget
this.filterWidget = FilterWidget(
pX = x + 126,
pY = y + 183,
update = { search = Search.of(filterWidget.value) }
)
this.addRenderableWidget(filterWidget)
// Add Storage
this.storageWidget = StorageWidget(
@@ -134,48 +150,97 @@ class PCGUI(
party = party
)
this.storageWidget.box = openOnBox
this.addRenderableWidget(storageWidget)
// Add Box Name
this.boxNameWidget = BoxNameWidget(
pX = x + 172,
pY = y + 15,
pX = x + 126,
pY = y + 12,
pcGui = this,
storageWidget = storageWidget
)
this.addRenderableWidget(boxNameWidget)
// Add Search Widget
this.searchWidget = SearchWidget(
pX = x + 117,
pY = y + 183,
update = { search = Search.of(searchWidget.value) }
)
// Add Wallpaper Button
this.addRenderableWidget(
WallpaperButton(
pX = x + 235,
pY = y + 12,
onPress = {
configuration.showParty = wallpaperWidget.visible
wallpaperWidget.visible = !wallpaperWidget.visible
},
pcGui = this
// Initialise box options if not pasture
if (storageWidget.pastureWidget == null) {
optionButtons.clear()
addRenderableWidget(
IconButton(
pX = x + 218,
pY = y + 186,
buttonWidth = 16,
buttonHeight = 16,
resource = buttonOptionsResource,
label = "options"
) {
displayOptions = !displayOptions
if (!displayOptions && wallpaperWidget.visible) {
configuration.showParty = true
wallpaperWidget.visible = false
}
(it as IconButton).highlighted = displayOptions
storageWidget.setupStorageSlots()
for (button in optionButtons) {
button.visible = displayOptions
button.highlighted = false
}
}.also { it.highlighted = displayOptions }
)
)
// Add Wallpaper Widget
this.wallpaperWidget = WallpapersScrollingWidget(
pX = x + 275,
pY = y + 30,
pcGui = this,
storageWidget = storageWidget
)
// Add Wallpaper Widget
this.wallpaperWidget = WallpapersScrollingWidget(
pX = x + 274,
pY = y + 29,
pcGui = this,
storageWidget = storageWidget
).also {
// Set default component visibility
it.visible = false
configuration.showParty = true
}
this.addRenderableWidget(wallpaperWidget)
// Add Wallpaper Settings Button
this.addRenderableWidget(IconButton(
pX = x + 242,
pY = y + 31,
buttonWidth = 20,
buttonHeight = 20,
resource = buttonWallpaperResource,
label = "open_wallpaper_settings"
) {
val isVisible = wallpaperWidget.visible
configuration.showParty = isVisible
wallpaperWidget.visible = !isVisible
(it as IconButton).highlighted = !isVisible
if (!unseenWallpapers.isEmpty()) MarkPCBoxWallpapersSeenPacket(unseenWallpapers).sendToServer()
}.also {
it.visible = displayOptions
optionButtons.add(it)
})
PokemonSortMode.entries.forEachIndexed { index, sortType ->
val typeName = sortType.name.lowercase()
this.addRenderableWidget(IconButton(
pX = x + 92 + (12 * index),
pY = y + 31,
buttonWidth = 20,
buttonHeight = 20,
resource = cobblemonResource("textures/gui/pc/pc_button_sort_${typeName}.png"),
altResource = cobblemonResource("textures/gui/pc/pc_button_sort_${typeName}_reverse.png"),
tooltipKey = "ui.sort.${typeName}",
label = "sort_${typeName}"
) {
SortPCBoxPacket(pc.uuid, storageWidget.box, sortType, hasShiftDown()).sendToServer()
}.also {
it.visible = displayOptions
optionButtons.add(it)
})
}
}
this.setPreviewPokemon(null)
this.addRenderableWidget(storageWidget)
this.addRenderableWidget(boxNameWidget)
this.addRenderableWidget(searchWidget)
this.addRenderableWidget(wallpaperWidget)
super.init()
}
@@ -460,6 +525,10 @@ class PCGUI(
scale = SCALE
)
if (!optionButtons.isEmpty() && displayOptions) {
for (button in optionButtons) button.showAlt = hasShiftDown()
}
super.render(context, mouseX, mouseY, delta)
// Item Tooltip
@@ -485,60 +554,59 @@ class PCGUI(
}
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
// Trigger child component function as they will need to check the entire screen click area
if (::boxNameWidget.isInitialized) boxNameWidget.mouseClicked(mouseX, mouseY, button)
return super.mouseClicked(mouseX, mouseY, button)
}
override fun mouseScrolled(mouseX: Double, mouseY: Double, amount: Double, verticalAmount: Double): Boolean {
if (storageWidget.pastureWidget != null) storageWidget.pastureWidget!!.pastureScrollList.mouseScrolled(
mouseX,
mouseY,
amount,
verticalAmount
)
if (storageWidget.pastureWidget != null) storageWidget.pastureWidget!!.pastureScrollList.mouseScrolled(mouseX, mouseY, amount, verticalAmount)
return children().any { it.mouseScrolled(mouseX, mouseY, amount, verticalAmount) }
}
override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
if (storageWidget.pastureWidget != null) storageWidget.pastureWidget!!.pastureScrollList.mouseDragged(
mouseX,
mouseY,
button,
deltaX,
deltaY
)
if (storageWidget.pastureWidget != null) storageWidget.pastureWidget!!.pastureScrollList.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
}
override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
val boxNameSelected = this::boxNameWidget.isInitialized && boxNameWidget.isFocused
val searchSelected = this::searchWidget.isInitialized && searchWidget.isFocused
if (isInventoryKeyPressed(minecraft, keyCode, scanCode) && !boxNameSelected && !searchSelected) {
val boxNameFocused = this::boxNameWidget.isInitialized && boxNameWidget.isFocused
val filterFocused = this::filterWidget.isInitialized && filterWidget.isFocused
if (isInventoryKeyPressed(minecraft, keyCode, scanCode) && !boxNameFocused && !filterFocused) {
playSound(CobblemonSounds.PC_OFF)
UnlinkPlayerFromPCPacket().sendToServer()
Minecraft.getInstance().setScreen(null)
return true
}
if ((keyCode == InputConstants.KEY_RETURN || keyCode == InputConstants.KEY_NUMPADENTER)
&& (boxNameSelected || searchSelected)) {
if (!filterFocused && !boxNameFocused) {
when (keyCode) {
InputConstants.KEY_ESCAPE -> {
playSound(CobblemonSounds.PC_OFF)
UnlinkPlayerFromPCPacket().sendToServer()
onClose()
return true
}
InputConstants.KEY_RIGHT -> {
playSound(CobblemonSounds.PC_CLICK)
this.storageWidget.box += 1
return true
}
InputConstants.KEY_LEFT -> {
playSound(CobblemonSounds.PC_CLICK)
this.storageWidget.box -= 1
return true
}
}
} else if (keyCode == InputConstants.KEY_ESCAPE) {
// Escape from text box
if (::boxNameWidget.isInitialized) boxNameWidget.keyPressed(keyCode, scanCode, modifiers)
this.focused = null
return true
}
when (keyCode) {
InputConstants.KEY_ESCAPE -> {
playSound(CobblemonSounds.PC_OFF)
UnlinkPlayerFromPCPacket().sendToServer()
onClose()
return true
}
InputConstants.KEY_RIGHT -> {
playSound(CobblemonSounds.PC_CLICK)
this.storageWidget.box += 1
return true
}
InputConstants.KEY_LEFT -> {
playSound(CobblemonSounds.PC_CLICK)
this.storageWidget.box -= 1
return true
}
}
return super.keyPressed(keyCode, scanCode, modifiers)
}
@@ -581,10 +649,4 @@ class PCGUI(
modelWidget = null
}
}
fun updateBoxName() {
if (this::boxNameWidget.isInitialized) {
boxNameWidget.update()
}
}
}
@@ -1,66 +0,0 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.pc
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.text.bold
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.client.CobblemonResources
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.components.EditBox
import net.minecraft.network.chat.Component
class SearchWidget(
private var pX: Int,
private var pY: Int,
text: Component = "SearchWidget".text(),
update: () -> (Unit)
): EditBox(Minecraft.getInstance().font, pX, pY, WIDTH, HEIGHT, text) {
companion object {
const val WIDTH = 110
const val HEIGHT = 14
const val HINT_COLOUR = 0x8D8D8D
private val hint = Component.translatable("cobblemon.ui.pc.search")
private val searchIcon = cobblemonResource("textures/gui/pc/pc_search_icon.png")
}
init {
setMaxLength(21)
setResponder { update() }
}
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
if (cursorPosition != value.length) moveCursorToEnd(false)
val input = if (isFocused) "${value}_".text() else value.text()
val useHint = !isFocused && value.isEmpty()
blitk(
matrixStack = context.pose(),
texture = searchIcon,
x = pX + 1,
y = pY + 3,
width = 8,
height = 8
)
drawScaledText(
context = context,
font = CobblemonResources.DEFAULT_LARGE,
text = if (useHint) hint.bold() else input.bold(),
x = pX + if (useHint) 55 else 14,
y = pY + 2,
colour = if (useHint) HINT_COLOUR else 0xFFFFFF,
centered = useHint
)
}
}

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