forked from mirrors/cobblemon
Compare commits
36
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29734ea067 | ||
|
|
500c7b2e5c | ||
|
|
4dde882398 | ||
|
|
84039fcfaf | ||
|
|
fc1853a23b | ||
|
|
72c46333d3 | ||
|
|
4faee1a5cf | ||
|
|
db3237df81 | ||
|
|
b76efa52d1 | ||
|
|
306e5e854f | ||
|
|
611a17b16d | ||
|
|
792329880b | ||
|
|
9ddd773182 | ||
|
|
dd89a220bc | ||
|
|
8b43ef0922 | ||
|
|
636c6701c0 | ||
|
|
da9ad072f1 | ||
|
|
ec91b597b7 | ||
|
|
c293c6cbfb | ||
|
|
6eccd3e72a | ||
|
|
480b125146 | ||
|
|
b29366af64 | ||
|
|
b4c1d10037 | ||
|
|
12fbd9ed32 | ||
|
|
644f0cb3b4 | ||
|
|
327548a8aa | ||
|
|
bc41a5294e | ||
|
|
c2f8602106 | ||
|
|
9ddfd707c2 | ||
|
|
98e9a673c0 | ||
|
|
beb4e4aa86 | ||
|
|
2567bc8819 | ||
|
|
6a300e3c5f | ||
|
|
4d8f4b2bfc | ||
|
|
94154776eb | ||
|
|
b76e75efcc |
@@ -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")
|
||||
|
||||
+15
@@ -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
|
||||
}
|
||||
}
|
||||
+14
@@ -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]
|
||||
}
|
||||
+52
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+17
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+8
-1
@@ -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)
|
||||
|
||||
+28
@@ -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) {}
|
||||
}
|
||||
+10
@@ -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) }
|
||||
}
|
||||
+43
@@ -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)
|
||||
}
|
||||
}
|
||||
+253
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+111
@@ -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)
|
||||
}
|
||||
}
|
||||
+150
@@ -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() {
|
||||
|
||||
+445
@@ -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))
|
||||
}
|
||||
|
||||
+16
@@ -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
|
||||
+397
@@ -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
|
||||
}
|
||||
}
|
||||
+30
@@ -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) {}
|
||||
}
|
||||
+16
@@ -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
|
||||
)
|
||||
+96
@@ -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()
|
||||
}
|
||||
}
|
||||
+51
-41
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+93
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+28
@@ -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)
|
||||
}
|
||||
}
|
||||
+20
@@ -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)
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.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)
|
||||
}
|
||||
}
|
||||
|
||||
+24
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+488
@@ -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
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.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)
|
||||
}
|
||||
}
|
||||
|
||||
+20
@@ -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)
|
||||
}
|
||||
}
|
||||
+33
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.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)
|
||||
}
|
||||
}
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+32
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
+23
-1
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
+383
-192
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+88
@@ -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('.')
|
||||
}
|
||||
}
|
||||
+114
@@ -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)
|
||||
}
|
||||
}
|
||||
+174
@@ -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()
|
||||
}
|
||||
}
|
||||
+84
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
@@ -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(
|
||||
|
||||
+103
@@ -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)
|
||||
}
|
||||
}
|
||||
+124
@@ -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)
|
||||
}
|
||||
}
|
||||
+73
@@ -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()
|
||||
}
|
||||
}
|
||||
+115
@@ -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)
|
||||
}
|
||||
}
|
||||
+30
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+62
@@ -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)
|
||||
|
||||
+77
-3
@@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+47
@@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+35
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
+53
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+152
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+140
@@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+43
@@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+53
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+39
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+32
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
+39
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+44
@@ -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() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Cobblemon Contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package com.cobblemon.mod.common.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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+20
@@ -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())
|
||||
}
|
||||
}
|
||||
+63
@@ -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
|
||||
}
|
||||
}
|
||||
+13
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
+511
@@ -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
|
||||
)
|
||||
}
|
||||
+62
@@ -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()
|
||||
}
|
||||
}
|
||||
+209
@@ -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
|
||||
)
|
||||
}
|
||||
+50
@@ -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)
|
||||
}
|
||||
}
|
||||
+1328
-12
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)
|
||||
}
|
||||
|
||||
+53
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
+13
@@ -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) {}
|
||||
}
|
||||
+61
@@ -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
Reference in New Issue
Block a user