Compare commits

...
Author SHA1 Message Date
Plastered_Crab 29734ea067 Push changes to block scanning and add render layer to scannable block entity renderers for scanning icon status 2026-06-08 23:49:42 -07:00
Plastered_Crab 500c7b2e5c Push changes to block scanning and add render layer to scannable block entity renderers for scanning icon status 2026-06-08 23:48:32 -07:00
Plastered_Crab 4dde882398 Move to new Block Scanning method instead of jsons 2026-06-08 15:32:41 -07:00
plastered_crab 84039fcfaf Push test changes from my home PC 2026-06-08 14:08:55 -07:00
plastered_crab fc1853a23b Add new photo bonus score metrics and how star ratings are presented 2026-06-05 12:54:35 -07:00
plastered_crab 72c46333d3 Add changes to raycast detection for testing 2026-06-05 03:50:56 -07:00
Plastered_Crab 4faee1a5cf Add more debug chat lines for testing, adjust animation detection jsons 2026-06-04 17:25:22 -07:00
plastered_crab db3237df81 Add star ratings 2026-06-04 14:50:11 -07:00
plastered_crab b76efa52d1 Adjust scoring and add tooltips and changes to gallery ui 2026-06-04 14:49:47 -07:00
plastered_crab 306e5e854f adjust raycasts, and edit configs 2026-06-04 12:19:54 -07:00
plastered_crab 611a17b16d Push teste configs for scoring 2026-06-04 11:13:55 -07:00
plastered_crab 792329880b Configs for scoring for testing 2026-06-04 10:45:05 -07:00
plastered_crab 9ddd773182 Changes to image capture area 2026-06-04 09:53:40 -07:00
plastered_crab dd89a220bc First push of a points system for photos 2026-06-04 09:18:56 -07:00
plastered_crab 8b43ef0922 Merge remote-tracking branch 'origin/1.21.1-pokedex-upgrades-camera-changes' into 1.21.1-pokedex-upgrades-camera-changes 2026-06-03 14:19:20 -07:00
plastered_crab 636c6701c0 Add basic shutter effect for Pokedex Camera 2026-06-03 14:18:46 -07:00
Plastered_Crab da9ad072f1 Make Photo Printer CUTOUT render type 2026-06-03 09:54:21 -07:00
plastered_crab ec91b597b7 Add Photo Printer to Creative tab and adjust inventory model render 2026-06-03 09:29:07 -07:00
plastered_crab c293c6cbfb Edits to Photo Printer block model to allow for animated screen textures and adjust hitbox 2026-06-03 09:00:36 -07:00
plastered_crab 6eccd3e72a Add first assets for Photo Printer and adjust printing photo animation 2026-06-03 01:23:25 -07:00
plastered_crab 480b125146 Printing photos using photo printer now takes a short time to print and you can see the photo slide out of the printer itself as it prints 2026-06-02 01:33:15 -07:00
plastered_crab b29366af64 Pokedex Camera to remember what mode it was left in (Scanning or Camera) 2026-06-01 13:01:22 -07:00
plastered_crab b4c1d10037 Fix distance from placement surface 2026-04-24 22:54:07 -07:00
plastered_crab 12fbd9ed32 Fix the spacing of 2x2 and 3x3 printed photos and distance from placement surface 2026-04-24 22:27:13 -07:00
Plastered_Crab 644f0cb3b4 Very Rough format of photos working like paintings and maps 2026-04-24 19:00:27 -07:00
Plastered_Crab 327548a8aa Cleanup Photo Printer GUI a tad 2026-04-24 13:53:57 -07:00
Plastered_Crab bc41a5294e Photos Galleries can be searched for specific Pokemon or Time of Day 2026-04-24 13:12:27 -07:00
Plastered_Crab c2f8602106 You can take pictures and detect pokemon through transparent blocks 2026-04-24 10:35:16 -07:00
Plastered_Crab 9ddfd707c2 Temporary visibility of raycasts for density testing. 2026-04-24 09:59:43 -07:00
Plastered_Crab 98e9a673c0 Test with having Photos store info about Pokemon, Time of Day, and Biome + Sharing dex entries with friends 2026-04-23 16:29:09 -07:00
Plastered_Crab beb4e4aa86 Add toggling between Scanning mode and Camera mode in Pokedex (with placeholder sounds) 2026-04-23 14:46:06 -07:00
Plastered_Crab 2567bc8819 Changes to UI and how data is stored and how printed photos are rendered 2026-04-22 20:36:47 -07:00
Plastered_Crab 6a300e3c5f Changes to UI and how data is stored and how printed photos are rendered 2026-04-22 16:49:37 -07:00
Plastered_Crab 4d8f4b2bfc Start of changes to WIP Camera system 2026-04-22 12:14:44 -07:00
Plastered_Crab 94154776eb Add WIP Photo Gallery page to pokedex 2026-04-14 15:51:41 -07:00
Plastered_Crab b76e75efcc Start to add Pokedex Scanner Camera functionality back in 2026-04-14 14:25:24 -07:00
119 changed files with 9353 additions and 262 deletions
@@ -37,6 +37,7 @@ import com.cobblemon.mod.common.api.pokemon.helditem.HeldItemProvider
import com.cobblemon.mod.common.api.pokemon.stats.EvCalculator
import com.cobblemon.mod.common.api.pokemon.stats.Generation8EvCalculator
import com.cobblemon.mod.common.api.pokemon.stats.StatProvider
import com.cobblemon.mod.common.api.pokedex.scan.BlockScanProviderRegistry
import com.cobblemon.mod.common.api.properties.CustomPokemonProperty
import com.cobblemon.mod.common.api.scheduling.ServerRealTimeTaskTracker
import com.cobblemon.mod.common.api.scheduling.ServerTaskTracker
@@ -62,6 +63,8 @@ import com.cobblemon.mod.common.api.storage.player.adapter.DexDataMongoBackend
import com.cobblemon.mod.common.api.storage.player.adapter.DexDataNbtBackend
import com.cobblemon.mod.common.api.storage.player.adapter.PlayerDataJsonBackend
import com.cobblemon.mod.common.api.storage.player.adapter.PlayerDataMongoBackend
import com.cobblemon.mod.common.api.storage.player.adapter.ScannedBlocksMongoBackend
import com.cobblemon.mod.common.api.storage.player.adapter.ScannedBlocksNbtBackend
import com.cobblemon.mod.common.api.storage.player.adapter.TMMoveMongoBackend
import com.cobblemon.mod.common.api.storage.player.adapter.TMMoveNbtBackend
import com.cobblemon.mod.common.api.storage.player.factory.CachedPlayerDataStoreFactory
@@ -91,6 +94,10 @@ import com.cobblemon.mod.common.events.StatHandler
import com.cobblemon.mod.common.net.messages.client.settings.ServerSettingsPacket
import com.cobblemon.mod.common.permission.LaxPermissionValidator
import com.cobblemon.mod.common.platform.events.PlatformEvents
import com.cobblemon.mod.common.pokedex.photo.PokedexPhotoRepository
import com.cobblemon.mod.common.pokedex.photo.PrintedPhotoAssetRepository
import com.cobblemon.mod.common.pokedex.scanner.block.BerryBlockScanProvider
import com.cobblemon.mod.common.pokedex.scanner.block.PrintedPhotoBlockScanProvider
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.pokemon.aspects.CHARACTERISTIC_RAINBOW_ASPECT
import com.cobblemon.mod.common.pokemon.aspects.COSMETIC_SLOT_ASPECT
@@ -237,6 +244,8 @@ object Cobblemon {
CobblemonGameRules // Init fields and register
ShoulderEffectRegistry.register()
BlockScanProviderRegistry.register(cobblemonResource("berry"), BerryBlockScanProvider)
BlockScanProviderRegistry.register(cobblemonResource("printed_photo_display"), PrintedPhotoBlockScanProvider)
DATA_SYNCHRONIZED.subscribe {
storage.onPlayerDataSync(it)
@@ -304,7 +313,29 @@ object Cobblemon {
this.config.maxPokemonLevel,
this.config.maxPokemonFriendship,
this.config.maxDynamaxLevel,
this.config.unlockAllMoveDexMovesByDefault
this.config.unlockAllMoveDexMovesByDefault,
this.config.pokedexScannerPhotoRaycastDistance,
this.config.pokedexScannerPhotoRaycastDensity,
this.config.pokedexScannerPhotoPerimeterRaycastDensity,
this.config.pokedexScannerPhotoCoverageScoreCap,
this.config.pokedexScannerPhotoCoverageScoreCapAtPercent,
this.config.pokedexScannerPhotoCenteredScoreCap,
this.config.pokedexScannerPhotoFacingBonus,
this.config.pokedexScannerPhotoFullBodyBonus,
this.config.pokedexScannerPhotoOutOfFrameMultiplier,
this.config.pokedexScannerPhotoShinyBonus,
this.config.pokedexScannerPhotoUncommonSpeciesMultiplier,
this.config.pokedexScannerPhotoRareSpeciesMultiplier,
this.config.pokedexScannerPhotoUltraRareSpeciesMultiplier,
this.config.pokedexScannerPhotoExtraPokemonBonusCap,
this.config.pokedexScannerPhotoExtraPokemonBonusPerSubject,
this.config.pokedexScannerPhotoFamilyBonus,
this.config.pokedexScannerPhotoHabitatBonus,
this.config.pokedexScannerPhotoTimeOfDayBonus,
this.config.pokedexScannerPhotoWeatherBonus,
this.config.pokedexScannerPhotoAlphaBonus,
this.config.pokedexScannerPhotoWildBonus,
this.config.pokedexScannerPhotoFormBonus
).sendToPlayer(player)
}
@@ -362,6 +393,8 @@ object Cobblemon {
val server = event.server
MoLangLoadedFilesCache.initialize(server)
playerDataManager = PlayerInstancedDataStoreManager().also { it.setup(server) }
PokedexPhotoRepository.initialize(server)
PrintedPhotoAssetRepository.initialize(server)
val mongoClient: MongoClient?
@@ -377,9 +410,13 @@ object Cobblemon {
val tmMoveFactory = CachedPlayerDataStoreFactory(TMMoveNbtBackend())
tmMoveFactory.setup(server)
val scannedBlocksFactory = CachedPlayerDataStoreFactory(ScannedBlocksNbtBackend())
scannedBlocksFactory.setup(server)
playerDataManager.setFactory(generalJsonFactory, PlayerInstancedDataStoreTypes.GENERAL)
playerDataManager.setFactory(pokedexNbtFactory, PlayerInstancedDataStoreTypes.POKEDEX)
playerDataManager.setFactory(tmMoveFactory, PlayerInstancedDataStoreTypes.TM_MOVES)
playerDataManager.setFactory(scannedBlocksFactory, PlayerInstancedDataStoreTypes.SCANNED_BLOCKS)
if (config.storageFormat == "nbt") {
NBTStoreAdapter(pokemonStoreRoot.absolutePath, useNestedFolders = true, folderPerClass = true)
@@ -411,9 +448,15 @@ object Cobblemon {
)
tmMovesMongoFactory.setup(server)
val scannedBlocksMongoFactory = CachedPlayerDataStoreFactory(
ScannedBlocksMongoBackend(mongoClient, config.mongoDBDatabaseName, "ScannedBlocksCollection")
)
scannedBlocksMongoFactory.setup(server)
playerDataManager.setFactory(generalMongoFactory, PlayerInstancedDataStoreTypes.GENERAL)
playerDataManager.setFactory(pokedexMongoFactory, PlayerInstancedDataStoreTypes.POKEDEX)
playerDataManager.setFactory(tmMovesMongoFactory, PlayerInstancedDataStoreTypes.TM_MOVES)
playerDataManager.setFactory(scannedBlocksMongoFactory, PlayerInstancedDataStoreTypes.SCANNED_BLOCKS)
MongoDBStoreAdapter(mongoClient, config.mongoDBDatabaseName)
} catch (e: ClassNotFoundException) {
LOGGER.error("MongoDB driver not found.")
@@ -442,6 +485,8 @@ object Cobblemon {
playerDataManager.saveAllStores()
playerDataManager.saveExecutor.shutdown()
playerDataManager.saveExecutor.awaitTermination(30L, TimeUnit.SECONDS)
PokedexPhotoRepository.shutdown()
PrintedPhotoAssetRepository.shutdown()
}
PlatformEvents.SERVER_STARTED.subscribe { event ->
bestSpawner.onServerStarted(event.server)
@@ -54,6 +54,15 @@ object CobblemonBlockEntities : PlatformRegistry<Registry<BlockEntityType<*>>, R
@JvmField
val TM_MACHINE: BlockEntityType<TMMachineBlockEntity> = this.create("tm_machine", BlockEntityType.Builder.of(::TMMachineBlockEntity, CobblemonBlocks.TM_MACHINE).build(null))
@JvmField
val PHOTO_PRINTER: BlockEntityType<PhotoPrinterBlockEntity> = this.create("photo_printer", BlockEntityType.Builder.of(::PhotoPrinterBlockEntity, CobblemonBlocks.PHOTO_PRINTER).build(null))
@JvmField
val PRINTED_PHOTO_DISPLAY: BlockEntityType<PrintedPhotoDisplayBlockEntity> = this.create(
"printed_photo_display",
BlockEntityType.Builder.of(::PrintedPhotoDisplayBlockEntity, CobblemonBlocks.PRINTED_PHOTO_DISPLAY).build(null)
)
@JvmField
val GILDED_CHEST: BlockEntityType<GildedChestBlockEntity> = this.create("chest", BlockEntityType.Builder.of(::GildedChestBlockEntity,
CobblemonBlocks.GILDED_CHEST,
@@ -678,6 +678,30 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<
)
)
@JvmField
val PHOTO_PRINTER = create(
"photo_printer",
PhotoPrinterBlock(
BlockBehaviour.Properties.of()
.mapColor(MapColor.COLOR_LIGHT_GRAY)
.sound(SoundType.METAL)
.strength(2.5F)
)
)
@JvmField
val PRINTED_PHOTO_DISPLAY = create(
"printed_photo_display",
PrintedPhotoDisplayBlock(
BlockBehaviour.Properties.of()
.mapColor(MapColor.COLOR_LIGHT_GRAY)
.sound(SoundType.WOOD)
.strength(0.2F)
.noOcclusion()
.pushReaction(PushReaction.DESTROY)
)
)
@JvmField
val LECTERN = create("lectern", LecternBlock(BlockBehaviour.Properties.of().mapColor(MapColor.WOOD).instrument(NoteBlockInstrument.BASS).sound(SoundType.WOOD).strength(2.5F).ignitedByLava().lightLevel { if (it.getValue(LecternBlock.EMIT_LIGHT) as Boolean) 13 else 0 }))
@@ -1012,4 +1036,4 @@ object CobblemonBlocks : PlatformRegistry<Registry<Block>, ResourceKey<Registry<
}
fun typeGemClusters() = typeGemClusters.toMap()
}
}
@@ -99,6 +99,12 @@ object CobblemonItemComponents : PlatformRegistry<Registry<DataComponentType<*>>
.networkSynchronized(ByteBufCodecs.BOOL)
.build())
@JvmField
val PRINTED_PHOTO: DataComponentType<PrintedPhotoComponent> = create("printed_photo", DataComponentType.builder<PrintedPhotoComponent>()
.persistent(PrintedPhotoComponent.CODEC)
.networkSynchronized(PrintedPhotoComponent.PACKET_CODEC)
.build())
fun register() {
Registry.register(BuiltInRegistries.DATA_COMPONENT_TYPE, ResourceLocation.parse("cobblemon:pokemon_item"), POKEMON_ITEM)
Registry.register(BuiltInRegistries.DATA_COMPONENT_TYPE, ResourceLocation.parse("cobblemon:bait"), BAIT)
@@ -112,9 +118,10 @@ object CobblemonItemComponents : PlatformRegistry<Registry<DataComponentType<*>>
Registry.register(BuiltInRegistries.DATA_COMPONENT_TYPE, ResourceLocation.parse("cobblemon:food"), FOOD)
Registry.register(BuiltInRegistries.DATA_COMPONENT_TYPE, ResourceLocation.parse("cobblemon:mob_effects"), MOB_EFFECTS)
Registry.register(BuiltInRegistries.DATA_COMPONENT_TYPE, ResourceLocation.parse("cobblemon:held_item_effect"), HELD_ITEM_EFFECT)
Registry.register(BuiltInRegistries.DATA_COMPONENT_TYPE, ResourceLocation.parse("cobblemon:printed_photo"), PRINTED_PHOTO)
}
override val registry = BuiltInRegistries.DATA_COMPONENT_TYPE
override val resourceKey = Registries.DATA_COMPONENT_TYPE
}
}
@@ -190,6 +190,9 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val POKEDEX_WHITE = pokedexItem(PokedexType.WHITE)
@JvmField
val PRINTED_PHOTO = create("printed_photo", PrintedPhotoItem(Item.Properties().stacksTo(1)))
val campfire_pots = mutableListOf<CampfirePotItem>()
@JvmField
val CAMPFIRE_POT_RED = campfirePotItem(CobblemonBlocks.RED_CAMPFIRE_POT, CampfirePotColor.RED)
@@ -459,6 +462,8 @@ object CobblemonItems : PlatformRegistry<Registry<Item>, ResourceKey<Registry<It
@JvmField
val TM_MACHINE = blockItem("tm_machine", CobblemonBlocks.TM_MACHINE)
@JvmField
val PHOTO_PRINTER = blockItem("photo_printer", CobblemonBlocks.PHOTO_PRINTER)
@JvmField
val TM_SHELF = blockItem("tm_shelf", CobblemonBlocks.TM_SHELF)
@JvmField
val PASTURE = blockItem("pasture", CobblemonBlocks.PASTURE)
@@ -10,6 +10,7 @@ package com.cobblemon.mod.common
import com.cobblemon.mod.common.block.campfirepot.CookingPotMenu
import com.cobblemon.mod.common.block.photoprinter.PhotoPrinterMenu
import com.cobblemon.mod.common.block.tmmachine.TMMachineMenu
import com.cobblemon.mod.common.platform.PlatformRegistry
import net.minecraft.core.Registry
@@ -22,9 +23,10 @@ object CobblemonMenuType : PlatformRegistry<Registry<MenuType<*>>, ResourceKey<R
val COOKING_POT = MenuType.register("cooking_pot_menu", ::CookingPotMenu)
val TM_MACHINE = MenuType.register("tm_machine_menu", ::TMMachineMenu)
val PHOTO_PRINTER = MenuType.register("photo_printer_menu", ::PhotoPrinterMenu)
override val registry: Registry<MenuType<*>>
get() = BuiltInRegistries.MENU
override val resourceKey: ResourceKey<Registry<MenuType<*>>>
get() = Registries.MENU
}
}
@@ -167,7 +167,9 @@ import com.cobblemon.mod.common.net.messages.server.pasture.PasturePokemonPacket
import com.cobblemon.mod.common.net.messages.server.pasture.SetPastureConflictPacket
import com.cobblemon.mod.common.net.messages.server.pasture.UnpastureAllPokemonPacket
import com.cobblemon.mod.common.net.messages.server.pasture.UnpasturePokemonPacket
import com.cobblemon.mod.common.net.messages.server.pokedex.scanner.FinishPrintedPhotoScanningPacket
import com.cobblemon.mod.common.net.messages.server.pokedex.scanner.FinishScanningPacket
import com.cobblemon.mod.common.net.messages.server.pokedex.scanner.RecordBlockScanPacket
import com.cobblemon.mod.common.net.messages.server.pokedex.scanner.StartScanningPacket
import com.cobblemon.mod.common.net.messages.server.pokemon.interact.InteractPokemonPacket
import com.cobblemon.mod.common.net.messages.server.pokemon.update.ServerboundUpdateRidingStatePacket
@@ -225,7 +227,9 @@ import com.cobblemon.mod.common.net.serverhandling.pasture.PasturePokemonHandler
import com.cobblemon.mod.common.net.serverhandling.pasture.SetPastureConflictHandler
import com.cobblemon.mod.common.net.serverhandling.pasture.UnpastureAllPokemonHandler
import com.cobblemon.mod.common.net.serverhandling.pasture.UnpasturePokemonHandler
import com.cobblemon.mod.common.net.serverhandling.pokedex.scanner.FinishPrintedPhotoScanningHandler
import com.cobblemon.mod.common.net.serverhandling.pokedex.scanner.FinishScanningHandler
import com.cobblemon.mod.common.net.serverhandling.pokedex.scanner.RecordBlockScanPacketHandler
import com.cobblemon.mod.common.net.serverhandling.pokedex.scanner.StartScanningHandler
import com.cobblemon.mod.common.net.serverhandling.pokemon.interact.InteractPokemonHandler
import com.cobblemon.mod.common.net.serverhandling.pokemon.update.*
@@ -482,6 +486,10 @@ object CobblemonNetwork {
// Pokédex scanning
list.add(PacketRegisterInfo(ServerConfirmedRegisterPacket.ID, ServerConfirmedRegisterPacket::decode, ServerConfirmedRegisterHandler))
list.add(PacketRegisterInfo(PokedexPhotoGalleryPacket.ID, PokedexPhotoGalleryPacket::decode, PokedexPhotoGalleryPacketHandler))
list.add(PacketRegisterInfo(PokedexPhotoBatchPacket.ID, PokedexPhotoBatchPacket::decode, PokedexPhotoBatchPacketHandler))
list.add(PacketRegisterInfo(PokedexPhotoUploadResultPacket.ID, PokedexPhotoUploadResultPacket::decode, PokedexPhotoUploadResultPacketHandler))
list.add(PacketRegisterInfo(PrintedPhotoAssetBatchPacket.ID, PrintedPhotoAssetBatchPacket::decode, PrintedPhotoAssetBatchPacketHandler))
// Debug / cheats
list.add(PacketRegisterInfo(CalculateSeatPositionsPacket.ID, CalculateSeatPositionsPacket::decode, CalculateSeatPositionsHandler))
@@ -562,6 +570,15 @@ object CobblemonNetwork {
// Pokédex scanning
list.add(PacketRegisterInfo(StartScanningPacket.ID, StartScanningPacket::decode, StartScanningHandler))
list.add(PacketRegisterInfo(FinishScanningPacket.ID, FinishScanningPacket::decode, FinishScanningHandler))
list.add(PacketRegisterInfo(FinishPrintedPhotoScanningPacket.ID, FinishPrintedPhotoScanningPacket::decode, FinishPrintedPhotoScanningHandler))
list.add(PacketRegisterInfo(RecordBlockScanPacket.ID, RecordBlockScanPacket::decode, RecordBlockScanPacketHandler))
list.add(PacketRegisterInfo(PokedexPhotoUploadPacket.ID, PokedexPhotoUploadPacket::decode, PokedexPhotoUploadPacketHandler))
list.add(PacketRegisterInfo(RequestPokedexPhotoGalleryPacket.ID, RequestPokedexPhotoGalleryPacket::decode, RequestPokedexPhotoGalleryPacketHandler))
list.add(PacketRegisterInfo(RequestPokedexPhotoBatchPacket.ID, RequestPokedexPhotoBatchPacket::decode, RequestPokedexPhotoBatchPacketHandler))
list.add(PacketRegisterInfo(DeletePokedexPhotoPacket.ID, DeletePokedexPhotoPacket::decode, DeletePokedexPhotoPacketHandler))
list.add(PacketRegisterInfo(SetPhotoPrinterSelectionPacket.ID, SetPhotoPrinterSelectionPacket::decode, SetPhotoPrinterSelectionPacketHandler))
list.add(PacketRegisterInfo(CraftPrintedPhotoPacket.ID, CraftPrintedPhotoPacket::decode, CraftPrintedPhotoPacketHandler))
list.add(PacketRegisterInfo(RequestPrintedPhotoAssetBatchPacket.ID, RequestPrintedPhotoAssetBatchPacket::decode, RequestPrintedPhotoAssetBatchPacketHandler))
// Pasture
list.add(PacketRegisterInfo(PasturePokemonPacket.ID, PasturePokemonPacket::decode, PasturePokemonHandler))
@@ -626,4 +643,4 @@ object CobblemonNetwork {
fun sendPacketToPlayer(player: ServerPlayer, packet: NetworkPacket<*>) {
Cobblemon.implementation.networkManager.sendPacketToPlayer(player, packet)
}
}
}
@@ -80,6 +80,10 @@ object CobblemonSounds : PlatformRegistry<Registry<SoundEvent>, ResourceKey<Regi
@JvmField
val POKEDEX_CLICK_SHORT = this.create("item.pokedex.click_short")
@JvmField
val POKEDEX_CAMERA_SNAP = this.create("item.pokedex.camera_snap")
@JvmField
val POKEDEX_SCAN_MODE_SWAP = this.create("item.pokedex.scan_mode_swap")
@JvmField
val POKEDEX_SCAN_OPEN = this.create("item.pokedex.scan_open")
@JvmField
val POKEDEX_SCAN_CLOSE = this.create("item.pokedex.scan_close")
@@ -0,0 +1,15 @@
package com.cobblemon.mod.common.api.pokedex.scan
object BlockScanDefinition {
data class Frame(
val slot: Int,
val text: ScanText,
val scale: Float? = null
)
sealed interface ScanText {
data class Literal(val text: String) : ScanText
data class Key(val key: String) : ScanText
data class Translate(val key: String, val with: List<ScanText> = emptyList()) : ScanText
}
}
@@ -0,0 +1,14 @@
package com.cobblemon.mod.common.api.pokedex.scan
import com.cobblemon.mod.common.pokedex.scanner.block.BlockScanProvider
import net.minecraft.resources.ResourceLocation
object BlockScanProviderRegistry {
private val providers = mutableMapOf<ResourceLocation, BlockScanProvider>()
fun register(typeId: ResourceLocation, provider: BlockScanProvider) {
providers[typeId] = provider
}
fun get(typeId: ResourceLocation): BlockScanProvider? = providers[typeId]
}
@@ -0,0 +1,52 @@
package com.cobblemon.mod.common.api.pokedex.scan
import com.cobblemon.mod.common.CobblemonNetwork.sendPacket
import com.cobblemon.mod.common.api.storage.player.InstancedPlayerData
import com.cobblemon.mod.common.api.storage.player.PlayerInstancedDataStoreTypes
import com.cobblemon.mod.common.api.storage.player.client.ClientScannedBlocksManager
import com.cobblemon.mod.common.net.messages.client.SetClientPlayerDataPacket
import com.cobblemon.mod.common.util.getPlayer
import com.mojang.serialization.Codec
import com.mojang.serialization.codecs.RecordCodecBuilder
import java.util.UUID
import net.minecraft.resources.ResourceLocation
class ScannedBlocksManager(
override val uuid: UUID,
val scannedKeys: MutableSet<ResourceLocation> = mutableSetOf()
) : InstancedPlayerData {
val storeType = PlayerInstancedDataStoreTypes.SCANNED_BLOCKS
override fun toClientData() = ClientScannedBlocksManager(scannedKeys.toMutableSet())
fun record(key: ResourceLocation): Boolean {
if (!scannedKeys.add(key)) return false
syncClient(setOf(key), isIncremental = true)
return true
}
fun syncClient(updateSet: Set<ResourceLocation> = scannedKeys, isIncremental: Boolean = true) {
uuid.getPlayer()?.sendPacket(
SetClientPlayerDataPacket(
type = storeType,
playerData = ClientScannedBlocksManager(updateSet.toMutableSet()),
isIncremental = isIncremental
)
)
}
companion object {
val CODEC: Codec<ScannedBlocksManager> = RecordCodecBuilder.create { instance ->
instance.group(
Codec.STRING.xmap(UUID::fromString, UUID::toString)
.fieldOf("uuid")
.forGetter { it.uuid },
Codec.list(ResourceLocation.CODEC)
.xmap({ it.toMutableSet() }, { it.toList() })
.fieldOf("scannedKeys")
.forGetter { it.scannedKeys }
).apply(instance, ::ScannedBlocksManager)
}
}
}
@@ -15,6 +15,7 @@ import com.cobblemon.mod.common.api.scheduling.ScheduledTask
import com.cobblemon.mod.common.api.scheduling.ServerTaskTracker
import com.cobblemon.mod.common.api.storage.player.client.ClientInstancedPlayerData
import com.cobblemon.mod.common.api.storage.player.factory.CachedPlayerDataStoreFactory
import com.cobblemon.mod.common.api.pokedex.scan.ScannedBlocksManager
import com.cobblemon.mod.common.api.tms.TMMoveManager
import com.google.common.util.concurrent.ThreadFactoryBuilder
import java.util.UUID
@@ -76,6 +77,14 @@ open class PlayerInstancedDataStoreManager {
.infiniteIterations()
.tracker(ServerTaskTracker)
.build()
saveTasks[PlayerInstancedDataStoreTypes.SCANNED_BLOCKS] = ScheduledTask.Builder()
.execute { saveAllOfOneType(PlayerInstancedDataStoreTypes.SCANNED_BLOCKS) }
.delay(30F)
.interval(120F)
.infiniteIterations()
.tracker(ServerTaskTracker)
.build()
}
open fun get(playerId: UUID, dataType: PlayerInstancedDataStoreType): InstancedPlayerData {
@@ -148,4 +157,12 @@ open class PlayerInstancedDataStoreManager {
}
return data as TMMoveManager
}
fun getScannedBlocksData(player: ServerPlayer): ScannedBlocksManager {
return getScannedBlocksData(player.uuid)
}
fun getScannedBlocksData(playerId: UUID): ScannedBlocksManager {
return get(playerId, PlayerInstancedDataStoreTypes.SCANNED_BLOCKS) as ScannedBlocksManager
}
}
@@ -9,8 +9,9 @@
package com.cobblemon.mod.common.api.storage.player
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.storage.player.client.ClientPokedexManager
import com.cobblemon.mod.common.api.storage.player.client.ClientGeneralPlayerData
import com.cobblemon.mod.common.api.storage.player.client.ClientPokedexManager
import com.cobblemon.mod.common.api.storage.player.client.ClientScannedBlocksManager
import com.cobblemon.mod.common.api.storage.player.client.ClientTMMoveManager
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.resources.ResourceLocation
@@ -35,6 +36,12 @@ object PlayerInstancedDataStoreTypes {
ClientTMMoveManager::runAction,
ClientTMMoveManager::runIncremental
))
val SCANNED_BLOCKS = register(PlayerInstancedDataStoreType(
cobblemonResource("scanned_blocks"),
ClientScannedBlocksManager::decode,
ClientScannedBlocksManager::runAction,
ClientScannedBlocksManager::runIncremental
))
fun register(type: PlayerInstancedDataStoreType): PlayerInstancedDataStoreType {
Cobblemon.LOGGER.debug("Registering PlayerInstancedDataStoreType: {}", type.id)
@@ -0,0 +1,28 @@
package com.cobblemon.mod.common.api.storage.player.adapter
import com.cobblemon.mod.common.api.pokedex.scan.ScannedBlocksManager
import com.cobblemon.mod.common.api.storage.player.PlayerInstancedDataStoreTypes
import com.cobblemon.mod.common.util.adapters.CodecBackedAdapter
import com.google.gson.GsonBuilder
import com.google.gson.reflect.TypeToken
import com.mongodb.client.MongoClient
import java.util.UUID
class ScannedBlocksMongoBackend(
mongoClient: MongoClient,
databaseName: String,
collectionName: String
) : MongoBackedPlayerDataStoreBackend<ScannedBlocksManager>(
mongoClient,
databaseName,
collectionName,
PlayerInstancedDataStoreTypes.SCANNED_BLOCKS
) {
override val gson = GsonBuilder()
.registerTypeAdapter(ScannedBlocksManager::class.java, CodecBackedAdapter(ScannedBlocksManager.CODEC))
.create()
override val classToken = TypeToken.get(ScannedBlocksManager::class.java)
override val defaultData = { uuid: UUID -> ScannedBlocksManager(uuid) }
override fun initialize(store: ScannedBlocksManager) {}
}
@@ -0,0 +1,10 @@
package com.cobblemon.mod.common.api.storage.player.adapter
import com.cobblemon.mod.common.api.pokedex.scan.ScannedBlocksManager
import com.cobblemon.mod.common.api.storage.player.PlayerInstancedDataStoreTypes
import java.util.UUID
class ScannedBlocksNbtBackend : NbtBackedPlayerData<ScannedBlocksManager>("scanned_blocks", PlayerInstancedDataStoreTypes.SCANNED_BLOCKS) {
override val codec = ScannedBlocksManager.CODEC
override val defaultData = { uuid: UUID -> ScannedBlocksManager(uuid) }
}
@@ -0,0 +1,43 @@
package com.cobblemon.mod.common.api.storage.player.client
import com.cobblemon.mod.common.api.storage.player.PlayerInstancedDataStoreTypes
import com.cobblemon.mod.common.client.CobblemonClient
import com.cobblemon.mod.common.net.messages.client.SetClientPlayerDataPacket
import com.cobblemon.mod.common.util.readString
import com.cobblemon.mod.common.util.writeString
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.resources.ResourceLocation
class ClientScannedBlocksManager(
val scannedKeys: MutableSet<ResourceLocation>
) : ClientInstancedPlayerData {
fun hasScanned(key: ResourceLocation): Boolean = key in scannedKeys
override fun encode(buf: RegistryFriendlyByteBuf) {
buf.writeCollection(scannedKeys) { _, id -> buf.writeString(id.toString()) }
}
companion object {
fun decode(buf: RegistryFriendlyByteBuf): SetClientPlayerDataPacket {
val keys = buf.readCollection(
{ mutableSetOf<ResourceLocation>() },
{ buf.readString().let(ResourceLocation::tryParse) ?: ResourceLocation("minecraft", "empty") }
)
return SetClientPlayerDataPacket(
PlayerInstancedDataStoreTypes.SCANNED_BLOCKS,
ClientScannedBlocksManager(keys)
)
}
fun runAction(data: ClientInstancedPlayerData) {
if (data !is ClientScannedBlocksManager) return
CobblemonClient.clientScannedBlocksData = data
}
fun runIncremental(data: ClientInstancedPlayerData) {
if (data !is ClientScannedBlocksManager) return
CobblemonClient.clientScannedBlocksData.scannedKeys.addAll(data.scannedKeys)
}
}
}
@@ -0,0 +1,196 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.block
import com.cobblemon.mod.common.CobblemonBlockEntities
import com.cobblemon.mod.common.block.entity.PhotoPrinterBlockEntity
import com.mojang.serialization.MapCodec
import com.mojang.serialization.codecs.RecordCodecBuilder
import net.minecraft.core.BlockPos
import net.minecraft.core.Direction
import net.minecraft.server.level.ServerPlayer
import net.minecraft.util.StringRepresentable
import net.minecraft.world.Containers
import net.minecraft.world.InteractionResult
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.player.Player
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.context.BlockPlaceContext
import net.minecraft.world.level.BlockGetter
import net.minecraft.world.level.Level
import net.minecraft.world.level.LevelAccessor
import net.minecraft.world.level.LevelReader
import net.minecraft.world.level.block.BaseEntityBlock
import net.minecraft.world.level.block.Block
import net.minecraft.world.level.block.Blocks
import net.minecraft.world.level.block.Mirror
import net.minecraft.world.level.block.RenderShape
import net.minecraft.world.level.block.Rotation
import net.minecraft.world.level.block.entity.BlockEntity
import net.minecraft.world.level.block.entity.BlockEntityTicker
import net.minecraft.world.level.block.entity.BlockEntityType
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.level.block.state.StateDefinition
import net.minecraft.world.level.block.state.properties.EnumProperty
import net.minecraft.world.level.block.state.properties.BlockStateProperties
import net.minecraft.world.phys.BlockHitResult
import net.minecraft.world.phys.shapes.CollisionContext
import net.minecraft.world.phys.shapes.Shapes
import net.minecraft.world.phys.shapes.VoxelShape
class PhotoPrinterBlock(properties: Properties) : BaseEntityBlock(properties) {
companion object {
val FACING = BlockStateProperties.HORIZONTAL_FACING
val PART: EnumProperty<PrinterPart> = EnumProperty.create("part", PrinterPart::class.java)
val STATE: EnumProperty<PrinterState> = EnumProperty.create("state", PrinterState::class.java)
private val BOTTOM_SHAPE: VoxelShape = Shapes.box(1.0 / 16.0, 0.0, 1.0 / 16.0, 15.0 / 16.0, 1.0, 15.0 / 16.0)
private val TOP_SHAPE: VoxelShape = Shapes.box(1.0 / 16.0, 0.0, 1.0 / 16.0, 15.0 / 16.0, 15.0 / 16.0, 15.0 / 16.0)
val CODEC: MapCodec<PhotoPrinterBlock> = RecordCodecBuilder.mapCodec { instance ->
instance.group(
Properties.CODEC.fieldOf("properties").forGetter { it.properties }
).apply(instance, ::PhotoPrinterBlock)
}
}
init {
registerDefaultState(
stateDefinition.any()
.setValue(FACING, Direction.NORTH)
.setValue(PART, PrinterPart.BOTTOM)
.setValue(STATE, PrinterState.OFF)
)
}
override fun codec(): MapCodec<PhotoPrinterBlock> = CODEC
override fun newBlockEntity(pos: BlockPos, state: BlockState): BlockEntity? {
return if (state.getValue(PART) == PrinterPart.BOTTOM) PhotoPrinterBlockEntity(pos, state) else null
}
override fun getStateForPlacement(context: BlockPlaceContext): BlockState? {
val abovePos = context.clickedPos.above()
val level = context.level
if (!level.getBlockState(abovePos).canBeReplaced(context) || level.isOutsideBuildHeight(abovePos)) {
return null
}
return defaultBlockState()
.setValue(FACING, context.horizontalDirection.opposite)
.setValue(PART, PrinterPart.BOTTOM)
.setValue(STATE, PrinterState.OFF)
}
override fun setPlacedBy(level: Level, pos: BlockPos, state: BlockState, placer: LivingEntity?, stack: ItemStack) {
level.setBlock(pos.above(), state.setValue(PART, PrinterPart.TOP), UPDATE_ALL)
level.blockUpdated(pos, Blocks.AIR)
state.updateNeighbourShapes(level, pos, UPDATE_ALL)
}
override fun createBlockStateDefinition(builder: StateDefinition.Builder<Block, BlockState>) {
builder.add(FACING, PART, STATE)
}
override fun rotate(state: BlockState, rotation: Rotation): BlockState {
return state.setValue(FACING, rotation.rotate(state.getValue(FACING)))
}
override fun mirror(state: BlockState, mirror: Mirror): BlockState {
return state.rotate(mirror.getRotation(state.getValue(FACING)))
}
override fun <T : BlockEntity> getTicker(level: Level, state: BlockState, type: BlockEntityType<T>): BlockEntityTicker<T>? {
if (state.getValue(PART) != PrinterPart.BOTTOM) return null
return createTickerHelper(type, CobblemonBlockEntities.PHOTO_PRINTER, PhotoPrinterBlockEntity::tick)
}
override fun useWithoutItem(
state: BlockState,
level: Level,
pos: BlockPos,
player: Player,
hit: BlockHitResult
): InteractionResult {
if (!level.isClientSide) {
val blockEntity = level.getBlockEntity(getBasePosition(state, pos)) as? PhotoPrinterBlockEntity ?: return InteractionResult.CONSUME
(player as? ServerPlayer)?.openMenu(blockEntity)
}
return InteractionResult.sidedSuccess(level.isClientSide)
}
override fun onRemove(state: BlockState, level: Level, pos: BlockPos, newState: BlockState, movedByPiston: Boolean) {
if (state.`is`(newState.block)) {
return
}
if (state.getValue(PART) == PrinterPart.TOP) {
super.onRemove(state, level, pos, newState, movedByPiston)
return
}
val blockEntity = level.getBlockEntity(pos) as? PhotoPrinterBlockEntity
blockEntity?.printingStack?.takeIf { !it.isEmpty }?.let { stack ->
Containers.dropItemStack(level, pos.x + 0.5, pos.y + 0.5, pos.z + 0.5, stack.copy())
}
Containers.dropContentsOnDestroy(state, newState, level, pos)
super.onRemove(state, level, pos, newState, movedByPiston)
}
override fun canSurvive(state: BlockState, level: LevelReader, pos: BlockPos): Boolean {
val belowPos = pos.below()
val belowState = level.getBlockState(belowPos)
return if (state.getValue(PART) == PrinterPart.BOTTOM) {
belowState.isFaceSturdy(level, belowPos, Direction.UP)
} else {
belowState.`is`(this) && belowState.getValue(PART) == PrinterPart.BOTTOM
}
}
override fun updateShape(
state: BlockState,
direction: Direction,
neighborState: BlockState,
level: LevelAccessor,
pos: BlockPos,
neighborPos: BlockPos
): BlockState {
val part = state.getValue(PART)
val expectedDirection = if (part == PrinterPart.BOTTOM) Direction.UP else Direction.DOWN
if (direction == expectedDirection && (!neighborState.`is`(this) || neighborState.getValue(PART) == part)) {
return Blocks.AIR.defaultBlockState()
}
return state
}
override fun getShape(state: BlockState, level: BlockGetter, pos: BlockPos, context: CollisionContext): VoxelShape {
return if (state.getValue(PART) == PrinterPart.BOTTOM) BOTTOM_SHAPE else TOP_SHAPE
}
override fun getRenderShape(state: BlockState): RenderShape = RenderShape.MODEL
fun getBasePosition(state: BlockState, pos: BlockPos): BlockPos {
return if (state.getValue(PART) == PrinterPart.BOTTOM) pos else pos.below()
}
enum class PrinterPart(private val serializedName: String) : StringRepresentable {
TOP("top"),
BOTTOM("bottom");
override fun getSerializedName(): String = serializedName
}
enum class PrinterState(private val serializedName: String) : StringRepresentable {
OFF("off"),
ON("on"),
PRINTING("printing");
override fun getSerializedName(): String = serializedName
}
}
@@ -0,0 +1,284 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.block
import com.cobblemon.mod.common.block.entity.PrintedPhotoDisplayBlockEntity
import com.cobblemon.mod.common.item.PokedexItem
import com.cobblemon.mod.common.pokedex.photo.PrintedPhotoPokedexScanner
import com.cobblemon.mod.common.util.rotateShape
import com.mojang.serialization.MapCodec
import com.mojang.serialization.codecs.RecordCodecBuilder
import net.minecraft.core.BlockPos
import net.minecraft.core.Direction
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.Containers
import net.minecraft.world.InteractionHand
import net.minecraft.world.InteractionResult
import net.minecraft.world.ItemInteractionResult
import net.minecraft.world.entity.player.Player
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.context.BlockPlaceContext
import net.minecraft.world.level.BlockGetter
import net.minecraft.world.level.GameRules
import net.minecraft.world.level.Level
import net.minecraft.world.level.LevelAccessor
import net.minecraft.world.level.LevelReader
import net.minecraft.world.level.block.Block
import net.minecraft.world.level.block.Blocks
import net.minecraft.world.level.block.EntityBlock
import net.minecraft.world.level.block.FaceAttachedHorizontalDirectionalBlock
import net.minecraft.world.level.block.RenderShape
import net.minecraft.world.level.block.SimpleWaterloggedBlock
import net.minecraft.world.level.block.entity.BlockEntity
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.level.block.state.StateDefinition
import net.minecraft.world.level.block.state.properties.AttachFace
import net.minecraft.world.level.block.state.properties.BlockStateProperties.WATERLOGGED
import net.minecraft.world.level.material.FluidState
import net.minecraft.world.level.material.Fluids
import net.minecraft.world.phys.BlockHitResult
import net.minecraft.world.phys.shapes.CollisionContext
import net.minecraft.world.phys.shapes.Shapes
import net.minecraft.world.phys.shapes.VoxelShape
class PrintedPhotoDisplayBlock(properties: Properties) :
FaceAttachedHorizontalDirectionalBlock(properties),
EntityBlock,
SimpleWaterloggedBlock {
data class SurfaceAxes(val normal: Direction, val right: Direction, val up: Direction)
companion object {
val CODEC: MapCodec<PrintedPhotoDisplayBlock> = RecordCodecBuilder.mapCodec { instance ->
instance.group(propertiesCodec()).apply(instance, ::PrintedPhotoDisplayBlock)
}
private val WALL_NORTH = Shapes.box(0.0, 0.0, 0.9375, 1.0, 1.0, 1.0)
private val FLOOR_NORTH = Shapes.box(0.0, 0.0, 0.0, 1.0, 0.0625, 1.0)
private val CEILING_NORTH = Shapes.box(0.0, 0.9375, 0.0, 1.0, 1.0, 1.0)
private val GROUP_REMOVE_GUARD = ThreadLocal.withInitial { false }
fun resolveAxes(state: BlockState): SurfaceAxes {
val face = state.getValue(FACE)
val facing = state.getValue(FACING)
val normal = when (face) {
AttachFace.WALL -> facing
AttachFace.FLOOR -> Direction.UP
AttachFace.CEILING -> Direction.DOWN
}
val up = when (face) {
AttachFace.WALL -> Direction.UP
AttachFace.FLOOR -> facing.opposite
AttachFace.CEILING -> facing
}
val right = cross(up, normal) ?: Direction.EAST
return SurfaceAxes(normal = normal, right = right, up = up)
}
private fun cross(a: Direction, b: Direction): Direction? {
val ax = a.stepX
val ay = a.stepY
val az = a.stepZ
val bx = b.stepX
val by = b.stepY
val bz = b.stepZ
val x = ay * bz - az * by
val y = az * bx - ax * bz
val z = ax * by - ay * bx
return Direction.fromDelta(x, y, z)
}
}
init {
registerDefaultState(
stateDefinition.any()
.setValue(FACING, Direction.NORTH)
.setValue(FACE, AttachFace.WALL)
.setValue(WATERLOGGED, false)
)
}
override fun codec(): MapCodec<PrintedPhotoDisplayBlock> = CODEC
override fun createBlockStateDefinition(builder: StateDefinition.Builder<Block, BlockState>) {
builder.add(FACING, FACE, WATERLOGGED)
}
override fun newBlockEntity(pos: BlockPos, state: BlockState): BlockEntity =
PrintedPhotoDisplayBlockEntity(pos, state)
override fun getRenderShape(state: BlockState): RenderShape = RenderShape.MODEL
override fun getStateForPlacement(context: BlockPlaceContext): BlockState? {
val fluidState = context.level.getFluidState(context.clickedPos)
for (direction in context.nearestLookingDirections) {
val candidate = if (direction.axis == Direction.Axis.Y) {
defaultBlockState()
.setValue(FACE, if (direction == Direction.UP) AttachFace.CEILING else AttachFace.FLOOR)
.setValue(FACING, context.horizontalDirection.opposite)
} else {
defaultBlockState()
.setValue(FACE, AttachFace.WALL)
.setValue(FACING, direction.opposite)
}.setValue(WATERLOGGED, fluidState.type == Fluids.WATER)
if (candidate.canSurvive(context.level, context.clickedPos)) {
return candidate
}
}
return null
}
override fun getFluidState(state: BlockState): FluidState? {
return if (state.getValue(WATERLOGGED)) Fluids.WATER.getSource(false) else super.getFluidState(state)
}
override fun getShape(
state: BlockState,
blockGetter: BlockGetter,
pos: BlockPos,
context: CollisionContext
): VoxelShape {
val direction = state.getValue(FACING)
return when (state.getValue(FACE)) {
AttachFace.WALL -> when (direction) {
Direction.NORTH, Direction.UP, Direction.DOWN -> WALL_NORTH
else -> rotateShape(Direction.NORTH, direction, WALL_NORTH)
}
AttachFace.FLOOR -> {
if (direction.axis == Direction.Axis.X) {
rotateShape(Direction.NORTH, Direction.WEST, FLOOR_NORTH)
} else {
FLOOR_NORTH
}
}
AttachFace.CEILING -> {
if (direction.axis == Direction.Axis.X) {
rotateShape(Direction.NORTH, Direction.WEST, CEILING_NORTH)
} else {
CEILING_NORTH
}
}
}
}
override fun useWithoutItem(
state: BlockState,
level: Level,
pos: BlockPos,
player: Player,
hitResult: BlockHitResult
): InteractionResult {
return InteractionResult.PASS
}
override fun useItemOn(
stack: ItemStack,
state: BlockState,
level: Level,
pos: BlockPos,
player: Player,
hand: InteractionHand,
hitResult: BlockHitResult
): ItemInteractionResult {
if (stack.item !is PokedexItem) {
return ItemInteractionResult.PASS_TO_DEFAULT_BLOCK_INTERACTION
}
if (level.isClientSide) {
return ItemInteractionResult.SUCCESS
}
val serverPlayer = player as? ServerPlayer ?: return ItemInteractionResult.PASS_TO_DEFAULT_BLOCK_INTERACTION
val component = (level.getBlockEntity(pos) as? PrintedPhotoDisplayBlockEntity)?.component
?: return ItemInteractionResult.PASS_TO_DEFAULT_BLOCK_INTERACTION
val newlySeen = PrintedPhotoPokedexScanner.grantSeenEntries(serverPlayer, component)
PrintedPhotoPokedexScanner.sendResultMessage(serverPlayer, newlySeen, component.detectedSpecies.distinct().size)
return ItemInteractionResult.CONSUME
}
override fun canSurvive(state: BlockState, level: LevelReader, pos: BlockPos): Boolean {
return if (state.getValue(FACE) == AttachFace.CEILING) {
val direction = getConnectedDirection(state)
canSupportCenter(level, pos.relative(direction.opposite), direction)
} else {
super.canSurvive(state, level, pos)
}
}
override fun updateShape(
state: BlockState,
direction: Direction,
neighbourState: BlockState,
level: LevelAccessor,
pos: BlockPos,
neighbourPos: BlockPos
): BlockState {
if (state.getValue(WATERLOGGED)) {
level.scheduleTick(pos, Fluids.WATER, Fluids.WATER.getTickDelay(level))
}
return if (getConnectedDirection(state).opposite == direction && !state.canSurvive(level, pos)) {
Blocks.AIR.defaultBlockState()
} else {
super.updateShape(state, direction, neighbourState, level, pos, neighbourPos)
}
}
override fun onRemove(
state: BlockState,
level: Level,
pos: BlockPos,
newState: BlockState,
movedByPiston: Boolean
) {
if (state.block == newState.block) {
super.onRemove(state, level, pos, newState, movedByPiston)
return
}
if (!GROUP_REMOVE_GUARD.get()) {
val blockEntity = level.getBlockEntity(pos) as? PrintedPhotoDisplayBlockEntity
if (blockEntity != null) {
if (!level.isClientSide) {
val axes = resolveAxes(state)
GROUP_REMOVE_GUARD.set(true)
try {
val root = blockEntity.rootPos
val size = blockEntity.gridSize.coerceIn(1, 3)
for (x in 0 until size) {
for (y in 0 until size) {
val otherPos = root
.relative(axes.right, x)
.relative(axes.up, y)
if (otherPos == pos) continue
if (level.getBlockState(otherPos).block != this) continue
val otherBe = level.getBlockEntity(otherPos) as? PrintedPhotoDisplayBlockEntity ?: continue
if (otherBe.rootPos != root) continue
level.setBlock(otherPos, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL_IMMEDIATE)
}
}
} finally {
GROUP_REMOVE_GUARD.set(false)
}
}
if (
!level.isClientSide &&
!movedByPiston &&
level.gameRules.getBoolean(GameRules.RULE_DOBLOCKDROPS)
) {
blockEntity.createDroppedStack().takeIf { !it.isEmpty }?.let { stack ->
Containers.dropItemStack(level, pos.x + 0.5, pos.y + 0.5, pos.z + 0.5, stack)
}
}
}
}
super.onRemove(state, level, pos, newState, movedByPiston)
}
}
@@ -0,0 +1,253 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.block.entity
import com.cobblemon.mod.common.CobblemonBlockEntities
import com.cobblemon.mod.common.block.PhotoPrinterBlock
import com.cobblemon.mod.common.block.photoprinter.PhotoPrinterMenu
import net.minecraft.core.BlockPos
import net.minecraft.core.HolderLookup
import net.minecraft.core.NonNullList
import net.minecraft.nbt.CompoundTag
import net.minecraft.network.chat.Component
import net.minecraft.network.protocol.Packet
import net.minecraft.network.protocol.game.ClientGamePacketListener
import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket
import net.minecraft.world.ContainerHelper
import net.minecraft.world.entity.player.Inventory
import net.minecraft.world.entity.player.Player
import net.minecraft.world.inventory.AbstractContainerMenu
import net.minecraft.world.inventory.ContainerData
import net.minecraft.world.item.ItemStack
import net.minecraft.world.level.Level
import net.minecraft.world.level.block.Block
import net.minecraft.world.level.block.entity.BaseContainerBlockEntity
import net.minecraft.world.level.block.state.BlockState
class PhotoPrinterBlockEntity(pos: BlockPos, state: BlockState) : BaseContainerBlockEntity(CobblemonBlockEntities.PHOTO_PRINTER, pos, state) {
companion object {
const val INPUT_SLOT = 0
const val RESULT_SLOT = 1
const val SLOT_COUNT = 2
const val PRINT_DURATION_TICKS = 120
const val POSITION_X_INDEX = 0
const val POSITION_Y_INDEX = 1
const val POSITION_Z_INDEX = 2
const val PRINT_PROGRESS_INDEX = 3
const val PRINT_PROGRESS_MAX_INDEX = 4
const val PRINTING_INDEX = 5
private const val SELECTED_PHOTO_TAG = "selectedPhotoId"
private const val PRINT_PROGRESS_TAG = "printProgress"
private const val PRINT_PROGRESS_MAX_TAG = "printProgressMax"
private const val PRINTING_STACK_TAG = "printingStack"
fun tick(level: Level, pos: BlockPos, state: BlockState, blockEntity: PhotoPrinterBlockEntity) {
if (blockEntity.printingStack.isEmpty) return
if (level.isClientSide) {
if (blockEntity.printProgress < blockEntity.printProgressMax) {
blockEntity.printProgress++
}
return
}
if (blockEntity.printProgress < blockEntity.printProgressMax) {
blockEntity.printProgress++
blockEntity.setChanged()
return
}
if (!blockEntity.getItem(RESULT_SLOT).isEmpty) return
blockEntity.setItem(RESULT_SLOT, blockEntity.printingStack.copy())
blockEntity.printingStack = ItemStack.EMPTY
blockEntity.printProgress = 0
blockEntity.updatePrinterBlockState()
blockEntity.setChanged()
blockEntity.syncToClient()
}
}
private var items: NonNullList<ItemStack> = NonNullList.withSize(SLOT_COUNT, ItemStack.EMPTY)
var selectedPhotoId: String? = null
var printProgress: Int = 0
var printProgressMax: Int = PRINT_DURATION_TICKS
var printingStack: ItemStack = ItemStack.EMPTY
private var openCount: Int = 0
val isPrinting: Boolean
get() = !printingStack.isEmpty
val containerData: ContainerData = object : ContainerData {
override fun getCount(): Int = 6
override fun get(index: Int): Int {
return when (index) {
POSITION_X_INDEX -> blockPos.x
POSITION_Y_INDEX -> blockPos.y
POSITION_Z_INDEX -> blockPos.z
PRINT_PROGRESS_INDEX -> printProgress
PRINT_PROGRESS_MAX_INDEX -> printProgressMax
PRINTING_INDEX -> if (isPrinting) 1 else 0
else -> 0
}
}
override fun set(index: Int, value: Int) {
when (index) {
PRINT_PROGRESS_INDEX -> printProgress = value
PRINT_PROGRESS_MAX_INDEX -> printProgressMax = value
}
}
}
fun startPrinting(stack: ItemStack, durationTicks: Int = PRINT_DURATION_TICKS): Boolean {
if (stack.isEmpty || isPrinting || !getItem(RESULT_SLOT).isEmpty) return false
printingStack = stack.copy()
printProgress = 0
printProgressMax = durationTicks.coerceAtLeast(1)
updatePrinterBlockState()
setChanged()
syncToClient()
return true
}
override fun createMenu(containerId: Int, inventory: Inventory): AbstractContainerMenu {
return PhotoPrinterMenu(containerId, inventory, this, containerData)
}
override fun getDefaultName(): Component = Component.translatable("cobblemon.container.photo_printer")
override fun getDisplayName(): Component = Component.translatable("block.cobblemon.photo_printer")
override fun getItems(): NonNullList<ItemStack> = items
override fun setItems(items: NonNullList<ItemStack>) {
this.items = items
}
override fun getContainerSize(): Int = SLOT_COUNT
override fun canPlaceItem(slot: Int, stack: ItemStack): Boolean = slot == INPUT_SLOT
override fun startOpen(player: Player) {
super.startOpen(player)
if (remove || player.isSpectator) return
openCount++
updatePrinterBlockState()
}
override fun stopOpen(player: Player) {
super.stopOpen(player)
if (remove || player.isSpectator) return
openCount = (openCount - 1).coerceAtLeast(0)
updatePrinterBlockState()
}
override fun setItem(slot: Int, stack: ItemStack) {
super.setItem(slot, stack)
if (slot == RESULT_SLOT) {
updatePrinterBlockState()
syncToClient()
}
}
override fun removeItem(slot: Int, amount: Int): ItemStack {
val stack = super.removeItem(slot, amount)
if (slot == RESULT_SLOT && !stack.isEmpty) {
updatePrinterBlockState()
syncToClient()
}
return stack
}
override fun removeItemNoUpdate(slot: Int): ItemStack {
val stack = super.removeItemNoUpdate(slot)
if (slot == RESULT_SLOT && !stack.isEmpty) {
updatePrinterBlockState()
syncToClient()
}
return stack
}
override fun saveAdditional(compound: CompoundTag, registries: HolderLookup.Provider) {
super.saveAdditional(compound, registries)
ContainerHelper.saveAllItems(compound, items, registries)
selectedPhotoId?.let { compound.putString(SELECTED_PHOTO_TAG, it) }
compound.putInt(PRINT_PROGRESS_TAG, printProgress)
compound.putInt(PRINT_PROGRESS_MAX_TAG, printProgressMax)
if (!printingStack.isEmpty) {
compound.put(PRINTING_STACK_TAG, printingStack.save(registries))
}
}
override fun loadAdditional(compound: CompoundTag, registries: HolderLookup.Provider) {
super.loadAdditional(compound, registries)
items = NonNullList.withSize(SLOT_COUNT, ItemStack.EMPTY)
ContainerHelper.loadAllItems(compound, items, registries)
selectedPhotoId = compound.getString(SELECTED_PHOTO_TAG).takeIf { it.isNotEmpty() }
printProgress = compound.getInt(PRINT_PROGRESS_TAG)
printProgressMax = compound.getInt(PRINT_PROGRESS_MAX_TAG).takeIf { it > 0 } ?: PRINT_DURATION_TICKS
printingStack = if (compound.contains(PRINTING_STACK_TAG)) {
ItemStack.parseOptional(registries, compound.getCompound(PRINTING_STACK_TAG))
} else {
ItemStack.EMPTY
}
}
override fun stillValid(player: Player): Boolean {
return super.stillValid(player)
}
override fun getUpdatePacket(): Packet<ClientGamePacketListener>? {
return ClientboundBlockEntityDataPacket.create(this)
}
override fun getUpdateTag(registries: HolderLookup.Provider): CompoundTag {
return saveWithoutMetadata(registries)
}
private fun syncToClient() {
val currentLevel = level ?: return
currentLevel.sendBlockUpdated(blockPos, blockState, blockState, Block.UPDATE_CLIENTS)
}
private fun updatePrinterBlockState() {
val currentLevel = level ?: return
if (currentLevel.isClientSide) return
val state = blockState
if (state.block !is PhotoPrinterBlock || !state.hasProperty(PhotoPrinterBlock.STATE)) return
val visualState = when {
isPrinting -> PhotoPrinterBlock.PrinterState.PRINTING
openCount > 0 -> PhotoPrinterBlock.PrinterState.ON
else -> PhotoPrinterBlock.PrinterState.OFF
}
if (state.getValue(PhotoPrinterBlock.STATE) != visualState) {
currentLevel.setBlock(blockPos, state.setValue(PhotoPrinterBlock.STATE, visualState), Block.UPDATE_CLIENTS)
}
val topPos = blockPos.above()
val topState = currentLevel.getBlockState(topPos)
if (
topState.block is PhotoPrinterBlock &&
topState.hasProperty(PhotoPrinterBlock.PART) &&
topState.hasProperty(PhotoPrinterBlock.STATE) &&
topState.getValue(PhotoPrinterBlock.PART) == PhotoPrinterBlock.PrinterPart.TOP &&
topState.getValue(PhotoPrinterBlock.STATE) != visualState
) {
currentLevel.setBlock(topPos, topState.setValue(PhotoPrinterBlock.STATE, visualState), Block.UPDATE_CLIENTS)
}
}
}
@@ -0,0 +1,111 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.block.entity
import com.cobblemon.mod.common.CobblemonBlockEntities
import com.cobblemon.mod.common.CobblemonItems
import com.cobblemon.mod.common.item.components.PrintedPhotoComponent
import net.minecraft.core.BlockPos
import net.minecraft.core.HolderLookup
import net.minecraft.core.component.DataComponents
import net.minecraft.nbt.CompoundTag
import net.minecraft.nbt.NbtOps
import net.minecraft.network.chat.Component
import net.minecraft.network.protocol.Packet
import net.minecraft.network.protocol.game.ClientGamePacketListener
import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket
import net.minecraft.world.item.ItemStack
import net.minecraft.world.level.block.Block
import net.minecraft.world.level.block.entity.BlockEntity
import net.minecraft.world.level.block.state.BlockState
class PrintedPhotoDisplayBlockEntity(pos: BlockPos, state: BlockState) :
BlockEntity(CobblemonBlockEntities.PRINTED_PHOTO_DISPLAY, pos, state) {
companion object {
private const val PRINTED_PHOTO_TAG = "printed_photo"
private const val ROOT_POS_TAG = "root_pos"
private const val GRID_SIZE_TAG = "grid_size"
private const val CELL_X_TAG = "cell_x"
private const val CELL_Y_TAG = "cell_y"
}
var component: PrintedPhotoComponent? = null
private set
var rootPos: BlockPos = blockPos
private set
var gridSize: Int = 1
private set
var cellX: Int = 0
private set
var cellY: Int = 0
private set
fun setComponent(component: PrintedPhotoComponent?) {
this.component = component
setChanged()
val currentLevel = level ?: return
currentLevel.sendBlockUpdated(blockPos, blockState, blockState, Block.UPDATE_CLIENTS)
}
fun setTileData(rootPos: BlockPos, gridSize: Int, cellX: Int, cellY: Int) {
this.rootPos = rootPos
this.gridSize = gridSize.coerceIn(1, 3)
this.cellX = cellX
this.cellY = cellY
setChanged()
val currentLevel = level ?: return
currentLevel.sendBlockUpdated(blockPos, blockState, blockState, Block.UPDATE_CLIENTS)
}
fun createDroppedStack(): ItemStack {
val value = component
if (value == null) return ItemStack.EMPTY
val stack = ItemStack(CobblemonItems.PRINTED_PHOTO)
PrintedPhotoComponent.set(stack, value)
stack.set(DataComponents.CUSTOM_NAME, Component.translatable("item.cobblemon.printed_photo.named", value.index))
return stack
}
override fun saveAdditional(compound: CompoundTag, registries: HolderLookup.Provider) {
super.saveAdditional(compound, registries)
component?.let { value ->
PrintedPhotoComponent.CODEC.encodeStart(NbtOps.INSTANCE, value).result().ifPresent {
compound.put(PRINTED_PHOTO_TAG, it)
}
}
compound.putLong(ROOT_POS_TAG, rootPos.asLong())
compound.putInt(GRID_SIZE_TAG, gridSize)
compound.putInt(CELL_X_TAG, cellX)
compound.putInt(CELL_Y_TAG, cellY)
}
override fun loadAdditional(compound: CompoundTag, registries: HolderLookup.Provider) {
super.loadAdditional(compound, registries)
component = compound.get(PRINTED_PHOTO_TAG)?.let { tag ->
PrintedPhotoComponent.CODEC.parse(NbtOps.INSTANCE, tag).result().orElse(null)
}
rootPos = if (compound.contains(ROOT_POS_TAG)) {
BlockPos.of(compound.getLong(ROOT_POS_TAG))
} else {
blockPos
}
gridSize = compound.getInt(GRID_SIZE_TAG).coerceIn(1, 3).let { if (it == 0) 1 else it }
cellX = compound.getInt(CELL_X_TAG).coerceIn(0, 2)
cellY = compound.getInt(CELL_Y_TAG).coerceIn(0, 2)
}
override fun getUpdatePacket(): Packet<ClientGamePacketListener>? {
return ClientboundBlockEntityDataPacket.create(this)
}
override fun getUpdateTag(registries: HolderLookup.Provider): CompoundTag {
return saveWithoutMetadata(registries)
}
}
@@ -0,0 +1,150 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.block.photoprinter
import com.cobblemon.mod.common.CobblemonMenuType
import com.cobblemon.mod.common.block.entity.PhotoPrinterBlockEntity
import net.minecraft.core.BlockPos
import net.minecraft.world.Container
import net.minecraft.world.SimpleContainer
import net.minecraft.world.entity.player.Inventory
import net.minecraft.world.entity.player.Player
import net.minecraft.world.inventory.AbstractContainerMenu
import net.minecraft.world.inventory.ContainerData
import net.minecraft.world.inventory.MenuType
import net.minecraft.world.inventory.SimpleContainerData
import net.minecraft.world.inventory.Slot
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.Items
class PhotoPrinterMenu(menuType: MenuType<*>, syncId: Int) : AbstractContainerMenu(menuType, syncId) {
companion object {
const val INPUT_SLOT_X = 24
const val INPUT_SLOT_Y = 157
const val OUTPUT_SLOT_X = 184
const val OUTPUT_SLOT_Y = 157
const val PLAYER_INVENTORY_Y = 188
const val HOTBAR_Y = 246
}
var playerInventory: Inventory? = null
var inventory: Container? = null
var containerData: ContainerData? = null
var blockEntity: PhotoPrinterBlockEntity? = null
constructor(syncId: Int, playerInventory: Inventory) : this(CobblemonMenuType.PHOTO_PRINTER, syncId) {
this.playerInventory = playerInventory
this.inventory = SimpleContainer(PhotoPrinterBlockEntity.SLOT_COUNT)
this.containerData = SimpleContainerData(6)
this.blockEntity = inventory as? PhotoPrinterBlockEntity
setupSlots(playerInventory)
}
constructor(syncId: Int, playerInventory: Inventory, inventory: Container, containerData: ContainerData) : this(CobblemonMenuType.PHOTO_PRINTER, syncId) {
this.playerInventory = playerInventory
this.inventory = inventory
this.containerData = containerData
this.blockEntity = inventory as? PhotoPrinterBlockEntity
setupSlots(playerInventory)
}
private fun setupSlots(playerInventory: Inventory) {
val inventory = inventory ?: return
val data = containerData ?: return
addDataSlots(data)
inventory.startOpen(playerInventory.player)
addSlot(InputSlot(inventory, PhotoPrinterBlockEntity.INPUT_SLOT, INPUT_SLOT_X, INPUT_SLOT_Y))
addSlot(OutputSlot(inventory, PhotoPrinterBlockEntity.RESULT_SLOT, OUTPUT_SLOT_X, OUTPUT_SLOT_Y))
for (row in 0..2) {
for (column in 0..8) {
addSlot(Slot(playerInventory, 9 + row * 9 + column, 8 + column * 18, PLAYER_INVENTORY_Y + row * 18))
}
}
for (column in 0..8) {
addSlot(Slot(playerInventory, column, 8 + column * 18, HOTBAR_Y))
}
}
fun blockPos(): BlockPos {
val data = containerData ?: return BlockPos.ZERO
return BlockPos(
data.get(PhotoPrinterBlockEntity.POSITION_X_INDEX),
data.get(PhotoPrinterBlockEntity.POSITION_Y_INDEX),
data.get(PhotoPrinterBlockEntity.POSITION_Z_INDEX)
)
}
fun printProgress(): Int = containerData?.get(PhotoPrinterBlockEntity.PRINT_PROGRESS_INDEX) ?: 0
fun printProgressMax(): Int = containerData?.get(PhotoPrinterBlockEntity.PRINT_PROGRESS_MAX_INDEX) ?: PhotoPrinterBlockEntity.PRINT_DURATION_TICKS
fun isPrinting(): Boolean = containerData?.get(PhotoPrinterBlockEntity.PRINTING_INDEX) == 1
fun printProgressFraction(): Float {
val max = printProgressMax().coerceAtLeast(1)
return (printProgress().toFloat() / max.toFloat()).coerceIn(0F, 1F)
}
override fun quickMoveStack(player: Player, index: Int): ItemStack {
val slot = slots.getOrNull(index) ?: return ItemStack.EMPTY
if (!slot.hasItem()) return ItemStack.EMPTY
val sourceStack = slot.item
val sourceCopy = sourceStack.copy()
val machineSlotsEnd = PhotoPrinterBlockEntity.SLOT_COUNT
if (index < machineSlotsEnd) {
if (!moveItemStackTo(sourceStack, machineSlotsEnd, slots.size, true)) {
return ItemStack.EMPTY
}
} else {
if (sourceStack.`is`(Items.PAPER)) {
if (!moveItemStackTo(sourceStack, PhotoPrinterBlockEntity.INPUT_SLOT, PhotoPrinterBlockEntity.INPUT_SLOT + 1, false)) {
return ItemStack.EMPTY
}
} else if (index < machineSlotsEnd + 27) {
if (!moveItemStackTo(sourceStack, machineSlotsEnd + 27, slots.size, false)) {
return ItemStack.EMPTY
}
} else if (!moveItemStackTo(sourceStack, machineSlotsEnd, machineSlotsEnd + 27, false)) {
return ItemStack.EMPTY
}
}
if (sourceStack.isEmpty) {
slot.setByPlayer(ItemStack.EMPTY)
} else {
slot.setChanged()
}
return sourceCopy
}
override fun stillValid(player: Player): Boolean {
return inventory?.stillValid(player) ?: false
}
override fun removed(player: Player) {
super.removed(player)
inventory?.stopOpen(player)
}
class InputSlot(container: Container, slot: Int, x: Int, y: Int) : Slot(container, slot, x, y) {
override fun mayPlace(stack: ItemStack): Boolean = stack.`is`(Items.PAPER)
}
class OutputSlot(container: Container, slot: Int, x: Int, y: Int) : Slot(container, slot, x, y) {
override fun mayPlace(stack: ItemStack): Boolean = false
}
}
@@ -9,6 +9,7 @@
package com.cobblemon.mod.common.client
import com.cobblemon.mod.common.Cobblemon.LOGGER
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.CobblemonBlockEntities
import com.cobblemon.mod.common.CobblemonBlocks
import com.cobblemon.mod.common.CobblemonClientImplementation
@@ -19,6 +20,7 @@ import com.cobblemon.mod.common.api.berry.Berries
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
import com.cobblemon.mod.common.api.storage.player.client.ClientScannedBlocksManager
import com.cobblemon.mod.common.api.storage.player.client.ClientTMMoveManager
import com.cobblemon.mod.common.api.tags.CobblemonItemTags
import com.cobblemon.mod.common.block.entity.TintBlockEntity
@@ -27,10 +29,15 @@ import com.cobblemon.mod.common.client.gui.PartyOverlay
import com.cobblemon.mod.common.client.gui.PartyOverlayDataControl
import com.cobblemon.mod.common.client.gui.RideControlsOverlay
import com.cobblemon.mod.common.client.gui.battle.BattleOverlay
import com.cobblemon.mod.common.client.gui.pokedex.page.PokedexPages
import com.cobblemon.mod.common.client.gui.cookingpot.CookingPotScreen
import com.cobblemon.mod.common.client.gui.photoprinter.PhotoPrinterScreen
import com.cobblemon.mod.common.client.gui.tmmachine.TMMachineScreen
import com.cobblemon.mod.common.client.gui.party.PartyTutorialToasts
import com.cobblemon.mod.common.client.particle.BedrockParticleOptionsRepository
import com.cobblemon.mod.common.client.pokedex.photo.PokedexPhotoAnimationTypes
import com.cobblemon.mod.common.client.pokedex.photo.PokedexPhotoClientStore
import com.cobblemon.mod.common.client.pokedex.photo.PrintedPhotoAssetClientStore
import com.cobblemon.mod.common.client.render.ClientPlayerIcon
import com.cobblemon.mod.common.client.render.DeferredRenderer
import com.cobblemon.mod.common.client.render.block.BerryBlockRenderer
@@ -40,7 +47,9 @@ import com.cobblemon.mod.common.client.render.block.FossilAnalyzerRenderer
import com.cobblemon.mod.common.client.render.block.GildedChestBlockRenderer
import com.cobblemon.mod.common.client.render.block.HealingMachineRenderer
import com.cobblemon.mod.common.client.render.block.LecternBlockEntityRenderer
import com.cobblemon.mod.common.client.render.block.PhotoPrinterBlockEntityRenderer
import com.cobblemon.mod.common.client.render.block.PokeSnackBlockEntityRenderer
import com.cobblemon.mod.common.client.render.block.PrintedPhotoDisplayBlockEntityRenderer
import com.cobblemon.mod.common.client.render.block.RestorationTankRenderer
import com.cobblemon.mod.common.client.render.block.TMShelfBlockEntityRenderer
import com.cobblemon.mod.common.client.render.boat.CobblemonBoatRenderer
@@ -54,6 +63,7 @@ import com.cobblemon.mod.common.client.render.entity.PokeBobberEntityRenderer
import com.cobblemon.mod.common.client.render.generic.GenericBedrockRenderer
import com.cobblemon.mod.common.client.render.item.CobblemonBuiltinItemRendererRegistry
import com.cobblemon.mod.common.client.render.item.PokemonItemRenderer
import com.cobblemon.mod.common.client.render.item.PrintedPhotoItemRenderer
import com.cobblemon.mod.common.client.render.layer.PokemonOnShoulderRenderer
import com.cobblemon.mod.common.client.render.models.blockbench.bedrock.animation.BedrockAnimationRepository
import com.cobblemon.mod.common.client.render.models.blockbench.repository.BerryModelRepository
@@ -109,6 +119,7 @@ object CobblemonClient {
var clientPlayerData = ClientGeneralPlayerData()
var clientPokedexData = ClientPokedexManager(mutableMapOf())
var clientTMMoveData = ClientTMMoveManager(mutableSetOf())
var clientScannedBlocksData = ClientScannedBlocksManager(mutableSetOf())
/** If true then we won't bother them anymore about choosing a starter even if it's a thing they can do. */
var checkedStarterScreen = false
@@ -118,6 +129,8 @@ object CobblemonClient {
val overlay: PartyOverlay by lazy { PartyOverlay() }
val battleOverlay: BattleOverlay by lazy { BattleOverlay() }
val pokedexUsageContext: PokedexUsageContext by lazy { PokedexUsageContext() }
val pokedexPhotoStore: PokedexPhotoClientStore by lazy { PokedexPhotoClientStore() }
val printedPhotoAssetStore: PrintedPhotoAssetClientStore by lazy { PrintedPhotoAssetClientStore() }
val rideControlsOverlay: RideControlsOverlay by lazy { RideControlsOverlay() }
fun onLogin() {
@@ -126,7 +139,10 @@ object CobblemonClient {
teamData = ClientPlayerTeamData()
clientPokedexData = ClientPokedexManager(mutableMapOf())
clientTMMoveData = ClientTMMoveManager(mutableSetOf())
clientScannedBlocksData = ClientScannedBlocksManager(mutableSetOf())
storage.onLogin()
pokedexPhotoStore.clear()
printedPhotoAssetStore.clear()
// CobblemonDataProvider.canReload = false
}
@@ -141,11 +157,15 @@ object CobblemonClient {
// CobblemonDataProvider.canReload = true
DeferredRenderer.clearAll()
ClientPlayerIcon.clear()
pokedexPhotoStore.clear()
printedPhotoAssetStore.clear()
}
fun initialize(implementation: CobblemonClientImplementation) {
LOGGER.info("Initializing Cobblemon client")
this.implementation = implementation
Cobblemon.dataProvider.register(PokedexPages, reloadable = true)
Cobblemon.dataProvider.register(PokedexPhotoAnimationTypes, reloadable = true)
PlatformEvents.CLIENT_PLAYER_LOGIN.subscribe { onLogin() }
PlatformEvents.CLIENT_PLAYER_LOGOUT.subscribe { onLogout() }
@@ -164,6 +184,7 @@ object CobblemonClient {
LOGGER.info("Registering custom BuiltinItemRenderers")
CobblemonBuiltinItemRendererRegistry.register(CobblemonItems.POKEMON_MODEL, PokemonItemRenderer())
CobblemonBuiltinItemRendererRegistry.register(CobblemonItems.PRINTED_PHOTO, PrintedPhotoItemRenderer())
PlatformEvents.CLIENT_ITEM_TOOLTIP.subscribe { event ->
val stack = event.stack
@@ -344,6 +365,7 @@ object CobblemonClient {
CobblemonBlocks.LECTERN,
CobblemonBlocks.CAMPFIRE,
CobblemonBlocks.SOUL_CAMPFIRE,
CobblemonBlocks.PRINTED_PHOTO_DISPLAY,
CobblemonBlocks.BLACK_CAMPFIRE_POT,
CobblemonBlocks.BLUE_CAMPFIRE_POT,
CobblemonBlocks.GREEN_CAMPFIRE_POT,
@@ -392,7 +414,8 @@ object CobblemonClient {
CobblemonBlocks.X_SP_ATK,
CobblemonBlocks.X_SP_DEF,
CobblemonBlocks.X_SPEED,
CobblemonBlocks.TM_MACHINE
CobblemonBlocks.TM_MACHINE,
CobblemonBlocks.PHOTO_PRINTER
)
this.createBoatModelLayers()
@@ -421,6 +444,7 @@ object CobblemonClient {
private fun registerMenuScreens() {
MenuScreens.register(CobblemonMenuType.COOKING_POT, ::CookingPotScreen)
MenuScreens.register(CobblemonMenuType.TM_MACHINE, ::TMMachineScreen)
MenuScreens.register(CobblemonMenuType.PHOTO_PRINTER, ::PhotoPrinterScreen)
}
private fun registerBlockEntityRenderers() {
@@ -445,6 +469,11 @@ object CobblemonClient {
this.implementation.registerBlockEntityRenderer(CobblemonBlockEntities.CAMPFIRE, ::CampfireBlockEntityRenderer)
this.implementation.registerBlockEntityRenderer(CobblemonBlockEntities.POKE_SNACK, ::PokeSnackBlockEntityRenderer)
this.implementation.registerBlockEntityRenderer(CobblemonBlockEntities.TM_SHELF, ::TMShelfBlockEntityRenderer)
this.implementation.registerBlockEntityRenderer(CobblemonBlockEntities.PHOTO_PRINTER, ::PhotoPrinterBlockEntityRenderer)
this.implementation.registerBlockEntityRenderer(
CobblemonBlockEntities.PRINTED_PHOTO_DISPLAY,
::PrintedPhotoDisplayBlockEntityRenderer
)
}
private fun registerEntityRenderers() {
@@ -0,0 +1,445 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of 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.photoprinter
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.block.entity.PhotoPrinterBlockEntity
import com.cobblemon.mod.common.block.photoprinter.PhotoPrinterMenu
import com.cobblemon.mod.common.client.CobblemonClient
import com.cobblemon.mod.common.client.gui.pokedex.widgets.SearchWidget
import com.cobblemon.mod.common.client.pokedex.photo.PokedexPhotoSearch
import com.cobblemon.mod.common.net.messages.client.ui.CraftPrintedPhotoPacket
import com.cobblemon.mod.common.net.messages.client.ui.SetPhotoPrinterSelectionPacket
import com.cobblemon.mod.common.pokedex.photo.PokedexPhotoMetadata
import com.cobblemon.mod.common.pokedex.photo.PokedexPhotoRatings
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.components.Button
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen
import net.minecraft.network.chat.Component
import net.minecraft.world.entity.player.Inventory
import net.minecraft.world.item.Items
class PhotoPrinterScreen(menu: PhotoPrinterMenu, inventory: Inventory, title: Component) : AbstractContainerScreen<PhotoPrinterMenu>(menu, inventory, title) {
private enum class PrintSizeOption(val blocks: Int, val paperCost: Int, val label: String) {
ONE(1, 1, "1x1"),
TWO(2, 4, "2x2"),
THREE(3, 9, "3x3")
}
companion object {
private const val BACKGROUND_WIDTH = 312
private const val BACKGROUND_HEIGHT = 270
private const val GRID_COLUMNS = 3
private const val GRID_ROWS = 3
private const val BUFFER_ROWS = 1
private const val THUMB_SIZE = 36
private const val THUMB_GAP = 4
private const val GRID_LEFT = 8
private const val SEARCH_TOP = 8
private const val SEARCH_WIDTH = GRID_COLUMNS * THUMB_SIZE + (GRID_COLUMNS - 1) * THUMB_GAP
private const val SEARCH_HEIGHT = 17
private const val SORT_FIELD_LEFT = GRID_LEFT + SEARCH_WIDTH + 4
private const val SORT_FIELD_WIDTH = 70
private const val SORT_DIRECTION_LEFT = SORT_FIELD_LEFT + SORT_FIELD_WIDTH + 4
private const val SORT_DIRECTION_WIDTH = 45
private const val GRID_TOP = SEARCH_TOP + SEARCH_HEIGHT + 8
private const val GRID_WIDTH = SEARCH_WIDTH
private const val GRID_HEIGHT = GRID_ROWS * THUMB_SIZE + (GRID_ROWS - 1) * THUMB_GAP
private const val PREVIEW_LEFT = GRID_LEFT + GRID_WIDTH + 20
private const val PREVIEW_TOP = GRID_TOP
private const val PREVIEW_SIZE = 96
private const val SIZE_BUTTON_X = PREVIEW_LEFT + PREVIEW_SIZE + 8
private const val SIZE_BUTTON_WIDTH = 48
private const val SIZE_BUTTON_HEIGHT = 20
private const val SIZE_BUTTON_GAP = 4
private const val INFO_LEFT = PREVIEW_LEFT
private const val INFO_TOP = PREVIEW_TOP + PREVIEW_SIZE + 6
private const val CONTROL_ROW_Y = GRID_TOP + GRID_HEIGHT + 8
private const val OUTPUT_SLOT_X = PhotoPrinterMenu.OUTPUT_SLOT_X
private const val OUTPUT_SLOT_Y = PhotoPrinterMenu.OUTPUT_SLOT_Y
private const val INPUT_SLOT_X = PhotoPrinterMenu.INPUT_SLOT_X
private const val INPUT_SLOT_Y = PhotoPrinterMenu.INPUT_SLOT_Y
private const val PRINT_BUTTON_X = 46
private const val PRINT_BUTTON_Y = CONTROL_ROW_Y - 1
private const val PRINT_BUTTON_WIDTH = 124
private const val PRINT_BUTTON_HEIGHT = 20
private const val PROGRESS_BAR_X = PRINT_BUTTON_X
private const val PROGRESS_BAR_Y = PRINT_BUTTON_Y - 8
private const val PROGRESS_BAR_WIDTH = PRINT_BUTTON_WIDTH
private const val PROGRESS_BAR_HEIGHT = 5
private const val MAX_THUMBS = GRID_COLUMNS * GRID_ROWS
}
private var selectedPhotoId: String? = null
private var scrollOffsetRows = 0
private var searchQuery = ""
private var filteredPhotoCount = 0
private var searchWidget: SearchWidget? = null
private var printButton: Button? = null
private var sortFieldButton: Button? = null
private var sortDirectionButton: Button? = null
private val sizeButtons = mutableMapOf<PrintSizeOption, Button>()
private var selectedPrintSize = PrintSizeOption.ONE
private var sortField = PokedexPhotoSearch.SortField.POINTS
private var sortDirection = PokedexPhotoSearch.SortDirection.DESCENDING
override fun init() {
imageWidth = BACKGROUND_WIDTH
imageHeight = BACKGROUND_HEIGHT
super.init()
scrollOffsetRows = 0
searchWidget = SearchWidget(
posX = leftPos + GRID_LEFT,
posY = topPos + SEARCH_TOP,
width = SEARCH_WIDTH,
height = SEARCH_HEIGHT,
clipOverlayToField = true,
update = {}
).also { widget ->
widget.setMaxLength(128)
widget.value = searchQuery
widget.setResponder { text ->
searchQuery = text
scrollOffsetRows = 0
}
addRenderableWidget(widget)
}
sortFieldButton = addRenderableWidget(
Button.builder(Component.literal("")) {
sortField = sortField.next()
scrollOffsetRows = 0
updateSortButtons()
}.pos(leftPos + SORT_FIELD_LEFT, topPos + SEARCH_TOP).size(SORT_FIELD_WIDTH, SEARCH_HEIGHT).build()
)
sortDirectionButton = addRenderableWidget(
Button.builder(Component.literal("")) {
sortDirection = sortDirection.toggled()
scrollOffsetRows = 0
updateSortButtons()
}.pos(leftPos + SORT_DIRECTION_LEFT, topPos + SEARCH_TOP).size(SORT_DIRECTION_WIDTH, SEARCH_HEIGHT).build()
)
updateSortButtons()
printButton = addRenderableWidget(
Button.builder(Component.literal("Print 1x1 (1 paper)")) {
CraftPrintedPhotoPacket(menu.blockPos(), selectedPhotoId, selectedPrintSize.blocks).sendToServer()
}.pos(leftPos + PRINT_BUTTON_X, topPos + PRINT_BUTTON_Y).size(PRINT_BUTTON_WIDTH, PRINT_BUTTON_HEIGHT).build()
)
sizeButtons.clear()
PrintSizeOption.entries.forEachIndexed { index, option ->
val button = addRenderableWidget(
Button.builder(Component.literal(option.label)) {
selectedPrintSize = option
updateSizeButtons()
}.pos(
leftPos + SIZE_BUTTON_X,
topPos + PREVIEW_TOP + index * (SIZE_BUTTON_HEIGHT + SIZE_BUTTON_GAP)
).size(SIZE_BUTTON_WIDTH, SIZE_BUTTON_HEIGHT).build()
)
sizeButtons[option] = button
}
updateSizeButtons()
CobblemonClient.pokedexPhotoStore.requestGallery()
}
override fun containerTick() {
super.containerTick()
searchQuery = searchWidget?.value ?: searchQuery
val allPhotos = CobblemonClient.pokedexPhotoStore.getPhotos()
val filteredPhotos = filteredPhotos(allPhotos)
filteredPhotoCount = filteredPhotos.size
val previousSelection = selectedPhotoId
selectedPhotoId = when {
filteredPhotos.isEmpty() -> null
selectedPhotoId == null -> filteredPhotos.first().id
filteredPhotos.none { it.id == selectedPhotoId } -> filteredPhotos.first().id
else -> selectedPhotoId
}
if (selectedPhotoId != previousSelection) {
SetPhotoPrinterSelectionPacket(menu.blockPos(), selectedPhotoId).sendToServer()
}
val inputStack = menu.getSlot(PhotoPrinterBlockEntity.INPUT_SLOT).item
val outputStack = menu.getSlot(PhotoPrinterBlockEntity.RESULT_SLOT).item
val paperCount = if (inputStack.`is`(Items.PAPER)) inputStack.count else 0
val canAfford = paperCount >= selectedPrintSize.paperCost
val isPrinting = menu.isPrinting()
printButton?.active = selectedPhotoId != null && outputStack.isEmpty && canAfford && !isPrinting
printButton?.message = if (isPrinting) {
Component.translatable("cobblemon.ui.photo_printer.printing")
} else {
Component.literal("Print ${selectedPrintSize.label} (${selectedPrintSize.paperCost} paper)")
}
}
override fun removed() {
searchWidget = null
CobblemonClient.pokedexPhotoStore.retain(emptySet())
super.removed()
}
override fun render(guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) {
renderBackground(guiGraphics, mouseX, mouseY, partialTick)
super.render(guiGraphics, mouseX, mouseY, partialTick)
renderTooltip(guiGraphics, mouseX, mouseY)
renderRatingTooltip(guiGraphics, mouseX, mouseY)
}
override fun renderBg(guiGraphics: GuiGraphics, partialTick: Float, mouseX: Int, mouseY: Int) {
val x0 = leftPos
val y0 = topPos
guiGraphics.fill(x0, y0, x0 + imageWidth, y0 + imageHeight, -12566464)
guiGraphics.fill(x0 + 1, y0 + 1, x0 + imageWidth - 1, y0 + imageHeight - 1, -4144960)
guiGraphics.fill(x0 + 2, y0 + 2, x0 + imageWidth - 2, y0 + imageHeight - 2, -6250336)
guiGraphics.fill(
leftPos + GRID_LEFT - 2,
topPos + GRID_TOP - 2,
leftPos + GRID_LEFT + GRID_WIDTH + 2,
topPos + GRID_TOP + GRID_HEIGHT + 2,
-12698050
)
guiGraphics.fill(
leftPos + GRID_LEFT - 1,
topPos + GRID_TOP - 1,
leftPos + GRID_LEFT + GRID_WIDTH + 1,
topPos + GRID_TOP + GRID_HEIGHT + 1,
-3552823
)
guiGraphics.fill(
leftPos + GRID_LEFT,
topPos + GRID_TOP,
leftPos + GRID_LEFT + GRID_WIDTH,
topPos + GRID_TOP + GRID_HEIGHT,
-986896
)
drawSlotOutline(guiGraphics, leftPos + INPUT_SLOT_X, topPos + INPUT_SLOT_Y)
drawSlotOutline(guiGraphics, leftPos + OUTPUT_SLOT_X, topPos + OUTPUT_SLOT_Y)
renderProgressBar(guiGraphics)
drawPanel(
guiGraphics = guiGraphics,
x = leftPos + PREVIEW_LEFT,
y = topPos + PREVIEW_TOP,
width = PREVIEW_SIZE,
height = PREVIEW_SIZE
)
val gridStartX = leftPos + GRID_LEFT
val gridStartY = topPos + GRID_TOP
val allPhotos = CobblemonClient.pokedexPhotoStore.getPhotos()
val photos = filteredPhotos(allPhotos)
filteredPhotoCount = photos.size
val totalRows = (photos.size + GRID_COLUMNS - 1) / GRID_COLUMNS
val maxOffset = maxOf(0, totalRows - GRID_ROWS)
scrollOffsetRows = scrollOffsetRows.coerceIn(0, maxOffset)
val visibleStart = scrollOffsetRows * GRID_COLUMNS
val visibleEnd = minOf(photos.size, visibleStart + MAX_THUMBS)
val visiblePhotos = photos.subList(visibleStart, visibleEnd)
val bufferStartRow = maxOf(0, scrollOffsetRows - BUFFER_ROWS)
val bufferEndRow = minOf(totalRows, scrollOffsetRows + GRID_ROWS + BUFFER_ROWS)
val bufferStartIndex = bufferStartRow * GRID_COLUMNS
val bufferEndIndex = minOf(photos.size, bufferEndRow * GRID_COLUMNS)
val retainedIds = photos.subList(bufferStartIndex, bufferEndIndex).map { it.id }.toMutableSet()
selectedPhotoId?.let { retainedIds.add(it) }
CobblemonClient.pokedexPhotoStore.prefetch(retainedIds)
CobblemonClient.pokedexPhotoStore.retain(retainedIds)
visiblePhotos.forEachIndexed { index, photo ->
val col = index % GRID_COLUMNS
val row = index / GRID_COLUMNS
val thumbX = gridStartX + col * (THUMB_SIZE + THUMB_GAP)
val thumbY = gridStartY + row * (THUMB_SIZE + THUMB_GAP)
val selected = photo.id == selectedPhotoId
val borderColor = if (selected) -8704 else -12698050
guiGraphics.fill(thumbX - 1, thumbY - 1, thumbX + THUMB_SIZE + 1, thumbY + THUMB_SIZE + 1, borderColor)
val texture = CobblemonClient.pokedexPhotoStore.getTexture(photo.id)
if (texture != null) {
blitk(guiGraphics.pose(), texture, thumbX, thumbY, THUMB_SIZE, THUMB_SIZE)
} else {
guiGraphics.fill(thumbX, thumbY, thumbX + THUMB_SIZE, thumbY + THUMB_SIZE, -12040120)
}
}
if (photos.isEmpty()) {
guiGraphics.drawString(
font,
Component.translatable("cobblemon.ui.photo_printer.no_match"),
leftPos + GRID_LEFT + 12,
topPos + GRID_TOP + (GRID_HEIGHT / 2) - 4,
0x3F3F3F,
false
)
}
renderSelectedPreview(guiGraphics)
}
override fun renderLabels(guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int) {
guiGraphics.drawString(font, title, 8, 6, 4210752, false)
guiGraphics.drawString(font, playerInventoryTitle, 8, imageHeight - 94, 4210752, false)
val total = CobblemonClient.pokedexPhotoStore.getPhotos().size
val maxAllowed = CobblemonClient.pokedexPhotoStore.maxPhotosAllowed
guiGraphics.drawString(font, Component.translatable("cobblemon.ui.photo_printer.count", total, maxAllowed), INFO_LEFT, INFO_TOP, 4210752, false)
guiGraphics.drawString(font, Component.translatable("cobblemon.ui.photo_printer.filtered", filteredPhotoCount, total), INFO_LEFT, INFO_TOP + 11, 4210752, false)
guiGraphics.drawString(font, Component.literal("Paper needed: ${selectedPrintSize.paperCost}"), INFO_LEFT, INFO_TOP + 22, 4210752, false)
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (hoveredSlot != null && super.mouseClicked(mouseX, mouseY, button)) {
return true
}
val filteredPhotos = filteredPhotos(CobblemonClient.pokedexPhotoStore.getPhotos())
val visibleStart = scrollOffsetRows * GRID_COLUMNS
val visibleEnd = minOf(filteredPhotos.size, visibleStart + MAX_THUMBS)
val visiblePhotos = filteredPhotos.subList(visibleStart, visibleEnd)
val selected = photoAt(mouseX, mouseY, visiblePhotos)
if (selected != null) {
selectedPhotoId = selected.id
SetPhotoPrinterSelectionPacket(menu.blockPos(), selected.id).sendToServer()
return true
}
return super.mouseClicked(mouseX, mouseY, button)
}
override fun mouseScrolled(mouseX: Double, mouseY: Double, deltaX: Double, deltaY: Double): Boolean {
val gridLeft = leftPos + GRID_LEFT
val gridTop = topPos + GRID_TOP
val gridRight = gridLeft + GRID_COLUMNS * THUMB_SIZE + (GRID_COLUMNS - 1) * THUMB_GAP
val gridBottom = gridTop + GRID_ROWS * THUMB_SIZE + (GRID_ROWS - 1) * THUMB_GAP
if (mouseX < gridLeft || mouseX >= gridRight || mouseY < gridTop || mouseY >= gridBottom) {
return super.mouseScrolled(mouseX, mouseY, deltaX, deltaY)
}
val step = if (deltaY > 0) -1 else if (deltaY < 0) 1 else 0
if (step == 0) return true
val totalPhotos = filteredPhotos(CobblemonClient.pokedexPhotoStore.getPhotos()).size
val totalRows = (totalPhotos + GRID_COLUMNS - 1) / GRID_COLUMNS
val maxOffset = maxOf(0, totalRows - GRID_ROWS)
val newOffset = (scrollOffsetRows + step).coerceIn(0, maxOffset)
if (newOffset == scrollOffsetRows) return true
scrollOffsetRows = newOffset
return true
}
private fun filteredPhotos(photos: List<PokedexPhotoMetadata>): List<PokedexPhotoMetadata> {
return PokedexPhotoSearch.filterAndSort(photos, searchQuery, sortField, sortDirection)
}
private fun drawSlotOutline(guiGraphics: GuiGraphics, x: Int, y: Int) {
guiGraphics.fill(x - 1, y - 1, x + 17, y + 17, -12698050)
guiGraphics.fill(x, y, x + 16, y + 16, -3552823)
guiGraphics.fill(x + 1, y + 1, x + 16, y + 16, -986896)
}
private fun drawPanel(guiGraphics: GuiGraphics, x: Int, y: Int, width: Int, height: Int) {
guiGraphics.fill(x - 2, y - 2, x + width + 2, y + height + 2, -12698050)
guiGraphics.fill(x - 1, y - 1, x + width + 1, y + height + 1, -3552823)
guiGraphics.fill(x, y, x + width, y + height, -986896)
}
private fun renderSelectedPreview(guiGraphics: GuiGraphics) {
val selectedPhoto = selectedPhoto() ?: return
val texture = CobblemonClient.pokedexPhotoStore.getTexture(selectedPhoto.id) ?: return
blitk(
matrixStack = guiGraphics.pose(),
texture = texture,
x = leftPos + PREVIEW_LEFT,
y = topPos + PREVIEW_TOP,
width = PREVIEW_SIZE,
height = PREVIEW_SIZE
)
guiGraphics.drawCenteredString(
font,
Component.literal(PokedexPhotoRatings.ratingText(selectedPhoto.qualityScore)),
leftPos + PREVIEW_LEFT + (PREVIEW_SIZE / 2),
topPos + PREVIEW_TOP + PREVIEW_SIZE - 12,
0xFFFFFF
)
}
private fun renderProgressBar(guiGraphics: GuiGraphics) {
val x = leftPos + PROGRESS_BAR_X
val y = topPos + PROGRESS_BAR_Y
val width = PROGRESS_BAR_WIDTH
val height = PROGRESS_BAR_HEIGHT
guiGraphics.fill(x, y, x + width, y + height, -12698050)
guiGraphics.fill(x + 1, y + 1, x + width - 1, y + height - 1, -986896)
if (!menu.isPrinting()) return
val fillWidth = ((width - 2) * menu.printProgressFraction()).toInt().coerceIn(0, width - 2)
guiGraphics.fill(x + 1, y + 1, x + 1 + fillWidth, y + height - 1, -8704)
}
private fun photoAt(mouseX: Double, mouseY: Double, visiblePhotos: List<PokedexPhotoMetadata>): PokedexPhotoMetadata? {
val gridStartX = leftPos + GRID_LEFT
val gridStartY = topPos + GRID_TOP
visiblePhotos.forEachIndexed { index, photo ->
val col = index % GRID_COLUMNS
val row = index / GRID_COLUMNS
val thumbX = gridStartX + col * (THUMB_SIZE + THUMB_GAP)
val thumbY = gridStartY + row * (THUMB_SIZE + THUMB_GAP)
if (mouseX >= thumbX && mouseX < thumbX + THUMB_SIZE && mouseY >= thumbY && mouseY < thumbY + THUMB_SIZE) {
return photo
}
}
return null
}
private fun updateSizeButtons() {
sizeButtons.forEach { (option, button) ->
val selected = option == selectedPrintSize
button.active = !selected
button.message = Component.literal(if (selected) "[${option.label}]" else option.label)
}
}
private fun updateSortButtons() {
sortFieldButton?.message = Component.literal("Sort: ${PokedexPhotoSearch.sortFieldLabel(sortField)}")
sortDirectionButton?.message = Component.literal(PokedexPhotoSearch.sortDirectionLabel(sortDirection))
}
private fun selectedPhoto(): PokedexPhotoMetadata? {
val selected = selectedPhotoId ?: return null
return CobblemonClient.pokedexPhotoStore.getPhotos().firstOrNull { it.id == selected }
}
private fun renderRatingTooltip(guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int) {
val selectedPhoto = selectedPhoto() ?: return
val starsX = leftPos + PREVIEW_LEFT + 10
val starsY = topPos + PREVIEW_TOP + PREVIEW_SIZE - 12
val starsWidth = PREVIEW_SIZE - 20
val starsHeight = 10
if (mouseX < starsX || mouseX >= starsX + starsWidth || mouseY < starsY || mouseY >= starsY + starsHeight) return
guiGraphics.renderComponentTooltip(font, PokedexPhotoRatings.scoreBreakdown(selectedPhoto), mouseX, mouseY)
}
}
@@ -33,6 +33,9 @@ import com.cobblemon.mod.common.client.ClientMoLangFunctions.setupClient
import com.cobblemon.mod.common.client.CobblemonClient
import com.cobblemon.mod.common.client.CobblemonResources
import com.cobblemon.mod.common.client.gui.CobblemonRenderable
import com.cobblemon.mod.common.client.gui.pokedex.page.MainDexPokedexPage
import com.cobblemon.mod.common.client.gui.pokedex.page.PokedexGuiPage
import com.cobblemon.mod.common.client.gui.pokedex.page.PokedexPages
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.BASE_HEIGHT
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.BASE_WIDTH
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.HEADER_BAR_HEIGHT
@@ -81,6 +84,8 @@ class PokedexGUI private constructor(
private val globeIcon = cobblemonResource("textures/gui/pokedex/globe_icon.png")
private val caughtSeenIcon = cobblemonResource("textures/gui/pokedex/caught_seen_icon.png")
private val pageArrowLeftIcon = cobblemonResource("textures/gui/pokedex/forms_arrow_left.png")
private val pageArrowRightIcon = cobblemonResource("textures/gui/pokedex/forms_arrow_right.png")
private val arrowUpIcon = cobblemonResource("textures/gui/pokedex/arrow_up.png")
private val arrowDownIcon = cobblemonResource("textures/gui/pokedex/arrow_down.png")
@@ -96,6 +101,10 @@ class PokedexGUI private constructor(
cobblemonResource("textures/gui/pokedex/tab_moves.png")
)
private const val PAGE_ARROW_WIDTH = 10
private const val PAGE_ARROW_HEIGHT = 16
private const val PAGE_SELECTOR_GAP = 7
/**
* Attempts to open this screen for a client.
*/
@@ -140,9 +149,13 @@ class PokedexGUI private constructor(
private var selectedSearchByType: SearchByType = SearchByType.SPECIES
private var selectedSecondaryFilter: SecondaryPokedexFilterType = SecondaryPokedexFilterType.ALL
private val tabButtons: MutableList<ScaledButton> = mutableListOf()
private var selectedPageIndex = 0
private var pages: List<PokedexGuiPage> = emptyList()
lateinit var tabInfoElement: GuiEventListener
var tabInfoIndex = TAB_DESCRIPTION
private val currentPage: PokedexGuiPage?
get() = pages.getOrNull(selectedPageIndex)
override fun renderBlurredBackground(delta: Float) {}
override fun renderMenuBackground(context: GuiGraphics) {}
@@ -150,6 +163,7 @@ class PokedexGUI private constructor(
public override fun init() {
super.init()
clearWidgets()
selectedPageIndex = 0
availableRegions = Dexes.dexEntryMap.keys.toList()
selectedRegionIndex = 0
@@ -233,6 +247,11 @@ class PokedexGUI private constructor(
addRenderableWidget(secondaryFilterButton)
updateFilters(true)
pages = PokedexPages.resolvePages()
if (pages.isNotEmpty()) {
selectedPageIndex = selectedPageIndex.coerceIn(0, pages.lastIndex)
currentPage?.onSelected(this)
}
}
override fun render(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
@@ -259,6 +278,13 @@ class PokedexGUI private constructor(
height = BASE_HEIGHT
)
renderPageNavigation(context, x, y, mouseX, mouseY)
val page = currentPage
if (page != null && page !is MainDexPokedexPage) {
page.render(this, context, x, y, mouseX, mouseY, delta)
return
}
// Region
blitk(
matrixStack = matrices,
@@ -371,11 +397,18 @@ class PokedexGUI private constructor(
override fun onClose() {
if (blockPos != null) AdjustBlockEntityViewerCountPacket(blockPos, false).sendToServer()
pages.forEach { it.onClose(this) }
playSound(CobblemonSounds.POKEDEX_CLOSE)
super.onClose()
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
if (handlePageNavigationClick(mouseX, mouseY)) return true
val page = currentPage
if (page != null && page !is MainDexPokedexPage) {
return page.mouseClicked(this, mouseX, mouseY, button)
}
val canDisplayEntry = true //selectedForm?.unlockForms
if (::pokemonInfoWidget.isInitialized
@@ -396,12 +429,20 @@ class PokedexGUI private constructor(
}
override fun mouseReleased(pMouseX: Double, pMouseY: Double, pButton: Int): Boolean {
val page = currentPage
if (page != null && page !is MainDexPokedexPage) {
return page.mouseReleased(this, pMouseX, pMouseY, pButton)
}
if (canDragRender) canDragRender = false
if (isDragging) isDragging = false
return super.mouseReleased(pMouseX, pMouseY, pButton)
}
override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
val page = currentPage
if (page != null && page !is MainDexPokedexPage) {
return page.mouseDragged(this, mouseX, mouseY, button, deltaX, deltaY)
}
if (isDragging && canDragRender) {
val dragOffsetY = (oldDragPosX - mouseX).toFloat()
pokemonInfoWidget.rotationY = (((pokemonInfoWidget.rotationY + dragOffsetY) % 360 + 360) % 360)
@@ -410,7 +451,20 @@ class PokedexGUI private constructor(
return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)
}
override fun mouseScrolled(mouseX: Double, mouseY: Double, deltaX: Double, deltaY: Double): Boolean {
val page = currentPage
if (page != null && page !is MainDexPokedexPage && page.mouseScrolled(this, mouseX, mouseY, deltaX, deltaY)) {
return true
}
return super.mouseScrolled(mouseX, mouseY, deltaX, deltaY)
}
override fun tick() {
val page = currentPage
if (page != null && page !is MainDexPokedexPage) {
page.tick(this)
return
}
if (::pokemonInfoWidget.isInitialized && !pokemonInfoWidget.isSuppressed) pokemonInfoWidget.tick()
}
@@ -682,6 +736,97 @@ class PokedexGUI private constructor(
updateTabInfoElement()
}
private fun renderPageNavigation(context: GuiGraphics, x: Int, y: Int, mouseX: Int, mouseY: Int) {
val centerX = x + (BASE_WIDTH / 2)
val title = lang(currentPage?.titleKey ?: "ui.pokedex.page.pokedex").bold()
drawScaledText(
context = context,
font = CobblemonResources.DEFAULT_LARGE,
text = title,
x = centerX,
y = y + 14,
shadow = true,
centered = true
)
if (pages.size <= 1) return
val leftX = centerX - PAGE_SELECTOR_GAP - PAGE_ARROW_WIDTH
val rightX = centerX + PAGE_SELECTOR_GAP
val arrowY = y + 13
val leftHovered = isInsidePageArrow(mouseX, mouseY, leftX, arrowY)
val rightHovered = isInsidePageArrow(mouseX, mouseY, rightX, arrowY)
blitk(
matrixStack = context.pose(),
texture = pageArrowLeftIcon,
x = leftX,
y = arrowY,
width = PAGE_ARROW_WIDTH,
height = PAGE_ARROW_HEIGHT,
vOffset = if (leftHovered) PAGE_ARROW_HEIGHT else 0,
textureHeight = PAGE_ARROW_HEIGHT * 2
)
blitk(
matrixStack = context.pose(),
texture = pageArrowRightIcon,
x = rightX,
y = arrowY,
width = PAGE_ARROW_WIDTH,
height = PAGE_ARROW_HEIGHT,
vOffset = if (rightHovered) PAGE_ARROW_HEIGHT else 0,
textureHeight = PAGE_ARROW_HEIGHT * 2
)
}
private fun handlePageNavigationClick(mouseX: Double, mouseY: Double): Boolean {
if (pages.size <= 1) return false
val x = (width - BASE_WIDTH) / 2
val y = (height - BASE_HEIGHT) / 2
val centerX = x + (BASE_WIDTH / 2)
val leftX = centerX - PAGE_SELECTOR_GAP - PAGE_ARROW_WIDTH
val rightX = centerX + PAGE_SELECTOR_GAP
val arrowY = y + 13
if (isInsidePageArrow(mouseX, mouseY, leftX, arrowY)) {
cyclePage(next = false)
return true
}
if (isInsidePageArrow(mouseX, mouseY, rightX, arrowY)) {
cyclePage(next = true)
return true
}
return false
}
private fun cyclePage(next: Boolean) {
if (pages.size <= 1) return
val oldPage = currentPage
selectedPageIndex = if (next) {
(selectedPageIndex + 1) % pages.size
} else {
(selectedPageIndex - 1 + pages.size) % pages.size
}
canDragRender = false
isDragging = false
setFocused(null)
oldPage?.onClose(this)
currentPage?.onSelected(this)
playSound(CobblemonSounds.POKEDEX_CLICK_SHORT)
}
private fun isInsidePageArrow(mouseX: Number, mouseY: Number, x: Int, y: Int): Boolean {
val mX = mouseX.toDouble()
val mY = mouseY.toDouble()
return mX >= x && mX <= (x + PAGE_ARROW_WIDTH) && mY >= y && mY <= (y + PAGE_ARROW_HEIGHT)
}
fun canSelectTab(tabIndex: Int): Boolean {
val selectedForm = this.selectedForm ?: return false
val selectedEntry = this.selectedEntry ?: return false
@@ -689,6 +834,18 @@ class PokedexGUI private constructor(
return encounteredForm && (tabIndex != tabInfoIndex)
}
fun <T> addPageWidget(widget: T): T where T : GuiEventListener, T : Renderable, T : NarratableEntry {
return addRenderableWidget(widget)
}
fun removePageWidget(widget: GuiEventListener) {
removeWidget(widget)
}
fun focusPageWidget(widget: GuiEventListener?) {
setFocused(widget)
}
override fun isPauseScreen(): Boolean = false
override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
@@ -697,9 +854,22 @@ class PokedexGUI private constructor(
return true
}
val page = currentPage
if (page != null && page !is MainDexPokedexPage && page.keyPressed(this, keyCode, scanCode, modifiers)) {
return true
}
return super.keyPressed(keyCode, scanCode, modifiers)
}
override fun charTyped(codePoint: Char, modifiers: Int): Boolean {
val page = currentPage
if (page != null && page !is MainDexPokedexPage && page.charTyped(this, codePoint, modifiers)) {
return true
}
return super.charTyped(codePoint, modifiers)
}
fun playSound(soundEvent: SoundEvent) {
Minecraft.getInstance().soundManager.play(SimpleSoundInstance.forUI(soundEvent, 1.0F))
}
@@ -0,0 +1,16 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.pokedex.page
import net.minecraft.resources.ResourceLocation
class MainDexPokedexPage(
override val id: ResourceLocation,
override val titleKey: String
) : PokedexGuiPage
@@ -0,0 +1,397 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of 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.pokedex.page
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.text.bold
import com.cobblemon.mod.common.client.CobblemonClient
import com.cobblemon.mod.common.client.CobblemonResources
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUI
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.BASE_HEIGHT
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.BASE_WIDTH
import com.cobblemon.mod.common.client.gui.pokedex.widgets.SearchWidget
import com.cobblemon.mod.common.client.pokedex.photo.PokedexPhotoSearch
import com.cobblemon.mod.common.client.render.drawScaledText
import com.cobblemon.mod.common.net.messages.client.ui.DeletePokedexPhotoPacket
import com.cobblemon.mod.common.pokedex.photo.PokedexPhotoMetadata
import com.cobblemon.mod.common.pokedex.photo.PokedexPhotoRatings
import com.cobblemon.mod.common.util.lang
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.resources.ResourceLocation
import kotlin.math.max
import kotlin.math.min
class PhotoGalleryPokedexPage(
override val id: ResourceLocation,
override val titleKey: String
) : PokedexGuiPage {
companion object {
private const val GRID_COLUMNS = 4
private const val VISIBLE_ROWS = 3
private const val BUFFER_ROWS = 1
private const val THUMB_WIDTH = 36
private const val THUMB_HEIGHT = 36
private const val THUMB_GAP = 4
private const val GRID_OFFSET_X = 28
private const val GRID_OFFSET_Y = 49
private const val SEARCH_OFFSET_X = 26
private const val SEARCH_OFFSET_Y = 28
private const val SEARCH_WIDTH = 120
private const val SEARCH_HEIGHT = 17
private const val PREVIEW_OFFSET_X = 218
private const val PREVIEW_OFFSET_Y = 43
private const val PREVIEW_SIZE = 112
private const val SORT_FIELD_X = 28
private const val SORT_FIELD_Y = 157
private const val SORT_FIELD_WIDTH = 70
private const val SORT_DIRECTION_X = 102
private const val SORT_DIRECTION_Y = 157
private const val SORT_DIRECTION_WIDTH = 40
private const val SORT_BUTTON_HEIGHT = 14
}
private var scrollOffsetRows = 0
private var selectedPhotoId: String? = null
private var searchQuery = ""
private var searchWidget: SearchWidget? = null
private var sortField = PokedexPhotoSearch.SortField.POINTS
private var sortDirection = PokedexPhotoSearch.SortDirection.DESCENDING
override fun onSelected(gui: PokedexGUI) {
scrollOffsetRows = 0
selectedPhotoId = null
val x = (gui.width - BASE_WIDTH) / 2
val y = (gui.height - BASE_HEIGHT) / 2
searchWidget?.let { gui.removePageWidget(it) }
searchWidget = SearchWidget(
posX = x + SEARCH_OFFSET_X,
posY = y + SEARCH_OFFSET_Y,
width = SEARCH_WIDTH,
height = SEARCH_HEIGHT,
update = {}
).also { widget ->
widget.setMaxLength(128)
widget.value = searchQuery
widget.setResponder { text ->
searchQuery = text
scrollOffsetRows = 0
}
gui.addPageWidget(widget)
}
CobblemonClient.pokedexPhotoStore.requestGallery()
}
override fun onClose(gui: PokedexGUI) {
searchWidget?.let { gui.removePageWidget(it) }
searchWidget = null
CobblemonClient.pokedexPhotoStore.retain(emptySet())
}
override fun render(gui: PokedexGUI, context: GuiGraphics, x: Int, y: Int, mouseX: Int, mouseY: Int, delta: Float) {
val photoStore = CobblemonClient.pokedexPhotoStore
searchQuery = searchWidget?.value ?: searchQuery
searchWidget?.render(context, mouseX, mouseY, delta)
val allPhotos = photoStore.getPhotos()
val photos = PokedexPhotoSearch.filterAndSort(allPhotos, searchQuery, sortField, sortDirection)
val countText = lang("ui.pokedex.gallery.filtered_count", photos.size, allPhotos.size).bold()
drawScaledText(
context = context,
font = CobblemonResources.DEFAULT_LARGE,
text = countText,
x = x + 173,
y = y + 31,
shadow = true,
centered = true
)
if (allPhotos.isEmpty()) {
selectedPhotoId = null
photoStore.retain(emptySet())
drawScaledText(
context = context,
font = CobblemonResources.DEFAULT_LARGE,
text = lang("ui.pokedex.gallery.empty").bold(),
x = x + 173,
y = y + 92,
shadow = true,
centered = true
)
drawScaledText(
context = context,
font = CobblemonResources.DEFAULT_LARGE,
text = lang("ui.pokedex.gallery.empty_hint"),
x = x + 173,
y = y + 103,
shadow = true,
centered = true
)
return
}
if (photos.isEmpty()) {
selectedPhotoId = null
photoStore.retain(emptySet())
drawScaledText(
context = context,
font = CobblemonResources.DEFAULT_LARGE,
text = lang("ui.pokedex.gallery.no_match").bold(),
x = x + 173,
y = y + 97,
shadow = true,
centered = true
)
return
}
if (selectedPhotoId == null || photos.none { it.id == selectedPhotoId }) {
selectedPhotoId = photos.first().id
}
val totalRows = (photos.size + GRID_COLUMNS - 1) / GRID_COLUMNS
val maxOffset = max(0, totalRows - VISIBLE_ROWS)
scrollOffsetRows = scrollOffsetRows.coerceIn(0, maxOffset)
val bufferStartRow = max(0, scrollOffsetRows - BUFFER_ROWS)
val bufferEndRow = min(totalRows, scrollOffsetRows + VISIBLE_ROWS + BUFFER_ROWS)
val bufferStartIndex = bufferStartRow * GRID_COLUMNS
val bufferEndIndex = min(photos.size, bufferEndRow * GRID_COLUMNS)
val bufferIds = photos.subList(bufferStartIndex, bufferEndIndex).map { it.id }.toMutableList()
selectedPhotoId?.let { selectedId ->
if (!bufferIds.contains(selectedId)) {
bufferIds.add(selectedId)
}
}
photoStore.prefetch(bufferIds)
photoStore.retain(bufferIds.toSet())
val startX = x + GRID_OFFSET_X
val startY = y + GRID_OFFSET_Y
val visibleStart = scrollOffsetRows * GRID_COLUMNS
val visibleEnd = min(photos.size, (scrollOffsetRows + VISIBLE_ROWS) * GRID_COLUMNS)
for (index in visibleStart until visibleEnd) {
val photo = photos[index]
val localIndex = index - visibleStart
val col = localIndex % GRID_COLUMNS
val row = localIndex / GRID_COLUMNS
val thumbX = startX + (col * (THUMB_WIDTH + THUMB_GAP))
val thumbY = startY + (row * (THUMB_HEIGHT + THUMB_GAP))
val selected = photo.id == selectedPhotoId
if (selected) {
context.fill(thumbX - 2, thumbY - 2, thumbX + THUMB_WIDTH + 2, thumbY + THUMB_HEIGHT + 2, -1)
}
val texture = photoStore.getTexture(photo.id)
if (texture != null) {
blitk(
matrixStack = context.pose(),
texture = texture,
x = thumbX,
y = thumbY,
width = THUMB_WIDTH,
height = THUMB_HEIGHT
)
}
}
selectedPhotoId?.let { selectedId ->
val selectedPhoto = photos.find { it.id == selectedId }
val texture = photoStore.getTexture(selectedId)
if (texture != null) {
val previewX = x + PREVIEW_OFFSET_X
val previewY = y + PREVIEW_OFFSET_Y
context.fill(previewX - 2, previewY - 2, previewX + PREVIEW_SIZE + 2, previewY + PREVIEW_SIZE + 2, -1)
blitk(
matrixStack = context.pose(),
texture = texture,
x = previewX,
y = previewY,
width = PREVIEW_SIZE,
height = PREVIEW_SIZE
)
}
if (selectedPhoto != null) {
val starsX = x + PREVIEW_OFFSET_X + (PREVIEW_SIZE / 2)
val starsY = y + PREVIEW_OFFSET_Y + PREVIEW_SIZE + 18
drawScaledText(
context = context,
font = CobblemonResources.DEFAULT_LARGE,
text = lang("ui.pokedex.gallery.quality_score", selectedPhoto.qualityScore),
x = x + PREVIEW_OFFSET_X + (PREVIEW_SIZE / 2),
y = y + PREVIEW_OFFSET_Y + PREVIEW_SIZE + 8,
shadow = true,
centered = true
)
drawScaledText(
context = context,
font = CobblemonResources.DEFAULT_LARGE,
text = lang("ui.pokedex.gallery.rating", PokedexPhotoRatings.ratingText(selectedPhoto.qualityScore)),
x = starsX,
y = starsY,
shadow = true,
centered = true
)
renderRatingTooltip(context, selectedPhoto, starsX - 38, starsY, 76, 10, mouseX, mouseY)
}
}
drawSortButton(
context = context,
x = x + SORT_FIELD_X,
y = y + SORT_FIELD_Y,
width = SORT_FIELD_WIDTH,
text = "Sort: ${PokedexPhotoSearch.sortFieldLabel(sortField)}"
)
drawSortButton(
context = context,
x = x + SORT_DIRECTION_X,
y = y + SORT_DIRECTION_Y,
width = SORT_DIRECTION_WIDTH,
text = PokedexPhotoSearch.sortDirectionLabel(sortDirection)
)
drawScaledText(
context = context,
font = CobblemonResources.DEFAULT_LARGE,
text = lang("ui.pokedex.gallery.filter_hint"),
x = x + 173,
y = y + 168,
shadow = true,
centered = true
)
drawScaledText(
context = context,
font = CobblemonResources.DEFAULT_LARGE,
text = lang("ui.pokedex.gallery.select_hint"),
x = x + 173,
y = y + 178,
shadow = true,
centered = true
)
}
override fun mouseClicked(gui: PokedexGUI, mouseX: Double, mouseY: Double, button: Int): Boolean {
searchWidget?.let { widget ->
if (widget.mouseClicked(mouseX, mouseY, button)) {
gui.focusPageWidget(widget)
return true
}
}
if (button != 0 && button != 1) return false
val x = (gui.width - BASE_WIDTH) / 2
val y = (gui.height - BASE_HEIGHT) / 2
if (isInside(mouseX, mouseY, x + SORT_FIELD_X, y + SORT_FIELD_Y, SORT_FIELD_WIDTH, SORT_BUTTON_HEIGHT)) {
sortField = sortField.next()
scrollOffsetRows = 0
return true
}
if (isInside(mouseX, mouseY, x + SORT_DIRECTION_X, y + SORT_DIRECTION_Y, SORT_DIRECTION_WIDTH, SORT_BUTTON_HEIGHT)) {
sortDirection = sortDirection.toggled()
scrollOffsetRows = 0
return true
}
val photos = PokedexPhotoSearch.filterAndSort(CobblemonClient.pokedexPhotoStore.getPhotos(), searchQuery, sortField, sortDirection)
if (photos.isEmpty()) return false
val visibleStart = scrollOffsetRows * GRID_COLUMNS
val visibleEnd = min(photos.size, (scrollOffsetRows + VISIBLE_ROWS) * GRID_COLUMNS)
val visiblePhotos = photos.subList(visibleStart, visibleEnd)
val photo = photoAt(mouseX, mouseY, x, y, visiblePhotos) ?: return false
if (button == 1) {
DeletePokedexPhotoPacket(photo.id).sendToServer()
if (selectedPhotoId == photo.id) {
selectedPhotoId = null
}
} else {
selectedPhotoId = photo.id
}
return true
}
override fun keyPressed(gui: PokedexGUI, keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
return searchWidget?.keyPressed(keyCode, scanCode, modifiers) == true
}
override fun charTyped(gui: PokedexGUI, codePoint: Char, modifiers: Int): Boolean {
return searchWidget?.charTyped(codePoint, modifiers) == true
}
override fun mouseScrolled(gui: PokedexGUI, mouseX: Double, mouseY: Double, deltaX: Double, deltaY: Double): Boolean {
val x = (gui.width - BASE_WIDTH) / 2
val y = (gui.height - BASE_HEIGHT) / 2
val gridLeft = x + GRID_OFFSET_X
val gridTop = y + GRID_OFFSET_Y
val gridRight = gridLeft + GRID_COLUMNS * THUMB_WIDTH + (GRID_COLUMNS - 1) * THUMB_GAP
val gridBottom = gridTop + VISIBLE_ROWS * THUMB_HEIGHT + (VISIBLE_ROWS - 1) * THUMB_GAP
if (mouseX < gridLeft || mouseX >= gridRight || mouseY < gridTop || mouseY >= gridBottom) return false
val step = if (deltaY > 0) -1 else if (deltaY < 0) 1 else 0
if (step == 0) return false
val photos = PokedexPhotoSearch.filterAndSort(CobblemonClient.pokedexPhotoStore.getPhotos(), searchQuery, sortField, sortDirection)
val totalRows = (photos.size + GRID_COLUMNS - 1) / GRID_COLUMNS
val maxOffset = max(0, totalRows - VISIBLE_ROWS)
scrollOffsetRows = (scrollOffsetRows + step).coerceIn(0, maxOffset)
return true
}
private fun drawSortButton(context: GuiGraphics, x: Int, y: Int, width: Int, text: String) {
context.fill(x, y, x + width, y + SORT_BUTTON_HEIGHT, -12698050)
context.fill(x + 1, y + 1, x + width - 1, y + SORT_BUTTON_HEIGHT - 1, -4144960)
context.drawString(Minecraft.getInstance().font, text, x + 4, y + 4, 0xFFFFFF, false)
}
private fun renderRatingTooltip(
context: GuiGraphics,
photo: PokedexPhotoMetadata,
x: Int,
y: Int,
width: Int,
height: Int,
mouseX: Int,
mouseY: Int
) {
if (!isInside(mouseX.toDouble(), mouseY.toDouble(), x, y, width, height)) return
context.renderComponentTooltip(Minecraft.getInstance().font, PokedexPhotoRatings.scoreBreakdown(photo), mouseX, mouseY)
}
private fun isInside(mouseX: Double, mouseY: Double, x: Int, y: Int, width: Int, height: Int): Boolean {
return mouseX >= x && mouseX < x + width && mouseY >= y && mouseY < y + height
}
private fun photoAt(
mouseX: Double,
mouseY: Double,
x: Int,
y: Int,
visiblePhotos: List<PokedexPhotoMetadata>
): PokedexPhotoMetadata? {
val startX = x + GRID_OFFSET_X
val startY = y + GRID_OFFSET_Y
visiblePhotos.forEachIndexed { index, photo ->
val col = index % GRID_COLUMNS
val row = index / GRID_COLUMNS
val thumbX = startX + (col * (THUMB_WIDTH + THUMB_GAP))
val thumbY = startY + (row * (THUMB_HEIGHT + THUMB_GAP))
if (mouseX >= thumbX && mouseX < thumbX + THUMB_WIDTH && mouseY >= thumbY && mouseY < thumbY + THUMB_HEIGHT) {
return photo
}
}
return null
}
}
@@ -0,0 +1,30 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.pokedex.page
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUI
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.resources.ResourceLocation
interface PokedexGuiPage {
val id: ResourceLocation
val titleKey: String
fun onSelected(gui: PokedexGUI) {}
fun onClose(gui: PokedexGUI) {}
fun render(gui: PokedexGUI, context: GuiGraphics, x: Int, y: Int, mouseX: Int, mouseY: Int, delta: Float) {}
fun mouseClicked(gui: PokedexGUI, mouseX: Double, mouseY: Double, button: Int): Boolean = false
fun mouseReleased(gui: PokedexGUI, mouseX: Double, mouseY: Double, button: Int): Boolean = false
fun mouseDragged(gui: PokedexGUI, mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean = false
fun mouseScrolled(gui: PokedexGUI, mouseX: Double, mouseY: Double, deltaX: Double, deltaY: Double): Boolean = false
fun keyPressed(gui: PokedexGUI, keyCode: Int, scanCode: Int, modifiers: Int): Boolean = false
fun charTyped(gui: PokedexGUI, codePoint: Char, modifiers: Int): Boolean = false
fun tick(gui: PokedexGUI) {}
}
@@ -0,0 +1,16 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.gui.pokedex.page
data class PokedexPageDefinition(
val pageType: String = "",
val titleKey: String = "ui.pokedex.page.pokedex",
val order: Int = 0,
val enabled: Boolean = true
)
@@ -0,0 +1,96 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of 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.pokedex.page
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.data.JsonDataRegistry
import com.cobblemon.mod.common.api.reactive.SimpleObservable
import com.cobblemon.mod.common.util.cobblemonResource
import com.google.gson.GsonBuilder
import com.google.gson.reflect.TypeToken
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import net.minecraft.server.packs.PackType
object PokedexPages : JsonDataRegistry<PokedexPageDefinition> {
override val id = cobblemonResource("pokedex_pages")
override val type = PackType.CLIENT_RESOURCES
override val observable = SimpleObservable<PokedexPages>()
override val gson = GsonBuilder()
.disableHtmlEscaping()
.setPrettyPrinting()
.create()
override val typeToken: TypeToken<PokedexPageDefinition> = TypeToken.get(PokedexPageDefinition::class.java)
override val resourcePath = "pokedex/pages"
private val definitions = linkedMapOf<ResourceLocation, PokedexPageDefinition>()
private val pageFactories = mutableMapOf<ResourceLocation, (ResourceLocation, PokedexPageDefinition) -> PokedexGuiPage>()
init {
registerPageType(cobblemonResource("main_dex")) { pageId, def -> MainDexPokedexPage(pageId, def.titleKey) }
registerPageType(cobblemonResource("photo_gallery")) { pageId, def -> PhotoGalleryPokedexPage(pageId, def.titleKey) }
}
override fun sync(player: ServerPlayer) {}
override fun reload(data: Map<ResourceLocation, PokedexPageDefinition>) {
definitions.clear()
data.forEach { (identifier, page) ->
if (page.enabled) {
definitions[identifier] = page
}
}
observable.emit(this)
}
fun registerPageType(
typeId: ResourceLocation,
factory: (ResourceLocation, PokedexPageDefinition) -> PokedexGuiPage
) {
pageFactories[typeId] = factory
}
fun resolvePages(): List<PokedexGuiPage> {
val pages = definitions
.entries
.sortedWith(compareBy<Map.Entry<ResourceLocation, PokedexPageDefinition>> { it.value.order }.thenBy { it.key.toString() })
.mapNotNull { (id, definition) ->
val typeId = parseType(definition) ?: return@mapNotNull null
val factory = pageFactories[typeId]
if (factory == null) {
Cobblemon.LOGGER.warn("Skipped Pokedex page {} - unknown page type {}", id, definition.pageType)
null
} else {
factory(id, definition)
}
}
.toMutableList()
if (pages.isEmpty()) {
pages.add(MainDexPokedexPage(cobblemonResource("fallback_main_dex"), "ui.pokedex.page.pokedex"))
pages.add(PhotoGalleryPokedexPage(cobblemonResource("fallback_photo_gallery"), "ui.pokedex.page.photo_gallery"))
return pages
}
if (pages.none { it is MainDexPokedexPage }) {
pages.add(0, MainDexPokedexPage(cobblemonResource("fallback_main_dex"), "ui.pokedex.page.pokedex"))
}
return pages
}
private fun parseType(definition: PokedexPageDefinition): ResourceLocation? {
if (definition.pageType.isBlank()) return null
return runCatching { ResourceLocation.parse(definition.pageType) }
.onFailure {
Cobblemon.LOGGER.warn("Skipped Pokedex page definition - invalid pageType {}", definition.pageType)
}
.getOrNull()
}
}
@@ -33,6 +33,7 @@ class SearchWidget(
width: Int,
height: Int,
text: Component = "Search".text(),
private val clipOverlayToField: Boolean = false,
val update: () -> (Unit)
): EditBox(Minecraft.getInstance().font, posX.toInt(), posY.toInt(), width, height, text), CobblemonRenderable {
@@ -62,48 +63,57 @@ class SearchWidget(
override fun renderWidget(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) {
val matrices = context.pose()
blitk(
matrixStack = matrices,
texture = backgroundOverlay,
x = posX, y = posY,
width = HALF_OVERLAY_WIDTH,
height = HALF_OVERLAY_HEIGHT
)
blitk(
matrixStack = matrices,
texture = searchIcon,
x = (posX.toInt() + 3) / SCALE,
y = (posY.toInt() + 2) / SCALE,
width = 14,
height = 14,
scale = SCALE
)
val showCursor = isFocused && ((Util.getMillis() - this.focusedTime) / 300L % 2L == 0L)
val input = if (isFocused) "${value}${if ((cursorPosition == value.length) && showCursor) "_" else ""}".text()
else (if(value.isEmpty()) lang("ui.pokedex.search") else value.text())
val startX = posX.toInt() + 13
val startY = posY.toInt() + 1
drawScaledText(
context = context,
font = CobblemonResources.DEFAULT_LARGE,
text = input.bold(),
x = startX,
y = startY,
shadow = true
)
if (showCursor && !value.isEmpty() && cursorPosition != value.length) {
val startToCursorWidth = Minecraft.getInstance().font.width((input.getString(cursorPosition).text().bold()).font(CobblemonResources.DEFAULT_LARGE))
context.fill(
RenderType.guiTextHighlight(),
startX + startToCursorWidth - 1,
startY,
startX + startToCursorWidth,
startY + 9,
-3092272
if (clipOverlayToField) {
context.enableScissor(x, y, x + width, y + height)
}
try {
blitk(
matrixStack = matrices,
texture = backgroundOverlay,
x = posX, y = posY,
width = HALF_OVERLAY_WIDTH,
height = HALF_OVERLAY_HEIGHT
)
blitk(
matrixStack = matrices,
texture = searchIcon,
x = (posX.toInt() + 3) / SCALE,
y = (posY.toInt() + 2) / SCALE,
width = 14,
height = 14,
scale = SCALE
)
val showCursor = isFocused && ((Util.getMillis() - this.focusedTime) / 300L % 2L == 0L)
val input = if (isFocused) "${value}${if ((cursorPosition == value.length) && showCursor) "_" else ""}".text()
else (if(value.isEmpty()) lang("ui.pokedex.search") else value.text())
val startX = posX.toInt() + 13
val startY = posY.toInt() + 1
drawScaledText(
context = context,
font = CobblemonResources.DEFAULT_LARGE,
text = input.bold(),
x = startX,
y = startY,
shadow = true
)
if (showCursor && !value.isEmpty() && cursorPosition != value.length) {
val startToCursorWidth = Minecraft.getInstance().font.width((input.getString(cursorPosition).text().bold()).font(CobblemonResources.DEFAULT_LARGE))
context.fill(
RenderType.guiTextHighlight(),
startX + startToCursorWidth - 1,
startY,
startX + startToCursorWidth,
startY + 9,
-3092272
)
}
} finally {
if (clipOverlayToField) {
context.disableScissor()
}
}
}
}
@@ -0,0 +1,93 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.net.gui
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.net.ServerNetworkPacketHandler
import com.cobblemon.mod.common.block.entity.PhotoPrinterBlockEntity
import com.cobblemon.mod.common.block.photoprinter.PhotoPrinterMenu
import com.cobblemon.mod.common.net.messages.client.ui.CraftPrintedPhotoPacket
import com.cobblemon.mod.common.net.messages.client.ui.PokedexPhotoUploadResultPacket
import com.cobblemon.mod.common.pokedex.photo.PokedexPhotoRepository
import com.cobblemon.mod.common.pokedex.photo.PokedexPrintedPhotoFactory
import net.minecraft.server.MinecraftServer
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.item.Items
object CraftPrintedPhotoPacketHandler : ServerNetworkPacketHandler<CraftPrintedPhotoPacket> {
override fun handle(packet: CraftPrintedPhotoPacket, server: MinecraftServer, player: ServerPlayer) {
val printSize = packet.printSize.coerceIn(1, 3)
val paperCost = printSize * printSize
val menu = player.containerMenu as? PhotoPrinterMenu ?: return
if (menu.blockPos() != packet.blockPos) return
val blockEntity = player.level().getBlockEntity(packet.blockPos) as? PhotoPrinterBlockEntity ?: return
val selectedId = (packet.photoId ?: blockEntity.selectedPhotoId)
if (selectedId.isNullOrEmpty()) {
PokedexPhotoUploadResultPacket(false, "No photo selected to print.").sendToPlayer(player)
return
}
if (blockEntity.isPrinting) {
PokedexPhotoUploadResultPacket(false, "The photo printer is already printing.").sendToPlayer(player)
return
}
val metadata = PokedexPhotoRepository.getMetadata(player.uuid, selectedId)
val imageData = PokedexPhotoRepository.getPhoto(player.uuid, selectedId)
if (metadata == null || imageData == null) {
PokedexPhotoUploadResultPacket(false, "That photo no longer exists on the server.").sendToPlayer(player)
return
}
val input = blockEntity.getItem(PhotoPrinterBlockEntity.INPUT_SLOT)
if (!input.`is`(Items.PAPER)) {
PokedexPhotoUploadResultPacket(false, "Photo printing requires paper.").sendToPlayer(player)
return
}
if (input.count < paperCost) {
PokedexPhotoUploadResultPacket(false, "Need $paperCost paper for a ${printSize}x${printSize} print.").sendToPlayer(player)
return
}
if (!blockEntity.getItem(PhotoPrinterBlockEntity.RESULT_SLOT).isEmpty) {
PokedexPhotoUploadResultPacket(false, "Take the printed photo from the output slot first.").sendToPlayer(player)
return
}
val printedStack = PokedexPrintedPhotoFactory.createPrintedStack(
level = player.serverLevel(),
metadata = metadata,
imageData = imageData,
printSize = printSize
)
if (printedStack == null) {
PokedexPhotoUploadResultPacket(false, "Failed to build printed photo data.").sendToPlayer(player)
return
}
input.shrink(paperCost)
if (!blockEntity.startPrinting(printedStack)) {
input.grow(paperCost)
PokedexPhotoUploadResultPacket(false, "The photo printer could not start printing.").sendToPlayer(player)
return
}
menu.broadcastChanges()
if (Cobblemon.config.pokedexDeletePhotoAfterPrint) {
PokedexPhotoRepository.delete(player.uuid, selectedId)
blockEntity.selectedPhotoId = null
PokedexPhotoNetworkUtil.sendGallery(player)
}
PokedexPhotoUploadResultPacket(true, "Printing photo...").sendToPlayer(player)
}
}
@@ -0,0 +1,28 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.net.gui
import com.cobblemon.mod.common.api.net.ServerNetworkPacketHandler
import com.cobblemon.mod.common.net.messages.client.ui.DeletePokedexPhotoPacket
import com.cobblemon.mod.common.net.messages.client.ui.PokedexPhotoUploadResultPacket
import com.cobblemon.mod.common.pokedex.photo.PokedexPhotoRepository
import net.minecraft.server.MinecraftServer
import net.minecraft.server.level.ServerPlayer
object DeletePokedexPhotoPacketHandler : ServerNetworkPacketHandler<DeletePokedexPhotoPacket> {
override fun handle(packet: DeletePokedexPhotoPacket, server: MinecraftServer, player: ServerPlayer) {
val deleted = PokedexPhotoRepository.delete(player.uuid, packet.photoId)
if (deleted) {
PokedexPhotoUploadResultPacket(true, "Deleted Pokedex photo: ${packet.photoId}").sendToPlayer(player)
} else {
PokedexPhotoUploadResultPacket(false, "Could not delete Pokedex photo: ${packet.photoId}").sendToPlayer(player)
}
PokedexPhotoNetworkUtil.sendGallery(player)
}
}
@@ -0,0 +1,20 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.net.gui
import com.cobblemon.mod.common.api.net.ClientNetworkPacketHandler
import com.cobblemon.mod.common.client.CobblemonClient
import com.cobblemon.mod.common.net.messages.client.ui.PokedexPhotoBatchPacket
import net.minecraft.client.Minecraft
object PokedexPhotoBatchPacketHandler : ClientNetworkPacketHandler<PokedexPhotoBatchPacket> {
override fun handle(packet: PokedexPhotoBatchPacket, client: Minecraft) {
CobblemonClient.pokedexPhotoStore.applyPhotoBatch(packet.entries)
}
}
@@ -0,0 +1,21 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.net.gui
import com.cobblemon.mod.common.api.net.ClientNetworkPacketHandler
import com.cobblemon.mod.common.client.CobblemonClient
import com.cobblemon.mod.common.net.messages.client.ui.PokedexPhotoGalleryPacket
import net.minecraft.client.Minecraft
object PokedexPhotoGalleryPacketHandler : ClientNetworkPacketHandler<PokedexPhotoGalleryPacket> {
override fun handle(packet: PokedexPhotoGalleryPacket, client: Minecraft) {
CobblemonClient.pokedexPhotoStore.applyGallery(packet.photos, packet.maxPhotosAllowed)
}
}
@@ -0,0 +1,24 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.net.gui
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.net.messages.client.ui.PokedexPhotoGalleryPacket
import com.cobblemon.mod.common.pokedex.photo.PokedexPhotoRepository
import net.minecraft.server.level.ServerPlayer
import kotlin.math.max
object PokedexPhotoNetworkUtil {
fun sendGallery(player: ServerPlayer) {
val photos = PokedexPhotoRepository.listFor(player.uuid)
val maxAllowed = max(1, Cobblemon.config.pokedexScannerPhotoLimitPerPlayer)
PokedexPhotoGalleryPacket(maxAllowed, photos).sendToPlayer(player)
}
}
@@ -0,0 +1,488 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.net.gui
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.net.ServerNetworkPacketHandler
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.api.spawning.CobblemonSpawnPools
import com.cobblemon.mod.common.api.spawning.detail.PokemonSpawnDetail
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.net.messages.client.ui.PokedexPhotoUploadPacket
import com.cobblemon.mod.common.net.messages.client.ui.PokedexPhotoUploadResultPacket
import com.cobblemon.mod.common.pokedex.photo.PokedexPhotoQuality
import com.cobblemon.mod.common.pokedex.photo.PokedexPhotoRepository
import com.cobblemon.mod.common.pokedex.photo.PokedexPhotoSubjectQuality
import com.cobblemon.mod.common.pokemon.Pokemon
import com.cobblemon.mod.common.pokemon.Species
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
import net.minecraft.network.chat.Component
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.MinecraftServer
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.entity.ai.behavior.EntityTracker
import net.minecraft.world.entity.ai.memory.MemoryModuleType
import kotlin.math.max
import kotlin.math.roundToInt
object PokedexPhotoUploadPacketHandler : ServerNetworkPacketHandler<PokedexPhotoUploadPacket> {
private const val MAX_UPLOAD_SIZE = 5 * 1024 * 1024
var showDebugPhotoScore = false
private data class OtherPokemonBonuses(
val extraPokemonScore: Int,
val herdScore: Int
)
override fun handle(packet: PokedexPhotoUploadPacket, server: MinecraftServer, player: ServerPlayer) {
val configuredResolution = max(64, Cobblemon.config.pokedexScannerPhotoResolution)
val expectedRawSize = configuredResolution.toLong() * configuredResolution.toLong() * 4L
val maxUploadSize = max(MAX_UPLOAD_SIZE.toLong(), expectedRawSize + (512L * 1024L))
.coerceAtMost(32L * 1024L * 1024L)
.toInt()
if (packet.pngData.isEmpty() || packet.pngData.size > maxUploadSize) {
PokedexPhotoUploadResultPacket(false, "Failed to save Pokedex photo: upload is invalid.").sendToPlayer(player)
return
}
val maxSpeciesEntries = 256
if (packet.detectedSpecies.size > maxSpeciesEntries) {
PokedexPhotoUploadResultPacket(false, "Failed to save Pokedex photo: metadata is invalid.").sendToPlayer(player)
return
}
val detectedSpecies = packet.detectedSpecies.asSequence()
.mapNotNull(ResourceLocation::tryParse)
.filter { PokemonSpecies.getByIdentifier(it) != null }
.distinct()
.take(maxSpeciesEntries)
.map { it.toString() }
.toList()
val safeBiome = ResourceLocation.tryParse(packet.biome)?.toString() ?: "minecraft:unknown"
val safeBiomeId = ResourceLocation.tryParse(safeBiome)
val safeDimension = ResourceLocation.tryParse(packet.dimension)?.toString() ?: "minecraft:overworld"
val safeDimensionId = ResourceLocation.tryParse(safeDimension)
val safeTimeOfDayTicks = packet.timeOfDayTicks.coerceIn(0L, 23999L)
val safeRaining = packet.raining
val safeThundering = packet.thundering
val safeSubjects = packet.qualitySubjects
.asSequence()
.mapNotNull { subject ->
val speciesId = ResourceLocation.tryParse(subject.species) ?: return@mapNotNull null
val species = speciesId.toString()
if (species !in detectedSpecies) return@mapNotNull null
PokedexPhotoSubjectQuality(
species = species,
entityUuid = subject.entityUuid,
primarySubject = subject.primarySubject,
subjectScore = subject.subjectScore.coerceIn(0, 5000),
sizeScore = subject.sizeScore.coerceIn(0, Cobblemon.config.pokedexScannerPhotoCoverageScoreCap.coerceIn(0, 5000)),
placementScore = subject.placementScore.coerceIn(0, Cobblemon.config.pokedexScannerPhotoCenteredScoreCap.coerceIn(0, 5000)),
centerDistance = subject.centerDistance.coerceIn(0F, 1F),
distanceScore = subject.distanceScore.coerceIn(0, 1000),
directionScore = subject.directionScore.coerceIn(0, Cobblemon.config.pokedexScannerPhotoFacingBonus.coerceIn(0, 5000)),
poseScore = subject.poseScore.coerceIn(0, 800),
fullBodyScore = subject.fullBodyScore.coerceIn(0, Cobblemon.config.pokedexScannerPhotoFullBodyBonus.coerceAtLeast(0)),
otherPokemonScore = subject.otherPokemonScore.coerceIn(0, Cobblemon.config.pokedexScannerPhotoExtraPokemonBonusCap.coerceAtLeast(0)),
herdScore = subject.herdScore.coerceIn(0, 5000),
habitatScore = subject.habitatScore.coerceIn(0, Cobblemon.config.pokedexScannerPhotoHabitatBonus.coerceAtLeast(0)),
timeOfDayScore = subject.timeOfDayScore.coerceIn(0, Cobblemon.config.pokedexScannerPhotoTimeOfDayBonus.coerceAtLeast(0)),
weatherScore = subject.weatherScore.coerceIn(0, Cobblemon.config.pokedexScannerPhotoWeatherBonus.coerceAtLeast(0)),
shinyScore = subject.shinyScore.coerceIn(0, Cobblemon.config.pokedexScannerPhotoShinyBonus.coerceAtLeast(0)),
alphaScore = subject.alphaScore.coerceIn(0, Cobblemon.config.pokedexScannerPhotoAlphaBonus.coerceAtLeast(0)),
wildScore = subject.wildScore.coerceIn(0, Cobblemon.config.pokedexScannerPhotoWildBonus.coerceAtLeast(0)),
formScore = subject.formScore.coerceIn(0, Cobblemon.config.pokedexScannerPhotoFormBonus.coerceAtLeast(0)),
rayHits = subject.rayHits.coerceIn(0, 4096),
rayCoverage = subject.rayCoverage.coerceIn(0F, 1F),
perimeterHits = subject.perimeterHits.coerceIn(0, 4096),
wholeBodyInFrame = subject.wholeBodyInFrame,
clippingMultiplier = subject.clippingMultiplier.coerceIn(0F, 1F),
activeAnimations = subject.activeAnimations.take(16),
shiny = subject.shiny,
shinyMultiplier = 1F,
alpha = subject.alpha,
wild = subject.wild,
alternateForm = subject.alternateForm,
rarityMultiplier = PokedexPhotoQuality.speciesRarityMultiplier(speciesId)
)
}
.take(64)
.toList()
val adjustedSubjects = applyServerAuthoritativeScores(
subjects = safeSubjects,
player = player,
biome = safeBiomeId,
dimension = safeDimensionId,
timeOfDayTicks = safeTimeOfDayTicks,
raining = safeRaining,
thundering = safeThundering
)
val primarySubject = adjustedSubjects.firstOrNull { it.primarySubject }
val safeBaseScore = calculateAdditiveSubtotal(primarySubject)
val safeFinalScore = primarySubject?.subjectScore ?: 0
val safeRarityMultiplier = primarySubject?.rarityMultiplier ?: 1F
sendServerScoreDebug(player, adjustedSubjects, safeBaseScore, safeFinalScore)
val result = PokedexPhotoRepository.saveFromPng(
owner = player.uuid,
pngData = packet.pngData,
detectedSpecies = detectedSpecies,
biome = safeBiome,
dimension = safeDimension,
timeOfDayTicks = safeTimeOfDayTicks,
raining = safeRaining,
thundering = safeThundering,
qualityScore = safeFinalScore,
qualityBaseScore = safeBaseScore,
qualitySpeciesMultiplier = safeRarityMultiplier,
qualitySubjects = adjustedSubjects
)
if (result.success && result.photo != null) {
val photoId = result.photo.id
PokedexPhotoUploadResultPacket(
true,
"Saved Pokedex scan photo: $photoId | Score ${result.photo.qualityScore}/5000 (base ${result.photo.qualityBaseScore}, rarity x${"%.2f".format(result.photo.qualitySpeciesMultiplier)})"
).sendToPlayer(player)
PokedexPhotoNetworkUtil.sendGallery(player)
} else {
val message = result.errorMessage ?: "Unknown save error."
PokedexPhotoUploadResultPacket(false, "Failed to save Pokedex photo: $message").sendToPlayer(player)
}
}
private fun sendServerScoreDebug(
player: ServerPlayer,
subjects: List<PokedexPhotoSubjectQuality>,
baseScore: Int,
finalScore: Int
) {
val primary = subjects.firstOrNull { it.primarySubject } ?: return
if (showDebugPhotoScore)
player.sendSystemMessage(
Component.literal(
"[Photo Score Debug] server primary=${primary.species}, " +
"size +${primary.sizeScore}, center +${primary.placementScore}, " +
"extra +${primary.otherPokemonScore}, look-at-camera +${primary.directionScore}, herd +${primary.herdScore}, " +
"wild +${primary.wildScore}, form +${primary.formScore}, " +
"pose +${primary.poseScore}, habitat +${primary.habitatScore}, time +${primary.timeOfDayScore}, " +
"weather +${primary.weatherScore}, shiny +${primary.shinyScore}, alpha +${primary.alphaScore}, " +
"rarity x${"%.2f".format(primary.rarityMultiplier)}, too close x${"%.2f".format(primary.clippingMultiplier)}, " +
"lookTargetMatched=${primary.directionScore > 0}, base=$baseScore, final=$finalScore"
)
)
}
private fun applyServerAuthoritativeScores(
subjects: List<PokedexPhotoSubjectQuality>,
player: ServerPlayer,
biome: ResourceLocation?,
dimension: ResourceLocation?,
timeOfDayTicks: Long,
raining: Boolean,
thundering: Boolean
): List<PokedexPhotoSubjectQuality> {
if (subjects.isEmpty()) return emptyList()
val facingBonus = Cobblemon.config.pokedexScannerPhotoFacingBonus.coerceIn(0, 5000)
val withServerLookScores = subjects.map { subject ->
subject.copy(
directionScore = 0,
primarySubject = false,
fullBodyScore = 0,
otherPokemonScore = 0,
herdScore = 0,
habitatScore = 0,
timeOfDayScore = 0,
weatherScore = 0,
shinyScore = 0,
alphaScore = 0,
wildScore = 0,
formScore = 0,
wholeBodyInFrame = true,
clippingMultiplier = 1F,
shinyMultiplier = 1F
)
}
val primary = withServerLookScores.maxWithOrNull(
compareBy<PokedexPhotoSubjectQuality> { it.rayHits }
.thenBy { it.rayCoverage }
.thenBy { -it.centerDistance }
) ?: return emptyList()
val primaryLookTargetMatched = isBrainLookingAtPlayer(primary, player)
val otherPokemonBonuses = calculateOtherPokemonBonuses(primary, withServerLookScores)
val familyCoverage = calculateFamilyCoverage(primary, withServerLookScores)
val entity = primary.entityUuid?.let { player.serverLevel().getEntity(it) } as? PokemonEntity
val serverShiny = entity?.pokemon?.shiny ?: primary.shiny
val serverAlpha = entity?.pokemon?.aspects?.contains("alpha") ?: primary.alpha
val serverWild = entity?.pokemon?.isWild() ?: primary.wild
val serverAlternateForm = entity?.pokemon?.let(::hasAlternateForm) ?: primary.alternateForm
val speciesId = ResourceLocation.tryParse(primary.species)
val spawnDetails = speciesId?.let(::spawnDetailsForSpecies).orEmpty()
val habitatScore = if (biome != null && dimension != null && spawnDetails.any { detail -> detailAllowsBiomeAndDimension(detail, biome, dimension) }) {
Cobblemon.config.pokedexScannerPhotoHabitatBonus.coerceAtLeast(0)
} else {
0
}
val timeOfDayScore = if (spawnDetails.any { detail -> detailAllowsTime(detail, timeOfDayTicks.toInt()) }) {
Cobblemon.config.pokedexScannerPhotoTimeOfDayBonus.coerceAtLeast(0)
} else {
0
}
val weatherScore = if (spawnDetails.any { detail -> detailAllowsWeather(detail, raining, thundering) }) {
Cobblemon.config.pokedexScannerPhotoWeatherBonus.coerceAtLeast(0)
} else {
0
}
return withServerLookScores.map { subject ->
if (subject === primary) {
scorePrimarySubject(
subject = subject.copy(
directionScore = if (primaryLookTargetMatched) facingBonus else 0,
habitatScore = habitatScore,
timeOfDayScore = timeOfDayScore,
weatherScore = weatherScore,
shiny = serverShiny,
shinyScore = if (serverShiny) Cobblemon.config.pokedexScannerPhotoShinyBonus.coerceAtLeast(0) else 0,
alpha = serverAlpha,
alphaScore = if (serverAlpha) Cobblemon.config.pokedexScannerPhotoAlphaBonus.coerceAtLeast(0) else 0,
wild = serverWild,
wildScore = if (serverWild) Cobblemon.config.pokedexScannerPhotoWildBonus.coerceAtLeast(0) else 0,
alternateForm = serverAlternateForm,
formScore = if (serverAlternateForm) Cobblemon.config.pokedexScannerPhotoFormBonus.coerceAtLeast(0) else 0,
rarityMultiplier = speciesId?.let(PokedexPhotoQuality::speciesRarityMultiplier) ?: 1F
),
otherPokemonBonuses = otherPokemonBonuses,
familyCoverage = familyCoverage
)
} else {
subject.copy(subjectScore = 0)
}
}.sortedWith(compareByDescending<PokedexPhotoSubjectQuality> { it.primarySubject }.thenByDescending { it.subjectScore })
}
private fun scorePrimarySubject(
subject: PokedexPhotoSubjectQuality,
otherPokemonBonuses: OtherPokemonBonuses,
familyCoverage: Float
): PokedexPhotoSubjectQuality {
val wholeBodyInFrame = subject.perimeterHits == 0
val scoringCoverage = (subject.rayCoverage + familyCoverage).coerceIn(0F, 1F)
val sizeScore = calculateSizeScore(scoringCoverage.toDouble(), wholeBodyInFrame)
val clippingMultiplier = if (wholeBodyInFrame) {
1.0
} else {
Cobblemon.config.pokedexScannerPhotoOutOfFrameMultiplier.coerceIn(0F, 1F).toDouble()
}
val additiveScore = sizeScore + subject.placementScore + otherPokemonBonuses.extraPokemonScore +
subject.directionScore + otherPokemonBonuses.herdScore + subject.wildScore +
subject.formScore + subject.poseScore +
subject.habitatScore + subject.timeOfDayScore + subject.weatherScore +
subject.shinyScore + subject.alphaScore
val subjectScore = (additiveScore * subject.rarityMultiplier * clippingMultiplier)
.roundToInt()
.coerceIn(0, 5000)
return subject.copy(
primarySubject = true,
subjectScore = subjectScore,
sizeScore = sizeScore,
fullBodyScore = 0,
otherPokemonScore = otherPokemonBonuses.extraPokemonScore,
herdScore = otherPokemonBonuses.herdScore,
wholeBodyInFrame = wholeBodyInFrame,
clippingMultiplier = clippingMultiplier.toFloat()
)
}
private fun isBrainLookingAtPlayer(subject: PokedexPhotoSubjectQuality, player: ServerPlayer): Boolean {
val entityUuid = subject.entityUuid ?: return false
val entity = player.serverLevel().getEntity(entityUuid) as? PokemonEntity ?: return false
val lookTarget = entity.brain
.getMemory(MemoryModuleType.LOOK_TARGET)
.orElse(null)
?: return false
val targetEntity = (lookTarget as? EntityTracker)?.entity
if (targetEntity === player || targetEntity?.id == player.id || targetEntity?.uuid == player.uuid) {
return true
}
return lookTarget.currentPosition().distanceTo(player.getEyePosition(1.0F)) <= 1.0
}
private fun calculateOtherPokemonBonuses(
primary: PokedexPhotoSubjectQuality,
subjects: List<PokedexPhotoSubjectQuality>
): OtherPokemonBonuses {
val secondarySubjects = subjects
.asSequence()
.filter { it !== primary }
.toList()
val extraPokemonScore = secondarySubjects.sumOf(::calculateSecondaryPokemonBonus)
.coerceIn(0, Cobblemon.config.pokedexScannerPhotoExtraPokemonBonusCap.coerceAtLeast(0))
val herdScore = secondarySubjects.sumOf { subject -> calculateFamilyBonus(primary, subject) }
.coerceAtLeast(0)
return OtherPokemonBonuses(extraPokemonScore, herdScore)
}
private fun calculateFamilyCoverage(
primary: PokedexPhotoSubjectQuality,
subjects: List<PokedexPhotoSubjectQuality>
): Float {
return subjects
.asSequence()
.filter { it !== primary }
.filter { subject -> isFamilyMember(primary.species, subject.species) }
.sumOf { subject -> subject.rayCoverage.toDouble() }
.toFloat()
.coerceIn(0F, 1F)
}
private fun calculateSecondaryPokemonBonus(subject: PokedexPhotoSubjectQuality): Int {
if (subject.rayHits <= 0) return 0
val visibility = (subject.rayCoverage / 0.08F).coerceIn(0.25F, 1F)
val placement = (1F - subject.centerDistance).coerceIn(0.35F, 1F)
val scoreCap = Cobblemon.config.pokedexScannerPhotoExtraPokemonBonusPerSubject.coerceAtLeast(0)
return (scoreCap * visibility * placement).roundToInt().coerceIn(0, scoreCap)
}
private fun calculateFamilyBonus(primary: PokedexPhotoSubjectQuality, subject: PokedexPhotoSubjectQuality): Int {
if (!isFamilyMember(primary.species, subject.species)) return 0
val visibility = (subject.rayCoverage / 0.12F).coerceIn(0F, 1F)
val placement = (1F - subject.centerDistance).coerceIn(0F, 1F)
val scoreCap = Cobblemon.config.pokedexScannerPhotoFamilyBonus.coerceAtLeast(0)
return (scoreCap * visibility * placement).roundToInt().coerceIn(0, scoreCap)
}
private fun isFamilyMember(primarySpecies: String, subjectSpecies: String): Boolean {
return primarySpecies == subjectSpecies || areSpeciesInSameEvolutionLine(primarySpecies, subjectSpecies)
}
private fun areSpeciesInSameEvolutionLine(firstSpecies: String, secondSpecies: String): Boolean {
val firstId = ResourceLocation.tryParse(firstSpecies) ?: return false
val secondId = ResourceLocation.tryParse(secondSpecies) ?: return false
if (firstId == secondId) return true
return secondId in collectEvolutionFamily(firstId)
}
private fun collectEvolutionFamily(speciesId: ResourceLocation): Set<ResourceLocation> {
val rootSpecies = PokemonSpecies.getByIdentifier(speciesId) ?: return setOf(speciesId)
val family = linkedSetOf<ResourceLocation>()
val processed = hashSetOf<ResourceLocation>()
val queue = ArrayDeque<Species>()
queue.add(rootSpecies)
var preEvolution = rootSpecies.preEvolution?.species
while (preEvolution != null && family.add(preEvolution.resourceIdentifier)) {
queue.add(preEvolution)
preEvolution = preEvolution.preEvolution?.species
}
while (queue.isNotEmpty()) {
val species = queue.removeFirst()
if (!processed.add(species.resourceIdentifier)) continue
family.add(species.resourceIdentifier)
species.evolutions
.asSequence()
.mapNotNull { evolution -> evolution.result.species?.asIdentifierDefaultingNamespace() }
.mapNotNull(PokemonSpecies::getByIdentifier)
.forEach(queue::add)
}
return family
}
private fun calculateSizeScore(rayCoverage: Double, wholeBodyInFrame: Boolean): Int {
val scoreCap = Cobblemon.config.pokedexScannerPhotoCoverageScoreCap.coerceIn(0, 5000)
val capAtCoverage = Cobblemon.config.pokedexScannerPhotoCoverageScoreCapAtPercent.toDouble().coerceIn(0.01, 1.0)
val normalizedCoverage = (rayCoverage / capAtCoverage).coerceIn(0.0, 1.0)
val coverageFactor = Math.pow(normalizedCoverage, 2.4)
val tooLargeFactor = if (rayCoverage <= 0.55 || wholeBodyInFrame) {
1.0
} else {
(1.0 - ((rayCoverage - 0.55) / 0.35 * 0.2)).coerceIn(0.8, 1.0)
}
return (scoreCap.toDouble() * coverageFactor * tooLargeFactor)
.roundToInt()
.coerceIn(0, scoreCap)
}
private fun calculateAdditiveSubtotal(subject: PokedexPhotoSubjectQuality?): Int {
if (subject == null) return 0
return (subject.sizeScore +
subject.placementScore +
subject.otherPokemonScore +
subject.directionScore +
subject.herdScore +
subject.wildScore +
subject.formScore +
subject.poseScore +
subject.habitatScore +
subject.timeOfDayScore +
subject.weatherScore +
subject.shinyScore +
subject.alphaScore
).coerceIn(0, 5000)
}
private fun hasAlternateForm(pokemon: Pokemon): Boolean {
return pokemon.form.name != pokemon.species.standardForm.name
}
private fun spawnDetailsForSpecies(species: ResourceLocation): List<PokemonSpawnDetail> {
return CobblemonSpawnPools.WORLD_SPAWN_POOL
.asSequence()
.filterIsInstance<PokemonSpawnDetail>()
.filter { detail -> detail.pokemon.species?.asIdentifierDefaultingNamespace() == species }
.toList()
}
private fun detailAllowsBiomeAndDimension(
detail: PokemonSpawnDetail,
biome: ResourceLocation,
dimension: ResourceLocation
): Boolean {
if (biome !in detail.validBiomes) return false
if (detail.conditions.isNotEmpty() && detail.conditions.none { condition ->
condition.dimensions.isNullOrEmpty() || dimension in condition.dimensions!!
}) {
return false
}
if (detail.anticonditions.any { condition -> !condition.dimensions.isNullOrEmpty() && dimension in condition.dimensions!! }) {
return false
}
return true
}
private fun detailAllowsTime(detail: PokemonSpawnDetail, timeOfDayTicks: Int): Boolean {
if (detail.conditions.isNotEmpty() && detail.conditions.none { condition ->
condition.timeRange == null || condition.timeRange!!.contains(timeOfDayTicks)
}) {
return false
}
if (detail.anticonditions.any { condition -> condition.timeRange?.contains(timeOfDayTicks) == true }) {
return false
}
return true
}
private fun detailAllowsWeather(detail: PokemonSpawnDetail, raining: Boolean, thundering: Boolean): Boolean {
if (detail.conditions.isNotEmpty() && detail.conditions.none { condition ->
(condition.isRaining == null || condition.isRaining == raining) &&
(condition.isThundering == null || condition.isThundering == thundering)
}) {
return false
}
if (detail.anticonditions.any { condition ->
(condition.isRaining != null && condition.isRaining == raining) ||
(condition.isThundering != null && condition.isThundering == thundering)
}) {
return false
}
return true
}
}
@@ -0,0 +1,21 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.net.gui
import com.cobblemon.mod.common.api.net.ClientNetworkPacketHandler
import com.cobblemon.mod.common.net.messages.client.ui.PokedexPhotoUploadResultPacket
import net.minecraft.client.Minecraft
import net.minecraft.network.chat.Component
object PokedexPhotoUploadResultPacketHandler : ClientNetworkPacketHandler<PokedexPhotoUploadResultPacket> {
override fun handle(packet: PokedexPhotoUploadResultPacket, client: Minecraft) {
client.player?.displayClientMessage(Component.literal(packet.message), true)
}
}
@@ -0,0 +1,20 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.net.gui
import com.cobblemon.mod.common.api.net.ClientNetworkPacketHandler
import com.cobblemon.mod.common.client.CobblemonClient
import com.cobblemon.mod.common.net.messages.client.ui.PrintedPhotoAssetBatchPacket
import net.minecraft.client.Minecraft
object PrintedPhotoAssetBatchPacketHandler : ClientNetworkPacketHandler<PrintedPhotoAssetBatchPacket> {
override fun handle(packet: PrintedPhotoAssetBatchPacket, client: Minecraft) {
CobblemonClient.printedPhotoAssetStore.applyAssetBatch(packet.entries)
}
}
@@ -0,0 +1,33 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.net.gui
import com.cobblemon.mod.common.api.net.ServerNetworkPacketHandler
import com.cobblemon.mod.common.net.messages.client.ui.PokedexPhotoBatchPacket
import com.cobblemon.mod.common.net.messages.client.ui.RequestPokedexPhotoBatchPacket
import com.cobblemon.mod.common.pokedex.photo.PokedexPhotoRepository
import net.minecraft.server.MinecraftServer
import net.minecraft.server.level.ServerPlayer
object RequestPokedexPhotoBatchPacketHandler : ServerNetworkPacketHandler<RequestPokedexPhotoBatchPacket> {
// Cap packets to avoid oversized payloads if a client ever asks for too many at once.
private const val MAX_ENTRIES_PER_RESPONSE = 8
override fun handle(packet: RequestPokedexPhotoBatchPacket, server: MinecraftServer, player: ServerPlayer) {
val entries = packet.ids.mapNotNull { id ->
val metadata = PokedexPhotoRepository.getMetadata(player.uuid, id) ?: return@mapNotNull null
val bytes = PokedexPhotoRepository.getPhoto(player.uuid, id) ?: return@mapNotNull null
PokedexPhotoBatchPacket.Entry(id, metadata.width, metadata.height, bytes)
}
if (entries.isEmpty()) return
entries.chunked(MAX_ENTRIES_PER_RESPONSE).forEach { chunk ->
PokedexPhotoBatchPacket(chunk).sendToPlayer(player)
}
}
}
@@ -0,0 +1,21 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.net.gui
import com.cobblemon.mod.common.api.net.ServerNetworkPacketHandler
import com.cobblemon.mod.common.net.messages.client.ui.RequestPokedexPhotoGalleryPacket
import net.minecraft.server.MinecraftServer
import net.minecraft.server.level.ServerPlayer
object RequestPokedexPhotoGalleryPacketHandler : ServerNetworkPacketHandler<RequestPokedexPhotoGalleryPacket> {
override fun handle(packet: RequestPokedexPhotoGalleryPacket, server: MinecraftServer, player: ServerPlayer) {
PokedexPhotoNetworkUtil.sendGallery(player)
}
}
@@ -0,0 +1,34 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.net.gui
import com.cobblemon.mod.common.api.net.ServerNetworkPacketHandler
import com.cobblemon.mod.common.net.messages.client.ui.PrintedPhotoAssetBatchPacket
import com.cobblemon.mod.common.net.messages.client.ui.RequestPrintedPhotoAssetBatchPacket
import com.cobblemon.mod.common.pokedex.photo.PrintedPhotoAssetRepository
import net.minecraft.server.MinecraftServer
import net.minecraft.server.level.ServerPlayer
object RequestPrintedPhotoAssetBatchPacketHandler : ServerNetworkPacketHandler<RequestPrintedPhotoAssetBatchPacket> {
private const val MAX_IDS_PER_REQUEST = 64
private const val MAX_ENTRIES_PER_RESPONSE = 8
override fun handle(packet: RequestPrintedPhotoAssetBatchPacket, server: MinecraftServer, player: ServerPlayer) {
val capped = packet.assetIds.distinct().take(MAX_IDS_PER_REQUEST)
val entries = capped.mapNotNull { id ->
val record = PrintedPhotoAssetRepository.getAssetRecord(id) ?: return@mapNotNull null
val bytes = PrintedPhotoAssetRepository.getAssetBytes(id) ?: return@mapNotNull null
PrintedPhotoAssetBatchPacket.Entry(id, record.width, record.height, bytes)
}
if (entries.isEmpty()) return
entries.chunked(MAX_ENTRIES_PER_RESPONSE).forEach { chunk ->
PrintedPhotoAssetBatchPacket(chunk).sendToPlayer(player)
}
}
}
@@ -0,0 +1,32 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.net.gui
import com.cobblemon.mod.common.api.net.ServerNetworkPacketHandler
import com.cobblemon.mod.common.block.entity.PhotoPrinterBlockEntity
import com.cobblemon.mod.common.block.photoprinter.PhotoPrinterMenu
import com.cobblemon.mod.common.net.messages.client.ui.SetPhotoPrinterSelectionPacket
import com.cobblemon.mod.common.pokedex.photo.PokedexPhotoRepository
import net.minecraft.server.MinecraftServer
import net.minecraft.server.level.ServerPlayer
object SetPhotoPrinterSelectionPacketHandler : ServerNetworkPacketHandler<SetPhotoPrinterSelectionPacket> {
override fun handle(packet: SetPhotoPrinterSelectionPacket, server: MinecraftServer, player: ServerPlayer) {
val menu = player.containerMenu as? PhotoPrinterMenu ?: return
if (menu.blockPos() != packet.blockPos) return
val blockEntity = player.level().getBlockEntity(packet.blockPos) as? PhotoPrinterBlockEntity ?: return
val selectedId = packet.photoId?.takeIf { PokedexPhotoRepository.getMetadata(player.uuid, it) != null }
blockEntity.selectedPhotoId = selectedId
blockEntity.setChanged()
menu.broadcastChanges()
}
}
@@ -22,6 +22,28 @@ object ServerSettingsPacketHandler : ClientNetworkPacketHandler<ServerSettingsPa
ServerSettings.maxPokemonFriendship = packet.maxPokemonFriendship
ServerSettings.maxDynamaxLevel = packet.maxDynamaxLevel
ServerSettings.unlockAllMoveDexMovesByDefault = packet.unlockAllMoveDexMovesByDefault
ServerSettings.pokedexScannerPhotoRaycastDistance = packet.pokedexScannerPhotoRaycastDistance
ServerSettings.pokedexScannerPhotoRaycastDensity = packet.pokedexScannerPhotoRaycastDensity
ServerSettings.pokedexScannerPhotoPerimeterRaycastDensity = packet.pokedexScannerPhotoPerimeterRaycastDensity
ServerSettings.pokedexScannerPhotoCoverageScoreCap = packet.pokedexScannerPhotoCoverageScoreCap
ServerSettings.pokedexScannerPhotoCoverageScoreCapAtPercent = packet.pokedexScannerPhotoCoverageScoreCapAtPercent
ServerSettings.pokedexScannerPhotoCenteredScoreCap = packet.pokedexScannerPhotoCenteredScoreCap
ServerSettings.pokedexScannerPhotoFacingBonus = packet.pokedexScannerPhotoFacingBonus
ServerSettings.pokedexScannerPhotoFullBodyBonus = packet.pokedexScannerPhotoFullBodyBonus
ServerSettings.pokedexScannerPhotoOutOfFrameMultiplier = packet.pokedexScannerPhotoOutOfFrameMultiplier
ServerSettings.pokedexScannerPhotoShinyBonus = packet.pokedexScannerPhotoShinyBonus
ServerSettings.pokedexScannerPhotoUncommonSpeciesMultiplier = packet.pokedexScannerPhotoUncommonSpeciesMultiplier
ServerSettings.pokedexScannerPhotoRareSpeciesMultiplier = packet.pokedexScannerPhotoRareSpeciesMultiplier
ServerSettings.pokedexScannerPhotoUltraRareSpeciesMultiplier = packet.pokedexScannerPhotoUltraRareSpeciesMultiplier
ServerSettings.pokedexScannerPhotoExtraPokemonBonusCap = packet.pokedexScannerPhotoExtraPokemonBonusCap
ServerSettings.pokedexScannerPhotoExtraPokemonBonusPerSubject = packet.pokedexScannerPhotoExtraPokemonBonusPerSubject
ServerSettings.pokedexScannerPhotoFamilyBonus = packet.pokedexScannerPhotoFamilyBonus
ServerSettings.pokedexScannerPhotoHabitatBonus = packet.pokedexScannerPhotoHabitatBonus
ServerSettings.pokedexScannerPhotoTimeOfDayBonus = packet.pokedexScannerPhotoTimeOfDayBonus
ServerSettings.pokedexScannerPhotoWeatherBonus = packet.pokedexScannerPhotoWeatherBonus
ServerSettings.pokedexScannerPhotoAlphaBonus = packet.pokedexScannerPhotoAlphaBonus
ServerSettings.pokedexScannerPhotoWildBonus = packet.pokedexScannerPhotoWildBonus
ServerSettings.pokedexScannerPhotoFormBonus = packet.pokedexScannerPhotoFormBonus
}
}
}
@@ -10,9 +10,12 @@ package com.cobblemon.mod.common.client.pokedex
import com.cobblemon.mod.common.api.gui.blitk
import com.cobblemon.mod.common.api.pokedex.PokedexLearnedInformation
import com.cobblemon.mod.common.api.pokedex.scan.BlockScanDefinition
import com.cobblemon.mod.common.api.pokedex.scan.BlockScanProviderRegistry
import com.cobblemon.mod.common.api.text.*
import com.cobblemon.mod.common.client.CobblemonClient
import com.cobblemon.mod.common.client.CobblemonResources
import com.cobblemon.mod.common.client.keybind.CobblemonKeyBinds
import com.cobblemon.mod.common.client.gui.battle.BattleOverlay.Companion.SCALE
import com.cobblemon.mod.common.client.gui.battle.BattleOverlay.Companion.caughtIndicator
import com.cobblemon.mod.common.client.gui.pokedex.PokedexGUIConstants.BASE_HEIGHT
@@ -28,9 +31,12 @@ import com.mojang.blaze3d.vertex.PoseStack
import net.minecraft.client.DeltaTracker
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.core.registries.BuiltInRegistries
import net.minecraft.network.chat.Component
import net.minecraft.network.chat.MutableComponent
import net.minecraft.resources.ResourceLocation
import net.minecraft.util.Mth.clamp
import net.minecraft.world.level.block.entity.BlockEntity
import org.joml.Quaternionf
import kotlin.math.*
@@ -60,8 +66,10 @@ class PokedexScannerRenderer {
val SCAN_RING_OUTER_DIAMETER = 116
val SCAN_RING_INNER_DIAMETER = 84
const val CAMERA_FRAME_DIAMETER = 158
val SCAN_SCREEN = cobblemonResource("textures/gui/pokedex/pokedex_screen_scan.png")
val CAMERA_SCREEN = cobblemonResource("textures/gui/pokedex/pokedex_screen_search_overlay.png")
val SCAN_OVERLAY_CORNERS = cobblemonResource("textures/gui/pokedex/scan/overlay_corners.png")
val SCAN_OVERLAY_TOP = cobblemonResource("textures/gui/pokedex/scan/overlay_border_top.png")
val SCAN_OVERLAY_BOTTOM = cobblemonResource("textures/gui/pokedex/scan/overlay_border_bottom.png")
@@ -73,6 +81,13 @@ class PokedexScannerRenderer {
val SCAN_RING_OUTER = cobblemonResource("textures/gui/pokedex/scan/scan_ring_outer.png")
val SCAN_RING_MIDDLE = cobblemonResource("textures/gui/pokedex/scan/scan_ring_middle.png")
val SCAN_RING_INNER = cobblemonResource("textures/gui/pokedex/scan/scan_ring_inner.png")
val CAMERA_FRAME = cobblemonResource("textures/gui/pokedex/scan/camera_frame.png")
const val PHOTO_CAPTURE_DEBUG_FRAME_ENABLED = true
private val PHOTO_CAPTURE_DEBUG_FRAME_COLOR = 0xCC40FF80.toInt()
private val PHOTO_CAPTURE_DEBUG_FRAME_CENTER_COLOR = 0xCCFFFFFF.toInt()
private const val BLINK_SHUTTER_RGB = 0x001F1D1B
private const val BLINK_SHUTTER_MAX_ALPHA = 245
private const val BLINK_SHUTTER_CENTER_OVERLAP = 2
val CENTER_INFO_FRAME = cobblemonResource("textures/gui/pokedex/scan/scan_info_frame.png")
val UNKNOWN_MARK = cobblemonResource("textures/gui/pokedex/scan/scan_unknown.png")
@@ -81,13 +96,53 @@ class PokedexScannerRenderer {
fun infoFrameResource(isLeft: Boolean, tier: Int): ResourceLocation = cobblemonResource("textures/gui/pokedex/scan/scan_info_frame_${if (isLeft) "left" else "right"}_$tier.png")
}
private var lastFocusedBlockTypeId: ResourceLocation? = null
// Whether the focused block's info frames should be visible: shown only once the block has been
// fully scanned this session or was scanned previously. Retained through the close animation.
private var lastFocusedBlockRevealed: Boolean = false
fun renderInfoFrames(graphics: GuiGraphics, poseStack: PoseStack, usageContext: PokedexUsageContext, centerX: Int, centerY: Int, opacity: Float) {
if (usageContext.focusIntervals > 0) {
var infoDisplayedCounter = 0
usageContext.availableInfoFrames.forEachIndexed {index, isLeftSide ->
if (isLeftSide !== null) {
if (infoDisplayedCounter > 1 && !usageContext.isPokemonInFocusOwned) return@forEachIndexed
val isPokemonFocus = usageContext.scannableEntityInFocus != null
if (isPokemonFocus && infoDisplayedCounter > 1 && !usageContext.isPokemonInFocusOwned) return@forEachIndexed
infoDisplayedCounter++
val blockEntityInFocus: BlockEntity? = usageContext.blockEntityInFocus
val pokedexEntityData = usageContext.scannableEntityInFocus?.resolvePokemonScan()
// store the block entity typeId so our close animation knows what to show
if (pokedexEntityData != null) {
lastFocusedBlockTypeId = null
} else if (blockEntityInFocus != null) {
val focusedTypeId = BuiltInRegistries.BLOCK_ENTITY_TYPE.getKey(blockEntityInFocus.type)
lastFocusedBlockTypeId = focusedTypeId
// Reveal the frames only once the block has been fully scanned or scanned before
val provider = focusedTypeId?.let { BlockScanProviderRegistry.get(it) }
val scannedBefore = provider?.getScanKey(blockEntityInFocus)
?.let { CobblemonClient.clientScannedBlocksData.hasScanned(it) } ?: false
lastFocusedBlockRevealed = usageContext.blockEntityScanCompleted || scannedBefore
} else if (usageContext.focusIntervals <= 0f) {
lastFocusedBlockTypeId = null
}
val effectiveTypeId = blockEntityInFocus
?.let { BuiltInRegistries.BLOCK_ENTITY_TYPE.getKey(it.type) }
?: lastFocusedBlockTypeId
// if a block entity is being focused on make sure we only show the frames needed
if (pokedexEntityData == null && effectiveTypeId != null) {
val provider = BlockScanProviderRegistry.get(effectiveTypeId)
if (provider != null) {
// Hide the panels entirely until the block is scanned (or was scanned before)
if (!lastFocusedBlockRevealed) return@forEachIndexed
if (!provider.frames.any { it.slot == infoDisplayedCounter }) return@forEachIndexed
}
}
// Frames
val isInnerFrame = index == 1 || index == 2
val frameHeight = if (isInnerFrame) INNER_INFO_FRAME_HEIGHT else OUTER_INFO_FRAME_HEIGHT
@@ -109,105 +164,142 @@ class PokedexScannerRenderer {
val yOffsetText = when(index) { 0 -> 5; 1 -> 4; 2 -> 8; 3 -> 42; else -> 0 }
// Text
if (usageContext.focusIntervals == PokedexUsageContext.FOCUS_INTERVALS && usageContext.scannableEntityInFocus != null) {
val pokedexEntityData = usageContext.scannableEntityInFocus?.resolvePokemonScan() ?: return
if (infoDisplayedCounter == 1) {
drawScaledText(
context = graphics,
font = CobblemonResources.DEFAULT_LARGE,
text = lang("ui.lv.number", pokedexEntityData.pokemon.level).bold(),
x = centerX + xOffset + xOffsetText,
y = centerY + yOffset + yOffsetText,
shadow = true,
centered = true
)
}
if (infoDisplayedCounter == 2) {
val hasTrainer = (usageContext.scannableEntityInFocus?.resolveEntityScan() as? PokemonEntity)?.ownerUUID !== null
val speciesName = pokedexEntityData.getApparentSpecies().translatedName.bold()
var yOffsetName = if (hasTrainer) 2 else 0
if (hasTrainer) {
if (usageContext.focusIntervals == PokedexUsageContext.FOCUS_INTERVALS && (pokedexEntityData != null || blockEntityInFocus != null)) {
if (pokedexEntityData != null) {
if (infoDisplayedCounter == 1) {
drawScaledText(
context = graphics,
text = lang("ui.pokedex.scan.trainer_owned"),
font = CobblemonResources.DEFAULT_LARGE,
text = lang("ui.lv.number", pokedexEntityData.pokemon.level).bold(),
x = centerX + xOffset + xOffsetText,
y = centerY + yOffset + yOffsetText - yOffsetName,
y = centerY + yOffset + yOffsetText,
shadow = true,
centered = true
)
}
if (infoDisplayedCounter == 2) {
val hasTrainer =
(usageContext.scannableEntityInFocus?.resolveEntityScan() as? PokemonEntity)?.ownerUUID !== null
val speciesName = pokedexEntityData.getApparentSpecies().translatedName.bold()
var yOffsetName = if (hasTrainer) 2 else 0
if (hasTrainer) {
drawScaledText(
context = graphics,
text = lang("ui.pokedex.scan.trainer_owned"),
x = centerX + xOffset + xOffsetText,
y = centerY + yOffset + yOffsetText - yOffsetName,
shadow = true,
centered = true,
scale = 0.5F
)
}
drawScaledText(
context = graphics,
font = CobblemonResources.DEFAULT_LARGE,
text = speciesName,
x = centerX + xOffset + xOffsetText,
y = centerY + yOffset + yOffsetText + yOffsetName,
shadow = true,
centered = true
)
val gender = pokedexEntityData.pokemon.gender
val speciesNameWidth =
Minecraft.getInstance().font.width(speciesName.font(CobblemonResources.DEFAULT_LARGE))
if (gender != Gender.GENDERLESS) {
val isMale = gender == Gender.MALE
val textSymbol = if (isMale) "♂".text().bold() else "♀".text().bold()
drawScaledText(
context = graphics,
font = CobblemonResources.DEFAULT_LARGE,
text = textSymbol,
x = centerX + xOffset + xOffsetText + 2 + (speciesNameWidth / 2),
y = centerY + yOffset + yOffsetText + yOffsetName,
colour = if (isMale) 0x32CBFF else 0xFC5454,
shadow = true
)
}
if (usageContext.isPokemonInFocusOwned) {
blitk(
matrixStack = poseStack,
texture = caughtIndicator,
x = (centerX + xOffset + xOffsetText - 7 - (speciesNameWidth / 2)) / SCALE,
y = (centerY + yOffset + yOffsetText + yOffsetName + 2) / SCALE,
height = 10,
width = 10,
scale = SCALE
)
}
}
if (infoDisplayedCounter == 3) {
val typeText = lang(
"type.suffix",
pokedexEntityData.getApparentForm().types.map { it.displayName.copy() }
.reduce { acc, next -> acc.plus("/").plus(next) }).bold()
val typeWidth =
Minecraft.getInstance().font.width(typeText.font(CobblemonResources.DEFAULT_LARGE))
// Split into 2 lines if text width is too long
if (typeWidth > (OUTER_INFO_FRAME_WIDTH - 8) && pokedexEntityData.getApparentForm().secondaryType !== null) {
val splitLines = typeText.string.split("/")
drawScaledText(
context = graphics,
font = CobblemonResources.DEFAULT_LARGE,
text = text(splitLines[0], "/").bold(),
x = centerX + xOffset + xOffsetText,
y = centerY + yOffset + yOffsetText - 4,
shadow = true,
centered = true
)
drawScaledText(
context = graphics,
font = CobblemonResources.DEFAULT_LARGE,
text = text(splitLines[1]).bold(),
x = centerX + xOffset + xOffsetText,
y = centerY + yOffset + yOffsetText + 3,
shadow = true,
centered = true
)
} else {
drawScaledText(
context = graphics,
font = CobblemonResources.DEFAULT_LARGE,
text = typeText,
x = centerX + xOffset + xOffsetText,
y = centerY + yOffset + yOffsetText,
shadow = true,
centered = true
)
}
}
} else if (blockEntityInFocus != null) {
val typeId = BuiltInRegistries.BLOCK_ENTITY_TYPE.getKey(blockEntityInFocus.type) ?: return@forEachIndexed
val provider = BlockScanProviderRegistry.get(typeId) ?: return@forEachIndexed
val values = provider.getValues(blockEntityInFocus)
val frame = provider.frames.firstOrNull { it.slot == infoDisplayedCounter } ?: return@forEachIndexed
val textComponent = resolveScanText(frame.text, values).bold()
val scale = frame.scale
if (scale != null) {
drawScaledText(
context = graphics,
font = CobblemonResources.DEFAULT_LARGE,
text = textComponent,
x = centerX + xOffset + xOffsetText,
y = centerY + yOffset + yOffsetText,
shadow = true,
centered = true,
scale = 0.5F
)
}
drawScaledText(
context = graphics,
font = CobblemonResources.DEFAULT_LARGE,
text = speciesName,
x = centerX + xOffset + xOffsetText,
y = centerY + yOffset + yOffsetText + yOffsetName,
shadow = true,
centered = true
)
val gender = pokedexEntityData.pokemon.gender
val speciesNameWidth = Minecraft.getInstance().font.width(speciesName.font(CobblemonResources.DEFAULT_LARGE))
if (gender != Gender.GENDERLESS) {
val isMale = gender == Gender.MALE
val textSymbol = if (isMale) "♂".text().bold() else "♀".text().bold()
drawScaledText(
context = graphics,
font = CobblemonResources.DEFAULT_LARGE,
text = textSymbol,
x = centerX + xOffset + xOffsetText + 2 + (speciesNameWidth / 2),
y = centerY + yOffset + yOffsetText + yOffsetName,
colour = if (isMale) 0x32CBFF else 0xFC5454,
shadow = true
)
}
if (usageContext.isPokemonInFocusOwned) {
blitk(
matrixStack = poseStack,
texture = caughtIndicator,
x = (centerX + xOffset + xOffsetText - 7 - (speciesNameWidth / 2)) / SCALE,
y = (centerY + yOffset + yOffsetText + yOffsetName + 2) / SCALE,
height = 10,
width = 10,
scale = SCALE
)
}
}
if (infoDisplayedCounter == 3) {
val typeText = lang("type.suffix", pokedexEntityData.getApparentForm().types.map { it.displayName.copy() }.reduce { acc, next -> acc.plus("/").plus(next) }).bold()
val typeWidth = Minecraft.getInstance().font.width(typeText.font(CobblemonResources.DEFAULT_LARGE))
// Split into 2 lines if text width is too long
if (typeWidth > (OUTER_INFO_FRAME_WIDTH - 8) && pokedexEntityData.getApparentForm().secondaryType !== null) {
val splitLines = typeText.string.split("/")
drawScaledText(
context = graphics,
font = CobblemonResources.DEFAULT_LARGE,
text = text(splitLines[0], "/").bold(),
x = centerX + xOffset + xOffsetText,
y = centerY + yOffset + yOffsetText - 4,
shadow = true,
centered = true
)
drawScaledText(
context = graphics,
font = CobblemonResources.DEFAULT_LARGE,
text = text(splitLines[1]).bold(),
x = centerX + xOffset + xOffsetText,
y = centerY + yOffset + yOffsetText + 3,
shadow = true,
centered = true
scale = scale
)
} else {
drawScaledText(
context = graphics,
font = CobblemonResources.DEFAULT_LARGE,
text = typeText,
text = textComponent,
x = centerX + xOffset + xOffsetText,
y = centerY + yOffset + yOffsetText,
shadow = true,
@@ -221,6 +313,26 @@ class PokedexScannerRenderer {
}
}
private fun resolveScanText(spec: BlockScanDefinition.ScanText, values: Map<String, Any?>): MutableComponent {
return when (spec) {
is BlockScanDefinition.ScanText.Literal -> text(spec.text)
is BlockScanDefinition.ScanText.Key -> anyToComponent(values[spec.key])
is BlockScanDefinition.ScanText.Translate -> lang(
spec.key,
*spec.with.map { resolveScanText(it, values) }.toTypedArray()
)
}
}
private fun anyToComponent(value: Any?): MutableComponent {
return when (value) {
null -> text("")
is MutableComponent -> value
is Component -> value.copy()
else -> text(value.toString())
}
}
fun renderScanRings(poseStack: PoseStack, usageContext: PokedexUsageContext, centerX: Int, centerY: Int, opacity: Float) {
val rotation = usageContext.usageIntervals % 360
@@ -275,6 +387,7 @@ class PokedexScannerRenderer {
val screenWidth = client.window.guiScaledWidth
val screenHeight = client.window.guiScaledHeight
val inCameraMode = usageContext.scannerMode == PokedexUsageContext.ScannerMode.CAMERA
RenderSystem.enableBlend()
@@ -288,16 +401,18 @@ class PokedexScannerRenderer {
val centerY = (screenHeight - (BASE_HEIGHT * scale)) / 2
val opacity = if (effectiveIntervals > 11) 0F else effectiveIntervals / PokedexUsageContext.TRANSITION_INTERVALS
val modeScreenTexture = if (inCameraMode) CAMERA_SCREEN else SCAN_SCREEN
blitk(matrixStack = matrices, texture = CobblemonClient.pokedexUsageContext.type.getTexturePath(), x = centerX / scale, y = centerY / scale, width = BASE_WIDTH, height = BASE_HEIGHT, scale = scale, alpha = opacity)
blitk(matrixStack = matrices, texture = SCAN_SCREEN, x = centerX / scale, y = centerY / scale, width = BASE_WIDTH, height = BASE_HEIGHT, scale = scale, alpha = opacity)
blitk(matrixStack = matrices, texture = modeScreenTexture, x = centerX / scale, y = centerY / scale, width = BASE_WIDTH, height = BASE_HEIGHT, scale = scale, alpha = opacity)
}
// Scanning overlay
val opacity = if (effectiveIntervals >= 10) 1F else effectiveIntervals/10F
// Draw scan lines
val interlacePos = ceil((usageContext.usageIntervals % 14) * 0.5) * 0.5
val scanLineStep = if (inCameraMode) 6 else 4
for (i in 0 until screenHeight) {
if (i % 4 == 0) blitk(matrixStack = matrices, texture = SCAN_OVERLAY_LINES, x = 0, y = i - interlacePos, width = screenWidth, height = 4, alpha = opacity)
if (i % scanLineStep == 0) blitk(matrixStack = matrices, texture = SCAN_OVERLAY_LINES, x = 0, y = i - interlacePos, width = screenWidth, height = 4, alpha = opacity)
}
// Draw borders
@@ -321,121 +436,197 @@ class PokedexScannerRenderer {
val centerX = screenWidth / 2
val centerY = screenHeight / 2
val modeLabel = if (inCameraMode) lang("ui.pokedex.scan.mode.camera").bold() else lang("ui.pokedex.scan.mode.scanning").bold()
drawScaledText(
context = graphics,
font = CobblemonResources.DEFAULT_LARGE,
text = modeLabel,
x = centerX,
y = 2,
shadow = true,
centered = true,
scale = 0.6F
)
drawScaledText(
context = graphics,
text = lang("ui.pokedex.scan.mode.toggle_hint", CobblemonKeyBinds.SEND_OUT_POKEMON.translatedKeyMessage),
x = centerX,
y = 10,
shadow = true,
centered = true,
scale = 0.5F
)
// Scan info frame
renderInfoFrames(graphics, matrices, usageContext, centerX, centerY, opacity)
if (!inCameraMode) {
// Scan info frame
renderInfoFrames(graphics, matrices, usageContext, centerX, centerY, opacity)
// Scan rings
renderScanRings(matrices, usageContext, centerX, centerY, opacity)
// Scan rings
renderScanRings(matrices, usageContext, centerX, centerY, opacity)
if (usageContext.displayRegisterInfoIntervals > 0) {
blitk(
matrixStack = matrices,
texture = CENTER_INFO_FRAME,
x = centerX - (CENTER_INFO_FRAME_WIDTH / 2),
y = centerY - (CENTER_INFO_FRAME_HEIGHT / 2),
width = CENTER_INFO_FRAME_WIDTH,
height = CENTER_INFO_FRAME_HEIGHT,
textureHeight = CENTER_INFO_FRAME_HEIGHT * 6,
vOffset = min(ceil(usageContext.displayRegisterInfoIntervals), PokedexUsageContext.CENTER_INFO_DISPLAY_INTERVALS) * CENTER_INFO_FRAME_HEIGHT,
alpha = opacity
)
if (usageContext.displayRegisterInfoIntervals >= PokedexUsageContext.CENTER_INFO_DISPLAY_INTERVALS) {
drawScaledText(
context = graphics,
font = CobblemonResources.DEFAULT_LARGE,
text = getRegisterText(usageContext.newPokemonInfo),
x = centerX,
y = centerY - (CENTER_INFO_FRAME_HEIGHT / 2) + 4,
shadow = true,
centered = true
if (usageContext.displayRegisterInfoIntervals > 0) {
blitk(
matrixStack = matrices,
texture = CENTER_INFO_FRAME,
x = centerX - (CENTER_INFO_FRAME_WIDTH / 2),
y = centerY - (CENTER_INFO_FRAME_HEIGHT / 2),
width = CENTER_INFO_FRAME_WIDTH,
height = CENTER_INFO_FRAME_HEIGHT,
textureHeight = CENTER_INFO_FRAME_HEIGHT * 6,
vOffset = min(ceil(usageContext.displayRegisterInfoIntervals), PokedexUsageContext.CENTER_INFO_DISPLAY_INTERVALS) * CENTER_INFO_FRAME_HEIGHT,
alpha = opacity
)
if (usageContext.displayRegisterInfoIntervals >= PokedexUsageContext.CENTER_INFO_DISPLAY_INTERVALS) {
drawScaledText(
context = graphics,
font = CobblemonResources.DEFAULT_LARGE,
text = getRegisterText(usageContext.newPokemonInfo),
x = centerX,
y = centerY - (CENTER_INFO_FRAME_HEIGHT / 2) + 4,
shadow = true,
centered = true
)
}
} else {
if (usageContext.scanningProgress > 0) {
// If scan progress reaches max ticks - 10,decrement opacity using last 10 ticks
// else increment opacity using scan progress
var centerOpacity = (
if (usageContext.scanningProgress > (PokedexUsageContext.MAX_SCAN_PROGRESS - 10))
(PokedexUsageContext.MAX_SCAN_PROGRESS - usageContext.scanningProgress)
else usageContext.scanningProgress
) * 0.1F
blitk(
matrixStack = matrices,
texture = UNKNOWN_MARK,
x = centerX - (UNKNOWN_MARK_WIDTH / 2),
y = centerY - (UNKNOWN_MARK_HEIGHT / 2) + 2,
width = UNKNOWN_MARK_WIDTH,
height = UNKNOWN_MARK_HEIGHT,
alpha = max(0F, min(opacity, centerOpacity))
)
matrices.pushPose()
matrices.translate(centerX.toFloat(), centerY.toFloat(), 0.0f)
matrices.pushPose()
matrices.mulPose(Quaternionf().rotateZ(Math.toRadians(usageContext.innerRingRotation * 0.5).toFloat()))
blitk(
matrixStack = matrices,
texture = POINTER,
x = -POINTER_WIDTH - POINTER_OFFSET,
y = -(POINTER_HEIGHT / 2),
width = POINTER_WIDTH,
height = POINTER_HEIGHT,
textureWidth = POINTER_WIDTH * 2,
alpha = max(0F, min(opacity, centerOpacity))
)
blitk(
matrixStack = matrices,
texture = POINTER,
x = POINTER_OFFSET,
y = -(POINTER_HEIGHT / 2),
width = POINTER_WIDTH,
height = POINTER_HEIGHT,
textureWidth = POINTER_WIDTH * 2,
uOffset = POINTER_WIDTH,
alpha = max(0F, min(opacity, centerOpacity))
)
matrices.popPose()
matrices.popPose()
} else if (usageContext.viewInfoTicks > 0) {
val pointerOpacity = usageContext.viewInfoTicks * 0.1F
blitk(
matrixStack = matrices,
texture = POINTER,
x = centerX - POINTER_WIDTH - POINTER_OFFSET + usageContext.viewInfoTicks,
y = centerY - (POINTER_HEIGHT / 2),
width = POINTER_WIDTH,
height = POINTER_HEIGHT,
textureWidth = POINTER_WIDTH * 2,
alpha = max(0F, min(opacity, pointerOpacity))
)
blitk(
matrixStack = matrices,
texture = POINTER,
x = centerX + POINTER_OFFSET - usageContext.viewInfoTicks,
y = centerY - (POINTER_HEIGHT / 2),
width = POINTER_WIDTH,
height = POINTER_HEIGHT,
textureWidth = POINTER_WIDTH * 2,
uOffset = POINTER_WIDTH,
alpha = max(0F, min(opacity, pointerOpacity))
)
}
}
} else {
if (usageContext.scanningProgress > 0) {
// If scan progress reaches max ticks - 10,decrement opacity using last 10 ticks
// else increment opacity using scan progress
var centerOpacity = (
if (usageContext.scanningProgress > (PokedexUsageContext.MAX_SCAN_PROGRESS - 10))
(PokedexUsageContext.MAX_SCAN_PROGRESS - usageContext.scanningProgress)
else usageContext.scanningProgress
) * 0.1F
blitk(
matrixStack = matrices,
texture = CAMERA_FRAME,
x = centerX - (CAMERA_FRAME_DIAMETER / 2),
y = centerY - (CAMERA_FRAME_DIAMETER / 2),
width = CAMERA_FRAME_DIAMETER,
height = CAMERA_FRAME_DIAMETER,
alpha = opacity
)
renderPhotoCaptureDebugFrame(graphics, screenWidth, screenHeight)
drawScaledText(
context = graphics,
text = lang("ui.pokedex.scan.mode.capture_hint"),
x = centerX,
y = centerY + 48,
shadow = true,
centered = true,
scale = 0.6F
)
}
blitk(
matrixStack = matrices,
texture = UNKNOWN_MARK,
x = centerX - (UNKNOWN_MARK_WIDTH / 2),
y = centerY - (UNKNOWN_MARK_HEIGHT / 2) + 2,
width = UNKNOWN_MARK_WIDTH,
height = UNKNOWN_MARK_HEIGHT,
alpha = max(0F, min(opacity, centerOpacity))
)
matrices.pushPose()
matrices.translate(centerX.toFloat(), centerY.toFloat(), 0.0f)
matrices.pushPose()
matrices.mulPose(Quaternionf().rotateZ(Math.toRadians(usageContext.innerRingRotation * 0.5).toFloat()))
blitk(
matrixStack = matrices,
texture = POINTER,
x = -POINTER_WIDTH - POINTER_OFFSET,
y = -(POINTER_HEIGHT / 2),
width = POINTER_WIDTH,
height = POINTER_HEIGHT,
textureWidth = POINTER_WIDTH * 2,
alpha = max(0F, min(opacity, centerOpacity))
)
blitk(
matrixStack = matrices,
texture = POINTER,
x = POINTER_OFFSET,
y = -(POINTER_HEIGHT / 2),
width = POINTER_WIDTH,
height = POINTER_HEIGHT,
textureWidth = POINTER_WIDTH * 2,
uOffset = POINTER_WIDTH,
alpha = max(0F, min(opacity, centerOpacity))
)
matrices.popPose()
matrices.popPose()
} else if (usageContext.viewInfoTicks > 0) {
val pointerOpacity = usageContext.viewInfoTicks * 0.1F
blitk(
matrixStack = matrices,
texture = POINTER,
x = centerX - POINTER_WIDTH - POINTER_OFFSET + usageContext.viewInfoTicks,
y = centerY - (POINTER_HEIGHT / 2),
width = POINTER_WIDTH,
height = POINTER_HEIGHT,
textureWidth = POINTER_WIDTH * 2,
alpha = max(0F, min(opacity, pointerOpacity))
)
blitk(
matrixStack = matrices,
texture = POINTER,
x = centerX + POINTER_OFFSET - usageContext.viewInfoTicks,
y = centerY - (POINTER_HEIGHT / 2),
width = POINTER_WIDTH,
height = POINTER_HEIGHT,
textureWidth = POINTER_WIDTH * 2,
uOffset = POINTER_WIDTH,
alpha = max(0F, min(opacity, pointerOpacity))
)
}
val shutterClosure = usageContext.getShutterBlinkClosure()
if (shutterClosure > 0F) {
renderBlinkShutter(graphics, screenWidth, screenHeight, shutterClosure)
usageContext.onShutterRendered()
}
RenderSystem.disableBlend()
}
private fun renderPhotoCaptureDebugFrame(graphics: GuiGraphics, screenWidth: Int, screenHeight: Int) {
if (!PHOTO_CAPTURE_DEBUG_FRAME_ENABLED) return
val bounds = PokedexUsageContext.photoCaptureBoundsForGui(screenWidth, screenHeight)
val frameCenterX = bounds.centerX
val frameCenterY = bounds.centerY
graphics.fill(bounds.left, bounds.top, bounds.right, bounds.top + 1, PHOTO_CAPTURE_DEBUG_FRAME_COLOR)
graphics.fill(bounds.left, bounds.bottom - 1, bounds.right, bounds.bottom, PHOTO_CAPTURE_DEBUG_FRAME_COLOR)
graphics.fill(bounds.left, bounds.top, bounds.left + 1, bounds.bottom, PHOTO_CAPTURE_DEBUG_FRAME_COLOR)
graphics.fill(bounds.right - 1, bounds.top, bounds.right, bounds.bottom, PHOTO_CAPTURE_DEBUG_FRAME_COLOR)
graphics.fill(frameCenterX - 3, frameCenterY, frameCenterX + 4, frameCenterY + 1, PHOTO_CAPTURE_DEBUG_FRAME_CENTER_COLOR)
graphics.fill(frameCenterX, frameCenterY - 3, frameCenterX + 1, frameCenterY + 4, PHOTO_CAPTURE_DEBUG_FRAME_CENTER_COLOR)
}
private fun renderBlinkShutter(graphics: GuiGraphics, screenWidth: Int, screenHeight: Int, closure: Float) {
val clampedClosure = closure.coerceIn(0F, 1F)
val alpha = (BLINK_SHUTTER_MAX_ALPHA * clampedClosure).toInt().coerceIn(0, BLINK_SHUTTER_MAX_ALPHA)
val color = (alpha shl 24) or BLINK_SHUTTER_RGB
val halfClosedHeight = ((screenHeight / 2F) * clampedClosure).roundToInt()
val centerOverlap = BLINK_SHUTTER_CENTER_OVERLAP
graphics.fill(0, 0, screenWidth, halfClosedHeight + centerOverlap, color)
graphics.fill(0, screenHeight - halfClosedHeight - centerOverlap, screenWidth, screenHeight, color)
}
fun onRenderOverlay(graphics: GuiGraphics, tickCounter: DeltaTracker) {
if ((CobblemonClient.pokedexUsageContext.scanningGuiOpen || CobblemonClient.pokedexUsageContext.transitionIntervals > 0) && Minecraft.getInstance().options.cameraType.isFirstPerson) {
CobblemonClient.pokedexUsageContext.cacheScannerFrame()
CobblemonClient.pokedexUsageContext.enqueuePhotoDebugRender()
val tickDelta = tickCounter.realtimeDeltaTicks
renderScanOverlay(graphics, tickDelta)
}
}
}
}
@@ -0,0 +1,88 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of 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.pokedex.photo
import com.cobblemon.mod.common.api.data.JsonDataRegistry
import com.cobblemon.mod.common.api.reactive.SimpleObservable
import com.cobblemon.mod.common.util.cobblemonResource
import com.google.gson.GsonBuilder
import com.google.gson.annotations.SerializedName
import com.google.gson.reflect.TypeToken
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import net.minecraft.server.packs.PackType
import java.util.Locale
object PokedexPhotoAnimationTypes : JsonDataRegistry<PokedexPhotoAnimationType> {
override val id = cobblemonResource("pokedex_photo_animation_types")
override val type = PackType.CLIENT_RESOURCES
override val observable = SimpleObservable<PokedexPhotoAnimationTypes>()
override val gson = GsonBuilder()
.disableHtmlEscaping()
.setPrettyPrinting()
.create()
override val typeToken: TypeToken<PokedexPhotoAnimationType> = TypeToken.get(PokedexPhotoAnimationType::class.java)
override val resourcePath = "pokedex/photo_animation_types"
private val types = linkedMapOf<ResourceLocation, PokedexPhotoAnimationType>()
override fun sync(player: ServerPlayer) {}
override fun reload(data: Map<ResourceLocation, PokedexPhotoAnimationType>) {
types.clear()
types.putAll(data)
observable.emit(this)
}
fun bestMatchFor(animationNames: Collection<String>): PokedexPhotoAnimationType? {
val normalizedNames = animationNames
.map(::normalize)
.filter { it.isNotBlank() }
.toSet()
if (normalizedNames.isEmpty()) return null
return types.values
.filter { type -> normalizedNames.any(type.match::matches) }
.maxWithOrNull(compareBy<PokedexPhotoAnimationType> { it.priority }.thenBy { it.poseScore })
}
private fun normalize(value: String): String {
return value.lowercase(Locale.ROOT).substringAfterLast('.')
}
}
class PokedexPhotoAnimationType {
var priority: Int = 0
var poseScore: Int = 0
var match: PokedexPhotoAnimationMatch = PokedexPhotoAnimationMatch()
}
class PokedexPhotoAnimationMatch {
var exact: List<String> = emptyList()
var contains: List<String> = emptyList()
@SerializedName("contains_any")
var containsAny: List<String> = emptyList()
@SerializedName("not_contains")
var notContains: List<String> = emptyList()
fun matches(animationName: String): Boolean {
val normalized = normalize(animationName)
if (notContains.any { normalized.contains(normalize(it)) }) return false
val hasExact = exact.any { normalized == normalize(it) }
val hasContains = contains.isNotEmpty() && contains.all { normalized.contains(normalize(it)) }
val hasContainsAny = containsAny.any { normalized.contains(normalize(it)) }
return hasExact || hasContains || hasContainsAny
}
private fun normalize(value: String): String {
return value.lowercase(Locale.ROOT).substringAfterLast('.')
}
}
@@ -0,0 +1,114 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.pokedex.photo
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.net.messages.client.ui.PokedexPhotoBatchPacket
import com.cobblemon.mod.common.net.messages.client.ui.RequestPokedexPhotoBatchPacket
import com.cobblemon.mod.common.net.messages.client.ui.RequestPokedexPhotoGalleryPacket
import com.cobblemon.mod.common.pokedex.photo.PokedexPhotoMetadata
import com.cobblemon.mod.common.util.cobblemonResource
import com.mojang.blaze3d.platform.NativeImage
import net.minecraft.client.Minecraft
import net.minecraft.client.renderer.texture.DynamicTexture
import net.minecraft.resources.ResourceLocation
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import javax.imageio.ImageIO
class PokedexPhotoClientStore {
private data class CachedTexture(val location: ResourceLocation, val texture: DynamicTexture)
private val photos = mutableListOf<PokedexPhotoMetadata>()
private val textures = mutableMapOf<String, CachedTexture>()
private val pendingPhotos = mutableSetOf<String>()
var maxPhotosAllowed: Int = 0
private set
fun clear() {
textures.values.forEach { it.texture.close() }
textures.clear()
pendingPhotos.clear()
photos.clear()
maxPhotosAllowed = 0
}
fun applyGallery(photoList: List<PokedexPhotoMetadata>, maxAllowed: Int) {
photos.clear()
photos.addAll(photoList.sortedByDescending { it.createdAt })
maxPhotosAllowed = maxAllowed
val activeIds = photos.map { it.id }.toSet()
val stale = textures.keys.filterNot { it in activeIds }
stale.forEach { id ->
textures.remove(id)?.texture?.close()
pendingPhotos.remove(id)
}
}
fun applyPhotoBatch(entries: List<PokedexPhotoBatchPacket.Entry>) {
entries.forEach { entry ->
// Skip entries for photos that were evicted while the response was in flight.
if (entry.photoId !in pendingPhotos) return@forEach
decodeAndRegister(entry.photoId, entry.imageData)
}
}
private fun decodeAndRegister(photoId: String, imageData: ByteArray) {
pendingPhotos.remove(photoId)
textures.remove(photoId)?.texture?.close()
val location = runCatching {
// Server stores photos as JPEG; NativeImage only reads PNG, so convert in memory first.
val pngBytes = ByteArrayInputStream(imageData).use { input ->
val bufferedImage = ImageIO.read(input) ?: throw java.io.IOException("Unable to decode image")
ByteArrayOutputStream().also { out -> ImageIO.write(bufferedImage, "PNG", out) }.toByteArray()
}
ByteArrayInputStream(pngBytes).use { stream ->
val image = NativeImage.read(stream)
val texture = DynamicTexture(image)
val resource = cobblemonResource("pokedex/gallery/${photoId.lowercase()}")
Minecraft.getInstance().textureManager.register(resource, texture)
textures[photoId] = CachedTexture(resource, texture)
resource
}
}.onFailure {
Cobblemon.LOGGER.warn("Failed to decode photo {}", photoId, it)
}.getOrNull()
if (location == null) {
textures.remove(photoId)?.texture?.close()
}
}
fun getPhotos(): List<PokedexPhotoMetadata> = photos
fun getTexture(photoId: String): ResourceLocation? = textures[photoId]?.location
fun requestGallery() {
RequestPokedexPhotoGalleryPacket().sendToServer()
}
/** Request any of [ids] that are not cached or already in flight, in a single batch. */
fun prefetch(ids: Collection<String>) {
val toRequest = ids.filter { it !in pendingPhotos && it !in textures.keys }.distinct()
if (toRequest.isEmpty()) return
pendingPhotos.addAll(toRequest)
RequestPokedexPhotoBatchPacket(toRequest).sendToServer()
}
/** Close and drop any textures (and pending requests) whose ids are not in [keepIds]. */
fun retain(keepIds: Set<String>) {
val toEvict = textures.keys.filterNot { it in keepIds }.toList()
toEvict.forEach { id -> textures.remove(id)?.texture?.close() }
val stalePending = pendingPhotos.filterNot { it in keepIds }.toSet()
pendingPhotos.removeAll(stalePending)
}
}
@@ -0,0 +1,174 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of 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.pokedex.photo
import com.cobblemon.mod.common.pokedex.photo.PokedexPhotoMetadata
import com.cobblemon.mod.common.pokedex.photo.PokedexPhotoRatings
import java.util.Locale
object PokedexPhotoSearch {
enum class SortField {
TIME_OF_DAY,
BIOME,
SPECIES,
POINTS;
fun next(): SortField {
val values = entries
return values[(ordinal + 1) % values.size]
}
}
enum class SortDirection {
ASCENDING,
DESCENDING;
fun toggled(): SortDirection = if (this == ASCENDING) DESCENDING else ASCENDING
}
private const val DAY_LENGTH_TICKS = 24_000L
private val nonAlphaNumeric = Regex("[^a-z0-9]+")
private val repeatedWhitespace = Regex("\\s+")
fun filter(photos: List<PokedexPhotoMetadata>, rawQuery: String): List<PokedexPhotoMetadata> {
val tokens = parseQueryTokens(rawQuery)
if (tokens.isEmpty()) {
return photos
}
return photos.filter { photo ->
val haystacks = buildHaystacks(photo)
tokens.all { token -> haystacks.any { haystack -> haystack.contains(token) } }
}
}
fun filterAndSort(
photos: List<PokedexPhotoMetadata>,
rawQuery: String,
sortField: SortField,
sortDirection: SortDirection
): List<PokedexPhotoMetadata> {
val filtered = filter(photos, rawQuery)
val comparator = when (sortField) {
SortField.TIME_OF_DAY -> compareBy<PokedexPhotoMetadata> { normalizedTime(it.timeOfDayTicks) }
.thenByDescending { it.createdAt }
SortField.BIOME -> compareBy<PokedexPhotoMetadata> { sortableResourceName(it.biome) }
.thenByDescending { it.createdAt }
SortField.SPECIES -> compareBy<PokedexPhotoMetadata> { primarySpeciesName(it) }
.thenByDescending { it.createdAt }
SortField.POINTS -> compareBy<PokedexPhotoMetadata> { PokedexPhotoRatings.starsForScore(it.qualityScore) }
.thenBy { it.qualityScore }
.thenByDescending { it.createdAt }
}
return if (sortDirection == SortDirection.ASCENDING) {
filtered.sortedWith(comparator)
} else {
filtered.sortedWith(comparator.reversed())
}
}
fun sortFieldLabel(sortField: SortField): String {
return when (sortField) {
SortField.TIME_OF_DAY -> "Time"
SortField.BIOME -> "Biome"
SortField.SPECIES -> "Species"
SortField.POINTS -> "Stars"
}
}
fun sortDirectionLabel(sortDirection: SortDirection): String {
return when (sortDirection) {
SortDirection.ASCENDING -> "Asc"
SortDirection.DESCENDING -> "Desc"
}
}
fun parseQueryTokens(rawQuery: String): List<String> {
return rawQuery
.split(',')
.map(::normalize)
.filter { it.isNotEmpty() }
}
private fun buildHaystacks(photo: PokedexPhotoMetadata): Set<String> {
val haystacks = linkedSetOf<String>()
haystacks += normalize(photo.id)
haystacks += normalize(photo.index.toString())
haystacks += normalize(photo.qualityScore.toString())
haystacks += normalize("quality ${photo.qualityScore}")
addResourceIdTerms(haystacks, photo.biome)
addResourceIdTerms(haystacks, photo.dimension)
addTimeTerms(haystacks, photo.timeOfDayTicks)
photo.detectedSpecies.forEach { speciesId ->
addResourceIdTerms(haystacks, speciesId)
}
return haystacks.filterTo(linkedSetOf()) { it.isNotEmpty() }
}
private fun addResourceIdTerms(haystacks: MutableSet<String>, value: String) {
val lower = value.lowercase(Locale.ROOT)
haystacks += normalize(lower)
val path = lower.substringAfter(':', lower)
haystacks += normalize(path)
haystacks += normalize(path.replace('_', ' '))
haystacks += normalize(path.replace('-', ' '))
haystacks += normalize(path.replace('_', ' ').replace('-', ' '))
}
private fun addTimeTerms(haystacks: MutableSet<String>, ticks: Long) {
val time = normalizedTime(ticks)
haystacks += normalize(time.toString())
haystacks += normalize("time $time")
haystacks += normalize("ticks $time")
val labels = mutableListOf(
if (time < 12_000L) "day" else "night"
)
when (time) {
in 0L..999L, in 23_000L..23_999L -> labels += listOf("sunrise", "dawn", "morning")
in 1_000L..5_999L -> labels += listOf("morning", "day")
in 6_000L..8_999L -> labels += listOf("day", "noon")
in 9_000L..11_999L -> labels += listOf("afternoon", "day")
in 12_000L..12_999L -> labels += listOf("sunset", "dusk", "evening")
in 13_000L..17_999L -> labels += listOf("night", "evening")
else -> labels += listOf("night", "midnight")
}
labels.forEach { haystacks += normalize(it) }
}
private fun normalizedTime(ticks: Long): Long {
return ((ticks % DAY_LENGTH_TICKS) + DAY_LENGTH_TICKS) % DAY_LENGTH_TICKS
}
private fun sortableResourceName(value: String): String {
return normalize(value.substringAfter(':', value))
}
private fun primarySpeciesName(photo: PokedexPhotoMetadata): String {
val primary = photo.qualitySubjects.firstOrNull { it.primarySubject }?.species
?: photo.detectedSpecies.firstOrNull()
?: ""
return sortableResourceName(primary)
}
private fun normalize(value: String): String {
return value
.lowercase(Locale.ROOT)
.replace(nonAlphaNumeric, " ")
.replace(repeatedWhitespace, " ")
.trim()
}
}
@@ -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.pokedex.photo
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.net.messages.client.ui.PrintedPhotoAssetBatchPacket
import com.cobblemon.mod.common.net.messages.client.ui.RequestPrintedPhotoAssetBatchPacket
import com.cobblemon.mod.common.util.cobblemonResource
import com.mojang.blaze3d.platform.NativeImage
import net.minecraft.client.Minecraft
import net.minecraft.client.renderer.texture.DynamicTexture
import net.minecraft.resources.ResourceLocation
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import javax.imageio.ImageIO
/**
* Client-side cache of printed-photo asset textures, indexed by asset id.
*
* Bounded LRU; evicted textures are closed to free GPU memory.
* Issuing the same id while a request is pending or while the texture is cached is a no-op.
*/
class PrintedPhotoAssetClientStore {
private data class CachedTexture(val location: ResourceLocation, val texture: DynamicTexture)
private val maxEntries = 64
private val textures = object : LinkedHashMap<Int, CachedTexture>(16, 0.75f, true) {
override fun removeEldestEntry(eldest: Map.Entry<Int, CachedTexture>?): Boolean {
if (size <= maxEntries) return false
eldest?.value?.texture?.close()
return true
}
}
private val pending = mutableSetOf<Int>()
fun clear() {
textures.values.forEach { it.texture.close() }
textures.clear()
pending.clear()
}
fun getTexture(assetId: Int): ResourceLocation? = textures[assetId]?.location
fun requestAssets(assetIds: Collection<Int>) {
val toRequest = assetIds.filter { it > 0 && it !in pending && it !in textures.keys }.distinct()
if (toRequest.isEmpty()) return
pending.addAll(toRequest)
RequestPrintedPhotoAssetBatchPacket(toRequest).sendToServer()
}
fun applyAssetBatch(entries: List<PrintedPhotoAssetBatchPacket.Entry>) {
entries.forEach { entry ->
pending.remove(entry.assetId)
decodeAndRegister(entry.assetId, entry.imageData)
}
}
private fun decodeAndRegister(assetId: Int, imageData: ByteArray) {
textures.remove(assetId)?.texture?.close()
runCatching {
// Convert JPEG to PNG in memory because NativeImage only reads PNG.
val pngBytes = ByteArrayInputStream(imageData).use { input ->
val bufferedImage = ImageIO.read(input) ?: throw java.io.IOException("Unable to decode printed photo asset")
ByteArrayOutputStream().also { out -> ImageIO.write(bufferedImage, "PNG", out) }.toByteArray()
}
ByteArrayInputStream(pngBytes).use { stream ->
val image = NativeImage.read(stream)
val texture = DynamicTexture(image)
val resource = cobblemonResource("printed_photo/asset_$assetId")
Minecraft.getInstance().textureManager.register(resource, texture)
textures[assetId] = CachedTexture(resource, texture)
}
}.onFailure {
Cobblemon.LOGGER.warn("Failed to decode printed photo asset {}", assetId, it)
}
}
}
@@ -12,6 +12,9 @@ import com.cobblemon.mod.common.api.mulch.MulchVariant
import com.cobblemon.mod.common.block.BerryBlock
import com.cobblemon.mod.common.block.entity.BerryBlockEntity
import com.cobblemon.mod.common.client.CobblemonBakingOverrides
import com.cobblemon.mod.common.client.CobblemonClient
import com.cobblemon.mod.common.pokedex.scanner.PokedexUsageContext
import com.cobblemon.mod.common.pokedex.scanner.block.BerryBlockScanProvider
import com.cobblemon.mod.common.client.render.atlas.CobblemonAtlases.BERRY_SPRITE_ATLAS
import com.cobblemon.mod.common.client.render.models.blockbench.repository.BerryModelRepository
import com.cobblemon.mod.common.client.render.models.blockbench.setPosition
@@ -81,6 +84,13 @@ class BerryBlockRenderer(private val context: BlockEntityRendererProvider.Contex
// matrices.popPose()
// }
drawMulch(matrices, vertexConsumers, entity, light, overlay)
val ctx = CobblemonClient.pokedexUsageContext
if (ctx.scanningGuiOpen && ctx.scannerMode == PokedexUsageContext.ScannerMode.SCANNING) {
val scanKey = BerryBlockScanProvider.getScanKey(entity)
val scanned = CobblemonClient.clientScannedBlocksData.hasScanned(scanKey)
ScanIndicatorRenderer.render(matrices, vertexConsumers, entity, scanned)
}
}
private fun drawMulch(
@@ -0,0 +1,103 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.render.block
import com.cobblemon.mod.common.block.PhotoPrinterBlock
import com.cobblemon.mod.common.block.entity.PhotoPrinterBlockEntity
import com.mojang.blaze3d.vertex.PoseStack
import com.mojang.math.Axis
import net.minecraft.client.Minecraft
import net.minecraft.client.renderer.LevelRenderer
import net.minecraft.client.renderer.LightTexture
import net.minecraft.client.renderer.MultiBufferSource
import net.minecraft.client.renderer.blockentity.BlockEntityRenderer
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider
import net.minecraft.client.renderer.texture.OverlayTexture
import net.minecraft.core.Direction
import net.minecraft.world.item.ItemDisplayContext
import net.minecraft.world.level.BlockAndTintGetter
class PhotoPrinterBlockEntityRenderer(
@Suppress("UNUSED_PARAMETER") context: BlockEntityRendererProvider.Context
) : BlockEntityRenderer<PhotoPrinterBlockEntity> {
override fun render(
blockEntity: PhotoPrinterBlockEntity,
partialTicks: Float,
matrices: PoseStack,
bufferSource: MultiBufferSource,
light: Int,
overlay: Int
) {
val printingStack = if (!blockEntity.printingStack.isEmpty) {
blockEntity.printingStack
} else {
blockEntity.getItem(PhotoPrinterBlockEntity.RESULT_SLOT)
}
if (printingStack.isEmpty) return
val progress = if (blockEntity.printingStack.isEmpty) {
1F
} else {
val progressMax = blockEntity.printProgressMax.coerceAtLeast(1)
((blockEntity.printProgress + partialTicks) / progressMax).coerceIn(0F, 1F)
}
val easedProgress = progress * progress * (3F - (2F * progress))
val facing = blockEntity.blockState.getValue(PhotoPrinterBlock.FACING)
val slide = -0.12F + (easedProgress * 0.55F)
val packedLight = resolvePhotoLight(blockEntity, facing, light)
matrices.pushPose()
matrices.translate(
0.5F + (facing.stepX * slide),
1.05F, // we can change this to alter the height of where the photo slides out
0.5F + (facing.stepZ * slide)
)
matrices.mulPose(Axis.YP.rotationDegrees(rotationFor(facing)))
matrices.mulPose(Axis.XP.rotationDegrees(90F))
matrices.scale(0.55F, 0.55F, 0.55F) // we can use this to change the scale of the animated photo
Minecraft.getInstance().itemRenderer.renderStatic(
printingStack,
ItemDisplayContext.FIXED,
packedLight,
OverlayTexture.NO_OVERLAY,
matrices,
bufferSource,
blockEntity.level,
0
)
matrices.popPose()
}
private fun rotationFor(facing: Direction): Float {
return when (facing) {
Direction.NORTH -> 180F
Direction.EAST -> 270F
Direction.SOUTH -> 0F
Direction.WEST -> 90F
else -> 0F
}
}
private fun resolvePhotoLight(blockEntity: PhotoPrinterBlockEntity, facing: Direction, fallback: Int): Int {
val level = blockEntity.level as? BlockAndTintGetter ?: return fallback
val pos = blockEntity.blockPos
val samples = listOf(
pos.above(),
pos.relative(facing),
pos.relative(facing).above(),
pos
).map { LevelRenderer.getLightColor(level, it) } + fallback
val sky = samples.maxOf(LightTexture::sky)
val block = samples.maxOf(LightTexture::block)
return LightTexture.pack(block, sky)
}
}
@@ -0,0 +1,124 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.render.block
import com.cobblemon.mod.common.block.PrintedPhotoDisplayBlock
import com.cobblemon.mod.common.block.entity.PrintedPhotoDisplayBlockEntity
import com.cobblemon.mod.common.client.CobblemonClient
import com.cobblemon.mod.common.pokedex.scanner.PokedexUsageContext
import com.cobblemon.mod.common.pokedex.scanner.block.PrintedPhotoBlockScanProvider
import com.mojang.blaze3d.vertex.PoseStack
import com.mojang.blaze3d.vertex.VertexConsumer
import net.minecraft.client.renderer.MultiBufferSource
import net.minecraft.client.renderer.RenderType
import net.minecraft.client.renderer.blockentity.BlockEntityRenderer
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider
import net.minecraft.client.renderer.texture.OverlayTexture
import net.minecraft.resources.ResourceLocation
class PrintedPhotoDisplayBlockEntityRenderer(
@Suppress("UNUSED_PARAMETER") context: BlockEntityRendererProvider.Context
) : BlockEntityRenderer<PrintedPhotoDisplayBlockEntity> {
companion object {
private val PLACEHOLDER: ResourceLocation = ResourceLocation.withDefaultNamespace("textures/item/map.png")
}
override fun render(
be: PrintedPhotoDisplayBlockEntity,
partialTicks: Float,
matrices: PoseStack,
bufferSource: MultiBufferSource,
light: Int,
overlay: Int
) {
val component = be.component
val store = CobblemonClient.printedPhotoAssetStore
val texture = if (component != null) {
store.getTexture(component.assetId) ?: run {
store.requestAssets(listOf(component.assetId))
PLACEHOLDER
}
} else PLACEHOLDER
val buffer = bufferSource.getBuffer(RenderType.entityCutout(texture))
val axes = PrintedPhotoDisplayBlock.resolveAxes(be.blockState)
drawQuad(buffer, matrices, light, axes, be.cellX, be.cellY, be.gridSize)
val ctx = CobblemonClient.pokedexUsageContext
if (ctx.scanningGuiOpen && ctx.scannerMode == PokedexUsageContext.ScannerMode.SCANNING) {
val scanKey = PrintedPhotoBlockScanProvider.getScanKey(be)
val scanned = CobblemonClient.clientScannedBlocksData.hasScanned(scanKey)
ScanIndicatorRenderer.render(matrices, bufferSource, be, scanned)
}
}
private fun drawQuad(
consumer: VertexConsumer,
matrices: PoseStack,
light: Int,
axes: PrintedPhotoDisplayBlock.SurfaceAxes,
cellX: Int,
cellY: Int,
gridSize: Int
) {
val pose = matrices.last()
val matrix = pose.pose()
val half = 0.5F
val normalX = axes.normal.stepX.toFloat()
val normalY = axes.normal.stepY.toFloat()
val normalZ = axes.normal.stepZ.toFloat()
val rightX = axes.right.stepX.toFloat()
val rightY = axes.right.stepY.toFloat()
val rightZ = axes.right.stepZ.toFloat()
val upX = axes.up.stepX.toFloat()
val upY = axes.up.stepY.toFloat()
val upZ = axes.up.stepZ.toFloat()
val offset = -0.499F
val centreX = 0.5F + normalX * offset
val centreY = 0.5F + normalY * offset
val centreZ = 0.5F + normalZ * offset
val size = gridSize.coerceIn(1, 3)
val tileX = cellX.coerceIn(0, size - 1)
val tileY = cellY.coerceIn(0, size - 1)
val inv = 1F / size.toFloat()
val u0 = tileX * inv
val u1 = (tileX + 1) * inv
val v0 = 1F - (tileY + 1) * inv
val v1 = 1F - tileY * inv
val p0x = centreX - rightX * half - upX * half
val p0y = centreY - rightY * half - upY * half
val p0z = centreZ - rightZ * half - upZ * half
val p1x = centreX + rightX * half - upX * half
val p1y = centreY + rightY * half - upY * half
val p1z = centreZ + rightZ * half - upZ * half
val p2x = centreX + rightX * half + upX * half
val p2y = centreY + rightY * half + upY * half
val p2z = centreZ + rightZ * half + upZ * half
val p3x = centreX - rightX * half + upX * half
val p3y = centreY - rightY * half + upY * half
val p3z = centreZ - rightZ * half + upZ * half
consumer.addVertex(matrix, p0x, p0y, p0z)
.setColor(255, 255, 255, 255).setUv(u0, v1)
.setOverlay(OverlayTexture.NO_OVERLAY).setLight(light).setNormal(pose, normalX, normalY, normalZ)
consumer.addVertex(matrix, p1x, p1y, p1z)
.setColor(255, 255, 255, 255).setUv(u1, v1)
.setOverlay(OverlayTexture.NO_OVERLAY).setLight(light).setNormal(pose, normalX, normalY, normalZ)
consumer.addVertex(matrix, p2x, p2y, p2z)
.setColor(255, 255, 255, 255).setUv(u1, v0)
.setOverlay(OverlayTexture.NO_OVERLAY).setLight(light).setNormal(pose, normalX, normalY, normalZ)
consumer.addVertex(matrix, p3x, p3y, p3z)
.setColor(255, 255, 255, 255).setUv(u0, v0)
.setOverlay(OverlayTexture.NO_OVERLAY).setLight(light).setNormal(pose, normalX, normalY, normalZ)
}
}
@@ -0,0 +1,73 @@
package com.cobblemon.mod.common.client.render.block
import com.cobblemon.mod.common.client.render.layer.CobblemonRenderLayers
import com.cobblemon.mod.common.util.cobblemonResource
import com.mojang.blaze3d.vertex.PoseStack
import net.minecraft.client.Minecraft
import net.minecraft.client.renderer.LightTexture
import net.minecraft.client.renderer.MultiBufferSource
import net.minecraft.client.renderer.texture.OverlayTexture
import net.minecraft.resources.ResourceLocation
import net.minecraft.world.level.block.entity.BlockEntity
import net.minecraft.world.phys.AABB
object ScanIndicatorRenderer {
val UNSCANNED: ResourceLocation = cobblemonResource("textures/gui/pokedex/scan/block_scan_indicator.png")
val SCANNED: ResourceLocation = cobblemonResource("textures/gui/pokedex/scan/block_scan_indicator_scanned.png")
private val FULL_BRIGHT = LightTexture.pack(15, 15)
private val FULL_BLOCK_BOUNDS = AABB(0.0, 0.0, 0.0, 1.0, 1.0, 1.0)
/**
* Renders a camera-facing icon at the centre of a block's model bounds to indicate it is
* scannable. The icon billboards toward the camera and ignores depth testing, so it is always
* visible regardless of viewing angle or whether the block model would otherwise occlude it.
*
* @param matrices the current PoseStack, translated to the block's local origin (0,0,0 = block corner)
* @param bufferSource the buffer source for rendering
* @param be the block entity being indicated; its outline shape determines the icon's centre
* @param scanned whether this block type has already been scanned by the player
* @param scale size of the icon in world units (default 0.5)
*/
fun render(
matrices: PoseStack,
bufferSource: MultiBufferSource,
be: BlockEntity,
scanned: Boolean,
scale: Float = 0.5f
) {
val texture = if (scanned) SCANNED else UNSCANNED
val camera = Minecraft.getInstance().gameRenderer.mainCamera
// Centre on the block's outline shape (its visual bounds), falling back to a full block
val bounds = be.level?.let { level ->
val shape = be.blockState.getShape(level, be.blockPos)
if (!shape.isEmpty) shape.bounds() else null
} ?: FULL_BLOCK_BOUNDS
val center = bounds.center
matrices.pushPose()
matrices.translate(center.x, center.y, center.z)
matrices.mulPose(camera.rotation())
matrices.scale(scale, scale, scale)
val pose = matrices.last()
val buffer = bufferSource.getBuffer(CobblemonRenderLayers.SCAN_INDICATOR.apply(texture))
val h = 0.5f
buffer.addVertex(pose.pose(), -h, -h, 0f)
.setColor(255, 255, 255, 255).setUv(0f, 1f)
.setOverlay(OverlayTexture.NO_OVERLAY).setLight(FULL_BRIGHT).setNormal(pose, 0f, 0f, 1f)
buffer.addVertex(pose.pose(), h, -h, 0f)
.setColor(255, 255, 255, 255).setUv(1f, 1f)
.setOverlay(OverlayTexture.NO_OVERLAY).setLight(FULL_BRIGHT).setNormal(pose, 0f, 0f, 1f)
buffer.addVertex(pose.pose(), h, h, 0f)
.setColor(255, 255, 255, 255).setUv(1f, 0f)
.setOverlay(OverlayTexture.NO_OVERLAY).setLight(FULL_BRIGHT).setNormal(pose, 0f, 0f, 1f)
buffer.addVertex(pose.pose(), -h, h, 0f)
.setColor(255, 255, 255, 255).setUv(0f, 0f)
.setOverlay(OverlayTexture.NO_OVERLAY).setLight(FULL_BRIGHT).setNormal(pose, 0f, 0f, 1f)
matrices.popPose()
}
}
@@ -0,0 +1,115 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.client.render.item
import com.cobblemon.mod.common.client.CobblemonClient
import com.cobblemon.mod.common.item.components.PrintedPhotoComponent
import com.mojang.blaze3d.vertex.PoseStack
import com.mojang.blaze3d.vertex.VertexConsumer
import com.mojang.math.Axis
import net.minecraft.client.renderer.MultiBufferSource
import net.minecraft.client.renderer.RenderType
import net.minecraft.client.renderer.texture.OverlayTexture
import net.minecraft.resources.ResourceLocation
import net.minecraft.world.item.ItemDisplayContext
import net.minecraft.world.item.ItemStack
/**
* Draws a printed photo as a flat textured quad in any item display context.
* Image bytes come from [com.cobblemon.mod.common.client.pokedex.photo.PrintedPhotoAssetClientStore];
* a missing/loading asset shows a neutral placeholder.
*/
class PrintedPhotoItemRenderer : CobblemonBuiltinItemRenderer {
companion object {
private val PLACEHOLDER: ResourceLocation = ResourceLocation.withDefaultNamespace("textures/item/map.png")
}
override fun render(
stack: ItemStack,
mode: ItemDisplayContext,
matrices: PoseStack,
vertexConsumers: MultiBufferSource,
light: Int,
overlay: Int
) {
val component = PrintedPhotoComponent.get(stack)
val store = CobblemonClient.printedPhotoAssetStore
val textureLocation = if (component != null) {
store.getTexture(component.assetId) ?: run {
store.requestAssets(listOf(component.assetId))
PLACEHOLDER
}
} else PLACEHOLDER
matrices.pushPose()
applyDisplayTransform(mode, matrices)
val buffer = vertexConsumers.getBuffer(RenderType.entityCutout(textureLocation))
drawQuad(buffer, matrices, light)
matrices.popPose()
}
private fun applyDisplayTransform(mode: ItemDisplayContext, matrices: PoseStack) {
when (mode) {
ItemDisplayContext.GUI -> {
matrices.translate(0.5, 0.5, 0.0)
matrices.mulPose(Axis.YP.rotationDegrees(180F))
matrices.scale(1.0F, -1.0F, 1.0F)
}
ItemDisplayContext.GROUND -> {
matrices.translate(0.5, 0.25, 0.5)
matrices.scale(0.5F, 0.5F, 0.5F)
}
ItemDisplayContext.FIXED -> {
matrices.translate(0.5, 0.5, 0.0)
matrices.mulPose(Axis.YP.rotationDegrees(180F))
matrices.scale(1.0F, 1.0F, 1.0F)
}
ItemDisplayContext.FIRST_PERSON_RIGHT_HAND,
ItemDisplayContext.FIRST_PERSON_LEFT_HAND -> {
// Hold up like a map / inspect — face the player camera.
matrices.translate(0.5, 0.5, -0.05)
matrices.scale(0.9F, 0.9F, 0.01F)
}
ItemDisplayContext.THIRD_PERSON_RIGHT_HAND,
ItemDisplayContext.THIRD_PERSON_LEFT_HAND -> {
matrices.translate(0.5, 0.5, 0.05)
matrices.scale(0.7F, 0.7F, 0.01F)
}
ItemDisplayContext.HEAD -> {
matrices.translate(0.5, 0.7, 0.5)
matrices.scale(1.0F, 1.0F, 0.05F)
}
else -> {
matrices.translate(0.5, 0.5, 0.0)
}
}
}
private fun drawQuad(consumer: VertexConsumer, matrices: PoseStack, light: Int) {
val pose = matrices.last()
val matrix = pose.pose()
val half = 0.5F
// CCW quad facing +Z; normal pointing toward viewer.
consumer.addVertex(matrix, -half, -half, 0F)
.setColor(255, 255, 255, 255).setUv(0F, 1F)
.setOverlay(OverlayTexture.NO_OVERLAY).setLight(light).setNormal(pose, 0F, 0F, 1F)
consumer.addVertex(matrix, half, -half, 0F)
.setColor(255, 255, 255, 255).setUv(1F, 1F)
.setOverlay(OverlayTexture.NO_OVERLAY).setLight(light).setNormal(pose, 0F, 0F, 1F)
consumer.addVertex(matrix, half, half, 0F)
.setColor(255, 255, 255, 255).setUv(1F, 0F)
.setOverlay(OverlayTexture.NO_OVERLAY).setLight(light).setNormal(pose, 0F, 0F, 1F)
consumer.addVertex(matrix, -half, half, 0F)
.setColor(255, 255, 255, 255).setUv(0F, 0F)
.setOverlay(OverlayTexture.NO_OVERLAY).setLight(light).setNormal(pose, 0F, 0F, 1F)
}
}
@@ -86,4 +86,34 @@ object CobblemonRenderLayers {
)
};
/**
* Translucent, fullbright, no-cull layer that ignores depth testing so the quad always renders
* on top of the world geometry. Used by the Pokédex scan indicator so it's visible from any angle.
*/
val SCAN_INDICATOR: Function<ResourceLocation, RenderType> = Util.memoize { texture: ResourceLocation ->
val multiPhaseParameters =
RenderType.CompositeState.builder()
// Emissive shader renders fullbright and ignores lightmap + directional diffuse shading,
// so the icon stays uniformly bright regardless of world light or viewing angle.
.setShaderState(RENDERTYPE_ENTITY_TRANSLUCENT_EMISSIVE_SHADER)
.setTextureState(TextureStateShard(texture, false, false))
.setTransparencyState(TRANSLUCENT_TRANSPARENCY)
.setCullState(NO_CULL)
.setLightmapState(LightmapStateShard(false))
.setOverlayState(OVERLAY)
.setDepthTestState(NO_DEPTH_TEST)
.setWriteMaskState(COLOR_WRITE)
.createCompositeState(false)
RenderType.create(
"cobblemon_scan_indicator",
DefaultVertexFormat.NEW_ENTITY,
VertexFormat.Mode.QUADS,
256,
false,
true,
multiPhaseParameters
)
};
}
@@ -28,5 +28,27 @@ object ServerSettings {
var maxPokemonFriendship = Cobblemon.config.maxPokemonFriendship
var maxDynamaxLevel = Cobblemon.config.maxDynamaxLevel
var unlockAllMoveDexMovesByDefault = Cobblemon.config.unlockAllMoveDexMovesByDefault
var pokedexScannerPhotoRaycastDistance = Cobblemon.config.pokedexScannerPhotoRaycastDistance
var pokedexScannerPhotoRaycastDensity = Cobblemon.config.pokedexScannerPhotoRaycastDensity
var pokedexScannerPhotoPerimeterRaycastDensity = Cobblemon.config.pokedexScannerPhotoPerimeterRaycastDensity
var pokedexScannerPhotoCoverageScoreCap = Cobblemon.config.pokedexScannerPhotoCoverageScoreCap
var pokedexScannerPhotoCoverageScoreCapAtPercent = Cobblemon.config.pokedexScannerPhotoCoverageScoreCapAtPercent
var pokedexScannerPhotoCenteredScoreCap = Cobblemon.config.pokedexScannerPhotoCenteredScoreCap
var pokedexScannerPhotoFacingBonus = Cobblemon.config.pokedexScannerPhotoFacingBonus
var pokedexScannerPhotoFullBodyBonus = Cobblemon.config.pokedexScannerPhotoFullBodyBonus
var pokedexScannerPhotoOutOfFrameMultiplier = Cobblemon.config.pokedexScannerPhotoOutOfFrameMultiplier
var pokedexScannerPhotoShinyBonus = Cobblemon.config.pokedexScannerPhotoShinyBonus
var pokedexScannerPhotoUncommonSpeciesMultiplier = Cobblemon.config.pokedexScannerPhotoUncommonSpeciesMultiplier
var pokedexScannerPhotoRareSpeciesMultiplier = Cobblemon.config.pokedexScannerPhotoRareSpeciesMultiplier
var pokedexScannerPhotoUltraRareSpeciesMultiplier = Cobblemon.config.pokedexScannerPhotoUltraRareSpeciesMultiplier
var pokedexScannerPhotoExtraPokemonBonusCap = Cobblemon.config.pokedexScannerPhotoExtraPokemonBonusCap
var pokedexScannerPhotoExtraPokemonBonusPerSubject = Cobblemon.config.pokedexScannerPhotoExtraPokemonBonusPerSubject
var pokedexScannerPhotoFamilyBonus = Cobblemon.config.pokedexScannerPhotoFamilyBonus
var pokedexScannerPhotoHabitatBonus = Cobblemon.config.pokedexScannerPhotoHabitatBonus
var pokedexScannerPhotoTimeOfDayBonus = Cobblemon.config.pokedexScannerPhotoTimeOfDayBonus
var pokedexScannerPhotoWeatherBonus = Cobblemon.config.pokedexScannerPhotoWeatherBonus
var pokedexScannerPhotoAlphaBonus = Cobblemon.config.pokedexScannerPhotoAlphaBonus
var pokedexScannerPhotoWildBonus = Cobblemon.config.pokedexScannerPhotoWildBonus
var pokedexScannerPhotoFormBonus = Cobblemon.config.pokedexScannerPhotoFormBonus
}
}
@@ -274,6 +274,102 @@ class CobblemonConfig {
@CobblemonConfigField(Category.Pokedex, lang = "max_pokedex_scanning_detection_range", SERVER)
var maxPokedexScanningDetectionRange = 10.0
@CobblemonConfigField(Category.Pokedex, lang = "pokedex_scanner_photo_resolution", SERVER)
@IntConstraint(min = 64, max = 2048)
var pokedexScannerPhotoResolution = 320
@CobblemonConfigField(Category.Pokedex, lang = "pokedex_scanner_photo_limit_per_player", SERVER)
@IntConstraint(min = 1, max = 100000)
var pokedexScannerPhotoLimitPerPlayer = 256
@CobblemonConfigField(Category.Pokedex, lang = "pokedex_scanner_photo_jpeg_quality", SERVER)
var pokedexScannerPhotoJpegQuality = 0.65
@CobblemonConfigField(Category.Pokedex, lang = "pokedex_scanner_photo_raycast_distance", SERVER)
var pokedexScannerPhotoRaycastDistance = 48.0
@CobblemonConfigField(Category.Pokedex, lang = "pokedex_scanner_photo_raycast_density", SERVER)
@IntConstraint(min = 1, max = 6)
var pokedexScannerPhotoRaycastDensity = 4
@CobblemonConfigField(Category.Pokedex, lang = "pokedex_scanner_photo_perimeter_raycast_density", SERVER)
@IntConstraint(min = 1, max = 6)
var pokedexScannerPhotoPerimeterRaycastDensity = 2
@CobblemonConfigField(Category.Pokedex, lang = "pokedex_scanner_photo_coverage_score_cap", SERVER)
@IntConstraint(min = 0, max = 5000)
var pokedexScannerPhotoCoverageScoreCap = 3000
@CobblemonConfigField(Category.Pokedex, lang = "pokedex_scanner_photo_coverage_score_cap_at_percent", SERVER)
var pokedexScannerPhotoCoverageScoreCapAtPercent = 0.5F
@CobblemonConfigField(Category.Pokedex, lang = "pokedex_scanner_photo_centered_score_cap", SERVER)
@IntConstraint(min = 0, max = 5000)
var pokedexScannerPhotoCenteredScoreCap = 1000
@CobblemonConfigField(Category.Pokedex, lang = "pokedex_scanner_photo_facing_bonus", SERVER)
@IntConstraint(min = 0, max = 5000)
var pokedexScannerPhotoFacingBonus = 900
@CobblemonConfigField(Category.Pokedex, lang = "pokedex_scanner_photo_full_body_bonus", SERVER)
@IntConstraint(min = 0, max = 5000)
var pokedexScannerPhotoFullBodyBonus = 200
@CobblemonConfigField(Category.Pokedex, lang = "pokedex_scanner_photo_out_of_frame_multiplier", SERVER)
var pokedexScannerPhotoOutOfFrameMultiplier = 0.55F
@CobblemonConfigField(Category.Pokedex, lang = "pokedex_scanner_photo_shiny_bonus", SERVER)
@IntConstraint(min = 0, max = 5000)
var pokedexScannerPhotoShinyBonus = 600
@CobblemonConfigField(Category.Pokedex, lang = "pokedex_scanner_photo_uncommon_species_multiplier", SERVER)
var pokedexScannerPhotoUncommonSpeciesMultiplier = 1.03F
@CobblemonConfigField(Category.Pokedex, lang = "pokedex_scanner_photo_rare_species_multiplier", SERVER)
var pokedexScannerPhotoRareSpeciesMultiplier = 1.07F
@CobblemonConfigField(Category.Pokedex, lang = "pokedex_scanner_photo_ultra_rare_species_multiplier", SERVER)
var pokedexScannerPhotoUltraRareSpeciesMultiplier = 1.12F
@CobblemonConfigField(Category.Pokedex, lang = "pokedex_scanner_photo_extra_pokemon_bonus_cap", SERVER)
@IntConstraint(min = 0, max = 5000)
var pokedexScannerPhotoExtraPokemonBonusCap = 500
@CobblemonConfigField(Category.Pokedex, lang = "pokedex_scanner_photo_extra_pokemon_bonus_per_subject", SERVER)
@IntConstraint(min = 0, max = 5000)
var pokedexScannerPhotoExtraPokemonBonusPerSubject = 150
@CobblemonConfigField(Category.Pokedex, lang = "pokedex_scanner_photo_family_bonus", SERVER)
@IntConstraint(min = 0, max = 5000)
var pokedexScannerPhotoFamilyBonus = 100
@CobblemonConfigField(Category.Pokedex, lang = "pokedex_scanner_photo_habitat_bonus", SERVER)
@IntConstraint(min = 0, max = 5000)
var pokedexScannerPhotoHabitatBonus = 150
@CobblemonConfigField(Category.Pokedex, lang = "pokedex_scanner_photo_time_of_day_bonus", SERVER)
@IntConstraint(min = 0, max = 5000)
var pokedexScannerPhotoTimeOfDayBonus = 150
@CobblemonConfigField(Category.Pokedex, lang = "pokedex_scanner_photo_weather_bonus", SERVER)
@IntConstraint(min = 0, max = 5000)
var pokedexScannerPhotoWeatherBonus = 150
@CobblemonConfigField(Category.Pokedex, lang = "pokedex_scanner_photo_alpha_bonus", SERVER)
@IntConstraint(min = 0, max = 5000)
var pokedexScannerPhotoAlphaBonus = 500
@CobblemonConfigField(Category.Pokedex, lang = "pokedex_scanner_photo_wild_bonus", SERVER)
@IntConstraint(min = 0, max = 5000)
var pokedexScannerPhotoWildBonus = 250
@CobblemonConfigField(Category.Pokedex, lang = "pokedex_scanner_photo_form_bonus", SERVER)
@IntConstraint(min = 0, max = 5000)
var pokedexScannerPhotoFormBonus = 400
@CobblemonConfigField(Category.Pokedex, lang = "pokedex_delete_photo_after_print", SERVER)
var pokedexDeletePhotoAfterPrint = false
@CobblemonConfigField(Category.Pokedex, lang = "unlock_all_move_dex_moves_by_default", SERVER)
var unlockAllMoveDexMovesByDefault = false
@@ -0,0 +1,139 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.item
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.CobblemonBlocks
import com.cobblemon.mod.common.api.pokedex.PokedexEntryProgress
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.block.PrintedPhotoDisplayBlock
import com.cobblemon.mod.common.block.entity.PrintedPhotoDisplayBlockEntity
import com.cobblemon.mod.common.item.components.PrintedPhotoComponent
import com.cobblemon.mod.common.pokedex.photo.PokedexPhotoRatings
import net.minecraft.network.chat.Component
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import net.minecraft.sounds.SoundSource
import net.minecraft.world.entity.Entity
import net.minecraft.world.item.Item
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.TooltipFlag
import net.minecraft.world.item.context.BlockPlaceContext
import net.minecraft.world.item.context.UseOnContext
import net.minecraft.world.level.block.Block
import net.minecraft.world.level.Level
import net.minecraft.world.level.material.Fluids
import net.minecraft.world.InteractionResult
class PrintedPhotoItem(properties: Item.Properties) : Item(properties) {
override fun inventoryTick(stack: ItemStack, level: Level, entity: Entity, slotId: Int, isSelected: Boolean) {
if (level.isClientSide) return
val player = entity as? ServerPlayer ?: return
if (player.tickCount % 20 != 0) return
val printedPhoto = PrintedPhotoComponent.get(stack) ?: return
if (printedPhoto.detectedSpecies.isEmpty()) return
val pokedex = Cobblemon.playerDataManager.getPokedexData(player.uuid)
printedPhoto.detectedSpecies.forEach { speciesKey ->
val speciesId = ResourceLocation.tryParse(speciesKey) ?: return@forEach
if (pokedex.getKnowledgeForSpecies(speciesId) >= PokedexEntryProgress.ENCOUNTERED) return@forEach
val species = PokemonSpecies.getByIdentifier(speciesId) ?: return@forEach
pokedex.encounter(species.create(level = 1))
}
}
override fun appendHoverText(
stack: ItemStack,
context: Item.TooltipContext,
tooltipComponents: MutableList<Component>,
tooltipFlag: TooltipFlag
) {
super.appendHoverText(stack, context, tooltipComponents, tooltipFlag)
val printedPhoto = PrintedPhotoComponent.get(stack) ?: return
tooltipComponents.add(Component.translatable("item.cobblemon.printed_photo.photo_id", printedPhoto.photoId))
tooltipComponents.add(Component.translatable("item.cobblemon.printed_photo.quality_score", printedPhoto.qualityScore))
tooltipComponents.add(Component.translatable("item.cobblemon.printed_photo.rating", PokedexPhotoRatings.ratingText(printedPhoto.qualityScore)))
tooltipComponents.add(Component.literal("Size: ${printedPhoto.printSize}x${printedPhoto.printSize}"))
}
override fun useOn(context: UseOnContext): InteractionResult {
val stack = context.itemInHand
val component = PrintedPhotoComponent.get(stack) ?: return InteractionResult.PASS
val level = context.level
val placeContext = BlockPlaceContext(context)
val displayState = CobblemonBlocks.PRINTED_PHOTO_DISPLAY.getStateForPlacement(placeContext) ?: return InteractionResult.FAIL
val size = component.printSize.coerceIn(1, 3)
val rootPos = placeContext.clickedPos
val axes = PrintedPhotoDisplayBlock.resolveAxes(displayState)
val plannedPlacements = mutableListOf<Triple<Int, Int, net.minecraft.core.BlockPos>>()
for (y in 0 until size) {
for (x in 0 until size) {
val targetPos = rootPos.relative(axes.right, x).relative(axes.up, y)
plannedPlacements += Triple(x, y, targetPos)
}
}
if (level.isClientSide) {
return InteractionResult.SUCCESS
}
for ((_, _, targetPos) in plannedPlacements) {
val stateAtTarget = level.getBlockState(targetPos)
if (!stateAtTarget.isAir && !stateAtTarget.canBeReplaced()) {
return InteractionResult.FAIL
}
val candidateState = displayState.setValue(
net.minecraft.world.level.block.state.properties.BlockStateProperties.WATERLOGGED,
level.getFluidState(targetPos).type == Fluids.WATER
)
if (!candidateState.canSurvive(level, targetPos)) {
return InteractionResult.FAIL
}
}
val placed = mutableListOf<net.minecraft.core.BlockPos>()
for ((_, _, targetPos) in plannedPlacements) {
val candidateState = displayState.setValue(
net.minecraft.world.level.block.state.properties.BlockStateProperties.WATERLOGGED,
level.getFluidState(targetPos).type == Fluids.WATER
)
if (!level.setBlock(targetPos, candidateState, Block.UPDATE_ALL_IMMEDIATE)) {
placed.forEach { level.setBlock(it, net.minecraft.world.level.block.Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL_IMMEDIATE) }
return InteractionResult.FAIL
}
placed += targetPos
}
for ((cellX, cellY, targetPos) in plannedPlacements) {
val blockEntity = level.getBlockEntity(targetPos) as? PrintedPhotoDisplayBlockEntity ?: continue
blockEntity.setComponent(component.copy(printSize = size))
blockEntity.setTileData(rootPos = rootPos, gridSize = size, cellX = cellX, cellY = cellY)
}
val soundType = displayState.soundType
level.playSound(
context.player,
rootPos,
soundType.placeSound,
SoundSource.BLOCKS,
(soundType.volume + 1.0F) / 2.0F,
soundType.pitch * 0.8F
)
if (context.player?.isCreative != true) {
stack.shrink(1)
}
return InteractionResult.CONSUME
}
}
@@ -0,0 +1,62 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.item.components
import com.cobblemon.mod.common.CobblemonItemComponents
import com.mojang.serialization.Codec
import com.mojang.serialization.codecs.RecordCodecBuilder
import io.netty.buffer.ByteBuf
import net.minecraft.network.codec.ByteBufCodecs
import net.minecraft.network.codec.StreamCodec
import net.minecraft.world.item.ItemStack
import java.util.UUID
data class PrintedPhotoComponent(
val photoId: String,
val owner: UUID,
val index: Long,
val assetId: Int = 0,
val printSize: Int = 1,
val createdAt: Long,
val detectedSpecies: List<String> = emptyList(),
val biome: String = "minecraft:unknown",
val dimension: String = "minecraft:overworld",
val timeOfDayTicks: Long = 0L,
val qualityScore: Int = 0
) {
companion object {
private val UUID_CODEC: Codec<UUID> = Codec.STRING.xmap(UUID::fromString, UUID::toString)
val CODEC: Codec<PrintedPhotoComponent> = RecordCodecBuilder.create { instance ->
instance.group(
Codec.STRING.fieldOf("photo_id").forGetter { it.photoId },
UUID_CODEC.fieldOf("owner").forGetter { it.owner },
Codec.LONG.fieldOf("index").forGetter { it.index },
Codec.INT.optionalFieldOf("asset_id", 0).forGetter { it.assetId },
Codec.intRange(1, 3).optionalFieldOf("print_size", 1).forGetter { it.printSize },
Codec.LONG.fieldOf("created_at").forGetter { it.createdAt },
Codec.list(Codec.STRING).optionalFieldOf("detected_species", emptyList()).forGetter { it.detectedSpecies },
Codec.STRING.optionalFieldOf("biome", "minecraft:unknown").forGetter { it.biome },
Codec.STRING.optionalFieldOf("dimension", "minecraft:overworld").forGetter { it.dimension },
Codec.LONG.optionalFieldOf("time_of_day_ticks", 0L).forGetter { it.timeOfDayTicks },
Codec.intRange(0, 5000).fieldOf("quality_score").forGetter { it.qualityScore }
).apply(instance, ::PrintedPhotoComponent)
}
val PACKET_CODEC: StreamCodec<ByteBuf, PrintedPhotoComponent> = ByteBufCodecs.fromCodec(CODEC)
fun get(stack: ItemStack): PrintedPhotoComponent? = stack.get(CobblemonItemComponents.PRINTED_PHOTO)
fun set(stack: ItemStack, component: PrintedPhotoComponent): ItemStack {
stack.set(CobblemonItemComponents.PRINTED_PHOTO, component)
return stack
}
}
}
@@ -251,6 +251,7 @@ object CobblemonItemGroups {
entries.accept(CobblemonItems.HEALING_MACHINE)
entries.accept(CobblemonItems.PASTURE)
entries.accept(CobblemonItems.TM_MACHINE)
entries.accept(CobblemonItems.PHOTO_PRINTER)
entries.accept(CobblemonItems.GILDED_CHEST)
entries.accept(CobblemonItems.YELLOW_GILDED_CHEST)
@@ -26,7 +26,29 @@ class ServerSettingsPacket internal constructor(
val maxPokemonLevel: Int,
val maxPokemonFriendship: Int,
val maxDynamaxLevel: Int,
val unlockAllMoveDexMovesByDefault: Boolean
val unlockAllMoveDexMovesByDefault: Boolean,
val pokedexScannerPhotoRaycastDistance: Double,
val pokedexScannerPhotoRaycastDensity: Int,
val pokedexScannerPhotoPerimeterRaycastDensity: Int,
val pokedexScannerPhotoCoverageScoreCap: Int,
val pokedexScannerPhotoCoverageScoreCapAtPercent: Float,
val pokedexScannerPhotoCenteredScoreCap: Int,
val pokedexScannerPhotoFacingBonus: Int,
val pokedexScannerPhotoFullBodyBonus: Int,
val pokedexScannerPhotoOutOfFrameMultiplier: Float,
val pokedexScannerPhotoShinyBonus: Int,
val pokedexScannerPhotoUncommonSpeciesMultiplier: Float,
val pokedexScannerPhotoRareSpeciesMultiplier: Float,
val pokedexScannerPhotoUltraRareSpeciesMultiplier: Float,
val pokedexScannerPhotoExtraPokemonBonusCap: Int,
val pokedexScannerPhotoExtraPokemonBonusPerSubject: Int,
val pokedexScannerPhotoFamilyBonus: Int,
val pokedexScannerPhotoHabitatBonus: Int,
val pokedexScannerPhotoTimeOfDayBonus: Int,
val pokedexScannerPhotoWeatherBonus: Int,
val pokedexScannerPhotoAlphaBonus: Int,
val pokedexScannerPhotoWildBonus: Int,
val pokedexScannerPhotoFormBonus: Int
) : NetworkPacket<ServerSettingsPacket> {
override val id = ID
override fun encode(buffer: RegistryFriendlyByteBuf) {
@@ -37,9 +59,61 @@ class ServerSettingsPacket internal constructor(
buffer.writeInt(Cobblemon.config.maxPokemonFriendship)
buffer.writeInt(Cobblemon.config.maxDynamaxLevel)
buffer.writeBoolean(Cobblemon.config.unlockAllMoveDexMovesByDefault)
buffer.writeDouble(Cobblemon.config.pokedexScannerPhotoRaycastDistance)
buffer.writeInt(Cobblemon.config.pokedexScannerPhotoRaycastDensity)
buffer.writeInt(Cobblemon.config.pokedexScannerPhotoPerimeterRaycastDensity)
buffer.writeInt(Cobblemon.config.pokedexScannerPhotoCoverageScoreCap)
buffer.writeFloat(Cobblemon.config.pokedexScannerPhotoCoverageScoreCapAtPercent)
buffer.writeInt(Cobblemon.config.pokedexScannerPhotoCenteredScoreCap)
buffer.writeInt(Cobblemon.config.pokedexScannerPhotoFacingBonus)
buffer.writeInt(Cobblemon.config.pokedexScannerPhotoFullBodyBonus)
buffer.writeFloat(Cobblemon.config.pokedexScannerPhotoOutOfFrameMultiplier)
buffer.writeInt(Cobblemon.config.pokedexScannerPhotoShinyBonus)
buffer.writeFloat(Cobblemon.config.pokedexScannerPhotoUncommonSpeciesMultiplier)
buffer.writeFloat(Cobblemon.config.pokedexScannerPhotoRareSpeciesMultiplier)
buffer.writeFloat(Cobblemon.config.pokedexScannerPhotoUltraRareSpeciesMultiplier)
buffer.writeInt(Cobblemon.config.pokedexScannerPhotoExtraPokemonBonusCap)
buffer.writeInt(Cobblemon.config.pokedexScannerPhotoExtraPokemonBonusPerSubject)
buffer.writeInt(Cobblemon.config.pokedexScannerPhotoFamilyBonus)
buffer.writeInt(Cobblemon.config.pokedexScannerPhotoHabitatBonus)
buffer.writeInt(Cobblemon.config.pokedexScannerPhotoTimeOfDayBonus)
buffer.writeInt(Cobblemon.config.pokedexScannerPhotoWeatherBonus)
buffer.writeInt(Cobblemon.config.pokedexScannerPhotoAlphaBonus)
buffer.writeInt(Cobblemon.config.pokedexScannerPhotoWildBonus)
buffer.writeInt(Cobblemon.config.pokedexScannerPhotoFormBonus)
}
companion object {
val ID = cobblemonResource("server_settings")
fun decode(buffer: RegistryFriendlyByteBuf) = ServerSettingsPacket(buffer.readBoolean(), buffer.readBoolean(), buffer.readBoolean(), buffer.readInt(), buffer.readInt(), buffer.readInt(), buffer.readBoolean())
fun decode(buffer: RegistryFriendlyByteBuf) = ServerSettingsPacket(
buffer.readBoolean(),
buffer.readBoolean(),
buffer.readBoolean(),
buffer.readInt(),
buffer.readInt(),
buffer.readInt(),
buffer.readBoolean(),
buffer.readDouble(),
buffer.readInt(),
buffer.readInt(),
buffer.readInt(),
buffer.readFloat(),
buffer.readInt(),
buffer.readInt(),
buffer.readInt(),
buffer.readFloat(),
buffer.readInt(),
buffer.readFloat(),
buffer.readFloat(),
buffer.readFloat(),
buffer.readInt(),
buffer.readInt(),
buffer.readInt(),
buffer.readInt(),
buffer.readInt(),
buffer.readInt(),
buffer.readInt(),
buffer.readInt(),
buffer.readInt()
)
}
}
}
@@ -0,0 +1,47 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.net.messages.client.ui
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.client.net.gui.CraftPrintedPhotoPacketHandler
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.core.BlockPos
import net.minecraft.network.RegistryFriendlyByteBuf
/**
* Triggers printing of the currently selected Pokédex photo.
*
* Handled by [CraftPrintedPhotoPacketHandler].
*/
class CraftPrintedPhotoPacket(
val blockPos: BlockPos,
val photoId: String?,
val printSize: Int
) : NetworkPacket<CraftPrintedPhotoPacket> {
override val id = ID
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeBlockPos(blockPos)
buffer.writeNullable(photoId) { packetBuffer, value -> packetBuffer.writeUtf(value) }
buffer.writeVarInt(printSize)
}
companion object {
val ID = cobblemonResource("craft_printed_photo")
fun decode(buffer: RegistryFriendlyByteBuf): CraftPrintedPhotoPacket {
return CraftPrintedPhotoPacket(
blockPos = buffer.readBlockPos(),
photoId = buffer.readNullable { it.readUtf() },
printSize = buffer.readVarInt()
)
}
}
}
@@ -0,0 +1,35 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.net.messages.client.ui
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.client.net.gui.DeletePokedexPhotoPacketHandler
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.network.RegistryFriendlyByteBuf
/**
* Requests deletion of a server-stored scanner photo owned by the sender.
*
* Handled by [DeletePokedexPhotoPacketHandler].
*/
class DeletePokedexPhotoPacket(val photoId: String) : NetworkPacket<DeletePokedexPhotoPacket> {
override val id = ID
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeUtf(photoId)
}
companion object {
val ID = cobblemonResource("delete_pokedex_photo")
fun decode(buffer: RegistryFriendlyByteBuf): DeletePokedexPhotoPacket {
return DeletePokedexPhotoPacket(buffer.readUtf())
}
}
}
@@ -0,0 +1,53 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.net.messages.client.ui
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.client.net.gui.PokedexPhotoBatchPacketHandler
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.network.RegistryFriendlyByteBuf
/**
* Sends encoded image bytes for a batch of Pokédex photos to the client in a single packet.
*
* Handled by [PokedexPhotoBatchPacketHandler].
*/
class PokedexPhotoBatchPacket(val entries: List<Entry>) : NetworkPacket<PokedexPhotoBatchPacket> {
override val id = ID
data class Entry(val photoId: String, val width: Int, val height: Int, val imageData: ByteArray)
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeInt(entries.size)
entries.forEach { entry ->
buffer.writeUtf(entry.photoId)
buffer.writeInt(entry.width)
buffer.writeInt(entry.height)
buffer.writeByteArray(entry.imageData)
}
}
companion object {
val ID = cobblemonResource("pokedex_photo_batch")
fun decode(buffer: RegistryFriendlyByteBuf): PokedexPhotoBatchPacket {
val size = buffer.readInt()
val entries = ArrayList<Entry>(size)
repeat(size) {
entries += Entry(
photoId = buffer.readUtf(),
width = buffer.readInt(),
height = buffer.readInt(),
imageData = buffer.readByteArray()
)
}
return PokedexPhotoBatchPacket(entries)
}
}
}
@@ -0,0 +1,152 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.net.messages.client.ui
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.client.net.gui.PokedexPhotoGalleryPacketHandler
import com.cobblemon.mod.common.pokedex.photo.PokedexPhotoMetadata
import com.cobblemon.mod.common.pokedex.photo.PokedexPhotoSubjectQuality
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.network.RegistryFriendlyByteBuf
import java.util.UUID
/**
* Sends a player's Pokédex photo listing to the client.
*
* Handled by [PokedexPhotoGalleryPacketHandler].
*/
class PokedexPhotoGalleryPacket(
val maxPhotosAllowed: Int,
val photos: List<PokedexPhotoMetadata>
) : NetworkPacket<PokedexPhotoGalleryPacket> {
override val id = ID
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeInt(maxPhotosAllowed)
buffer.writeInt(photos.size)
photos.forEach { photo ->
buffer.writeUtf(photo.id)
buffer.writeUUID(photo.owner)
buffer.writeLong(photo.index)
buffer.writeInt(photo.width)
buffer.writeInt(photo.height)
buffer.writeLong(photo.createdAt)
buffer.writeInt(photo.dataSize)
buffer.writeCollection(photo.detectedSpecies.orEmpty()) { _, species -> buffer.writeUtf(species) }
buffer.writeUtf(photo.biome)
buffer.writeUtf(photo.dimension)
buffer.writeLong(photo.timeOfDayTicks)
buffer.writeBoolean(photo.raining)
buffer.writeBoolean(photo.thundering)
buffer.writeInt(photo.qualityScore)
buffer.writeInt(photo.qualityBaseScore)
buffer.writeFloat(photo.qualitySpeciesMultiplier)
buffer.writeCollection(photo.qualitySubjects.orEmpty()) { _, subject ->
buffer.writeUtf(subject.species)
buffer.writeBoolean(subject.primarySubject)
buffer.writeInt(subject.subjectScore)
buffer.writeInt(subject.sizeScore)
buffer.writeInt(subject.placementScore)
buffer.writeFloat(subject.centerDistance)
buffer.writeInt(subject.distanceScore)
buffer.writeInt(subject.directionScore)
buffer.writeInt(subject.poseScore)
buffer.writeInt(subject.fullBodyScore)
buffer.writeInt(subject.otherPokemonScore)
buffer.writeInt(subject.herdScore)
buffer.writeInt(subject.habitatScore)
buffer.writeInt(subject.timeOfDayScore)
buffer.writeInt(subject.weatherScore)
buffer.writeInt(subject.shinyScore)
buffer.writeInt(subject.alphaScore)
buffer.writeInt(subject.wildScore)
buffer.writeInt(subject.formScore)
buffer.writeInt(subject.rayHits)
buffer.writeFloat(subject.rayCoverage)
buffer.writeInt(subject.perimeterHits)
buffer.writeBoolean(subject.wholeBodyInFrame)
buffer.writeFloat(subject.clippingMultiplier)
buffer.writeCollection(subject.activeAnimations.orEmpty()) { _, animation -> buffer.writeUtf(animation) }
buffer.writeBoolean(subject.shiny)
buffer.writeFloat(subject.shinyMultiplier)
buffer.writeBoolean(subject.alpha)
buffer.writeBoolean(subject.wild)
buffer.writeBoolean(subject.alternateForm)
buffer.writeFloat(subject.rarityMultiplier)
}
}
}
companion object {
val ID = cobblemonResource("pokedex_photo_gallery")
fun decode(buffer: RegistryFriendlyByteBuf): PokedexPhotoGalleryPacket {
val maxAllowed = buffer.readInt()
val photoCount = buffer.readInt()
val photos = ArrayList<PokedexPhotoMetadata>(photoCount)
repeat(photoCount) {
photos += PokedexPhotoMetadata(
id = buffer.readUtf(),
owner = buffer.readUUID(),
index = buffer.readLong(),
width = buffer.readInt(),
height = buffer.readInt(),
createdAt = buffer.readLong(),
dataSize = buffer.readInt(),
detectedSpecies = buffer.readList { it.readUtf() },
biome = buffer.readUtf(),
dimension = buffer.readUtf(),
timeOfDayTicks = buffer.readLong(),
raining = buffer.readBoolean(),
thundering = buffer.readBoolean(),
qualityScore = buffer.readInt(),
qualityBaseScore = buffer.readInt(),
qualitySpeciesMultiplier = buffer.readFloat(),
qualitySubjects = buffer.readList {
PokedexPhotoSubjectQuality(
species = it.readUtf(),
primarySubject = it.readBoolean(),
subjectScore = it.readInt(),
sizeScore = it.readInt(),
placementScore = it.readInt(),
centerDistance = it.readFloat(),
distanceScore = it.readInt(),
directionScore = it.readInt(),
poseScore = it.readInt(),
fullBodyScore = it.readInt(),
otherPokemonScore = it.readInt(),
herdScore = it.readInt(),
habitatScore = it.readInt(),
timeOfDayScore = it.readInt(),
weatherScore = it.readInt(),
shinyScore = it.readInt(),
alphaScore = it.readInt(),
wildScore = it.readInt(),
formScore = it.readInt(),
rayHits = it.readInt(),
rayCoverage = it.readFloat(),
perimeterHits = it.readInt(),
wholeBodyInFrame = it.readBoolean(),
clippingMultiplier = it.readFloat(),
activeAnimations = it.readList { buffer -> buffer.readUtf() },
shiny = it.readBoolean(),
shinyMultiplier = it.readFloat(),
alpha = it.readBoolean(),
wild = it.readBoolean(),
alternateForm = it.readBoolean(),
rarityMultiplier = it.readFloat()
)
}
)
}
return PokedexPhotoGalleryPacket(maxAllowed, photos)
}
}
}
@@ -0,0 +1,140 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.net.messages.client.ui
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.client.net.gui.PokedexPhotoUploadPacketHandler
import com.cobblemon.mod.common.pokedex.photo.PokedexPhotoSubjectQuality
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.network.RegistryFriendlyByteBuf
/**
* Uploads a captured Pokédex scan photo to the server.
*
* Handled by [PokedexPhotoUploadPacketHandler].
*/
class PokedexPhotoUploadPacket(
val width: Int,
val height: Int,
val detectedSpecies: List<String>,
val biome: String,
val dimension: String,
val timeOfDayTicks: Long,
val raining: Boolean,
val thundering: Boolean,
val qualityScore: Int,
val qualitySubjects: List<PokedexPhotoSubjectQuality>,
val pngData: ByteArray
) : NetworkPacket<PokedexPhotoUploadPacket> {
override val id = ID
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeInt(width)
buffer.writeInt(height)
buffer.writeCollection(detectedSpecies.orEmpty()) { _, species -> buffer.writeUtf(species) }
buffer.writeUtf(biome)
buffer.writeUtf(dimension)
buffer.writeLong(timeOfDayTicks)
buffer.writeBoolean(raining)
buffer.writeBoolean(thundering)
buffer.writeInt(qualityScore)
buffer.writeCollection(qualitySubjects.orEmpty()) { _, subject ->
buffer.writeUtf(subject.species)
buffer.writeBoolean(subject.entityUuid != null)
subject.entityUuid?.let(buffer::writeUUID)
buffer.writeBoolean(subject.primarySubject)
buffer.writeInt(subject.subjectScore)
buffer.writeInt(subject.sizeScore)
buffer.writeInt(subject.placementScore)
buffer.writeFloat(subject.centerDistance)
buffer.writeInt(subject.distanceScore)
buffer.writeInt(subject.directionScore)
buffer.writeInt(subject.poseScore)
buffer.writeInt(subject.fullBodyScore)
buffer.writeInt(subject.otherPokemonScore)
buffer.writeInt(subject.herdScore)
buffer.writeInt(subject.habitatScore)
buffer.writeInt(subject.timeOfDayScore)
buffer.writeInt(subject.weatherScore)
buffer.writeInt(subject.shinyScore)
buffer.writeInt(subject.alphaScore)
buffer.writeInt(subject.wildScore)
buffer.writeInt(subject.formScore)
buffer.writeInt(subject.rayHits)
buffer.writeFloat(subject.rayCoverage)
buffer.writeInt(subject.perimeterHits)
buffer.writeBoolean(subject.wholeBodyInFrame)
buffer.writeFloat(subject.clippingMultiplier)
buffer.writeCollection(subject.activeAnimations.orEmpty()) { _, animation -> buffer.writeUtf(animation) }
buffer.writeBoolean(subject.shiny)
buffer.writeFloat(subject.shinyMultiplier)
buffer.writeBoolean(subject.alpha)
buffer.writeBoolean(subject.wild)
buffer.writeBoolean(subject.alternateForm)
buffer.writeFloat(subject.rarityMultiplier)
}
buffer.writeByteArray(pngData)
}
companion object {
val ID = cobblemonResource("pokedex_photo_upload")
fun decode(buffer: RegistryFriendlyByteBuf): PokedexPhotoUploadPacket {
return PokedexPhotoUploadPacket(
width = buffer.readInt(),
height = buffer.readInt(),
detectedSpecies = buffer.readList { it.readUtf() },
biome = buffer.readUtf(),
dimension = buffer.readUtf(),
timeOfDayTicks = buffer.readLong(),
raining = buffer.readBoolean(),
thundering = buffer.readBoolean(),
qualityScore = buffer.readInt(),
qualitySubjects = buffer.readList {
PokedexPhotoSubjectQuality(
species = it.readUtf(),
entityUuid = if (it.readBoolean()) it.readUUID() else null,
primarySubject = it.readBoolean(),
subjectScore = it.readInt(),
sizeScore = it.readInt(),
placementScore = it.readInt(),
centerDistance = it.readFloat(),
distanceScore = it.readInt(),
directionScore = it.readInt(),
poseScore = it.readInt(),
fullBodyScore = it.readInt(),
otherPokemonScore = it.readInt(),
herdScore = it.readInt(),
habitatScore = it.readInt(),
timeOfDayScore = it.readInt(),
weatherScore = it.readInt(),
shinyScore = it.readInt(),
alphaScore = it.readInt(),
wildScore = it.readInt(),
formScore = it.readInt(),
rayHits = it.readInt(),
rayCoverage = it.readFloat(),
perimeterHits = it.readInt(),
wholeBodyInFrame = it.readBoolean(),
clippingMultiplier = it.readFloat(),
activeAnimations = it.readList { buffer -> buffer.readUtf() },
shiny = it.readBoolean(),
shinyMultiplier = it.readFloat(),
alpha = it.readBoolean(),
wild = it.readBoolean(),
alternateForm = it.readBoolean(),
rarityMultiplier = it.readFloat()
)
},
pngData = buffer.readByteArray()
)
}
}
}
@@ -0,0 +1,43 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.net.messages.client.ui
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.client.net.gui.PokedexPhotoUploadResultPacketHandler
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.network.RegistryFriendlyByteBuf
/**
* Sends upload success/failure feedback for a scanner photo upload.
*
* Handled by [PokedexPhotoUploadResultPacketHandler].
*/
class PokedexPhotoUploadResultPacket(
val success: Boolean,
val message: String
) : NetworkPacket<PokedexPhotoUploadResultPacket> {
override val id = ID
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeBoolean(success)
buffer.writeUtf(message)
}
companion object {
val ID = cobblemonResource("pokedex_photo_upload_result")
fun decode(buffer: RegistryFriendlyByteBuf): PokedexPhotoUploadResultPacket {
return PokedexPhotoUploadResultPacket(
success = buffer.readBoolean(),
message = buffer.readUtf()
)
}
}
}
@@ -0,0 +1,53 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.net.messages.client.ui
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.client.net.gui.PrintedPhotoAssetBatchPacketHandler
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.network.RegistryFriendlyByteBuf
/**
* Sends printed-photo asset image bytes to the client in batches.
*
* Handled by [PrintedPhotoAssetBatchPacketHandler].
*/
class PrintedPhotoAssetBatchPacket(val entries: List<Entry>) : NetworkPacket<PrintedPhotoAssetBatchPacket> {
override val id = ID
data class Entry(val assetId: Int, val width: Int, val height: Int, val imageData: ByteArray)
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeInt(entries.size)
entries.forEach { entry ->
buffer.writeInt(entry.assetId)
buffer.writeInt(entry.width)
buffer.writeInt(entry.height)
buffer.writeByteArray(entry.imageData)
}
}
companion object {
val ID = cobblemonResource("printed_photo_asset_batch")
fun decode(buffer: RegistryFriendlyByteBuf): PrintedPhotoAssetBatchPacket {
val size = buffer.readInt()
val entries = ArrayList<Entry>(size)
repeat(size) {
entries += Entry(
assetId = buffer.readInt(),
width = buffer.readInt(),
height = buffer.readInt(),
imageData = buffer.readByteArray()
)
}
return PrintedPhotoAssetBatchPacket(entries)
}
}
}
@@ -0,0 +1,39 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.net.messages.client.ui
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.client.net.gui.RequestPokedexPhotoBatchPacketHandler
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.network.RegistryFriendlyByteBuf
/**
* Requests a batch of Pokédex photos by id from the server in a single round trip.
*
* Handled by [RequestPokedexPhotoBatchPacketHandler].
*/
class RequestPokedexPhotoBatchPacket(val ids: List<String>) : NetworkPacket<RequestPokedexPhotoBatchPacket> {
override val id = ID
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeInt(ids.size)
ids.forEach { buffer.writeUtf(it) }
}
companion object {
val ID = cobblemonResource("request_pokedex_photo_batch")
fun decode(buffer: RegistryFriendlyByteBuf): RequestPokedexPhotoBatchPacket {
val size = buffer.readInt()
val ids = ArrayList<String>(size)
repeat(size) { ids += buffer.readUtf() }
return RequestPokedexPhotoBatchPacket(ids)
}
}
}
@@ -0,0 +1,32 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.net.messages.client.ui
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.client.net.gui.RequestPokedexPhotoGalleryPacketHandler
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.network.RegistryFriendlyByteBuf
/**
* Requests the player's current Pokédex photo listing from the server.
*
* Handled by [RequestPokedexPhotoGalleryPacketHandler].
*/
class RequestPokedexPhotoGalleryPacket : NetworkPacket<RequestPokedexPhotoGalleryPacket> {
override val id = ID
override fun encode(buffer: RegistryFriendlyByteBuf) = Unit
companion object {
val ID = cobblemonResource("request_pokedex_photo_gallery")
fun decode(buffer: RegistryFriendlyByteBuf) = RequestPokedexPhotoGalleryPacket()
}
}
@@ -0,0 +1,39 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.net.messages.client.ui
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.client.net.gui.RequestPrintedPhotoAssetBatchPacketHandler
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.network.RegistryFriendlyByteBuf
/**
* Requests printed-photo asset bytes by id from the server.
*
* Handled by [RequestPrintedPhotoAssetBatchPacketHandler].
*/
class RequestPrintedPhotoAssetBatchPacket(val assetIds: List<Int>) : NetworkPacket<RequestPrintedPhotoAssetBatchPacket> {
override val id = ID
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeInt(assetIds.size)
assetIds.forEach { buffer.writeInt(it) }
}
companion object {
val ID = cobblemonResource("request_printed_photo_asset_batch")
fun decode(buffer: RegistryFriendlyByteBuf): RequestPrintedPhotoAssetBatchPacket {
val size = buffer.readInt()
val ids = ArrayList<Int>(size)
repeat(size) { ids += buffer.readInt() }
return RequestPrintedPhotoAssetBatchPacket(ids)
}
}
}
@@ -0,0 +1,44 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.net.messages.client.ui
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.client.net.gui.SetPhotoPrinterSelectionPacketHandler
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.core.BlockPos
import net.minecraft.network.RegistryFriendlyByteBuf
/**
* Sets currently selected photo for a photo printer block entity.
*
* Handled by [SetPhotoPrinterSelectionPacketHandler].
*/
class SetPhotoPrinterSelectionPacket(
val blockPos: BlockPos,
val photoId: String?
) : NetworkPacket<SetPhotoPrinterSelectionPacket> {
override val id = ID
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeBlockPos(blockPos)
buffer.writeNullable(photoId) { packetBuffer, value -> packetBuffer.writeUtf(value) }
}
companion object {
val ID = cobblemonResource("set_photo_printer_selection")
fun decode(buffer: RegistryFriendlyByteBuf): SetPhotoPrinterSelectionPacket {
return SetPhotoPrinterSelectionPacket(
blockPos = buffer.readBlockPos(),
photoId = buffer.readNullable { it.readUtf() }
)
}
}
}
@@ -0,0 +1,34 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.net.messages.server.pokedex.scanner
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.core.BlockPos
import net.minecraft.network.RegistryFriendlyByteBuf
class FinishPrintedPhotoScanningPacket(val blockPos: BlockPos, val zoomLevel: Int) : NetworkPacket<FinishPrintedPhotoScanningPacket> {
override val id = ID
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeBlockPos(blockPos)
buffer.writeInt(zoomLevel)
}
companion object {
val ID = cobblemonResource("finish_printed_photo_scanning_packet")
fun decode(buffer: RegistryFriendlyByteBuf): FinishPrintedPhotoScanningPacket {
return FinishPrintedPhotoScanningPacket(
blockPos = buffer.readBlockPos(),
zoomLevel = buffer.readInt()
)
}
}
}
@@ -0,0 +1,20 @@
package com.cobblemon.mod.common.net.messages.server.pokedex.scanner
import com.cobblemon.mod.common.api.net.NetworkPacket
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.resources.ResourceLocation
class RecordBlockScanPacket(val scanKey: ResourceLocation) : NetworkPacket<RecordBlockScanPacket> {
override val id = ID
override fun encode(buffer: RegistryFriendlyByteBuf) {
buffer.writeResourceLocation(scanKey)
}
companion object {
val ID = cobblemonResource("record_block_scan")
fun decode(buffer: RegistryFriendlyByteBuf) = RecordBlockScanPacket(buffer.readResourceLocation())
}
}
@@ -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.net.serverhandling.pokedex.scanner
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.block.entity.PrintedPhotoDisplayBlockEntity
import com.cobblemon.mod.common.net.messages.server.pokedex.scanner.FinishPrintedPhotoScanningPacket
import com.cobblemon.mod.common.pokedex.photo.PrintedPhotoPokedexScanner
import com.cobblemon.mod.common.pokedex.scanner.PokedexUsageContext
import com.cobblemon.mod.common.pokedex.scanner.PlayerScanningDetails
import net.minecraft.server.MinecraftServer
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.level.ClipContext
import net.minecraft.world.phys.BlockHitResult
import net.minecraft.world.phys.HitResult
import net.minecraft.world.phys.Vec3
object FinishPrintedPhotoScanningHandler : com.cobblemon.mod.common.api.net.ServerNetworkPacketHandler<FinishPrintedPhotoScanningPacket> {
override fun handle(
packet: FinishPrintedPhotoScanningPacket,
server: MinecraftServer,
player: ServerPlayer
) {
val progressTick = PlayerScanningDetails.playerToTickMap[player.uuid]
val ticksScan = progressTick?.let { server.tickCount - it } ?: PokedexUsageContext.SUCCESS_SCAN_SERVER_TICKS
if (ticksScan < PokedexUsageContext.SUCCESS_SCAN_SERVER_TICKS) return
if (!isLookingAtPrintedPhoto(player, packet)) return
val component = (player.level().getBlockEntity(packet.blockPos) as? PrintedPhotoDisplayBlockEntity)?.component ?: return
val newlySeen = PrintedPhotoPokedexScanner.grantSeenEntries(player, component)
PrintedPhotoPokedexScanner.sendResultMessage(player, newlySeen, component.detectedSpecies.distinct().size)
}
private fun isLookingAtPrintedPhoto(player: ServerPlayer, packet: FinishPrintedPhotoScanningPacket): Boolean {
val maxDistance = Cobblemon.config.maxPokedexScanningDetectionRange +
(packet.zoomLevel * PokedexUsageContext.BLOCK_LENGTH_PER_ZOOM_STAGE)
if (player.position().distanceTo(Vec3.atCenterOf(packet.blockPos)) > maxDistance) {
return false
}
val eye = player.getEyePosition(1.0F)
val end = eye.add(player.getViewVector(1.0F).scale(maxDistance.toDouble()))
val hit = player.level().clip(
ClipContext(
eye,
end,
ClipContext.Block.COLLIDER,
ClipContext.Fluid.NONE,
player
)
)
if (hit.type != HitResult.Type.BLOCK) return false
val blockHit = hit as? BlockHitResult ?: return false
if (blockHit.blockPos != packet.blockPos) return false
return player.level().getBlockEntity(packet.blockPos) is PrintedPhotoDisplayBlockEntity
}
}
@@ -0,0 +1,13 @@
package com.cobblemon.mod.common.net.serverhandling.pokedex.scanner
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.net.ServerNetworkPacketHandler
import com.cobblemon.mod.common.net.messages.server.pokedex.scanner.RecordBlockScanPacket
import net.minecraft.server.MinecraftServer
import net.minecraft.server.level.ServerPlayer
object RecordBlockScanPacketHandler : ServerNetworkPacketHandler<RecordBlockScanPacket> {
override fun handle(packet: RecordBlockScanPacket, server: MinecraftServer, player: ServerPlayer) {
Cobblemon.playerDataManager.getScannedBlocksData(player).record(packet.scanKey)
}
}
@@ -0,0 +1,67 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.pokedex.photo
import java.util.UUID
data class PokedexPhotoMetadata(
val id: String,
val owner: UUID,
val index: Long,
val width: Int,
val height: Int,
val createdAt: Long,
val dataSize: Int,
val detectedSpecies: List<String> = emptyList(),
val biome: String = "minecraft:unknown",
val dimension: String = "minecraft:overworld",
val timeOfDayTicks: Long = 0L,
val raining: Boolean = false,
val thundering: Boolean = false,
val qualityScore: Int = 0,
val qualityBaseScore: Int = 0,
val qualitySpeciesMultiplier: Float = 1F,
val qualitySubjects: List<PokedexPhotoSubjectQuality> = emptyList()
)
data class PokedexPhotoSubjectQuality(
val species: String,
@Transient val entityUuid: UUID? = null,
val primarySubject: Boolean = false,
val subjectScore: Int = 0,
val sizeScore: Int = 0,
val placementScore: Int = 0,
val centerDistance: Float = 1F,
val distanceScore: Int = 0,
val directionScore: Int = 0,
val poseScore: Int = 0,
val fullBodyScore: Int = 0,
val otherPokemonScore: Int = 0,
val herdScore: Int = 0,
val habitatScore: Int = 0,
val timeOfDayScore: Int = 0,
val weatherScore: Int = 0,
val shinyScore: Int = 0,
val alphaScore: Int = 0,
val wildScore: Int = 0,
val formScore: Int = 0,
val rayHits: Int = 0,
val rayCoverage: Float = 0F,
val perimeterHits: Int = 0,
val wholeBodyInFrame: Boolean = true,
val clippingMultiplier: Float = 1F,
val activeAnimations: List<String> = emptyList(),
val shiny: Boolean = false,
val shinyMultiplier: Float = 1F,
val alpha: Boolean = false,
val wild: Boolean = false,
val alternateForm: Boolean = false,
val rarityMultiplier: Float = 1F
)
@@ -0,0 +1,82 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.pokedex.photo
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.spawning.CobblemonSpawnPools
import com.cobblemon.mod.common.api.spawning.detail.PokemonSpawnDetail
import com.cobblemon.mod.common.util.asIdentifierDefaultingNamespace
import net.minecraft.resources.ResourceLocation
import kotlin.math.roundToInt
object PokedexPhotoQuality {
data class AppliedScore(
val score: Int,
val baseScore: Int,
val speciesMultiplier: Float
)
fun applySpeciesMultiplier(baseScore: Int, detectedSpecies: List<String>): AppliedScore {
val safeBaseScore = baseScore.coerceIn(0, 5000)
val multiplier = detectedSpecies
.asSequence()
.mapNotNull(ResourceLocation::tryParse)
.map(::speciesRarityMultiplier)
.maxOrNull()
?: 1F
return AppliedScore(
score = (safeBaseScore * multiplier).roundToInt().coerceIn(0, 5000),
baseScore = safeBaseScore,
speciesMultiplier = multiplier
)
}
fun speciesRarityMultiplier(species: ResourceLocation): Float {
return speciesRarityMultiplier(
species = species,
uncommonMultiplier = Cobblemon.config.pokedexScannerPhotoUncommonSpeciesMultiplier,
rareMultiplier = Cobblemon.config.pokedexScannerPhotoRareSpeciesMultiplier,
ultraRareMultiplier = Cobblemon.config.pokedexScannerPhotoUltraRareSpeciesMultiplier
)
}
fun speciesRarityMultiplier(
species: ResourceLocation,
uncommonMultiplier: Float,
rareMultiplier: Float,
ultraRareMultiplier: Float
): Float {
val bucketRank = runCatching {
CobblemonSpawnPools.WORLD_SPAWN_POOL
.asSequence()
.filterIsInstance<PokemonSpawnDetail>()
.filter { detail -> detail.pokemon.species?.asIdentifierDefaultingNamespace() == species }
.map { detail -> rankBucket(detail.bucket.name) }
.maxOrNull()
}.getOrNull() ?: 0
return when (bucketRank) {
4 -> ultraRareMultiplier.coerceAtLeast(1F)
3 -> rareMultiplier.coerceAtLeast(1F)
2 -> uncommonMultiplier.coerceAtLeast(1F)
else -> 1F
}
}
private fun rankBucket(bucketName: String): Int {
return when (bucketName.lowercase()) {
"ultra-rare", "ultrarare", "ultra_rare" -> 4
"rare" -> 3
"uncommon" -> 2
"common" -> 1
else -> 0
}
}
}
@@ -0,0 +1,85 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.pokedex.photo
import net.minecraft.network.chat.Component
import java.util.Locale
object PokedexPhotoRatings {
fun starsForScore(score: Int): Int {
return when (score.coerceIn(0, 5000)) {
in 0..1000 -> 1
in 1001..2000 -> 2
in 2001..3000 -> 3
in 3001..4000 -> 4
else -> 5
}
}
fun starText(score: Int): String {
val stars = starsForScore(score)
return "★".repeat(stars) + "☆".repeat(5 - stars)
}
fun ratingText(score: Int): String {
return "${starText(score)} (${starsForScore(score)}/5)"
}
fun scoreBreakdown(photo: PokedexPhotoMetadata): List<Component> {
val lines = mutableListOf<Component>()
val primary = photo.qualitySubjects.firstOrNull { it.primarySubject } ?: photo.qualitySubjects.firstOrNull()
if (primary != null) {
lines += Component.literal("Species: ${primary.species.substringAfter(':')}")
lines += Component.literal("Size: +${primary.sizeScore}")
lines += Component.literal("Centering: +${primary.placementScore}")
val bonusLines = listOf(
"Extra Pokemon" to primary.otherPokemonScore,
"Looking at Camera" to primary.directionScore,
"Herd" to primary.herdScore,
"Wild" to primary.wildScore,
"Form" to primary.formScore,
"Pose" to primary.poseScore,
"Habitat" to primary.habitatScore,
"Time of Day" to primary.timeOfDayScore,
"Weather" to primary.weatherScore,
"Shiny" to primary.shinyScore,
"Alpha" to primary.alphaScore
).filter { (_, score) -> score > 0 }
if (bonusLines.isNotEmpty()) {
lines += Component.literal("Bonus Points")
bonusLines.forEach { (label, score) ->
lines += Component.literal("$label: +$score")
}
}
val multipliers = listOf(
"Rarity" to primary.rarityMultiplier,
"Too Close" to primary.clippingMultiplier
).filter { (_, multiplier) -> multiplier != 1F }
if (multipliers.isNotEmpty()) {
lines += Component.literal("Multipliers:")
multipliers.forEach { (label, multiplier) ->
lines += Component.literal("$label: x${formatMultiplier(multiplier)}")
}
}
}
lines += Component.literal("--------------------")
lines += Component.literal("Total: ${photo.qualityScore}")
lines += Component.literal("Rating: ${ratingText(photo.qualityScore)}")
return lines
}
private fun formatMultiplier(value: Float): String {
return String.format(Locale.ROOT, "%.2f", value)
}
}
@@ -0,0 +1,511 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.pokedex.photo
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.config.CobblemonConfig
import com.google.gson.annotations.SerializedName
import net.minecraft.server.MinecraftServer
import net.minecraft.world.level.storage.LevelResource
import java.awt.RenderingHints
import java.awt.image.BufferedImage
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.nio.file.Files
import java.nio.file.Path
import java.security.MessageDigest
import java.util.UUID
import javax.imageio.IIOImage
import javax.imageio.ImageIO
import javax.imageio.ImageWriteParam
import javax.imageio.stream.MemoryCacheImageOutputStream
import kotlin.math.max
import kotlin.math.min
object PokedexPhotoRepository {
private const val INDEX_FILE_NAME = "index.json"
private const val BLOBS_DIRECTORY = "blobs"
private const val LEGACY_PLAYERS_DIRECTORY = "players"
private const val PHOTO_EXTENSION = "jpg"
private const val DEFAULT_FIRST_INDEX = 1L
private const val ID_INDEX_PAD = 10
private const val BLOB_CACHE_MAX_ENTRIES = 64
private val lock = Any()
private var rootPath: Path? = null
private var dirty = false
private val nextIndexByOwner = mutableMapOf<UUID, Long>()
private val photosByOwner = mutableMapOf<UUID, MutableList<PokedexPhotoMetadata>>()
private val photosById = mutableMapOf<String, PokedexPhotoMetadata>()
private val photoToBlobHash = mutableMapOf<String, String>()
private val blobsByHash = mutableMapOf<String, BlobRecord>()
// Small bounded LRU. Deduplication means one cached entry serves every photo that shares its bytes.
private val blobCache = object : LinkedHashMap<String, ByteArray>(16, 0.75f, true) {
override fun removeEldestEntry(eldest: Map.Entry<String, ByteArray>?): Boolean = size > BLOB_CACHE_MAX_ENTRIES
}
data class SaveResult(val success: Boolean, val photo: PokedexPhotoMetadata? = null, val errorMessage: String? = null)
private data class BlobRecord(
val hash: String,
var refCount: Int,
var mapId: Int? = null,
var mapRenderVersion: Int = 0
)
fun initialize(server: MinecraftServer) {
synchronized(lock) {
val root = server.getWorldPath(LevelResource.ROOT).resolve("pokedex_photos")
rootPath = root
Files.createDirectories(root)
Files.createDirectories(root.resolve(BLOBS_DIRECTORY))
nextIndexByOwner.clear()
photosByOwner.clear()
photosById.clear()
photoToBlobHash.clear()
blobsByHash.clear()
blobCache.clear()
dirty = false
loadIndex(root)
}
}
fun shutdown() {
synchronized(lock) {
if (dirty) rootPath?.let(::saveIndex)
rootPath = null
nextIndexByOwner.clear()
photosByOwner.clear()
photosById.clear()
photoToBlobHash.clear()
blobsByHash.clear()
blobCache.clear()
dirty = false
}
}
fun saveFromPng(
owner: UUID,
pngData: ByteArray,
detectedSpecies: List<String> = emptyList(),
biome: String = "minecraft:unknown",
dimension: String = "minecraft:overworld",
timeOfDayTicks: Long = 0L,
raining: Boolean = false,
thundering: Boolean = false,
qualityScore: Int = 0,
qualityBaseScore: Int = qualityScore,
qualitySpeciesMultiplier: Float = 1F,
qualitySubjects: List<PokedexPhotoSubjectQuality> = emptyList()
): SaveResult {
synchronized(lock) {
val root = rootPath ?: return SaveResult(success = false, errorMessage = "Photo storage is unavailable.")
val currentPhotos = photosByOwner.getOrPut(owner) { mutableListOf() }
val maxPhotos = max(1, Cobblemon.config.pokedexScannerPhotoLimitPerPlayer)
if (currentPhotos.size >= maxPhotos) {
return SaveResult(success = false, errorMessage = "Photo limit reached ($maxPhotos).")
}
val decoded = decodeImage(pngData) ?: return SaveResult(success = false, errorMessage = "Invalid photo data.")
val outputSize = max(64, Cobblemon.config.pokedexScannerPhotoResolution)
val resized = resizeToSquare(decoded, outputSize)
val quality = Cobblemon.config.pokedexScannerPhotoJpegQuality.toFloat().coerceIn(0.1F, 1.0F)
val encoded = encodeJpeg(resized, quality) ?: return SaveResult(success = false, errorMessage = "Failed to encode photo.")
val blobHash = computeHash(encoded)
if (!blobsByHash.containsKey(blobHash)) {
val blobFile = blobPath(root, blobHash)
val saved = runCatching {
Files.createDirectories(blobFile.parent)
Files.write(blobFile, encoded)
}.onFailure {
Cobblemon.LOGGER.warn("Failed to save pokedex photo blob {}", blobHash, it)
}.isSuccess
if (!saved) return SaveResult(success = false, errorMessage = "Server failed to write photo file.")
blobsByHash[blobHash] = BlobRecord(hash = blobHash, refCount = 0)
}
blobsByHash[blobHash]!!.refCount++
var nextIndex = nextIndexByOwner.getOrDefault(owner, DEFAULT_FIRST_INDEX)
var id = createPhotoId(owner, nextIndex)
while (photosById.containsKey(id)) {
nextIndex += 1
id = createPhotoId(owner, nextIndex)
}
val metadata = PokedexPhotoMetadata(
id = id,
owner = owner,
index = nextIndex,
width = outputSize,
height = outputSize,
createdAt = System.currentTimeMillis(),
dataSize = encoded.size,
detectedSpecies = detectedSpecies.distinct(),
biome = biome,
dimension = dimension,
timeOfDayTicks = timeOfDayTicks.coerceIn(0L, 23999L),
raining = raining,
thundering = thundering,
qualityScore = qualityScore.coerceIn(0, 5000),
qualityBaseScore = qualityBaseScore.coerceIn(0, 5000),
qualitySpeciesMultiplier = qualitySpeciesMultiplier.coerceAtLeast(0F),
qualitySubjects = qualitySubjects
)
nextIndexByOwner[owner] = nextIndex + 1
currentPhotos.add(0, metadata)
photosById[id] = metadata
photoToBlobHash[id] = blobHash
blobCache[blobHash] = encoded
dirty = true
saveIndex(root)
return SaveResult(success = true, photo = metadata)
}
}
fun listFor(owner: UUID): List<PokedexPhotoMetadata> {
synchronized(lock) {
return photosByOwner[owner]?.toList() ?: emptyList()
}
}
fun getPhoto(owner: UUID, id: String): ByteArray? {
synchronized(lock) {
val root = rootPath ?: return null
val metadata = photosById[id] ?: return null
if (metadata.owner != owner) return null
val hash = photoToBlobHash[id] ?: return null
blobCache[hash]?.let { return it }
val path = blobPath(root, hash)
if (!Files.exists(path)) return null
val bytes = runCatching { Files.readAllBytes(path) }
.onFailure { Cobblemon.LOGGER.warn("Failed to read pokedex photo blob {}", hash, it) }
.getOrNull() ?: return null
blobCache[hash] = bytes
return bytes
}
}
fun getMetadata(owner: UUID, id: String): PokedexPhotoMetadata? {
synchronized(lock) {
val metadata = photosById[id] ?: return null
return metadata.takeIf { it.owner == owner }
}
}
fun delete(owner: UUID, id: String): Boolean {
synchronized(lock) {
val root = rootPath ?: return false
val metadata = photosById[id] ?: return false
if (metadata.owner != owner) return false
val removedFromOwner = photosByOwner[owner]?.removeIf { it.id == id } == true
photosById.remove(id)
val hash = photoToBlobHash.remove(id)
if (hash != null) {
val blob = blobsByHash[hash]
if (blob != null) {
blob.refCount--
if (blob.refCount <= 0) {
blobsByHash.remove(hash)
blobCache.remove(hash)
runCatching { Files.deleteIfExists(blobPath(root, hash)) }
.onFailure { Cobblemon.LOGGER.warn("Failed to delete pokedex photo blob {}", hash, it) }
}
}
}
if (removedFromOwner) {
dirty = true
saveIndex(root)
}
return removedFromOwner
}
}
fun setMapIdForBlob(owner: UUID, photoId: String, mapId: Int, renderVersion: Int) {
// Cached optimization only — persist on the next real mutation or on shutdown, not eagerly.
synchronized(lock) {
val hash = photoToBlobHash[photoId] ?: return
val blob = blobsByHash[hash] ?: return
if (blob.mapId == mapId && blob.mapRenderVersion == renderVersion) return
blob.mapId = mapId
blob.mapRenderVersion = renderVersion
dirty = true
}
}
private fun computeHash(data: ByteArray): String {
val bytes = MessageDigest.getInstance("SHA-256").digest(data)
return bytes.joinToString("") { "%02x".format(it) }
}
private fun blobPath(root: Path, hash: String): Path =
root.resolve(BLOBS_DIRECTORY).resolve("$hash.$PHOTO_EXTENSION")
private fun legacyPhotoPath(root: Path, metadata: PokedexPhotoMetadata): Path =
root.resolve(LEGACY_PLAYERS_DIRECTORY).resolve(metadata.owner.toString()).resolve("${metadata.id}.$PHOTO_EXTENSION")
private fun decodeImage(imageBytes: ByteArray): BufferedImage? {
return runCatching {
ByteArrayInputStream(imageBytes).use(ImageIO::read)
}.getOrNull()
}
private fun resizeToSquare(image: BufferedImage, size: Int): BufferedImage {
val sourceWidth = image.width
val sourceHeight = image.height
val side = min(sourceWidth, sourceHeight)
val startX = (sourceWidth - side) / 2
val startY = (sourceHeight - side) / 2
val target = BufferedImage(size, size, BufferedImage.TYPE_INT_RGB)
val graphics = target.createGraphics()
graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR)
graphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY)
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
graphics.drawImage(image, 0, 0, size, size, startX, startY, startX + side, startY + side, null)
graphics.dispose()
return target
}
private fun encodeJpeg(image: BufferedImage, quality: Float): ByteArray? {
val writer = ImageIO.getImageWritersByFormatName(PHOTO_EXTENSION).asSequence().firstOrNull() ?: return null
val output = ByteArrayOutputStream()
val imageOutput = MemoryCacheImageOutputStream(output)
val params = writer.defaultWriteParam
if (params.canWriteCompressed()) {
params.compressionMode = ImageWriteParam.MODE_EXPLICIT
params.compressionQuality = quality
}
return runCatching {
writer.output = imageOutput
writer.write(null, IIOImage(image, null, null), params)
imageOutput.flush()
output.toByteArray()
}.onFailure {
Cobblemon.LOGGER.warn("Failed to encode pokedex photo jpeg.", it)
}.getOrNull().also {
writer.dispose()
imageOutput.close()
output.close()
}
}
private fun createPhotoId(owner: UUID, index: Long): String {
val ownerKey = owner.toString().replace("-", "")
val indexKey = index.toString().padStart(ID_INDEX_PAD, '0')
return "${ownerKey}_$indexKey"
}
private fun saveIndex(root: Path) {
val indexPath = root.resolve(INDEX_FILE_NAME)
val serialized = IndexFile(
nextIndices = nextIndexByOwner.mapKeys { it.key.toString() },
photos = photosById.values.map {
PhotoRecord(
id = it.id,
owner = it.owner.toString(),
index = it.index,
width = it.width,
height = it.height,
createdAt = it.createdAt,
dataSize = it.dataSize,
detectedSpecies = it.detectedSpecies.orEmpty(),
biome = it.biome,
dimension = it.dimension,
timeOfDayTicks = it.timeOfDayTicks,
raining = it.raining,
thundering = it.thundering,
qualityScore = it.qualityScore,
qualityBaseScore = it.qualityBaseScore,
qualitySpeciesMultiplier = it.qualitySpeciesMultiplier,
qualitySubjects = sanitizeQualitySubjects(it.qualitySubjects),
blobHash = photoToBlobHash[it.id] ?: ""
)
},
blobs = blobsByHash.values.map {
BlobIndexRecord(
hash = it.hash,
refCount = it.refCount,
mapId = it.mapId,
mapRenderVersion = it.mapRenderVersion
)
}
)
runCatching {
Files.newBufferedWriter(indexPath).use { writer ->
CobblemonConfig.GSON.toJson(serialized, writer)
}
dirty = false
}.onFailure {
Cobblemon.LOGGER.warn("Failed to save pokedex photo index {}", indexPath, it)
}
}
private fun loadIndex(root: Path) {
val indexPath = root.resolve(INDEX_FILE_NAME)
if (!Files.exists(indexPath)) return
val parsed = runCatching {
Files.newBufferedReader(indexPath).use { reader ->
CobblemonConfig.GSON.fromJson(reader, IndexFile::class.java)
}
}.onFailure {
Cobblemon.LOGGER.warn("Failed to read pokedex photo index {}", indexPath, it)
}.getOrNull() ?: return
val maxIndexByOwner = mutableMapOf<UUID, Long>()
parsed.nextIndices?.forEach { (owner, nextValue) ->
runCatching { UUID.fromString(owner) }.getOrNull()?.let { uuid ->
nextIndexByOwner[uuid] = max(DEFAULT_FIRST_INDEX, nextValue)
}
}
parsed.blobs?.forEach { record ->
blobsByHash[record.hash] = BlobRecord(
hash = record.hash,
refCount = record.refCount,
mapId = record.mapId,
mapRenderVersion = record.mapRenderVersion ?: 0
)
}
var needsSave = false
parsed.photos?.forEach { record ->
val ownerUuid = runCatching { UUID.fromString(record.owner) }.getOrNull() ?: return@forEach
val blobHash = if (!record.blobHash.isNullOrEmpty()) {
record.blobHash
} else {
// Legacy migration: move old per-player file into blobs/
val metadata = PokedexPhotoMetadata(
id = record.id, owner = ownerUuid, index = record.index,
width = record.width, height = record.height,
createdAt = record.createdAt, dataSize = record.dataSize
)
val legacyPath = legacyPhotoPath(root, metadata)
if (!Files.exists(legacyPath)) return@forEach
val bytes = runCatching { Files.readAllBytes(legacyPath) }.getOrNull() ?: return@forEach
val hash = computeHash(bytes)
if (!blobsByHash.containsKey(hash)) {
val dest = blobPath(root, hash)
val copied = runCatching {
Files.createDirectories(dest.parent)
Files.copy(legacyPath, dest)
}.onFailure {
Cobblemon.LOGGER.warn("Failed to migrate legacy photo {} to blob storage", record.id, it)
}.isSuccess
if (!copied) return@forEach
blobsByHash[hash] = BlobRecord(hash = hash, refCount = 0)
}
runCatching { Files.deleteIfExists(legacyPath) }
needsSave = true
hash
}
if (!Files.exists(blobPath(root, blobHash))) return@forEach
val metadata = PokedexPhotoMetadata(
id = record.id, owner = ownerUuid, index = record.index,
width = record.width, height = record.height,
createdAt = record.createdAt, dataSize = record.dataSize,
detectedSpecies = record.detectedSpecies ?: emptyList(),
biome = record.biome ?: "minecraft:unknown",
dimension = record.dimension ?: "minecraft:overworld",
timeOfDayTicks = record.timeOfDayTicks ?: 0L,
raining = record.raining ?: false,
thundering = record.thundering ?: false,
qualityScore = record.qualityScore,
qualityBaseScore = record.qualityBaseScore,
qualitySpeciesMultiplier = record.qualitySpeciesMultiplier,
qualitySubjects = sanitizeQualitySubjects(record.qualitySubjects)
)
photosById[metadata.id] = metadata
photosByOwner.getOrPut(ownerUuid) { mutableListOf() }.add(metadata)
photoToBlobHash[metadata.id] = blobHash
maxIndexByOwner[ownerUuid] = max(maxIndexByOwner.getOrDefault(ownerUuid, 0L), metadata.index)
}
// Recompute ref counts from actual photo->blob mapping to stay consistent after any migration.
if (needsSave) {
blobsByHash.values.forEach { it.refCount = 0 }
photoToBlobHash.values.forEach { hash -> blobsByHash[hash]?.refCount = (blobsByHash[hash]?.refCount ?: 0) + 1 }
}
photosByOwner.values.forEach { entries -> entries.sortByDescending { it.createdAt } }
maxIndexByOwner.forEach { (owner, maxIndex) ->
val fromIndex = maxIndex + 1
val existing = nextIndexByOwner.getOrDefault(owner, DEFAULT_FIRST_INDEX)
nextIndexByOwner[owner] = max(existing, fromIndex)
}
if (needsSave) {
saveIndex(root)
}
}
private fun sanitizeQualitySubjects(subjects: List<PokedexPhotoSubjectQuality>?): List<PokedexPhotoSubjectQuality> {
return subjects.orEmpty().map { subject ->
subject.copy(activeAnimations = subject.activeAnimations.orEmpty())
}
}
// Fields are nullable because Gson's reflection-based deserialization bypasses Kotlin default values
// — an older index.json written without blob storage won't have the "blobs" key at all.
private data class IndexFile(
@SerializedName("next_indices") val nextIndices: Map<String, Long>? = null,
@SerializedName("photos") val photos: List<PhotoRecord>? = null,
@SerializedName("blobs") val blobs: List<BlobIndexRecord>? = null
)
private data class PhotoRecord(
val id: String,
val owner: String,
val index: Long,
val width: Int,
val height: Int,
@SerializedName("created_at") val createdAt: Long,
@SerializedName("data_size") val dataSize: Int,
@SerializedName("detected_species") val detectedSpecies: List<String>? = null,
@SerializedName("biome") val biome: String? = null,
@SerializedName("dimension") val dimension: String? = null,
@SerializedName("time_of_day_ticks") val timeOfDayTicks: Long? = null,
@SerializedName("raining") val raining: Boolean? = null,
@SerializedName("thundering") val thundering: Boolean? = null,
@SerializedName("quality_score") val qualityScore: Int,
@SerializedName("quality_base_score") val qualityBaseScore: Int,
@SerializedName("quality_species_multiplier") val qualitySpeciesMultiplier: Float,
@SerializedName("quality_subjects") val qualitySubjects: List<PokedexPhotoSubjectQuality>,
// Nullable because Gson's reflection-based deserialization bypasses Kotlin default values;
// older indexes written before blob storage simply lack this field.
@SerializedName("blob_hash") val blobHash: String? = null
)
private data class BlobIndexRecord(
val hash: String,
@SerializedName("ref_count") val refCount: Int,
@SerializedName("map_id") val mapId: Int? = null,
@SerializedName("map_render_version") val mapRenderVersion: Int? = null
)
}
@@ -0,0 +1,62 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.pokedex.photo
import com.cobblemon.mod.common.CobblemonItems
import com.cobblemon.mod.common.item.components.PrintedPhotoComponent
import net.minecraft.core.component.DataComponents
import net.minecraft.network.chat.Component
import net.minecraft.server.level.ServerLevel
import net.minecraft.world.item.ItemStack
import java.io.ByteArrayInputStream
import javax.imageio.ImageIO
object PokedexPrintedPhotoFactory {
@Suppress("UNUSED_PARAMETER")
fun createPrintedStack(level: ServerLevel, metadata: PokedexPhotoMetadata, imageData: ByteArray, printSize: Int): ItemStack? {
val decoded = decodeImageDimensions(imageData) ?: return null
val asset = PrintedPhotoAssetRepository.createOrGetAssetFromJpeg(
jpegBytes = imageData,
width = decoded.first,
height = decoded.second
) ?: return null
return buildStack(metadata, asset.assetId, printSize.coerceIn(1, 3))
}
private fun buildStack(metadata: PokedexPhotoMetadata, assetId: Int, printSize: Int): ItemStack {
val stack = ItemStack(CobblemonItems.PRINTED_PHOTO)
PrintedPhotoComponent.set(
stack,
PrintedPhotoComponent(
photoId = metadata.id,
owner = metadata.owner,
index = metadata.index,
assetId = assetId,
printSize = printSize,
createdAt = metadata.createdAt,
detectedSpecies = metadata.detectedSpecies,
biome = metadata.biome,
dimension = metadata.dimension,
timeOfDayTicks = metadata.timeOfDayTicks,
qualityScore = metadata.qualityScore
)
)
stack.set(DataComponents.CUSTOM_NAME, Component.translatable("item.cobblemon.printed_photo.named", metadata.index))
return stack
}
private fun decodeImageDimensions(imageBytes: ByteArray): Pair<Int, Int>? {
return runCatching {
ByteArrayInputStream(imageBytes).use { stream ->
val image = ImageIO.read(stream) ?: return@runCatching null
image.width to image.height
}
}.getOrNull()
}
}
@@ -0,0 +1,209 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.pokedex.photo
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.config.CobblemonConfig
import com.google.gson.annotations.SerializedName
import net.minecraft.server.MinecraftServer
import net.minecraft.world.level.storage.LevelResource
import java.nio.file.Files
import java.nio.file.Path
import java.security.MessageDigest
import kotlin.math.max
/**
* Server-side, world-scoped, deduplicated storage for printed-photo image assets.
*
* Assets are written once per content hash (sha-256) and assigned a monotonic integer asset id.
* Item components reference an asset by id; clients fetch the bytes on demand.
*/
object PrintedPhotoAssetRepository {
private const val INDEX_FILE_NAME = "index.json"
private const val BLOBS_DIRECTORY = "blobs"
private const val ASSET_EXTENSION = "jpg"
private val lock = Any()
private var rootPath: Path? = null
private var dirty = false
private var nextAssetId: Int = 1
private val assetById = mutableMapOf<Int, AssetRecord>()
private val idByHash = mutableMapOf<String, Int>()
data class AssetRecord(
val assetId: Int,
val hash: String,
val width: Int,
val height: Int,
val createdAt: Long,
val byteSize: Int
)
fun initialize(server: MinecraftServer) {
synchronized(lock) {
val root = server.getWorldPath(LevelResource.ROOT).resolve("printed_photo_assets")
rootPath = root
Files.createDirectories(root)
Files.createDirectories(root.resolve(BLOBS_DIRECTORY))
nextAssetId = 1
assetById.clear()
idByHash.clear()
dirty = false
loadIndex(root)
}
}
fun shutdown() {
synchronized(lock) {
if (dirty) rootPath?.let(::saveIndex)
rootPath = null
assetById.clear()
idByHash.clear()
dirty = false
}
}
/**
* Returns the existing asset record for [jpegBytes] if its hash is already stored,
* otherwise writes the bytes to disk under a new asset id and returns the new record.
*/
fun createOrGetAssetFromJpeg(jpegBytes: ByteArray, width: Int, height: Int): AssetRecord? {
synchronized(lock) {
val root = rootPath ?: return null
val hash = computeHash(jpegBytes)
idByHash[hash]?.let { existingId ->
return assetById[existingId]
}
val blobFile = blobPath(root, hash)
if (!Files.exists(blobFile)) {
val written = runCatching {
Files.createDirectories(blobFile.parent)
Files.write(blobFile, jpegBytes)
}.onFailure {
Cobblemon.LOGGER.warn("Failed to write printed photo asset blob {}", hash, it)
}.isSuccess
if (!written) return null
}
val assetId = nextAssetId++
val record = AssetRecord(
assetId = assetId,
hash = hash,
width = width,
height = height,
createdAt = System.currentTimeMillis(),
byteSize = jpegBytes.size
)
assetById[assetId] = record
idByHash[hash] = assetId
dirty = true
saveIndex(root)
return record
}
}
fun getAssetBytes(assetId: Int): ByteArray? {
synchronized(lock) {
val root = rootPath ?: return null
val record = assetById[assetId] ?: return null
val path = blobPath(root, record.hash)
if (!Files.exists(path)) return null
return runCatching { Files.readAllBytes(path) }
.onFailure { Cobblemon.LOGGER.warn("Failed to read printed photo asset blob {}", record.hash, it) }
.getOrNull()
}
}
fun getAssetRecord(assetId: Int): AssetRecord? {
synchronized(lock) {
return assetById[assetId]
}
}
private fun computeHash(data: ByteArray): String {
val bytes = MessageDigest.getInstance("SHA-256").digest(data)
return bytes.joinToString("") { "%02x".format(it) }
}
private fun blobPath(root: Path, hash: String): Path =
root.resolve(BLOBS_DIRECTORY).resolve("$hash.$ASSET_EXTENSION")
private fun saveIndex(root: Path) {
val indexPath = root.resolve(INDEX_FILE_NAME)
val serialized = IndexFile(
nextAssetId = nextAssetId,
assets = assetById.values.map {
AssetRecordJson(
assetId = it.assetId,
hash = it.hash,
width = it.width,
height = it.height,
createdAt = it.createdAt,
byteSize = it.byteSize
)
}
)
runCatching {
Files.newBufferedWriter(indexPath).use { writer ->
CobblemonConfig.GSON.toJson(serialized, writer)
}
dirty = false
}.onFailure {
Cobblemon.LOGGER.warn("Failed to save printed photo asset index {}", indexPath, it)
}
}
private fun loadIndex(root: Path) {
val indexPath = root.resolve(INDEX_FILE_NAME)
if (!Files.exists(indexPath)) return
val parsed = runCatching {
Files.newBufferedReader(indexPath).use { reader ->
CobblemonConfig.GSON.fromJson(reader, IndexFile::class.java)
}
}.onFailure {
Cobblemon.LOGGER.warn("Failed to read printed photo asset index {}", indexPath, it)
}.getOrNull() ?: return
nextAssetId = max(1, parsed.nextAssetId ?: 1)
parsed.assets?.forEach { json ->
val record = AssetRecord(
assetId = json.assetId,
hash = json.hash,
width = json.width,
height = json.height,
createdAt = json.createdAt,
byteSize = json.byteSize
)
assetById[record.assetId] = record
idByHash[record.hash] = record.assetId
if (record.assetId >= nextAssetId) nextAssetId = record.assetId + 1
}
}
// Nullable fields because Gson reflection-based deserialization bypasses Kotlin defaults.
private data class IndexFile(
@SerializedName("next_asset_id") val nextAssetId: Int? = null,
@SerializedName("assets") val assets: List<AssetRecordJson>? = null
)
private data class AssetRecordJson(
@SerializedName("asset_id") val assetId: Int,
val hash: String,
val width: Int,
val height: Int,
@SerializedName("created_at") val createdAt: Long,
@SerializedName("byte_size") val byteSize: Int
)
}
@@ -0,0 +1,50 @@
/*
* Copyright (C) 2023 Cobblemon Contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package com.cobblemon.mod.common.pokedex.photo
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.pokedex.PokedexEntryProgress
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.item.components.PrintedPhotoComponent
import net.minecraft.network.chat.Component
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
object PrintedPhotoPokedexScanner {
fun grantSeenEntries(player: ServerPlayer, component: PrintedPhotoComponent): Int {
val pokedex = Cobblemon.playerDataManager.getPokedexData(player)
var newlySeen = 0
component.detectedSpecies
.asSequence()
.mapNotNull(ResourceLocation::tryParse)
.distinct()
.forEach { speciesId ->
if (pokedex.getKnowledgeForSpecies(speciesId) >= PokedexEntryProgress.ENCOUNTERED) {
return@forEach
}
val species = PokemonSpecies.getByIdentifier(speciesId) ?: return@forEach
pokedex.encounter(species.create(level = 1))
newlySeen++
}
return newlySeen
}
fun sendResultMessage(player: ServerPlayer, newlySeen: Int, totalSpecies: Int) {
val message = if (totalSpecies <= 0) {
Component.translatable("cobblemon.ui.pokedex.photo_scan.empty")
} else if (newlySeen > 0) {
Component.translatable("cobblemon.ui.pokedex.photo_scan.success", newlySeen, totalSpecies)
} else {
Component.translatable("cobblemon.ui.pokedex.photo_scan.already_seen", totalSpecies)
}
player.displayClientMessage(message, true)
}
}
File diff suppressed because it is too large Load Diff
@@ -9,10 +9,15 @@
package com.cobblemon.mod.common.pokedex.scanner
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.pokedex.scan.BlockScanProviderRegistry
import com.cobblemon.mod.common.pokedex.scanner.PokedexUsageContext.Companion.BLOCK_LENGTH_PER_ZOOM_STAGE
import net.minecraft.core.BlockPos
import net.minecraft.core.registries.BuiltInRegistries
import net.minecraft.world.entity.Entity
import net.minecraft.world.level.ClipContext
import net.minecraft.world.level.block.entity.BlockEntity
import net.minecraft.world.phys.AABB
import net.minecraft.world.phys.BlockHitResult
import net.minecraft.world.phys.HitResult
//Handles the actual raycasting to figure out what pokemon we are looking at
@@ -97,6 +102,29 @@ object PokemonScanner {
return targetedEntity as? ScannableEntity
}
fun findScannableBlockEntity(castingEntity: Entity, zoomLevel: Int): Pair<BlockPos, BlockEntity>? {
val eyePos = castingEntity.getEyePosition(1.0F)
val lookVec = castingEntity.getViewVector(1.0F)
val maxDistance = Cobblemon.config.maxPokedexScanningDetectionRange + (zoomLevel * BLOCK_LENGTH_PER_ZOOM_STAGE)
val hit = castingEntity.level().clip(
ClipContext(
eyePos,
eyePos.add(lookVec.scale(maxDistance.toDouble())),
ClipContext.Block.OUTLINE,
ClipContext.Fluid.NONE,
castingEntity
)
)
if (hit.type != HitResult.Type.BLOCK) return null
val blockPos = (hit as? BlockHitResult)?.blockPos ?: return null
val blockEntity = castingEntity.level().getBlockEntity(blockPos) ?: return null
val typeId = BuiltInRegistries.BLOCK_ENTITY_TYPE.getKey(blockEntity.type) ?: return null
if (BlockScanProviderRegistry.get(typeId) == null) return null
return blockPos to blockEntity
}
fun isEntityInRange(castingEntity: Entity, targetEntity: Entity, zoomLevel: Int): Boolean {
return targetEntity.position().distanceTo(castingEntity.position()) <= Cobblemon.config.maxPokedexScanningDetectionRange + (zoomLevel * BLOCK_LENGTH_PER_ZOOM_STAGE)
}
@@ -0,0 +1,53 @@
package com.cobblemon.mod.common.pokedex.scanner.block
import com.cobblemon.mod.common.api.pokedex.scan.BlockScanDefinition
import com.cobblemon.mod.common.api.text.bold
import com.cobblemon.mod.common.api.text.text
import com.cobblemon.mod.common.block.BerryBlock
import com.cobblemon.mod.common.block.entity.BerryBlockEntity
import net.minecraft.resources.ResourceLocation
import net.minecraft.world.item.ItemStack
import net.minecraft.world.level.block.entity.BlockEntity
object BerryBlockScanProvider : BlockScanProvider {
override val frames = listOf(
BlockScanDefinition.Frame(
slot = 1,
text = BlockScanDefinition.ScanText.Translate(
key = "ui.pokedex.scan.block.berry_stage",
with = listOf(BlockScanDefinition.ScanText.Key("stage"))
)
),
BlockScanDefinition.Frame(
slot = 2,
text = BlockScanDefinition.ScanText.Translate(
key = "ui.pokedex.scan.block.berry",
with = listOf(BlockScanDefinition.ScanText.Key("berry_name"))
)
),
BlockScanDefinition.Frame(
slot = 3,
text = BlockScanDefinition.ScanText.Translate(
key = "ui.pokedex.scan.block.berry_mulch",
with = listOf(BlockScanDefinition.ScanText.Key("mulch_variant"))
),
scale = 1.0f
)
)
override fun getScanKey(be: BlockEntity): ResourceLocation = (be as BerryBlockEntity).berryIdentifier
override fun getValues(be: BlockEntity): Map<String, Any?> {
val berry = be as BerryBlockEntity
val stage = runCatching { berry.blockState.getValue(BerryBlock.AGE) }.getOrDefault(0)
val berryName = berry.berry()?.item()?.let { ItemStack(it).hoverName.copy().bold() }
?: berry.berryIdentifier.toString().text().bold()
return mapOf(
"stage" to stage,
"mulch_variant" to berry.mulchVariant.toString(),
"berry_identifier" to berry.berryIdentifier.toString(),
"berry_name" to berryName
)
}
}
@@ -0,0 +1,13 @@
package com.cobblemon.mod.common.pokedex.scanner.block
import com.cobblemon.mod.common.api.pokedex.scan.BlockScanDefinition
import net.minecraft.core.BlockPos
import net.minecraft.resources.ResourceLocation
import net.minecraft.world.level.block.entity.BlockEntity
interface BlockScanProvider {
val frames: List<BlockScanDefinition.Frame>
fun getScanKey(be: BlockEntity): ResourceLocation
fun getValues(be: BlockEntity): Map<String, Any?>
fun onScanComplete(pos: BlockPos, zoomLevel: Int) {}
}
@@ -0,0 +1,61 @@
package com.cobblemon.mod.common.pokedex.scanner.block
import com.cobblemon.mod.common.api.pokedex.scan.BlockScanDefinition
import com.cobblemon.mod.common.block.entity.PrintedPhotoDisplayBlockEntity
import com.cobblemon.mod.common.net.messages.server.pokedex.scanner.FinishPrintedPhotoScanningPacket
import com.cobblemon.mod.common.pokedex.photo.PokedexPhotoRatings
import com.cobblemon.mod.common.util.cobblemonResource
import net.minecraft.client.Minecraft
import net.minecraft.core.BlockPos
import net.minecraft.network.chat.Component
import net.minecraft.resources.ResourceLocation
import net.minecraft.world.level.block.entity.BlockEntity
object PrintedPhotoBlockScanProvider : BlockScanProvider {
override val frames = listOf(
BlockScanDefinition.Frame(
slot = 1,
text = BlockScanDefinition.ScanText.Translate(
key = "ui.pokedex.scan.block.printed_photo_rating",
with = listOf(BlockScanDefinition.ScanText.Key("rating"))
)
),
BlockScanDefinition.Frame(
slot = 2,
text = BlockScanDefinition.ScanText.Translate(
key = "ui.pokedex.scan.block.printed_photo_score",
with = listOf(BlockScanDefinition.ScanText.Key("score"))
)
),
BlockScanDefinition.Frame(
slot = 3,
text = BlockScanDefinition.ScanText.Translate(
key = "ui.pokedex.scan.block.printed_photo_species_count",
with = listOf(BlockScanDefinition.ScanText.Key("species_count"))
),
scale = 1.0f
)
)
override fun getScanKey(be: BlockEntity): ResourceLocation = cobblemonResource("printed_photo_display")
override fun getValues(be: BlockEntity): Map<String, Any?> {
val component = (be as? PrintedPhotoDisplayBlockEntity)?.component
val score = component?.qualityScore ?: 0
val speciesCount = component?.detectedSpecies?.size ?: 0
return mapOf(
"rating" to PokedexPhotoRatings.ratingText(score),
"score" to "$score/5000",
"species_count" to speciesCount
)
}
override fun onScanComplete(pos: BlockPos, zoomLevel: Int) {
FinishPrintedPhotoScanningPacket(pos, zoomLevel).sendToServer()
Minecraft.getInstance().player?.displayClientMessage(
Component.translatable("cobblemon.ui.pokedex.photo_scan.sent"),
true
)
}
}
@@ -0,0 +1,94 @@
{
"variants": {
"facing=north,part=bottom,state=off": {
"model": "cobblemon:block/photo_printer"
},
"facing=north,part=bottom,state=on": {
"model": "cobblemon:block/photo_printer_on"
},
"facing=north,part=bottom,state=printing": {
"model": "cobblemon:block/photo_printer_printing"
},
"facing=north,part=top,state=off": {
"model": "cobblemon:block/photo_printer_top"
},
"facing=north,part=top,state=on": {
"model": "cobblemon:block/photo_printer_top"
},
"facing=north,part=top,state=printing": {
"model": "cobblemon:block/photo_printer_top"
},
"facing=east,part=bottom,state=off": {
"model": "cobblemon:block/photo_printer",
"y": 90
},
"facing=east,part=bottom,state=on": {
"model": "cobblemon:block/photo_printer_on",
"y": 90
},
"facing=east,part=bottom,state=printing": {
"model": "cobblemon:block/photo_printer_printing",
"y": 90
},
"facing=east,part=top,state=off": {
"model": "cobblemon:block/photo_printer_top",
"y": 90
},
"facing=east,part=top,state=on": {
"model": "cobblemon:block/photo_printer_top",
"y": 90
},
"facing=east,part=top,state=printing": {
"model": "cobblemon:block/photo_printer_top",
"y": 90
},
"facing=south,part=bottom,state=off": {
"model": "cobblemon:block/photo_printer",
"y": 180
},
"facing=south,part=bottom,state=on": {
"model": "cobblemon:block/photo_printer_on",
"y": 180
},
"facing=south,part=bottom,state=printing": {
"model": "cobblemon:block/photo_printer_printing",
"y": 180
},
"facing=south,part=top,state=off": {
"model": "cobblemon:block/photo_printer_top",
"y": 180
},
"facing=south,part=top,state=on": {
"model": "cobblemon:block/photo_printer_top",
"y": 180
},
"facing=south,part=top,state=printing": {
"model": "cobblemon:block/photo_printer_top",
"y": 180
},
"facing=west,part=bottom,state=off": {
"model": "cobblemon:block/photo_printer",
"y": 270
},
"facing=west,part=bottom,state=on": {
"model": "cobblemon:block/photo_printer_on",
"y": 270
},
"facing=west,part=bottom,state=printing": {
"model": "cobblemon:block/photo_printer_printing",
"y": 270
},
"facing=west,part=top,state=off": {
"model": "cobblemon:block/photo_printer_top",
"y": 270
},
"facing=west,part=top,state=on": {
"model": "cobblemon:block/photo_printer_top",
"y": 270
},
"facing=west,part=top,state=printing": {
"model": "cobblemon:block/photo_printer_top",
"y": 270
}
}
}
@@ -0,0 +1,9 @@
{
"multipart": [
{
"apply": {
"model": "cobblemon:block/printed_photo_display"
}
}
]
}
@@ -93,6 +93,11 @@
"item.cobblemon.pokedex_red": "Pokédex",
"item.cobblemon.pokedex_white": "Pokédex",
"item.cobblemon.pokedex_yellow": "Pokédex",
"item.cobblemon.printed_photo": "Printed Photo",
"item.cobblemon.printed_photo.photo_id": "Photo ID: %s",
"item.cobblemon.printed_photo.quality_score": "Quality: %s",
"item.cobblemon.printed_photo.rating": "Rating: %s",
"item.cobblemon.printed_photo.named": "Printed Photo #%s",
"item.cobblemon.npc_editor": "NPC's Trainer Card",
@@ -1319,6 +1324,18 @@
"cobblemon.ui.pokedex.scan.registered_suffix": "%1$s Registered",
"cobblemon.ui.pokedex.scan.trainer_owned": "Trainer Owned",
"cobblemon.ui.pokedex.scan.mode.scanning": "Scanning Mode",
"cobblemon.ui.pokedex.scan.mode.camera": "Camera Mode",
"cobblemon.ui.pokedex.scan.mode.toggle_hint": "%1$s: Toggle Mode",
"cobblemon.ui.pokedex.scan.mode.capture_hint": "Left Click: Take Picture",
"cobblemon.ui.pokedex.scan.mode.scanning_enabled": "Scanning Mode Enabled",
"cobblemon.ui.pokedex.scan.mode.camera_enabled": "Camera Mode Enabled",
"cobblemon.ui.pokedex.scan.block.berry": "%1$s",
"cobblemon.ui.pokedex.scan.block.berry_stage": "Growth: %1$s",
"cobblemon.ui.pokedex.scan.block.berry_mulch": "Mulch: %1$s",
"cobblemon.ui.pokedex.scan.block.printed_photo_rating": "Rating: %1$s",
"cobblemon.ui.pokedex.scan.block.printed_photo_score": "Score: %1$s",
"cobblemon.ui.pokedex.scan.block.printed_photo_species_count": "Pokemon: %1$s",
"cobblemon.ui.pokedex.region.national": "National",
"cobblemon.ui.pokedex.region.alola": "Alola",
@@ -2652,6 +2669,58 @@
"cobblemon.config.ui.battle_spectate_max_distance.tooltip": "The maximum distance at which players can spectate battles.",
"cobblemon.config.ui.max_pokedex_scanning_detection_range": "Max Pokédex Scanning Detection Range",
"cobblemon.config.ui.max_pokedex_scanning_detection_range.tooltip": "The maximum distance at which the Pokédex can detect and scan Pokémon.",
"cobblemon.config.ui.pokedex_scanner_photo_resolution": "Pokédex Photo Resolution",
"cobblemon.config.ui.pokedex_scanner_photo_resolution.tooltip": "Resolution used for scanner captures before server storage.",
"cobblemon.config.ui.pokedex_scanner_photo_limit_per_player": "Pokédex Photos Per Player",
"cobblemon.config.ui.pokedex_scanner_photo_limit_per_player.tooltip": "Maximum number of scanner photos each player can keep on server storage.",
"cobblemon.config.ui.pokedex_scanner_photo_jpeg_quality": "Pokédex Photo JPEG Quality",
"cobblemon.config.ui.pokedex_scanner_photo_jpeg_quality.tooltip": "JPEG compression quality for server-side scanner photos (0.1 to 1.0).",
"cobblemon.config.ui.pokedex_scanner_photo_raycast_distance": "Pokédex Photo Raycast Distance",
"cobblemon.config.ui.pokedex_scanner_photo_raycast_distance.tooltip": "Maximum distance (in blocks) used for client-side photo species raycasts.",
"cobblemon.config.ui.pokedex_scanner_photo_raycast_density": "Pokédex Photo Raycast Density",
"cobblemon.config.ui.pokedex_scanner_photo_raycast_density.tooltip": "Density level for raycasts inside the configured photo capture bounds. Each level doubles the number of rays (1=low, 2=double, etc).",
"cobblemon.config.ui.pokedex_scanner_photo_perimeter_raycast_density": "Pokédex Photo Perimeter Raycast Density",
"cobblemon.config.ui.pokedex_scanner_photo_perimeter_raycast_density.tooltip": "Density level for out-of-frame detection raycasts just outside the photo capture bounds.",
"cobblemon.config.ui.pokedex_scanner_photo_coverage_score_cap": "Pokédex Photo Coverage Score Cap",
"cobblemon.config.ui.pokedex_scanner_photo_coverage_score_cap.tooltip": "Maximum points awarded for how much of the photo ray area the main subject covers.",
"cobblemon.config.ui.pokedex_scanner_photo_coverage_score_cap_at_percent": "Pokédex Photo Coverage Cap Percent",
"cobblemon.config.ui.pokedex_scanner_photo_coverage_score_cap_at_percent.tooltip": "The subject coverage percentage where coverage scoring reaches its cap.",
"cobblemon.config.ui.pokedex_scanner_photo_centered_score_cap": "Pokédex Photo Centered Score Cap",
"cobblemon.config.ui.pokedex_scanner_photo_centered_score_cap.tooltip": "Maximum points awarded for placing the main subject near the center of the photo.",
"cobblemon.config.ui.pokedex_scanner_photo_facing_bonus": "Pokédex Photo Look-at-Camera Bonus",
"cobblemon.config.ui.pokedex_scanner_photo_facing_bonus.tooltip": "Maximum bonus points awarded when the main subject is looking at the player taking the photo.",
"cobblemon.config.ui.pokedex_scanner_photo_full_body_bonus": "Pokédex Photo Full Body Bonus",
"cobblemon.config.ui.pokedex_scanner_photo_full_body_bonus.tooltip": "Bonus points awarded when the subject is detected fully inside the photo bounds.",
"cobblemon.config.ui.pokedex_scanner_photo_out_of_frame_multiplier": "Pokédex Photo Out-of-Frame Multiplier",
"cobblemon.config.ui.pokedex_scanner_photo_out_of_frame_multiplier.tooltip": "Score multiplier applied to the main subject when out-of-frame perimeter rays detect clipping.",
"cobblemon.config.ui.pokedex_scanner_photo_shiny_bonus": "Pokédex Photo Shiny Bonus",
"cobblemon.config.ui.pokedex_scanner_photo_shiny_bonus.tooltip": "Bonus points awarded when the primary photographed Pokémon is shiny.",
"cobblemon.config.ui.pokedex_scanner_photo_uncommon_species_multiplier": "Pokédex Photo Uncommon Species Multiplier",
"cobblemon.config.ui.pokedex_scanner_photo_uncommon_species_multiplier.tooltip": "Species rarity multiplier for Pokémon whose rarest spawn bucket is uncommon.",
"cobblemon.config.ui.pokedex_scanner_photo_rare_species_multiplier": "Pokédex Photo Rare Species Multiplier",
"cobblemon.config.ui.pokedex_scanner_photo_rare_species_multiplier.tooltip": "Species rarity multiplier for Pokémon whose rarest spawn bucket is rare.",
"cobblemon.config.ui.pokedex_scanner_photo_ultra_rare_species_multiplier": "Pokédex Photo Ultra-Rare Species Multiplier",
"cobblemon.config.ui.pokedex_scanner_photo_ultra_rare_species_multiplier.tooltip": "Species rarity multiplier for Pokémon whose rarest spawn bucket is ultra-rare.",
"cobblemon.config.ui.pokedex_scanner_photo_extra_pokemon_bonus_cap": "Pokédex Photo Extra Pokémon Bonus Cap",
"cobblemon.config.ui.pokedex_scanner_photo_extra_pokemon_bonus_cap.tooltip": "Maximum total bonus points awarded for additional Pokémon in the shot.",
"cobblemon.config.ui.pokedex_scanner_photo_extra_pokemon_bonus_per_subject": "Pokédex Photo Extra Pokémon Per-Subject Bonus",
"cobblemon.config.ui.pokedex_scanner_photo_extra_pokemon_bonus_per_subject.tooltip": "Maximum bonus points contributed by each additional Pokémon in the shot.",
"cobblemon.config.ui.pokedex_scanner_photo_family_bonus": "Pokédex Photo Family Bonus",
"cobblemon.config.ui.pokedex_scanner_photo_family_bonus.tooltip": "Bonus points awarded for each additional Pokémon in the shot that is the same species or shares an evolution line with the primary subject.",
"cobblemon.config.ui.pokedex_scanner_photo_habitat_bonus": "Pokédex Photo Habitat Bonus",
"cobblemon.config.ui.pokedex_scanner_photo_habitat_bonus.tooltip": "Bonus points awarded when the primary photographed Pokémon can spawn in the biome where the photo was taken.",
"cobblemon.config.ui.pokedex_scanner_photo_time_of_day_bonus": "Pokédex Photo Time of Day Bonus",
"cobblemon.config.ui.pokedex_scanner_photo_time_of_day_bonus.tooltip": "Bonus points awarded when the primary photographed Pokémon can spawn at the time of day when the photo was taken.",
"cobblemon.config.ui.pokedex_scanner_photo_weather_bonus": "Pokédex Photo Weather Bonus",
"cobblemon.config.ui.pokedex_scanner_photo_weather_bonus.tooltip": "Bonus points awarded when the primary photographed Pokémon can spawn in the weather when the photo was taken.",
"cobblemon.config.ui.pokedex_scanner_photo_alpha_bonus": "Pokédex Photo Alpha Bonus",
"cobblemon.config.ui.pokedex_scanner_photo_alpha_bonus.tooltip": "Bonus points awarded when the primary photographed Pokémon has the alpha aspect.",
"cobblemon.config.ui.pokedex_scanner_photo_wild_bonus": "Pokédex Photo Wild Bonus",
"cobblemon.config.ui.pokedex_scanner_photo_wild_bonus.tooltip": "Bonus points awarded when the primary photographed Pokémon is wild.",
"cobblemon.config.ui.pokedex_scanner_photo_form_bonus": "Pokédex Photo Form Bonus",
"cobblemon.config.ui.pokedex_scanner_photo_form_bonus.tooltip": "Bonus points awarded when the primary photographed Pokémon is in a non-standard form.",
"cobblemon.config.ui.pokedex_delete_photo_after_print": "Delete Source Photo After Print",
"cobblemon.config.ui.pokedex_delete_photo_after_print.tooltip": "If enabled, scanner photo entries are deleted after printing.",
"cobblemon.config.ui.unlock_all_move_dex_moves_by_default": "Unlock All Moves",
"cobblemon.config.ui.unlock_all_move_dex_moves_by_default.tooltip": "If enabled, TM and level-up moves are unlocked and discovered in the TM Machine and Pokédex Move Dex.",
"cobblemon.config.ui.infinite_tm_uses": "Infinite TM Uses",
@@ -9553,6 +9622,33 @@
"cobblemon.snapshots.warning.dont_show_again": "Don't Show Again",
"cobblemon.snapshots.warning.dont_show_again_tip": "This setting may be ignored on newer Cobblemon snapshots (e.g. 1.7.0 vs 1.8.0)",
"cobblemon.ui.pokedex.page.pokedex": "Pokedex",
"cobblemon.ui.pokedex.page.photo_gallery": "Photo Gallery",
"cobblemon.ui.pokedex.gallery.count": "Photos: %s",
"cobblemon.ui.pokedex.gallery.filtered_count": "Showing %s / %s",
"cobblemon.ui.pokedex.gallery.empty": "No Pokedex photos yet.",
"cobblemon.ui.pokedex.gallery.empty_hint": "Use scan mode and click to snap.",
"cobblemon.ui.pokedex.gallery.no_match": "No photos match that filter.",
"cobblemon.ui.pokedex.gallery.quality_score": "Quality: %s",
"cobblemon.ui.pokedex.gallery.rating": "%s",
"cobblemon.ui.pokedex.gallery.filter_hint": "Use commas to combine filters (species, biome, dimension, time).",
"cobblemon.ui.pokedex.gallery.select_hint": "Left-click a thumbnail to select and preview.",
"cobblemon.ui.pokedex.gallery.delete_hint": "Right-click a thumbnail to delete it from server storage.",
"cobblemon.ui.pokedex.photo_scan.sent": "Scanning printed photo...",
"cobblemon.ui.pokedex.photo_scan.empty": "No Pokémon were found in this printed photo.",
"cobblemon.ui.pokedex.photo_scan.success": "Registered %s of %s Pokémon from the printed photo.",
"cobblemon.ui.pokedex.photo_scan.already_seen": "All %s Pokémon in this printed photo were already seen.",
"cobblemon.ui.photo_printer.print": "Print",
"cobblemon.ui.photo_printer.printing": "Printing...",
"cobblemon.ui.photo_printer.count": "Photos: %s / %s",
"cobblemon.ui.photo_printer.filtered": "Showing: %s / %s",
"cobblemon.ui.photo_printer.selected": "Selected: %s",
"cobblemon.ui.photo_printer.scroll_hint": "Scroll mouse wheel over thumbnails to browse.",
"cobblemon.ui.photo_printer.no_match": "No photos match that filter.",
"cobblemon.ui.photo_printer.filter_hint": "Filter supports comma-separated terms (species, biome, dimension, time).",
"cobblemon.container.photo_printer": "Photo Printer",
"block.cobblemon.photo_printer": "Photo Printer",
"stat.cobblemon.battles_fled": "Pokémon Battles Fled",
"stat.cobblemon.battles_lost": "Pokémon Battles Lost",
"stat.cobblemon.battles_total": "Pokémon Total Battles",
@@ -0,0 +1,195 @@
{
"format_version": "1.21.11",
"credit": "Made with Blockbench",
"texture_size": [64, 64],
"textures": {
"0": "cobblemon:block/printer",
"screen": "cobblemon:block/printer_screen_off",
"particle": "cobblemon:block/printer"
},
"elements": [
{
"from": [1, 0, 1],
"to": [15, 1, 15],
"rotation": {"angle": 0, "axis": "y", "origin": [1, 0, 1]},
"faces": {
"north": {"uv": [12.5, 15.75, 16, 16], "texture": "#0"},
"east": {"uv": [8.75, 15.75, 12.25, 16], "texture": "#0"},
"south": {"uv": [5, 15.75, 8.5, 16], "texture": "#0"},
"west": {"uv": [1.25, 15.75, 4.75, 16], "texture": "#0"},
"up": {"uv": [3.5, 3.5, 0, 0], "texture": "#0"},
"down": {"uv": [3.5, 3.75, 0, 7.25], "texture": "#0"}
}
},
{
"from": [2, 14, 2],
"to": [14, 31, 14],
"rotation": {"angle": 0, "axis": "y", "origin": [1, 30, 1]},
"faces": {
"north": {"uv": [12.75, 8.25, 15.75, 12.5], "texture": "#0"},
"east": {"uv": [9, 8.25, 12, 12.5], "texture": "#0"},
"south": {"uv": [5.25, 8.25, 8.25, 12.5], "texture": "#0"},
"west": {"uv": [1.5, 8.25, 4.5, 12.5], "texture": "#0"},
"up": {"uv": [6.75, 3, 3.75, 0], "texture": "#0"},
"down": {"uv": [6.75, 3.25, 3.75, 6.25], "texture": "#0"}
}
},
{
"from": [3, 23, 3],
"to": [13, 30, 3],
"rotation": {"angle": 0, "axis": "y", "origin": [1, 35, -11]},
"faces": {
"north": {"uv": [0, 0, 16, 16], "texture": "#screen"},
"east": {"uv": [0, 0, 0, 1.75], "texture": "#0"},
"west": {"uv": [0, 0, 0, 1.75], "texture": "#0"},
"up": {"uv": [2.5, 0, 0, 0], "texture": "#0"},
"down": {"uv": [2.5, 0, 0, 0], "texture": "#0"}
}
},
{
"from": [3, 23, 2],
"to": [3, 30, 3],
"rotation": {"angle": 0, "axis": "y", "origin": [1, 35, -10]},
"faces": {
"north": {"uv": [0, 0, 0, 1.75], "texture": "#0"},
"east": {"uv": [15.5, 6, 15.75, 7.75], "texture": "#0"},
"south": {"uv": [0, 0, 0, 1.75], "texture": "#0"},
"up": {"uv": [0, 0.25, 0, 0], "texture": "#0"},
"down": {"uv": [0, 0, 0, 0.25], "texture": "#0"}
}
},
{
"from": [3, 23, 2],
"to": [13, 23, 3],
"rotation": {"angle": 0, "axis": "y", "origin": [1, 35, -10]},
"faces": {
"north": {"uv": [0, 0, 2.5, 0], "texture": "#0"},
"east": {"uv": [0, 0, 0.25, 0], "texture": "#0"},
"south": {"uv": [0, 0, 2.5, 0], "texture": "#0"},
"west": {"uv": [0, 0, 0.25, 0], "texture": "#0"},
"up": {"uv": [15.5, 8, 13, 7.75], "texture": "#0"}
}
},
{
"from": [3, 30, 2],
"to": [13, 30, 3],
"rotation": {"angle": 0, "axis": "y", "origin": [1, 35, -10]},
"faces": {
"north": {"uv": [0, 0, 2.5, 0], "texture": "#0"},
"east": {"uv": [0, 0, 0.25, 0], "texture": "#0"},
"south": {"uv": [0, 0, 2.5, 0], "texture": "#0"},
"west": {"uv": [0, 0, 0.25, 0], "texture": "#0"},
"down": {"uv": [15.5, 5.75, 13, 6], "texture": "#0"}
}
},
{
"from": [13, 23, 2],
"to": [13, 30, 3],
"rotation": {"angle": 0, "axis": "y", "origin": [1, 35, -10]},
"faces": {
"north": {"uv": [0, 0, 0, 1.75], "texture": "#0"},
"south": {"uv": [0, 0, 0, 1.75], "texture": "#0"},
"west": {"uv": [12.75, 6, 13, 7.75], "texture": "#0"},
"up": {"uv": [0, 0.25, 0, 0], "texture": "#0"},
"down": {"uv": [0, 0, 0, 0.25], "texture": "#0"}
}
},
{
"from": [4.01, 8.01, 4.01],
"to": [11.99, 8.99, 11.99],
"rotation": {"angle": 0, "axis": "y", "origin": [1, 24, 1]},
"faces": {
"north": {"uv": [13.25, 13.75, 15.25, 14], "texture": "#0"},
"east": {"uv": [9.5, 13.75, 11.5, 14], "texture": "#0"},
"south": {"uv": [5.75, 13.75, 7.75, 14], "texture": "#0"},
"west": {"uv": [2, 13.75, 4, 14], "texture": "#0"},
"up": {"uv": [9, 2, 7, 0], "texture": "#0"},
"down": {"uv": [11.25, 0, 9.25, 2], "texture": "#0"}
}
},
{
"from": [4.5, 1, 4.5],
"to": [5.5, 14, 5.5],
"rotation": {"angle": 0, "axis": "y", "origin": [1.5, 29, 1.5]},
"faces": {
"north": {"uv": [15, 12.5, 15.25, 15.75], "texture": "#0"},
"east": {"uv": [8.5, 2.25, 8.75, 5.5], "texture": "#0"},
"south": {"uv": [8.75, 2.25, 9, 5.5], "texture": "#0"},
"west": {"uv": [2, 12.5, 2.25, 15.75], "texture": "#0"}
}
},
{
"from": [10.5, 1, 4.5],
"to": [11.5, 14, 5.5],
"rotation": {"angle": 0, "axis": "y", "origin": [14.5, 29, 1.5]},
"faces": {
"north": {"uv": [13.25, 12.5, 13.5, 15.75], "texture": "#0"},
"east": {"uv": [11.25, 12.5, 11.5, 15.75], "texture": "#0"},
"south": {"uv": [7, 2.25, 7.25, 5.5], "texture": "#0"},
"west": {"uv": [7.25, 2.25, 7.5, 5.5], "texture": "#0"}
}
},
{
"from": [4.5, 1, 10.5],
"to": [5.5, 14, 11.5],
"rotation": {"angle": 0, "axis": "y", "origin": [1.5, 29, 14.5]},
"faces": {
"north": {"uv": [7.5, 2.25, 7.75, 5.5], "texture": "#0"},
"east": {"uv": [7.75, 2.25, 8, 5.5], "texture": "#0"},
"south": {"uv": [5.75, 12.5, 6, 15.75], "texture": "#0"},
"west": {"uv": [3.75, 12.5, 4, 15.75], "texture": "#0"}
}
},
{
"from": [10.5, 1, 10.5],
"to": [11.5, 14, 11.5],
"rotation": {"angle": 0, "axis": "y", "origin": [14.5, 29, 14.5]},
"faces": {
"north": {"uv": [8.25, 2.25, 8.5, 5.5], "texture": "#0"},
"east": {"uv": [9.5, 12.5, 9.75, 15.75], "texture": "#0"},
"south": {"uv": [7.5, 12.5, 7.75, 15.75], "texture": "#0"},
"west": {"uv": [9, 2.25, 9.25, 5.5], "texture": "#0"}
}
}
],
"groups": [
{
"name": "printer",
"origin": [1, 0, 1],
"color": 0,
"children": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
}
],
"display": {
"gui": {
"rotation": [30, 225, 0],
"translation": [0, -3, 0],
"scale": [0.425, 0.425, 0.425]
},
"ground": {
"rotation": [0, 0, 0],
"translation": [0, 3, 0],
"scale": [0.25, 0.25, 0.25]
},
"fixed": {
"rotation": [0, 0, 0],
"translation": [0, -3.75, 0],
"scale": [0.5, 0.5, 0.5]
},
"thirdperson_righthand": {
"rotation": [75, 135, 0],
"translation": [0, 2.5, 0],
"scale": [0.375, 0.375, 0.375]
},
"firstperson_righthand": {
"rotation": [0, 135, 0],
"translation": [0, -1, 0],
"scale": [0.4, 0.4, 0.4]
},
"firstperson_lefthand": {
"rotation": [0, 135, 0],
"translation": [0, -1, 0],
"scale": [0.4, 0.4, 0.4]
}
}
}
@@ -0,0 +1,163 @@
{
"format_version": "1.21.11",
"credit": "Made with Blockbench",
"texture_size": [64, 64],
"textures": {
"0": "cobblemon:block/printer",
"screen": "cobblemon:block/printer_screen_on",
"particle": "cobblemon:block/printer"
},
"elements": [
{
"from": [1, 0, 1],
"to": [15, 1, 15],
"rotation": {"angle": 0, "axis": "y", "origin": [1, 0, 1]},
"faces": {
"north": {"uv": [12.5, 15.75, 16, 16], "texture": "#0"},
"east": {"uv": [8.75, 15.75, 12.25, 16], "texture": "#0"},
"south": {"uv": [5, 15.75, 8.5, 16], "texture": "#0"},
"west": {"uv": [1.25, 15.75, 4.75, 16], "texture": "#0"},
"up": {"uv": [3.5, 3.5, 0, 0], "texture": "#0"},
"down": {"uv": [3.5, 3.75, 0, 7.25], "texture": "#0"}
}
},
{
"from": [2, 14, 2],
"to": [14, 31, 14],
"rotation": {"angle": 0, "axis": "y", "origin": [1, 30, 1]},
"faces": {
"north": {"uv": [12.75, 8.25, 15.75, 12.5], "texture": "#0"},
"east": {"uv": [9, 8.25, 12, 12.5], "texture": "#0"},
"south": {"uv": [5.25, 8.25, 8.25, 12.5], "texture": "#0"},
"west": {"uv": [1.5, 8.25, 4.5, 12.5], "texture": "#0"},
"up": {"uv": [6.75, 3, 3.75, 0], "texture": "#0"},
"down": {"uv": [6.75, 3.25, 3.75, 6.25], "texture": "#0"}
}
},
{
"from": [3, 23, 3],
"to": [13, 30, 3],
"rotation": {"angle": 0, "axis": "y", "origin": [1, 35, -11]},
"faces": {
"north": {"uv": [0, 0, 16, 16], "texture": "#screen"},
"east": {"uv": [0, 0, 0, 1.75], "texture": "#0"},
"west": {"uv": [0, 0, 0, 1.75], "texture": "#0"},
"up": {"uv": [2.5, 0, 0, 0], "texture": "#0"},
"down": {"uv": [2.5, 0, 0, 0], "texture": "#0"}
}
},
{
"from": [3, 23, 2],
"to": [3, 30, 3],
"rotation": {"angle": 0, "axis": "y", "origin": [1, 35, -10]},
"faces": {
"north": {"uv": [0, 0, 0, 1.75], "texture": "#0"},
"east": {"uv": [15.5, 6, 15.75, 7.75], "texture": "#0"},
"south": {"uv": [0, 0, 0, 1.75], "texture": "#0"},
"up": {"uv": [0, 0.25, 0, 0], "texture": "#0"},
"down": {"uv": [0, 0, 0, 0.25], "texture": "#0"}
}
},
{
"from": [3, 23, 2],
"to": [13, 23, 3],
"rotation": {"angle": 0, "axis": "y", "origin": [1, 35, -10]},
"faces": {
"north": {"uv": [0, 0, 2.5, 0], "texture": "#0"},
"east": {"uv": [0, 0, 0.25, 0], "texture": "#0"},
"south": {"uv": [0, 0, 2.5, 0], "texture": "#0"},
"west": {"uv": [0, 0, 0.25, 0], "texture": "#0"},
"up": {"uv": [15.5, 8, 13, 7.75], "texture": "#0"}
}
},
{
"from": [3, 30, 2],
"to": [13, 30, 3],
"rotation": {"angle": 0, "axis": "y", "origin": [1, 35, -10]},
"faces": {
"north": {"uv": [0, 0, 2.5, 0], "texture": "#0"},
"east": {"uv": [0, 0, 0.25, 0], "texture": "#0"},
"south": {"uv": [0, 0, 2.5, 0], "texture": "#0"},
"west": {"uv": [0, 0, 0.25, 0], "texture": "#0"},
"down": {"uv": [15.5, 5.75, 13, 6], "texture": "#0"}
}
},
{
"from": [13, 23, 2],
"to": [13, 30, 3],
"rotation": {"angle": 0, "axis": "y", "origin": [1, 35, -10]},
"faces": {
"north": {"uv": [0, 0, 0, 1.75], "texture": "#0"},
"south": {"uv": [0, 0, 0, 1.75], "texture": "#0"},
"west": {"uv": [12.75, 6, 13, 7.75], "texture": "#0"},
"up": {"uv": [0, 0.25, 0, 0], "texture": "#0"},
"down": {"uv": [0, 0, 0, 0.25], "texture": "#0"}
}
},
{
"from": [4.01, 8.01, 4.01],
"to": [11.99, 8.99, 11.99],
"rotation": {"angle": 0, "axis": "y", "origin": [1, 24, 1]},
"faces": {
"north": {"uv": [13.25, 13.75, 15.25, 14], "texture": "#0"},
"east": {"uv": [9.5, 13.75, 11.5, 14], "texture": "#0"},
"south": {"uv": [5.75, 13.75, 7.75, 14], "texture": "#0"},
"west": {"uv": [2, 13.75, 4, 14], "texture": "#0"},
"up": {"uv": [9, 2, 7, 0], "texture": "#0"},
"down": {"uv": [11.25, 0, 9.25, 2], "texture": "#0"}
}
},
{
"from": [4.5, 1, 4.5],
"to": [5.5, 14, 5.5],
"rotation": {"angle": 0, "axis": "y", "origin": [1.5, 29, 1.5]},
"faces": {
"north": {"uv": [15, 12.5, 15.25, 15.75], "texture": "#0"},
"east": {"uv": [8.5, 2.25, 8.75, 5.5], "texture": "#0"},
"south": {"uv": [8.75, 2.25, 9, 5.5], "texture": "#0"},
"west": {"uv": [2, 12.5, 2.25, 15.75], "texture": "#0"}
}
},
{
"from": [10.5, 1, 4.5],
"to": [11.5, 14, 5.5],
"rotation": {"angle": 0, "axis": "y", "origin": [14.5, 29, 1.5]},
"faces": {
"north": {"uv": [13.25, 12.5, 13.5, 15.75], "texture": "#0"},
"east": {"uv": [11.25, 12.5, 11.5, 15.75], "texture": "#0"},
"south": {"uv": [7, 2.25, 7.25, 5.5], "texture": "#0"},
"west": {"uv": [7.25, 2.25, 7.5, 5.5], "texture": "#0"}
}
},
{
"from": [4.5, 1, 10.5],
"to": [5.5, 14, 11.5],
"rotation": {"angle": 0, "axis": "y", "origin": [1.5, 29, 14.5]},
"faces": {
"north": {"uv": [7.5, 2.25, 7.75, 5.5], "texture": "#0"},
"east": {"uv": [7.75, 2.25, 8, 5.5], "texture": "#0"},
"south": {"uv": [5.75, 12.5, 6, 15.75], "texture": "#0"},
"west": {"uv": [3.75, 12.5, 4, 15.75], "texture": "#0"}
}
},
{
"from": [10.5, 1, 10.5],
"to": [11.5, 14, 11.5],
"rotation": {"angle": 0, "axis": "y", "origin": [14.5, 29, 14.5]},
"faces": {
"north": {"uv": [8.25, 2.25, 8.5, 5.5], "texture": "#0"},
"east": {"uv": [9.5, 12.5, 9.75, 15.75], "texture": "#0"},
"south": {"uv": [7.5, 12.5, 7.75, 15.75], "texture": "#0"},
"west": {"uv": [9, 2.25, 9.25, 5.5], "texture": "#0"}
}
}
],
"groups": [
{
"name": "printer",
"origin": [1, 0, 1],
"color": 0,
"children": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
}
]
}
@@ -0,0 +1,163 @@
{
"format_version": "1.21.11",
"credit": "Made with Blockbench",
"texture_size": [64, 64],
"textures": {
"0": "cobblemon:block/printer",
"screen": "cobblemon:block/printer_screen_printing",
"particle": "cobblemon:block/printer"
},
"elements": [
{
"from": [1, 0, 1],
"to": [15, 1, 15],
"rotation": {"angle": 0, "axis": "y", "origin": [1, 0, 1]},
"faces": {
"north": {"uv": [12.5, 15.75, 16, 16], "texture": "#0"},
"east": {"uv": [8.75, 15.75, 12.25, 16], "texture": "#0"},
"south": {"uv": [5, 15.75, 8.5, 16], "texture": "#0"},
"west": {"uv": [1.25, 15.75, 4.75, 16], "texture": "#0"},
"up": {"uv": [3.5, 3.5, 0, 0], "texture": "#0"},
"down": {"uv": [3.5, 3.75, 0, 7.25], "texture": "#0"}
}
},
{
"from": [2, 14, 2],
"to": [14, 31, 14],
"rotation": {"angle": 0, "axis": "y", "origin": [1, 30, 1]},
"faces": {
"north": {"uv": [12.75, 8.25, 15.75, 12.5], "texture": "#0"},
"east": {"uv": [9, 8.25, 12, 12.5], "texture": "#0"},
"south": {"uv": [5.25, 8.25, 8.25, 12.5], "texture": "#0"},
"west": {"uv": [1.5, 8.25, 4.5, 12.5], "texture": "#0"},
"up": {"uv": [6.75, 3, 3.75, 0], "texture": "#0"},
"down": {"uv": [6.75, 3.25, 3.75, 6.25], "texture": "#0"}
}
},
{
"from": [3, 23, 3],
"to": [13, 30, 3],
"rotation": {"angle": 0, "axis": "y", "origin": [1, 35, -11]},
"faces": {
"north": {"uv": [0, 0, 16, 16], "texture": "#screen"},
"east": {"uv": [0, 0, 0, 1.75], "texture": "#0"},
"west": {"uv": [0, 0, 0, 1.75], "texture": "#0"},
"up": {"uv": [2.5, 0, 0, 0], "texture": "#0"},
"down": {"uv": [2.5, 0, 0, 0], "texture": "#0"}
}
},
{
"from": [3, 23, 2],
"to": [3, 30, 3],
"rotation": {"angle": 0, "axis": "y", "origin": [1, 35, -10]},
"faces": {
"north": {"uv": [0, 0, 0, 1.75], "texture": "#0"},
"east": {"uv": [15.5, 6, 15.75, 7.75], "texture": "#0"},
"south": {"uv": [0, 0, 0, 1.75], "texture": "#0"},
"up": {"uv": [0, 0.25, 0, 0], "texture": "#0"},
"down": {"uv": [0, 0, 0, 0.25], "texture": "#0"}
}
},
{
"from": [3, 23, 2],
"to": [13, 23, 3],
"rotation": {"angle": 0, "axis": "y", "origin": [1, 35, -10]},
"faces": {
"north": {"uv": [0, 0, 2.5, 0], "texture": "#0"},
"east": {"uv": [0, 0, 0.25, 0], "texture": "#0"},
"south": {"uv": [0, 0, 2.5, 0], "texture": "#0"},
"west": {"uv": [0, 0, 0.25, 0], "texture": "#0"},
"up": {"uv": [15.5, 8, 13, 7.75], "texture": "#0"}
}
},
{
"from": [3, 30, 2],
"to": [13, 30, 3],
"rotation": {"angle": 0, "axis": "y", "origin": [1, 35, -10]},
"faces": {
"north": {"uv": [0, 0, 2.5, 0], "texture": "#0"},
"east": {"uv": [0, 0, 0.25, 0], "texture": "#0"},
"south": {"uv": [0, 0, 2.5, 0], "texture": "#0"},
"west": {"uv": [0, 0, 0.25, 0], "texture": "#0"},
"down": {"uv": [15.5, 5.75, 13, 6], "texture": "#0"}
}
},
{
"from": [13, 23, 2],
"to": [13, 30, 3],
"rotation": {"angle": 0, "axis": "y", "origin": [1, 35, -10]},
"faces": {
"north": {"uv": [0, 0, 0, 1.75], "texture": "#0"},
"south": {"uv": [0, 0, 0, 1.75], "texture": "#0"},
"west": {"uv": [12.75, 6, 13, 7.75], "texture": "#0"},
"up": {"uv": [0, 0.25, 0, 0], "texture": "#0"},
"down": {"uv": [0, 0, 0, 0.25], "texture": "#0"}
}
},
{
"from": [4.01, 8.01, 4.01],
"to": [11.99, 8.99, 11.99],
"rotation": {"angle": 0, "axis": "y", "origin": [1, 24, 1]},
"faces": {
"north": {"uv": [13.25, 13.75, 15.25, 14], "texture": "#0"},
"east": {"uv": [9.5, 13.75, 11.5, 14], "texture": "#0"},
"south": {"uv": [5.75, 13.75, 7.75, 14], "texture": "#0"},
"west": {"uv": [2, 13.75, 4, 14], "texture": "#0"},
"up": {"uv": [9, 2, 7, 0], "texture": "#0"},
"down": {"uv": [11.25, 0, 9.25, 2], "texture": "#0"}
}
},
{
"from": [4.5, 1, 4.5],
"to": [5.5, 14, 5.5],
"rotation": {"angle": 0, "axis": "y", "origin": [1.5, 29, 1.5]},
"faces": {
"north": {"uv": [15, 12.5, 15.25, 15.75], "texture": "#0"},
"east": {"uv": [8.5, 2.25, 8.75, 5.5], "texture": "#0"},
"south": {"uv": [8.75, 2.25, 9, 5.5], "texture": "#0"},
"west": {"uv": [2, 12.5, 2.25, 15.75], "texture": "#0"}
}
},
{
"from": [10.5, 1, 4.5],
"to": [11.5, 14, 5.5],
"rotation": {"angle": 0, "axis": "y", "origin": [14.5, 29, 1.5]},
"faces": {
"north": {"uv": [13.25, 12.5, 13.5, 15.75], "texture": "#0"},
"east": {"uv": [11.25, 12.5, 11.5, 15.75], "texture": "#0"},
"south": {"uv": [7, 2.25, 7.25, 5.5], "texture": "#0"},
"west": {"uv": [7.25, 2.25, 7.5, 5.5], "texture": "#0"}
}
},
{
"from": [4.5, 1, 10.5],
"to": [5.5, 14, 11.5],
"rotation": {"angle": 0, "axis": "y", "origin": [1.5, 29, 14.5]},
"faces": {
"north": {"uv": [7.5, 2.25, 7.75, 5.5], "texture": "#0"},
"east": {"uv": [7.75, 2.25, 8, 5.5], "texture": "#0"},
"south": {"uv": [5.75, 12.5, 6, 15.75], "texture": "#0"},
"west": {"uv": [3.75, 12.5, 4, 15.75], "texture": "#0"}
}
},
{
"from": [10.5, 1, 10.5],
"to": [11.5, 14, 11.5],
"rotation": {"angle": 0, "axis": "y", "origin": [14.5, 29, 14.5]},
"faces": {
"north": {"uv": [8.25, 2.25, 8.5, 5.5], "texture": "#0"},
"east": {"uv": [9.5, 12.5, 9.75, 15.75], "texture": "#0"},
"south": {"uv": [7.5, 12.5, 7.75, 15.75], "texture": "#0"},
"west": {"uv": [9, 2.25, 9.25, 5.5], "texture": "#0"}
}
}
],
"groups": [
{
"name": "printer",
"origin": [1, 0, 1],
"color": 0,
"children": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
}
]
}
@@ -0,0 +1,6 @@
{
"textures": {
"particle": "cobblemon:block/printer"
},
"elements": []
}
@@ -0,0 +1,3 @@
{
"parent": "minecraft:block/block"
}
@@ -0,0 +1,4 @@
{
"parent": "cobblemon:block/photo_printer"
}
@@ -0,0 +1,3 @@
{
"parent": "minecraft:builtin/entity"
}
@@ -0,0 +1,6 @@
{
"pageType": "cobblemon:photo_gallery",
"titleKey": "ui.pokedex.page.photo_gallery",
"order": 2,
"enabled": true
}
@@ -0,0 +1,6 @@
{
"pageType": "cobblemon:main_dex",
"titleKey": "ui.pokedex.page.pokedex",
"order": 0,
"enabled": true
}
@@ -0,0 +1,18 @@
{
"priority": 80,
"poseScore": 750,
"match": {
"contains_any": [
"physical",
"special",
"status",
"battle_quirk",
"quirk_battle"
],
"not_contains": [
"sleep",
"cry",
"faint"
]
}
}

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